Skip to content

Commit 4905d2a

Browse files
committed
Do not inherit ED extensions that should not be inherited
SUSHI already supported removing uninheritable SD extensions. Now we apply similar logic to remove uninheritable extensions from EDs.
1 parent 014181f commit 4905d2a

8 files changed

Lines changed: 2807 additions & 42 deletions

src/export/StructureDefinitionExporter.ts

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,23 @@ import {
6868
TYPE_CHARACTERISTICS_CODE,
6969
TYPE_CHARACTERISTICS_EXTENSION,
7070
LOGICAL_TARGET_EXTENSION,
71-
checkForMultipleChoice
71+
checkForMultipleChoice,
72+
removeMatchingExtensions
7273
} from '../fhirtypes/common';
7374
import { Package } from './Package';
7475
import { isUri } from 'valid-url';
7576
import chalk from 'chalk';
7677
import { getValueFromRules, findAssignmentByPath } from '../fshtypes/common';
7778

78-
// Extensions that should not be inherited by derived profiles
79+
// Extensions that should not be inherited by derived profiles.
7980
// See: https://jira.hl7.org/browse/FHIR-28441
80-
const UNINHERITED_EXTENSIONS = [
81+
const UNINHERITED_SD_EXTENSIONS = [
8182
'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm',
8283
'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm-no-warnings',
83-
'http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy',
8484
'http://hl7.org/fhir/StructureDefinition/structuredefinition-interface',
8585
'http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version',
8686
'http://hl7.org/fhir/StructureDefinition/structuredefinition-applicable-version',
8787
'http://hl7.org/fhir/StructureDefinition/structuredefinition-category',
88-
'http://hl7.org/fhir/StructureDefinition/structuredefinition-codegen-super',
8988
'http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category',
9089
'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status',
9190
'http://hl7.org/fhir/StructureDefinition/structuredefinition-summary',
@@ -354,18 +353,7 @@ export class StructureDefinitionExporter implements Fishable {
354353
delete structDef.language;
355354
delete structDef.text;
356355
delete structDef.contained;
357-
structDef.extension = structDef.extension?.filter(e => !UNINHERITED_EXTENSIONS.includes(e.url));
358-
if (!structDef.extension?.length) {
359-
// for consistency, delete rather than leaving null-valued
360-
delete structDef.extension;
361-
}
362-
structDef.modifierExtension = structDef.modifierExtension?.filter(
363-
e => !UNINHERITED_EXTENSIONS.includes(e.url)
364-
);
365-
if (!structDef.modifierExtension?.length) {
366-
// for consistency, delete rather than leaving null-valued
367-
delete structDef.modifierExtension;
368-
}
356+
removeMatchingExtensions(structDef, UNINHERITED_SD_EXTENSIONS, true);
369357
// keep url since it was already defined when the StructureDefinition was initially created
370358
delete structDef.identifier;
371359

@@ -663,23 +651,10 @@ export class StructureDefinitionExporter implements Fishable {
663651
structDef: StructureDefinition,
664652
fshDefinition: Profile | Extension | Logical | Resource
665653
): void {
666-
// In some cases, uninherited extensions may be on the root element, so filter them out
667-
structDef.elements[0].extension = structDef.elements[0].extension?.filter(
668-
e => !UNINHERITED_EXTENSIONS.includes(e.url)
669-
);
670-
if (!structDef.elements[0].extension?.length) {
671-
// for consistency, delete rather than leaving null-valued
672-
delete structDef.elements[0].extension;
673-
}
674-
structDef.elements[0].modifierExtension = structDef.elements[0].modifierExtension?.filter(
675-
e => !UNINHERITED_EXTENSIONS.includes(e.url)
676-
);
677-
if (!structDef.elements[0].modifierExtension?.length) {
678-
// for consistency, delete rather than leaving null-valued
679-
delete structDef.elements[0].modifierExtension;
680-
}
654+
// In some cases, uninherited extensions may be on the elements, so filter them out
655+
structDef.elements.forEach(el => el.removeUninheritedExtensions());
681656
structDef.captureOriginalMapping();
682-
structDef.elements[0].captureOriginal();
657+
structDef.captureOriginalElements();
683658

684659
// The remaining logic only pertains to logicals and resources, so return here if otherwise
685660
if (fshDefinition instanceof Profile || fshDefinition instanceof Extension) {

src/fhirtypes/ElementDefinition.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
CodeableReference,
1818
Coding,
1919
Element,
20+
Extension,
2021
Quantity,
2122
Ratio,
2223
Reference
@@ -67,7 +68,8 @@ import {
6768
splitOnPathPeriods,
6869
isReferenceType,
6970
isModifierExtension,
70-
getArrayIndex
71+
getArrayIndex,
72+
removeMatchingExtensions
7173
} from './common';
7274
import {
7375
Fishable,
@@ -81,6 +83,54 @@ import { InstanceDefinition } from './InstanceDefinition';
8183
import { idRegex } from './primitiveTypes';
8284
import sax from 'sax';
8385

86+
// ED extensions that should not be inherited by derived profiles
87+
// See: https://github.com/hapifhir/org.hl7.fhir.core/blob/master/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java
88+
const UNINHERITED_ED_EXTENSIONS = [
89+
'http://hl7.org/fhir/tools/StructureDefinition/binding-definition',
90+
'http://hl7.org/fhir/tools/StructureDefinition/no-binding',
91+
'http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding',
92+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status',
93+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-category',
94+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm',
95+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-implements',
96+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name',
97+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category',
98+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg',
99+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version',
100+
'http://hl7.org/fhir/tools/StructureDefinition/obligation-profile',
101+
'http://hl7.org/fhir/StructureDefinition/obligation-profile',
102+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status-reason',
103+
'http://hl7.org/fhir/StructureDefinition/structuredefinition-summary'
104+
];
105+
106+
// ED.type extensions that should not be inherited by derived profiles
107+
// See: https://chat.fhir.org/#narrow/channel/179239-tooling/topic/Forge.20added.20extension.20explicit-type-name/near/328203575
108+
// NOTE: Commented out until the extension list and approach are confirmed (IG Publisher does not seem to do this yet)
109+
// const UNINHERITED_ED_TYPE_EXTENSIONS = [
110+
// 'http://hl7.org/fhir/StructureDefinition/elementdefinition-pattern',
111+
// 'http://hl7.org/fhir/StructureDefinition/regex',
112+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy'
113+
// ];
114+
115+
// ED.binding extensions that should not be inherited by derived profiles
116+
// See: https://chat.fhir.org/#narrow/channel/179239-tooling/topic/Forge.20added.20extension.20explicit-type-name/near/328203575
117+
// NOTE: Commented out until the extension list and approach are confirmed (IG Publisher does not seem to do this yet)
118+
// const UNINHERITED_ED_BINDING_EXTENSIONS = [
119+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-category',
120+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg',
121+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-normative-version',
122+
// 'http://hl7.org/fhir/build/StructureDefinition/summary',
123+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status',
124+
// 'http://hl7.org/fhir/build/StructureDefinition/notes',
125+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm',
126+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-summary',
127+
// 'http://hl7.org/fhir/build/StructureDefinition/introduction',
128+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-type-characteristics',
129+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-security-category',
130+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-implements',
131+
// 'http://hl7.org/fhir/StructureDefinition/structuredefinition-interface'
132+
// ];
133+
84134
const PROFILE_ELEMENT_EXTENSION =
85135
'http://hl7.org/fhir/StructureDefinition/elementdefinition-profile-element';
86136

@@ -2739,6 +2789,7 @@ export class ElementDefinition {
27392789
const eClone = e.clone();
27402790
eClone.id = eClone.id.replace(def.pathType, `${this.id}`);
27412791
eClone.structDef = this.structDef;
2792+
eClone.removeUninheritedExtensions();
27422793
// Capture the original so that diffs only show what changed *after* unfolding
27432794
eClone.captureOriginal();
27442795
return eClone;
@@ -2772,6 +2823,7 @@ export class ElementDefinition {
27722823
const eClone = e.clone(shouldCaptureOriginal);
27732824
eClone.id = eClone.id.replace(targetElement.id, this.id);
27742825
eClone.structDef = this.structDef;
2826+
eClone.removeUninheritedExtensions();
27752827
if (shouldCaptureOriginal) {
27762828
eClone.captureOriginal();
27772829
}
@@ -2868,6 +2920,7 @@ export class ElementDefinition {
28682920
const eClone = e.clone();
28692921
eClone.id = eClone.id.replace(commonAncestor.pathType, `${this.id}`);
28702922
eClone.structDef = this.structDef;
2923+
eClone.removeUninheritedExtensions();
28712924
// Capture the original so that diffs only show what changed *after* unfolding
28722925
eClone.captureOriginal();
28732926
return eClone;
@@ -2881,6 +2934,20 @@ export class ElementDefinition {
28812934
return [];
28822935
}
28832936

2937+
removeUninheritedExtensions(): void {
2938+
removeMatchingExtensions(this, UNINHERITED_ED_EXTENSIONS, true);
2939+
2940+
// NOTE: Commented out until the type and binding extensions list -- as well as this approach -- are confirmed
2941+
// removeMatchingExtensions(this.binding ?? {}, UNINHERITED_ED_BINDING_EXTENSIONS, false);
2942+
// this.type?.forEach(t => {
2943+
// removeMatchingExtensions(t, UNINHERITED_ED_TYPE_EXTENSIONS, false);
2944+
// t?._profile?.forEach(p => removeMatchingExtensions(p, UNINHERITED_ED_TYPE_EXTENSIONS, false));
2945+
// t?._targetProfile?.forEach(tp =>
2946+
// removeMatchingExtensions(tp, UNINHERITED_ED_TYPE_EXTENSIONS, false)
2947+
// );
2948+
// });
2949+
}
2950+
28842951
/**
28852952
* Sets up slicings on an element by adding or modifying the element's `slicing`. If a matching slicing discriminator
28862953
* already exists, it will be used
@@ -3175,6 +3242,7 @@ export type ElementDefinitionConstraint = {
31753242
};
31763243

31773244
export type ElementDefinitionBinding = {
3245+
extension?: Extension[];
31783246
strength: ElementDefinitionBindingStrength;
31793247
description?: string;
31803248
valueSet?: string;

src/fhirtypes/common.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
import { isEmpty, cloneDeep, upperFirst, remove, isEqual, zip, isObjectLike, pull } from 'lodash';
1+
import {
2+
isEmpty,
3+
cloneDeep,
4+
upperFirst,
5+
remove,
6+
isEqual,
7+
zip,
8+
isObjectLike,
9+
pull,
10+
pullAllWith
11+
} from 'lodash';
212
import {
313
StructureDefinition,
414
PathPart,
515
ElementDefinition,
616
InstanceDefinition,
717
ValueSet,
818
CodeSystem,
9-
CodeSystemConcept
19+
CodeSystemConcept,
20+
Extension as FhirExtension
1021
} from '.';
1122
import {
1223
AssignmentRule,
@@ -1120,6 +1131,26 @@ export function getSliceName(pathPart: PathPart): string {
11201131
return nonNumericBrackets.join('/');
11211132
}
11221133

1134+
/**
1135+
*
1136+
* @param {any} object - an object containing extensions and potentially modifierExtensions
1137+
* @param {string[]} urls - the list of URLs matching extensions that should be removed
1138+
* @param {boolean} checkModifiers - whether or not modifierExtensions should be checked
1139+
*/
1140+
export function removeMatchingExtensions(object: any, urls: string[], checkModifiers: boolean) {
1141+
if (object != null) {
1142+
const props = checkModifiers ? ['extension', 'modifierExtension'] : ['extension'];
1143+
props.forEach(ext => {
1144+
if (object[ext]?.length) {
1145+
pullAllWith(object[ext], urls, (ext: FhirExtension, url: string) => ext.url === url);
1146+
if (object[ext].length === 0) {
1147+
delete object[ext];
1148+
}
1149+
}
1150+
});
1151+
}
1152+
}
1153+
11231154
/**
11241155
* Replaces fields in an object that match a certain condition
11251156
* @param { {[key: string]: any} } object - The object to replace fields on

test/export/StructureDefinitionExporter.test.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ describe('StructureDefinitionExporter R4', () => {
964964
it('should only inherit inheritable extensions for a profile', () => {
965965
const parent = new Profile('FooParent');
966966
parent.parent = 'Observation';
967-
// Set a few uninheritable extensions
967+
// Set a few uninheritable extensions directly on the SD
968968
const fmmRule = new CaretValueRule('');
969969
fmmRule.caretPath =
970970
'extension[http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm].valueInteger';
@@ -973,7 +973,7 @@ describe('StructureDefinitionExporter R4', () => {
973973
wgRule.caretPath =
974974
'extension[http://hl7.org/fhir/StructureDefinition/structuredefinition-wg].valueCode';
975975
wgRule.value = new FshCode('cds');
976-
// Set a few inheritable extensions
976+
// Set a few inheritable extensions directly on the SD
977977
const ancestorRule = new CaretValueRule('');
978978
ancestorRule.caretPath =
979979
'extension[http://hl7.org/fhir/StructureDefinition/structuredefinition-ancestor].valueUri';
@@ -982,7 +982,25 @@ describe('StructureDefinitionExporter R4', () => {
982982
xmlNoOrderRule.caretPath =
983983
'extension[http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-no-order].valueBoolean';
984984
xmlNoOrderRule.value = true;
985-
parent.rules.push(fmmRule, wgRule, ancestorRule, xmlNoOrderRule);
985+
// Set an inheritable extension on the Observation.code element
986+
const codeQuestionRule = new CaretValueRule('code');
987+
codeQuestionRule.caretPath =
988+
'extension[http://hl7.org/fhir/StructureDefinition/elementdefinition-question].valueString';
989+
codeQuestionRule.value = 'What is the value?';
990+
// Set an uninheritable extension on the Observation.code element
991+
const codeFmmRule = new CaretValueRule('code');
992+
codeFmmRule.caretPath =
993+
'extension[http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status].valueCode';
994+
codeFmmRule.value = new FshCode('draft');
995+
996+
parent.rules.push(
997+
fmmRule,
998+
wgRule,
999+
ancestorRule,
1000+
xmlNoOrderRule,
1001+
codeQuestionRule,
1002+
codeFmmRule
1003+
);
9861004
doc.profiles.set(parent.name, parent);
9871005
exporter.exportStructDef(parent);
9881006

@@ -994,7 +1012,7 @@ describe('StructureDefinitionExporter R4', () => {
9941012
const exportedParent = pkg.profiles.find(p => p.id === 'FooParent');
9951013
expect(exportedParent).toBeDefined();
9961014
// The parent should have all the extensions it defined
997-
expect(exportedParent?.extension).toEqual([
1015+
expect(exportedParent.extension).toEqual([
9981016
{ url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm', valueInteger: 2 },
9991017
{ url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg', valueCode: 'cds' },
10001018
{
@@ -1006,15 +1024,26 @@ describe('StructureDefinitionExporter R4', () => {
10061024
valueBoolean: true
10071025
}
10081026
]);
1027+
const parentCodeElement = exportedParent.elements?.find(el => el.id === 'Observation.code');
1028+
expect(parentCodeElement?.extension).toEqual([
1029+
{
1030+
url: 'http://hl7.org/fhir/StructureDefinition/elementdefinition-question',
1031+
valueString: 'What is the value?'
1032+
},
1033+
{
1034+
url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status',
1035+
valueCode: 'draft'
1036+
}
1037+
]);
10091038

10101039
const exported = pkg.profiles.find(p => p.id === 'Foo');
10111040
expect(exported).toBeDefined();
10121041
// The following extensions should be stripped out as uninherited extensions:
10131042
// { url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm', valueInteger: 2 },
10141043
// { url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-wg', valueCode: 'cds' },
10151044
//
1016-
// BUT the following should extensions should remain:
1017-
expect(exported?.extension).toEqual([
1045+
// BUT the following extensions should remain:
1046+
expect(exported.extension).toEqual([
10181047
{
10191048
url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-ancestor',
10201049
valueUri: 'http://example.org/some/ancestor'
@@ -1024,6 +1053,17 @@ describe('StructureDefinitionExporter R4', () => {
10241053
valueBoolean: true
10251054
}
10261055
]);
1056+
const exportedCodeElement = exported.elements?.find(el => el.id === 'Observation.code');
1057+
// The following extension should be stripped out as uninherited extension:
1058+
// { url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status', valueCode: 'draft' },
1059+
//
1060+
// BUT the following extension should remain:
1061+
expect(exportedCodeElement?.extension).toEqual([
1062+
{
1063+
url: 'http://hl7.org/fhir/StructureDefinition/elementdefinition-question',
1064+
valueString: 'What is the value?'
1065+
}
1066+
]);
10271067
});
10281068

10291069
it('should not overwrite metadata that is not given for a profile', () => {

0 commit comments

Comments
 (0)