Skip to content

Commit aef0deb

Browse files
committed
Retain VS version in VS Include rules and improve fishForMetadata for FSH definitions
- VS include rules that reference a FSH-defined VS using a versioned reference will export the versioned canonical - fishing for metadata now returns more complete metadata for FSH definitions Fixes #1610
1 parent bbddab8 commit aef0deb

File tree

5 files changed

+291
-8
lines changed

5 files changed

+291
-8
lines changed

src/export/ValueSetExporter.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { FshValueSet, FshCode, ValueSetFilterValue, FshCodeSystem, Instance } fr
1010
import { logger } from '../utils/FSHLogger';
1111
import { ValueSetComposeError, InvalidUriError, MismatchedTypeError } from '../errors';
1212
import { InstanceExporter, Package } from '.';
13-
import { MasterFisher, Type, assembleFSHPath, resolveSoftIndexing } from '../utils';
13+
import {
14+
MasterFisher,
15+
Type,
16+
assembleFSHPath,
17+
fishForMetadataBestVersion,
18+
resolveSoftIndexing
19+
} from '../utils';
1420
import {
1521
CaretValueRule,
1622
ValueSetComponentRule,
@@ -134,7 +140,18 @@ export class ValueSetExporter {
134140
}
135141
if (component.from.valueSets) {
136142
composeElement.valueSet = component.from.valueSets.map(vs => {
137-
return this.fisher.fishForMetadata(vs, Type.ValueSet)?.url ?? vs;
143+
const vsMetadata = fishForMetadataBestVersion(
144+
this.fisher,
145+
vs,
146+
undefined,
147+
Type.ValueSet
148+
);
149+
if (vsMetadata && vsMetadata.url) {
150+
const version = vs.split('|').slice(1).join('|');
151+
return version.length ? `${vsMetadata.url}|${version}` : vsMetadata.url;
152+
} else {
153+
return vs;
154+
}
138155
});
139156
composeElement.valueSet = composeElement.valueSet.filter(vs => {
140157
if (vs == valueSet.url) {

src/import/FSHTank.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import flatMap from 'lodash/flatMap';
2727
import { getNonInstanceValueFromRules } from '../fshtypes/common';
2828
import { logger } from '../utils/FSHLogger';
2929

30+
const HL7_SD_BASE_URL = 'http://hl7.org/fhir/StructureDefinition/';
31+
3032
export class FSHTank implements Fishable {
3133
constructor(
3234
public readonly docs: FSHDocument[],
@@ -560,6 +562,7 @@ export class FSHTank implements Fishable {
560562
entity instanceof Resource
561563
) {
562564
meta.url = getUrlFromFshDefinition(entity, this.config.canonical);
565+
meta.version = getVersionFromFshDefinition(entity, this.config.version);
563566
meta.parent = entity.parent;
564567
meta.resourceType = 'StructureDefinition';
565568
const imposeProfiles = this.findExtensionValues(
@@ -576,24 +579,59 @@ export class FSHTank implements Fishable {
576579
// unless HL7 published them. In that case, the URL is relative to
577580
// http://hl7.org/fhir/StructureDefinition/.
578581
// Ref: https://chat.fhir.org/#narrow/stream/179177-conformance/topic/StructureDefinition.2Etype.20for.20Logical.20Models.2FCustom.20Resources/near/240488388
579-
const HL7_URL = 'http://hl7.org/fhir/StructureDefinition/';
580-
meta.sdType = meta.url.startsWith(HL7_URL) ? meta.url.slice(HL7_URL.length) : meta.url;
582+
meta.sdType = meta.url.startsWith(HL7_SD_BASE_URL)
583+
? meta.url.slice(HL7_SD_BASE_URL.length)
584+
: meta.url;
581585
meta.canBeTarget = this.hasLogicalCharacteristic(entity, 'can-be-target');
582586
meta.canBind = this.hasLogicalCharacteristic(entity, 'can-bind');
583587
}
584588
} else if (entity instanceof FshValueSet || entity instanceof FshCodeSystem) {
585589
meta.url = getUrlFromFshDefinition(entity, this.config.canonical);
590+
meta.version = getVersionFromFshDefinition(entity, this.config.version);
586591
if (entity instanceof FshValueSet) {
587592
meta.resourceType = 'ValueSet';
588593
} else {
589594
meta.resourceType = 'CodeSystem';
590595
}
591596
} else if (entity instanceof Instance) {
597+
meta.instanceUsage = entity.usage;
592598
const assignedUrl = getNonInstanceValueFromRules(entity, 'url', '', 'url');
593599
if (typeof assignedUrl === 'string') {
594600
meta.url = assignedUrl;
595601
}
596-
meta.instanceUsage = entity.usage;
602+
meta.version = getVersionFromFshDefinition(entity, this.config.version);
603+
if (entity.instanceOf === 'StructureDefinition') {
604+
meta.resourceType = 'StructureDefinition';
605+
const assignedBaseDefinition = getNonInstanceValueFromRules(
606+
entity,
607+
'baseDefinition',
608+
'',
609+
'baseDefinition'
610+
);
611+
if (typeof assignedBaseDefinition === 'string') {
612+
meta.parent = assignedBaseDefinition.startsWith(HL7_SD_BASE_URL)
613+
? assignedBaseDefinition.slice(HL7_SD_BASE_URL.length)
614+
: assignedBaseDefinition;
615+
}
616+
const assignedKind = getNonInstanceValueFromRules(entity, 'kind', '', 'kind');
617+
if (
618+
assignedKind instanceof FshCode &&
619+
assignedKind.code === 'logical' &&
620+
meta.url != null
621+
) {
622+
// Logical models should always use an absolute URL as their StructureDefinition.type
623+
// unless HL7 published them. In that case, the URL is relative to
624+
// http://hl7.org/fhir/StructureDefinition/.
625+
// Ref: https://chat.fhir.org/#narrow/stream/179177-conformance/topic/StructureDefinition.2Etype.20for.20Logical.20Models.2FCustom.20Resources/near/240488388
626+
meta.sdType = meta.url.startsWith(HL7_SD_BASE_URL)
627+
? meta.url.slice(HL7_SD_BASE_URL.length)
628+
: meta.url;
629+
}
630+
} else if (entity.instanceOf === 'ValueSet') {
631+
meta.resourceType = 'ValueSet';
632+
} else if (entity.instanceOf === 'CodeSystem') {
633+
meta.resourceType = 'CodeSystem';
634+
}
597635
}
598636
return meta;
599637
}

test/export/ValueSetExporter.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,43 @@ describe('ValueSetExporter', () => {
650650
});
651651
});
652652

653+
it('should export a value set that includes a component from a local value set with a version', () => {
654+
const valueSet = new FshValueSet('DinnerVS');
655+
const component = new ValueSetConceptComponentRule(true);
656+
component.from = {
657+
valueSets: ['http://food.org/food/ValueSet/hot-food|1.2.3']
658+
};
659+
valueSet.rules.push(component);
660+
doc.valueSets.set(valueSet.name, valueSet);
661+
const hotFoodVS = new FshValueSet('HotFoodVS');
662+
hotFoodVS.id = 'hot-food';
663+
const setVsUrlRule = new CaretValueRule('');
664+
setVsUrlRule.caretPath = 'url';
665+
setVsUrlRule.value = 'http://food.org/food/ValueSet/hot-food';
666+
hotFoodVS.rules.push(setVsUrlRule);
667+
const setVsVersionRule = new CaretValueRule('');
668+
setVsVersionRule.caretPath = 'version';
669+
setVsVersionRule.value = '1.2.3';
670+
hotFoodVS.rules.push(setVsVersionRule);
671+
doc.valueSets.set(hotFoodVS.name, hotFoodVS);
672+
const exported = exporter.export().valueSets;
673+
expect(exported.length).toBe(2);
674+
expect(exported[0]).toEqual({
675+
resourceType: 'ValueSet',
676+
id: 'DinnerVS',
677+
name: 'DinnerVS',
678+
url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS',
679+
status: 'draft',
680+
compose: {
681+
include: [
682+
{
683+
valueSet: ['http://food.org/food/ValueSet/hot-food|1.2.3']
684+
}
685+
]
686+
}
687+
});
688+
});
689+
653690
it('should export a value set that includes a component from a named value set', () => {
654691
const valueSet = new FshValueSet('DinnerVS');
655692
const component = new ValueSetConceptComponentRule(true);
@@ -685,6 +722,91 @@ describe('ValueSetExporter', () => {
685722
});
686723
});
687724

725+
it('should export a value set that includes a component from a named versioned value set', () => {
726+
const valueSet = new FshValueSet('DinnerVS');
727+
const component = new ValueSetConceptComponentRule(true);
728+
component.from = {
729+
valueSets: ['HotFoodVS|1.2.3', 'ColdFoodVS']
730+
};
731+
valueSet.rules.push(component);
732+
doc.valueSets.set(valueSet.name, valueSet);
733+
const hotFoodVS = new FshValueSet('HotFoodVS');
734+
hotFoodVS.id = 'hot-food';
735+
const setHotVsVersionRule = new CaretValueRule('');
736+
setHotVsVersionRule.caretPath = 'version';
737+
setHotVsVersionRule.value = '1.2.3';
738+
hotFoodVS.rules.push(setHotVsVersionRule);
739+
doc.valueSets.set(hotFoodVS.name, hotFoodVS);
740+
const coldFoodVS = new FshValueSet('ColdFoodVS');
741+
coldFoodVS.id = 'cold-food';
742+
doc.valueSets.set(coldFoodVS.name, coldFoodVS);
743+
const exported = exporter.export().valueSets;
744+
expect(exported.length).toBe(3);
745+
expect(exported[0]).toEqual({
746+
resourceType: 'ValueSet',
747+
id: 'DinnerVS',
748+
name: 'DinnerVS',
749+
url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS',
750+
status: 'draft',
751+
compose: {
752+
include: [
753+
{
754+
valueSet: [
755+
'http://hl7.org/fhir/us/minimal/ValueSet/hot-food|1.2.3',
756+
'http://hl7.org/fhir/us/minimal/ValueSet/cold-food'
757+
]
758+
}
759+
]
760+
}
761+
});
762+
expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
763+
expect(loggerSpy.getAllMessages('warn')).toHaveLength(0);
764+
});
765+
766+
it('should export a value set that includes a component from a named versioned value set and warn on version mismatch', () => {
767+
const valueSet = new FshValueSet('DinnerVS');
768+
const component = new ValueSetConceptComponentRule(true);
769+
component.from = {
770+
valueSets: ['HotFoodVS|4.5.6', 'ColdFoodVS']
771+
};
772+
valueSet.rules.push(component);
773+
doc.valueSets.set(valueSet.name, valueSet);
774+
const hotFoodVS = new FshValueSet('HotFoodVS');
775+
hotFoodVS.id = 'hot-food';
776+
const setHotVsVersionRule = new CaretValueRule('');
777+
setHotVsVersionRule.caretPath = 'version';
778+
setHotVsVersionRule.value = '1.2.3';
779+
hotFoodVS.rules.push(setHotVsVersionRule);
780+
doc.valueSets.set(hotFoodVS.name, hotFoodVS);
781+
const coldFoodVS = new FshValueSet('ColdFoodVS');
782+
coldFoodVS.id = 'cold-food';
783+
doc.valueSets.set(coldFoodVS.name, coldFoodVS);
784+
const exported = exporter.export().valueSets;
785+
expect(exported.length).toBe(3);
786+
expect(exported[0]).toEqual({
787+
resourceType: 'ValueSet',
788+
id: 'DinnerVS',
789+
name: 'DinnerVS',
790+
url: 'http://hl7.org/fhir/us/minimal/ValueSet/DinnerVS',
791+
status: 'draft',
792+
compose: {
793+
include: [
794+
{
795+
valueSet: [
796+
'http://hl7.org/fhir/us/minimal/ValueSet/hot-food|4.5.6',
797+
'http://hl7.org/fhir/us/minimal/ValueSet/cold-food'
798+
]
799+
}
800+
]
801+
}
802+
});
803+
expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
804+
expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
805+
expect(loggerSpy.getLastMessage('warn')).toBe(
806+
'HotFoodVS|4.5.6 was requested, but SUSHI found HotFoodVS|1.2.3'
807+
);
808+
});
809+
688810
// TODO: as part of a later task, confirm that this is in fact correct. it seems to be what the IG publisher expects,
689811
// but doesn't quite fit the spec for ValueSet.compose.include.valueSet
690812
it.skip('should export a value set that includes a component from a contained inline instance of value set', () => {

0 commit comments

Comments
 (0)