From dbb94f0e539beefd8262ef33b5c7266776e8d3b1 Mon Sep 17 00:00:00 2001 From: Mint Thompson Date: Tue, 24 Sep 2024 16:12:57 -0400 Subject: [PATCH] Allow ValueSet to reference contained inline CodeSystem If a ValueSetComponentRule specifies a system, check if that system is present in the list of contained resources. If so, add the valueset-system extension to the system, using the relative reference as the extension's value. If the system is not present in the list of contained resources, and the system is an inline Instance, log an error and do not add the component to the ValueSet. Fishing in the tank for a CodeSystem will now return inline instances of CodeSystem. This matches the operation of fishing in the tank for a ValueSet. Add _system.extension to the type definition for elements of a ValueSet's include and exclude lists. Use the Extension fhirtype as part of this definition. Replace the Extension fshtype with the Extension fhirtype in other parts of the ValueSet definition. --- src/export/ValueSetExporter.ts | 22 +++++ src/fhirtypes/ValueSet.ts | 6 +- src/import/FSHTank.ts | 2 +- test/export/FHIRExporter.test.ts | 65 ++++++++++++- test/export/ValueSetExporter.test.ts | 132 +++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 5 deletions(-) diff --git a/src/export/ValueSetExporter.ts b/src/export/ValueSetExporter.ts index 19c1b776e..947eebec0 100644 --- a/src/export/ValueSetExporter.ts +++ b/src/export/ValueSetExporter.ts @@ -76,6 +76,28 @@ export class ValueSetExporter { .replace(/^([^|]+)/, csMetadata?.url ?? '$1') .split('|'); composeElement.system = foundSystem[0]; + // if the code system is also a contained resource, add the special extension + // if it's not a contained resource, and the system we found is an inline instance, that's a problem + const containedSystem = valueSet.contained?.find((resource: any) => { + return resource?.id === csMetadata.id && resource.resourceType === 'CodeSystem'; + }); + if (containedSystem != null) { + composeElement._system = { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: `#${csMetadata.id}` + } + ] + }; + } else if (csMetadata?.instanceUsage === 'Inline') { + logger.error( + `Can not reference CodeSystem ${component.from.system}: this CodeSystem is an inline instance, but it is not present in the list of contained resources.`, + component.sourceInfo + ); + return; + } + // if the rule specified a version, use that version. composeElement.version = systemParts.slice(1).join('|') || undefined; if (!isUri(composeElement.system)) { diff --git a/src/fhirtypes/ValueSet.ts b/src/fhirtypes/ValueSet.ts index 57305788f..db7fe71b3 100644 --- a/src/fhirtypes/ValueSet.ts +++ b/src/fhirtypes/ValueSet.ts @@ -1,7 +1,6 @@ import sanitize from 'sanitize-filename'; import { Meta } from './specialTypes'; -import { Extension } from '../fshtypes'; -import { Narrative, Resource, Identifier, CodeableConcept, Coding } from './dataTypes'; +import { Narrative, Resource, Identifier, CodeableConcept, Coding, Extension } from './dataTypes'; import { ContactDetail, UsageContext } from './metaDataTypes'; import { HasName, HasId } from './mixins'; import { applyMixins } from '../utils/Mixin'; @@ -83,6 +82,9 @@ export type ValueSetCompose = { export type ValueSetComposeIncludeOrExclude = { system?: string; + _system?: { + extension?: Extension[]; + }; version?: string; valueSet?: string[]; concept?: ValueSetComposeConcept[]; diff --git a/src/import/FSHTank.ts b/src/import/FSHTank.ts index 8755d67e6..8f9b1ffe7 100644 --- a/src/import/FSHTank.ts +++ b/src/import/FSHTank.ts @@ -356,7 +356,7 @@ export class FSHTank implements Fishable { result = this.getAllInstances().find( csInstance => csInstance?.instanceOf === 'CodeSystem' && - csInstance?.usage === 'Definition' && + (csInstance?.usage === 'Definition' || csInstance?.usage === 'Inline') && (csInstance?.name === base || csInstance.id === base || getUrlFromFshDefinition(csInstance, this.config.canonical) === base || diff --git a/test/export/FHIRExporter.test.ts b/test/export/FHIRExporter.test.ts index af036d362..676feee9e 100644 --- a/test/export/FHIRExporter.test.ts +++ b/test/export/FHIRExporter.test.ts @@ -4,8 +4,13 @@ import { exportFHIR, Package, FHIRExporter } from '../../src/export'; import { FSHTank, FSHDocument } from '../../src/import'; import { FHIRDefinitions } from '../../src/fhirdefs'; import { minimalConfig } from '../utils/minimalConfig'; -import { FshValueSet, Instance, Profile } from '../../src/fshtypes'; -import { AssignmentRule, BindingRule, CaretValueRule } from '../../src/fshtypes/rules'; +import { FshCodeSystem, FshValueSet, Instance, Profile } from '../../src/fshtypes'; +import { + AssignmentRule, + BindingRule, + CaretValueRule, + ValueSetConceptComponentRule +} from '../../src/fshtypes/rules'; import { TestFisher, loggerSpy } from '../testhelpers'; describe('FHIRExporter', () => { @@ -507,6 +512,62 @@ describe('FHIRExporter', () => { }); }); + it('should export a value set that includes a component from a contained FSH code system and add the valueset-system extension', () => { + // CodeSystem: FoodCS + // Id: food + const foodCS = new FshCodeSystem('FoodCS'); + foodCS.id = 'food'; + doc.codeSystems.set(foodCS.name, foodCS); + // ValueSet: DinnerVS + // * ^contained[0] = FoodCS + // * include codes from system food + const valueSet = new FshValueSet('DinnerVS'); + const containedCS = new CaretValueRule(''); + containedCS.caretPath = 'contained[0]'; + containedCS.value = 'FoodCS'; + containedCS.isInstance = true; + const component = new ValueSetConceptComponentRule(true); + component.from = { system: 'FoodCS' }; + valueSet.rules.push(containedCS, component); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export(); + expect(exported.valueSets.length).toBe(1); + expect(exported.valueSets[0]).toEqual({ + resourceType: 'ValueSet', + name: 'DinnerVS', + id: 'DinnerVS', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS', + contained: [ + { + content: 'complete', + id: 'food', + name: 'FoodCS', + resourceType: 'CodeSystem', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/CodeSystem/food' + } + ], + compose: { + include: [ + { + system: 'http://hl7.org/fhir/us/minimal/CodeSystem/food', + _system: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: '#food' + } + ] + } + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + it('should log a message when trying to assign a value that is numeric and refers to an Instance, but both types are wrong', () => { // Profile: MyObservation // Parent: Observation diff --git a/test/export/ValueSetExporter.test.ts b/test/export/ValueSetExporter.test.ts index 78baa7b51..e7796552b 100644 --- a/test/export/ValueSetExporter.test.ts +++ b/test/export/ValueSetExporter.test.ts @@ -380,6 +380,138 @@ describe('ValueSetExporter', () => { }); }); + it('should export a value set that includes a component from a contained inline instance of code system and add the valueset-system extension', () => { + // Instance: example-codesystem + // InstanceOf: CodeSystem + // Usage: #inline + // * url = "http://example.org/codesystem" + // * version = "1.0.0" + // * status = #active + // * content = #complete + const inlineCodeSystem = new Instance('example-codesystem'); + inlineCodeSystem.instanceOf = 'CodeSystem'; + inlineCodeSystem.usage = 'Inline'; + const urlRule = new AssignmentRule('url'); + urlRule.value = 'http://example.org/codesystem'; + const versionRule = new AssignmentRule('version'); + versionRule.value = '1.0.0'; + const statusRule = new AssignmentRule('status'); + statusRule.value = new FshCode('active'); + const contentRule = new AssignmentRule('content'); + contentRule.value = new FshCode('complete'); + inlineCodeSystem.rules.push(urlRule, versionRule, statusRule, contentRule); + doc.instances.set(inlineCodeSystem.name, inlineCodeSystem); + // ValueSet: ExampleValueset + // Id: example-valueset + // * ^contained = example-codesystem + // * include codes from system example-codesystem + const valueSet = new FshValueSet('ExampleValueset'); + valueSet.id = 'example-valueset'; + const containedSystem = new CaretValueRule(''); + containedSystem.caretPath = 'contained'; + containedSystem.value = 'example-codesystem'; + containedSystem.isInstance = true; + const component = new ValueSetConceptComponentRule(true); + component.from = { system: 'example-codesystem' }; + valueSet.rules.push(containedSystem, component); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'ExampleValueset', + id: 'example-valueset', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset', + contained: [ + { + resourceType: 'CodeSystem', + id: 'example-codesystem', + url: 'http://example.org/codesystem', + version: '1.0.0', + status: 'active', + content: 'complete' + } + ], + compose: { + include: [ + { + system: 'http://example.org/codesystem', + _system: { + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/valueset-system', + valueCanonical: '#example-codesystem' + } + ] + } + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(0); + }); + + it('should log an error and not add the component when attempting to reference an inline instance of code system that is not contained', () => { + // Instance: example-codesystem + // InstanceOf: CodeSystem + // Usage: #inline + // * url = "http://example.org/codesystem" + // * version = "1.0.0" + // * status = #active + // * content = #complete + const inlineCodeSystem = new Instance('example-codesystem'); + inlineCodeSystem.instanceOf = 'CodeSystem'; + inlineCodeSystem.usage = 'Inline'; + const urlRule = new AssignmentRule('url'); + urlRule.value = 'http://example.org/codesystem'; + const versionRule = new AssignmentRule('version'); + versionRule.value = '1.0.0'; + const statusRule = new AssignmentRule('status'); + statusRule.value = new FshCode('active'); + const contentRule = new AssignmentRule('content'); + contentRule.value = new FshCode('complete'); + inlineCodeSystem.rules.push(urlRule, versionRule, statusRule, contentRule); + doc.instances.set(inlineCodeSystem.name, inlineCodeSystem); + // ValueSet: ExampleValueset + // Id: example-valueset + // * include codes from system example-codesystem + // * include codes from system http://hl7.org/fhir/us/minimal/CodeSystem/food + const valueSet = new FshValueSet('ExampleValueset'); + valueSet.id = 'example-valueset'; + const exampleComponent = new ValueSetConceptComponentRule(true) + .withFile('ExampleVS.fsh') + .withLocation([5, 3, 5, 48]); + exampleComponent.from = { system: 'example-codesystem' }; + const foodComponent = new ValueSetConceptComponentRule(true); + foodComponent.from = { system: 'http://hl7.org/fhir/us/minimal/CodeSystem/food' }; + valueSet.rules.push(exampleComponent, foodComponent); + doc.valueSets.set(valueSet.name, valueSet); + + const exported = exporter.export().valueSets; + expect(exported.length).toBe(1); + expect(exported[0]).toEqual({ + resourceType: 'ValueSet', + name: 'ExampleValueset', + id: 'example-valueset', + status: 'draft', + url: 'http://hl7.org/fhir/us/minimal/ValueSet/example-valueset', + compose: { + include: [ + { + system: 'http://hl7.org/fhir/us/minimal/CodeSystem/food' + } + ] + } + }); + expect(loggerSpy.getAllMessages('error')).toHaveLength(1); + expect(loggerSpy.getLastMessage('error')).toMatch( + /Can not reference CodeSystem example-codesystem/s + ); + expect(loggerSpy.getLastMessage('error')).toMatch(/File: ExampleVS\.fsh.*Line: 5\D*/s); + }); + it('should export a value set that includes a component from a value set', () => { const valueSet = new FshValueSet('DinnerVS'); const component = new ValueSetConceptComponentRule(true);