Skip to content

Commit f579da9

Browse files
authored
Do not inherit ED extensions that should not be inherited (#1604)
SUSHI already supported removing uninheritable SD extensions. Now we apply similar logic to remove uninheritable extensions from EDs. List of uninherited extensions derived from source code in IG Publisher: https://github.com/hapifhir/org.hl7.fhir.core/blob/ce719b040ae8661601084777556d4466efbf41fd/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java#L208-L223
1 parent 01f6705 commit f579da9

File tree

8 files changed

+2807
-42
lines changed

8 files changed

+2807
-42
lines changed

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)