Skip to content

Commit

Permalink
Merge branch 'master' into url-encode-brackets-2
Browse files Browse the repository at this point in the history
  • Loading branch information
cmoesel authored Dec 23, 2024
2 parents fdcce94 + 2bfe6ad commit d73db7d
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 15 deletions.
17 changes: 12 additions & 5 deletions src/export/CodeSystemExporter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FSHTank } from '../import/FSHTank';
import { CodeSystem, CodeSystemConcept, PathPart } from '../fhirtypes';
import { CodeSystem, CodeSystemConcept, PathPart, StructureDefinition } from '../fhirtypes';
import {
setPropertyOnDefinitionInstance,
applyInsertRules,
Expand All @@ -9,7 +9,8 @@ import {
validateInstanceFromRawValue,
isExtension,
replaceReferences,
splitOnPathPeriods
splitOnPathPeriods,
checkForMultipleChoice
} from '../fhirtypes/common';
import { FshCodeSystem } from '../fshtypes';
import { CaretValueRule, ConceptRule } from '../fshtypes/rules';
Expand Down Expand Up @@ -104,7 +105,11 @@ export class CodeSystemExporter {
}
}

private setCaretPathRules(codeSystem: CodeSystem, rules: CaretValueRule[]) {
private setCaretPathRules(
codeSystem: CodeSystem,
rules: CaretValueRule[],
codeSystemSD: StructureDefinition
) {
// soft index resolution relies on the rule's path attribute.
// a CaretValueRule is created with an empty path, so first
// transform its arrayPath into a path.
Expand All @@ -130,7 +135,6 @@ export class CodeSystemExporter {
// a codesystem is a specific case where the only implied values are going to be extension urls.
// so, we only need to track rules that involve an extension.
const ruleMap: Map<string, { pathParts: PathPart[] }> = new Map();
const codeSystemSD = codeSystem.getOwnStructureDefinition(this.fisher);
// in order to validate rules that set values on contained resources, we need to track information from rules
// that define the types of those resources. those types could be defined by rules on the "resourceType" element,
// or they could be defined by the existing resource that is being assigned.
Expand Down Expand Up @@ -344,14 +348,16 @@ export class CodeSystemExporter {
return;
}
const codeSystem = new CodeSystem();
const codeSystemSD = codeSystem.getOwnStructureDefinition(this.fisher);
this.setMetadata(codeSystem, fshDefinition);
this.setConcepts(
codeSystem,
fshDefinition.rules.filter(rule => rule instanceof ConceptRule) as ConceptRule[]
);
this.setCaretPathRules(
codeSystem,
fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[]
fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[],
codeSystemSD
);

// check for another code system with the same id
Expand All @@ -364,6 +370,7 @@ export class CodeSystemExporter {
}

cleanResource(codeSystem, (prop: string) => ['_sliceName', '_primitive'].includes(prop));
checkForMultipleChoice(fshDefinition, codeSystem, codeSystemSD);
this.updateCount(codeSystem, fshDefinition);
this.pkg.codeSystems.push(codeSystem);
this.pkg.fshMap.set(codeSystem.getFileName(), {
Expand Down
4 changes: 3 additions & 1 deletion src/export/InstanceExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
createUsefulSlices,
determineKnownSlices,
setImpliedPropertiesOnInstance,
getMatchingContainedReferenceId
getMatchingContainedReferenceId,
checkForMultipleChoice
} from '../fhirtypes/common';
import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError';
import { AbstractInstanceOfError } from '../errors/AbstractInstanceOfError';
Expand Down Expand Up @@ -922,6 +923,7 @@ export class InstanceExporter implements Fishable {
);
this.checkForNamelessSlices(fshDefinition, instanceDef, instanceOfStructureDefinition);
cleanResource(instanceDef);
checkForMultipleChoice(fshDefinition, instanceDef, instanceOfStructureDefinition);
this.pkg.instances.push(instanceDef);
if (fshDefinition.usage !== 'Inline') {
this.pkg.fshMap.set(instanceDef.getFileName(), {
Expand Down
9 changes: 8 additions & 1 deletion src/export/StructureDefinitionExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ import {
getAllConcepts,
TYPE_CHARACTERISTICS_CODE,
TYPE_CHARACTERISTICS_EXTENSION,
LOGICAL_TARGET_EXTENSION
LOGICAL_TARGET_EXTENSION,
checkForMultipleChoice
} from '../fhirtypes/common';
import { Package } from './Package';
import { isUri } from 'valid-url';
Expand Down Expand Up @@ -1548,6 +1549,12 @@ export class StructureDefinitionExporter implements Fishable {
logger.log(err.severity, err.message, fshDefinition.sourceInfo);
});

checkForMultipleChoice(
fshDefinition,
structDef,
structDef.getOwnStructureDefinition(this.fisher)
);

// check for another structure definition with the same id
// see https://www.hl7.org/fhir/resource.html#id
// the structure definition has already been added to the package, so it's fine if it matches itself
Expand Down
26 changes: 18 additions & 8 deletions src/export/ValueSetExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
ValueSet,
ValueSetComposeIncludeOrExclude,
ValueSetComposeConcept,
PathPart
PathPart,
StructureDefinition
} from '../fhirtypes';
import { FSHTank } from '../import/FSHTank';
import { FshValueSet, FshCode, ValueSetFilterValue, FshCodeSystem, Instance } from '../fshtypes';
Expand All @@ -24,7 +25,8 @@ import {
validateInstanceFromRawValue,
determineKnownSlices,
setImpliedPropertiesOnInstance,
splitOnPathPeriods
splitOnPathPeriods,
checkForMultipleChoice
} from '../fhirtypes/common';
import { isUri } from 'valid-url';
import { flatMap, partition, xor } from 'lodash';
Expand Down Expand Up @@ -269,11 +271,14 @@ export class ValueSetExporter {
}
}

private setCaretRules(valueSet: ValueSet, rules: CaretValueRule[]) {
private setCaretRules(
valueSet: ValueSet,
rules: CaretValueRule[],
valueSetSD: StructureDefinition
) {
resolveSoftIndexing(rules);

const ruleMap: Map<string, { pathParts: PathPart[] }> = new Map();
const valueSetSD = valueSet.getOwnStructureDefinition(this.fisher);
// in order to validate rules that set values on contained resources, we need to track information from rules
// that define the types of those resources. those types could be defined by rules on the "resourceType" element,
// or they could be defined by the existing resource that is being assigned.
Expand Down Expand Up @@ -416,10 +421,13 @@ export class ValueSetExporter {
}
}

private setConceptCaretRules(vs: ValueSet, rules: CaretValueRule[]) {
private setConceptCaretRules(
vs: ValueSet,
rules: CaretValueRule[],
valueSetSD: StructureDefinition
) {
resolveSoftIndexing(rules);
const ruleMap: Map<string, { pathParts: PathPart[]; rule: CaretValueRule }> = new Map();
const valueSetSD = vs.getOwnStructureDefinition(this.fisher);
for (const rule of rules) {
const splitConcept = rule.pathArray[0].split('#');
const system = splitConcept[0];
Expand Down Expand Up @@ -552,22 +560,23 @@ export class ValueSetExporter {
return;
}
const vs = new ValueSet();
const valueSetSD = vs.getOwnStructureDefinition(this.fisher);
this.setMetadata(vs, fshDefinition);
const [conceptCaretRules, otherCaretRules] = partition(
fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[],
caretRule => {
return caretRule.pathArray.length > 0;
}
);
this.setCaretRules(vs, otherCaretRules);
this.setCaretRules(vs, otherCaretRules, valueSetSD);
this.setCompose(
vs,
fshDefinition.rules.filter(
rule => rule instanceof ValueSetComponentRule
) as ValueSetComponentRule[]
);
conceptCaretRules.forEach(rule => (rule.isCodeCaretRule = true));
this.setConceptCaretRules(vs, conceptCaretRules);
this.setConceptCaretRules(vs, conceptCaretRules, valueSetSD);
if (vs.compose && vs.compose.include.length == 0) {
throw new ValueSetComposeError(fshDefinition.name);
}
Expand All @@ -582,6 +591,7 @@ export class ValueSetExporter {
}

cleanResource(vs, (prop: string) => ['_sliceName', '_primitive'].includes(prop));
checkForMultipleChoice(fshDefinition, vs, valueSetSD);
this.pkg.valueSets.push(vs);
this.pkg.fshMap.set(vs.getFileName(), {
...fshDefinition.sourceInfo,
Expand Down
62 changes: 62 additions & 0 deletions src/fhirtypes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1647,3 +1647,65 @@ export function getMatchingContainedReferenceId(
}
}
}

export function checkForMultipleChoice(
fshDef: Profile | Extension | Logical | Resource | FshCodeSystem | FshValueSet | Instance,
fhirDef: { [key: string]: any },
structDef: StructureDefinition
) {
checkChildrenForMultipleChoice(fshDef, fhirDef, structDef.elements[0]);
}

function checkChildrenForMultipleChoice(
fshDef: Profile | Extension | Logical | Resource | FshCodeSystem | FshValueSet | Instance,
instance: { [key: string]: any },
element: ElementDefinition
) {
const children = element.children(true);
children.forEach(child => {
// does this child represent a choice element, such as value[x]?
// if so, check for choices
if (child.id.endsWith('[x]')) {
// get the element names for each type choice
const idStart = splitOnPathPeriods(child.id).slice(-1)[0].slice(0, -3);
const availableChoices = child.type.map(edType => `${idStart}${upperFirst(edType.code)}`);
if (availableChoices.length > 1) {
const existingChoices = availableChoices.filter(choice => {
return instance[choice] != null || instance[`_${choice}`] != null;
});
if (existingChoices.length > 1) {
logger.error(
`${fshDef.name} contains multiple choice value assignments for choice element ${child.id}.`,
fshDef.sourceInfo
);
}
}
}
// does the instance have an object value for this element?
// if so, recursively check that object.
// since there may also be children of primitives, also check underscore properties
const childPathEnd = child.path.split('.').slice(-1)[0];
if (instance[childPathEnd] != null && typeof instance[childPathEnd] === 'object') {
if (Array.isArray(instance[childPathEnd])) {
instance[childPathEnd].forEach((childProperty: any) => {
if (childProperty != null && typeof childProperty === 'object') {
checkChildrenForMultipleChoice(fshDef, childProperty, child);
}
});
} else {
checkChildrenForMultipleChoice(fshDef, instance[childPathEnd], child);
}
}
if (instance[`_${childPathEnd}`] != null && typeof instance[`_${childPathEnd}`] === 'object') {
if (Array.isArray(instance[`_${childPathEnd}`])) {
instance[`_${childPathEnd}`].forEach((childProperty: any) => {
if (childProperty != null && typeof childProperty === 'object') {
checkChildrenForMultipleChoice(fshDef, childProperty, child);
}
});
} else {
checkChildrenForMultipleChoice(fshDef, instance[`_${childPathEnd}`], child);
}
}
});
}
30 changes: 30 additions & 0 deletions test/export/CodeSystemExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,36 @@ describe('CodeSystemExporter', () => {
});
});

it('should output an error when a choice element has values assigned to more than one choice type', () => {
const codeSystem = new FshCodeSystem('MultiChoiceSystem')
.withFile('MultipleChoice.fsh')
.withLocation([3, 4, 8, 24]);
const extensionUrl = new CaretValueRule('');
extensionUrl.caretPath = 'extension[0].url';
extensionUrl.value = 'http://example.org/SomeExt';
const extensionString = new CaretValueRule('');
extensionString.caretPath = 'extension[0].valueString';
extensionString.value = 'multi value';
const extensionInteger = new CaretValueRule('');
extensionInteger.caretPath = 'extension[0].valueInteger';
extensionInteger.value = BigInt(24);
const conceptRule = new ConceptRule('bar', 'Bar', 'Bar');
codeSystem.rules.push(extensionUrl, extensionString, extensionInteger, conceptRule);
doc.codeSystems.set(codeSystem.name, codeSystem);

const exported = exporter.export().codeSystems;
expect(exported.length).toBe(1);
expect(exported[0].extension[0]).toEqual({
url: 'http://example.org/SomeExt',
valueString: 'multi value',
valueInteger: 24
});
expect(loggerSpy.getAllMessages('error')).toHaveLength(1);
expect(loggerSpy.getLastMessage('error')).toMatch(
/MultiChoiceSystem contains multiple choice value assignments for choice element CodeSystem\.extension\.value\[x\]\..*File: MultipleChoice\.fsh.*Line: 3 - 8\D*/s
);
});

it('should not override count when ^count is provided by user', () => {
const codeSystem = new FshCodeSystem('MyCodeSystem');
const rule = new CaretValueRule('');
Expand Down
Loading

0 comments on commit d73db7d

Please sign in to comment.