diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts index bf2b325b4..3409308b7 100644 --- a/src/export/CodeSystemExporter.ts +++ b/src/export/CodeSystemExporter.ts @@ -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, @@ -9,7 +9,8 @@ import { validateInstanceFromRawValue, isExtension, replaceReferences, - splitOnPathPeriods + splitOnPathPeriods, + checkForMultipleChoice } from '../fhirtypes/common'; import { FshCodeSystem } from '../fshtypes'; import { CaretValueRule, ConceptRule } from '../fshtypes/rules'; @@ -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. @@ -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 = 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. @@ -344,6 +348,7 @@ export class CodeSystemExporter { return; } const codeSystem = new CodeSystem(); + const codeSystemSD = codeSystem.getOwnStructureDefinition(this.fisher); this.setMetadata(codeSystem, fshDefinition); this.setConcepts( codeSystem, @@ -351,7 +356,8 @@ export class CodeSystemExporter { ); 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 @@ -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(), { diff --git a/src/export/InstanceExporter.ts b/src/export/InstanceExporter.ts index 108c042e6..076a10d72 100644 --- a/src/export/InstanceExporter.ts +++ b/src/export/InstanceExporter.ts @@ -23,7 +23,8 @@ import { createUsefulSlices, determineKnownSlices, setImpliedPropertiesOnInstance, - getMatchingContainedReferenceId + getMatchingContainedReferenceId, + checkForMultipleChoice } from '../fhirtypes/common'; import { InstanceOfNotDefinedError } from '../errors/InstanceOfNotDefinedError'; import { AbstractInstanceOfError } from '../errors/AbstractInstanceOfError'; @@ -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(), { diff --git a/src/export/StructureDefinitionExporter.ts b/src/export/StructureDefinitionExporter.ts index 7f34dbe0c..e585cdf87 100644 --- a/src/export/StructureDefinitionExporter.ts +++ b/src/export/StructureDefinitionExporter.ts @@ -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'; @@ -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 diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts index 9a6019eb2..2ec0b1c17 100644 --- a/src/export/ValueSetExporter.ts +++ b/src/export/ValueSetExporter.ts @@ -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'; @@ -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'; @@ -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 = 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. @@ -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 = new Map(); - const valueSetSD = vs.getOwnStructureDefinition(this.fisher); for (const rule of rules) { const splitConcept = rule.pathArray[0].split('#'); const system = splitConcept[0]; @@ -552,6 +560,7 @@ 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[], @@ -559,7 +568,7 @@ export class ValueSetExporter { return caretRule.pathArray.length > 0; } ); - this.setCaretRules(vs, otherCaretRules); + this.setCaretRules(vs, otherCaretRules, valueSetSD); this.setCompose( vs, fshDefinition.rules.filter( @@ -567,7 +576,7 @@ export class ValueSetExporter { ) 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); } @@ -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, diff --git a/src/fhirtypes/common.ts b/src/fhirtypes/common.ts index 42f359b05..45119bd31 100644 --- a/src/fhirtypes/common.ts +++ b/src/fhirtypes/common.ts @@ -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); + } + } + }); +} diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts index 99fd9f56b..3a9b0aaa2 100644 --- a/test/export/CodeSystemExporter.test.ts +++ b/test/export/CodeSystemExporter.test.ts @@ -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(''); diff --git a/test/export/InstanceExporter.test.ts b/test/export/InstanceExporter.test.ts index 75de12134..ff3a19ed4 100644 --- a/test/export/InstanceExporter.test.ts +++ b/test/export/InstanceExporter.test.ts @@ -5330,6 +5330,155 @@ describe('InstanceExporter', () => { expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); }); + it('should output an error when a choice element has values assigned to more than one choice type', () => { + // * multipleBirthBoolean = true + // * multipleBirthInteger = 2 + const multipleBirthBoolean = new AssignmentRule('multipleBirthBoolean'); + multipleBirthBoolean.value = true; + const multipleBirthInteger = new AssignmentRule('multipleBirthInteger') + .withFile('Twins.fsh') + .withLocation([4, 3, 4, 34]); + multipleBirthInteger.value = BigInt(2); + patientInstance.rules.push(multipleBirthBoolean, multipleBirthInteger); + + const exported = exportInstance(patientInstance); + expect(exported.multipleBirthBoolean).toBe(true); + expect(exported.multipleBirthInteger).toBe(2); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /Bar contains multiple choice value assignments for choice element Patient\.multipleBirth\[x\]\..*File: PatientInstance\.fsh.*Line: 10 - 20\D*/s + ); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); + }); + + it('should output an error when a choice element has values assigned to more than one choice type, some of which are a complex type', () => { + // Instance: sample-observation + // InstanceOf: Observation + // * status = #draft + // * code = #123 + // * valueString = "my string value" + // * valueCodeableConcept.text = "explanation of codeable concept" + const obsInstance = new Instance('sample-observation') + .withFile('Observations.fsh') + .withLocation([8, 3, 15, 44]); + obsInstance.instanceOf = 'Observation'; + const obsStatus = new AssignmentRule('status'); + obsStatus.value = new FshCode('draft'); + const obsCode = new AssignmentRule('code'); + obsCode.value = new FshCode('123'); + const obsString = new AssignmentRule('valueString'); + obsString.value = 'my string value'; + const obsCodeableConceptText = new AssignmentRule('valueCodeableConcept.text'); + obsCodeableConceptText.value = 'explanation of codeable concept'; + obsInstance.rules.push(obsStatus, obsCode, obsString, obsCodeableConceptText); + + const exported = exportInstance(obsInstance); + expect(exported.valueString).toBe('my string value'); + expect(exported.valueCodeableConcept.text).toBe('explanation of codeable concept'); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /sample-observation contains multiple choice value assignments for choice element Observation\.value\[x\]\..*File: Observations\.fsh.*Line: 8 - 15\D*/s + ); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); + }); + + it('should not output an error when a multiple-cardinality choice element has different types at different indices', () => { + // Instance: sample-observation + // InstanceOf: Observation + // * status = #draft + // * code = #123 + // * component[0].code = #123string + // * component[0].valueString = "my string value" + // * component[1].code = #123codeableConcept + // * component[1].valueCodeableConcept = http://example.org#paper "the paper" + const obsInstance = new Instance('sample-observation'); + obsInstance.instanceOf = 'Observation'; + const obsStatus = new AssignmentRule('status'); + obsStatus.value = new FshCode('draft'); + const obsCode = new AssignmentRule('code'); + obsCode.value = new FshCode('123'); + const firstComponentCode = new AssignmentRule('component[0].code'); + firstComponentCode.value = new FshCode('123string'); + const firstComponentValue = new AssignmentRule('component[0].valueString'); + firstComponentValue.value = 'my string value'; + const secondComponentCode = new AssignmentRule('component[1].code'); + secondComponentCode.value = new FshCode('123codeableConcept'); + const secondComponentValue = new AssignmentRule('component[1].valueCodeableConcept'); + secondComponentValue.value = new FshCode('paper', 'http://example.org', 'the paper'); + + obsInstance.rules.push( + obsStatus, + obsCode, + firstComponentCode, + firstComponentValue, + secondComponentCode, + secondComponentValue + ); + const exported = exportInstance(obsInstance); + expect(exported.component[0].valueString).toBe('my string value'); + expect(exported.component[1].valueCodeableConcept).toEqual({ + coding: [ + { + code: 'paper', + system: 'http://example.org', + display: 'the paper' + } + ] + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should output an error when a choice element within another element has values assigned to more than one choice type', () => { + // * extension[0].url = "https://example.org/SomeExt" + // * extension[0].valueString = "extension value is false" + // * extension[0].valueBoolean = false + const extensionUrl = new AssignmentRule('extension[0].url'); + extensionUrl.value = 'https://example.org/SomeExt'; + const extensionString = new AssignmentRule('extension[0].valueString'); + extensionString.value = 'extension value is false'; + const extensionBoolean = new AssignmentRule('extension[0].valueBoolean'); + extensionBoolean.value = false; + extensionBoolean.rawValue = 'false'; + patientInstance.rules.push(extensionUrl, extensionString, extensionBoolean); + + const exported = exportInstance(patientInstance); + expect(exported.extension[0]).toEqual({ + url: 'https://example.org/SomeExt', + valueString: 'extension value is false', + valueBoolean: false + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /Bar contains multiple choice value assignments for choice element Patient\.extension\.value\[x\]\..*File: PatientInstance\.fsh.*Line: 10 - 20\D*/s + ); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); + }); + + it('should output an error when a choice element that is a descendant of a primitive has values assigned to more than one type', () => { + // * gender.extension[0].url = "https://example.org/SomeExt" + // * gender.extension[0].valueString = "patient's gender is unknowable" + // * gender.extension[0].valueCode = #unknowable + const extensionUrl = new AssignmentRule('gender.extension[0].url'); + extensionUrl.value = 'https://example.org/SomeExt'; + const extensionString = new AssignmentRule('gender.extension[0].valueString'); + extensionString.value = "patient's gender is unknowable"; + const extensionCode = new AssignmentRule('gender.extension[0].valueCode'); + extensionCode.value = new FshCode('unknowable'); + patientInstance.rules.push(extensionUrl, extensionString, extensionCode); + + const exported = exportInstance(patientInstance); + expect(exported._gender.extension[0]).toEqual({ + url: 'https://example.org/SomeExt', + valueString: "patient's gender is unknowable", + valueCode: 'unknowable' + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /Bar contains multiple choice value assignments for choice element Patient\.gender\.extension\.value\[x\]\..*File: PatientInstance\.fsh.*Line: 10 - 20\D*/s + ); + expect(loggerSpy.getAllMessages('warn')).toHaveLength(0); + }); + it('should assign cardinality 1..n elements that are assigned by array pattern[x] from a parent on the SD', () => { const assignedValRule = new AssignmentRule('maritalStatus'); assignedValRule.value = new FshCode('foo', 'http://foo.com'); diff --git a/test/export/StructureDefinitionExporter.test.ts b/test/export/StructureDefinitionExporter.test.ts index bf357210f..797b0f5e2 100644 --- a/test/export/StructureDefinitionExporter.test.ts +++ b/test/export/StructureDefinitionExporter.test.ts @@ -5648,6 +5648,36 @@ describe('StructureDefinitionExporter R4', () => { ); }); + it('should apply AssignmentRules to different types of a choice element', () => { + // Profile: MyObservation + // Parent: Observation + // * valueString = "Hello" + // * valueCodeableConcept = http://example.org#world + const profile = new Profile('MyObservation'); + profile.parent = 'Observation'; + const stringRule = new AssignmentRule('valueString'); + stringRule.value = 'Hello'; + const codeableConceptRule = new AssignmentRule('valueCodeableConcept'); + codeableConceptRule.value = new FshCode('world', 'http://example.org'); + profile.rules.push(stringRule, codeableConceptRule); + exporter.exportStructDef(profile); + const sd = pkg.profiles[0]; + + const stringChoice = sd.findElement('Observation.value[x]:valueString'); + expect(stringChoice.patternString).toBe('Hello'); + const codeableConceptChoice = sd.findElement('Observation.value[x]:valueCodeableConcept'); + expect(codeableConceptChoice.patternCodeableConcept).toEqual({ + coding: [ + { + code: 'world', + system: 'http://example.org' + } + ] + }); + + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + it('should apply a Code AssignmentRule and replace the local complete code system name with its url', () => { const profile = new Profile('LightObservation'); profile.parent = 'Observation'; @@ -8633,6 +8663,42 @@ describe('StructureDefinitionExporter R4', () => { null ]); }); + + it('should output an error when a choice element has values assigned to more than one choice type', () => { + // Profile: MyObservation + // Parent: Observation + // * ^extension[0].url = "http://example.org/SomeExt" + // * ^extension[0].valueString = "string value" + // * ^extension[0].valueInteger = 7 + const profile = new Profile('MyObservation') + .withFile('Observation.fsh') + .withLocation([8, 3, 15, 25]); + profile.parent = 'Observation'; + 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 = 'string value'; + const extensionInteger = new CaretValueRule(''); + extensionInteger.caretPath = 'extension[0].valueInteger'; + extensionInteger.value = BigInt(7); + profile.rules.push(extensionUrl, extensionString, extensionInteger); + doc.profiles.set(profile.name, profile); + + exporter.exportStructDef(profile); + const sd = pkg.profiles[0]; + expect(sd.extension).toHaveLength(1); + expect(sd.extension[0]).toEqual({ + url: 'http://example.org/SomeExt', + valueString: 'string value', + valueInteger: 7 + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /MyObservation contains multiple choice value assignments for choice element StructureDefinition\.extension\.value\[x\]\..*File: Observation\.fsh.*Line: 8 - 15\D*/s + ); + }); }); describe('#ObeysRule', () => { diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts index ffb34b722..248a25ea8 100644 --- a/test/export/ValueSetExporter.test.ts +++ b/test/export/ValueSetExporter.test.ts @@ -3195,6 +3195,36 @@ describe('ValueSetExporter', () => { expect(loggerSpy.getAllMessages('error')).toHaveLength(0); }); + it('should output an error when a choice element has values assigned to more than one choice type', () => { + const valueSet = new FshValueSet('BreakfastVS') + .withFile('Breakfast.fsh') + .withLocation([8, 3, 25, 33]); + valueSet.title = 'Breakfast Values'; + 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 = 'string value'; + const extensionInteger = new CaretValueRule(''); + extensionInteger.caretPath = 'extension[0].valueInteger'; + extensionInteger.value = BigInt(7); + valueSet.rules.push(extensionUrl, extensionString, extensionInteger); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported.length).toBe(1); + expect(exported[0].extension[0]).toEqual({ + url: 'http://example.org/SomeExt', + valueString: 'string value', + valueInteger: 7 + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /BreakfastVS contains multiple choice value assignments for choice element ValueSet\.extension\.value\[x\]\..*File: Breakfast\.fsh.*Line: 8 - 25\D*/s + ); + }); + describe('#insertRules', () => { let vs: FshValueSet; let ruleSet: RuleSet;