From 2bfe6ad3e11561b969fb9eb74398ecb472cac984 Mon Sep 17 00:00:00 2001 From: Mint Thompson Date: Mon, 23 Dec 2024 13:14:02 -0500 Subject: [PATCH] Check exported resources for presence of multiple choice values (#1537) * Check exported resources for presence of multiple choice values When a choice element is defined in FHIR, it allows for values of different types with names based on those types. However, any choice element that is present on resource should have only one named value. For example, Observation.value[x] allows for many types, but an Observation resource should not have both valueString and valueInteger. After exporting a resource, check the resource for the presence of multiple values for the same choice element, and log an error when they are detected. As assigned values are validated in exporters, the relevant StructureDefinitions will have their elements unfolded. Keep the same instance of that StructureDefinition that was used for validation so that the unfolded elements are present when performing the multiple choice validation check. * Check exported FHIR for multiple choice values Only one type choice of a choice element should be present on exported FHIR. Check for the presence of multiple type choices. If multiples are found, log an error. Note that it is still fine to apply rules to different type choice elements on a Profile, since those are two separate element definitions in the snapshot and differential lists. --- src/export/CodeSystemExporter.ts | 17 +- src/export/InstanceExporter.ts | 4 +- src/export/StructureDefinitionExporter.ts | 9 +- src/export/ValueSetExporter.ts | 26 ++- src/fhirtypes/common.ts | 62 ++++++++ test/export/CodeSystemExporter.test.ts | 30 ++++ test/export/InstanceExporter.test.ts | 149 ++++++++++++++++++ .../StructureDefinitionExporter.test.ts | 66 ++++++++ test/export/ValueSetExporter.test.ts | 30 ++++ 9 files changed, 378 insertions(+), 15 deletions(-) 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;