Skip to content

Commit

Permalink
Allow ValueSet to reference contained inline CodeSystem
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mint-thompson committed Sep 24, 2024
1 parent 04b9d7b commit dbb94f0
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 5 deletions.
22 changes: 22 additions & 0 deletions src/export/ValueSetExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 4 additions & 2 deletions src/fhirtypes/ValueSet.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -83,6 +82,9 @@ export type ValueSetCompose = {

export type ValueSetComposeIncludeOrExclude = {
system?: string;
_system?: {
extension?: Extension[];
};
version?: string;
valueSet?: string[];
concept?: ValueSetComposeConcept[];
Expand Down
2 changes: 1 addition & 1 deletion src/import/FSHTank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
65 changes: 63 additions & 2 deletions test/export/FHIRExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
Expand Down
132 changes: 132 additions & 0 deletions test/export/ValueSetExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit dbb94f0

Please sign in to comment.