Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace type.aggregation on ElementDefinition when modified #1433

Merged
merged 8 commits into from
Mar 29, 2024
65 changes: 63 additions & 2 deletions src/fhirtypes/ElementDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'lodash';
import { minify } from 'html-minifier-terser';
import { isUri } from 'valid-url';
import { StructureDefinition } from './StructureDefinition';
import { PathPart, StructureDefinition } from './StructureDefinition';
import {
CodeableConcept,
CodeableReference,
Expand Down Expand Up @@ -66,7 +66,8 @@ import {
setPropertyOnDefinitionInstance,
splitOnPathPeriods,
isReferenceType,
isModifierExtension
isModifierExtension,
getArrayIndex
} from './common';
import {
Fishable,
Expand Down Expand Up @@ -284,13 +285,15 @@ export class ElementDefinition {
structDef: StructureDefinition;
private _original: ElementDefinition;
private _edStructureDefinition: StructureDefinition;
private _replacementProps: string[][];

/**
* Constructs a new ElementDefinition with the given ID.
* @param {string} id - the ID of the ElementDefinition
*/
constructor(id = '') {
this.id = id;
this._replacementProps = cloneDeep(REPLACEMENT_PROPS);
}

get id(): string {
Expand Down Expand Up @@ -472,6 +475,54 @@ export class ElementDefinition {
this._original = undefined;
}

clearOriginalProperty(pathParts: PathPart[]): void {
let currentOriginalElement: any = this._original;
// eslint-disable-next-line
let currentActualElement: any = this;
jafeltra marked this conversation as resolved.
Show resolved Hide resolved
const clearPath = this.calculateClearPath(pathParts);
for (const [i, pathPart] of clearPath.entries()) {
if (i < clearPath.length - 1) {
const key = pathPart.primitive ? `_${pathPart.base}` : pathPart.base;
currentOriginalElement = currentOriginalElement?.[key];
currentActualElement = currentActualElement?.[key];
const currentIndex = getArrayIndex(pathPart);
if (currentIndex != null) {
if (Array.isArray(currentOriginalElement)) {
currentOriginalElement = currentOriginalElement[currentIndex];
}
if (Array.isArray(currentActualElement)) {
currentActualElement = currentActualElement[currentIndex];
}
}
} else {
if (currentOriginalElement?.[pathPart.base] != null) {
currentOriginalElement[pathPart.base] = undefined;
currentActualElement[pathPart.base] = undefined;
}
if (pathPart.primitive && currentOriginalElement?.[`_${pathPart.base}`] != null) {
currentOriginalElement[`_${pathPart.base}`] = undefined;
currentActualElement[`_${pathPart.base}`] = undefined;
}
}
}
}

private calculateClearPath(pathParts: PathPart[]): PathPart[] {
const replacementIndex = this._replacementProps.findIndex(
replacementPath =>
replacementPath.length <= pathParts.length &&
replacementPath.every((rp, index) => rp === pathParts[index].base)
);
if (replacementIndex >= 0) {
// Build the clearPath from the actual pathParts, since we want to know about primitive types.
const clearPath = pathParts.slice(0, this._replacementProps[replacementIndex].length);
// each replacement property only has to get cleared once, so remove it from the list.
this._replacementProps.splice(replacementIndex, 1);
return clearPath;
}
return [];
}

/**
* Determines if the state of the current element differs from the stored "original".
* @returns {boolean} true if the state of the current element differs from the stored "original", false otherwise
Expand Down Expand Up @@ -3136,3 +3187,13 @@ const PROPS_AND_UNDERPROPS: string[] = PROPS.reduce((collect: string[], prop) =>
* See http://hl7.org/fhir/elementdefinition.html#interpretation.
*/
const ADDITIVE_PROPS = ['mapping', 'constraint'];

/**
* These list properties are replaced in child profiles. If they are modified
* in a profile, the snapshot should contain only entries in that profile,
* and not in the parent profile. Each property is given as a list of
* path parts.
cmoesel marked this conversation as resolved.
Show resolved Hide resolved
* For more context and a specific example, see this Zulip thread:
* https://chat.fhir.org/#narrow/stream/215610-shorthand/topic/restricting.20aggregation.20type/near/413120070
*/
const REPLACEMENT_PROPS = [['type', 'aggregation']];
3 changes: 3 additions & 0 deletions src/fhirtypes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export function setPropertyOnDefinitionInstance(
): void {
const instanceSD = instance.getOwnStructureDefinition(fisher);
const { assignedValue, pathParts } = instanceSD.validateValueAtPath(path, value, fisher);
if (instance instanceof ElementDefinition) {
instance.clearOriginalProperty(pathParts);
}
if (!(instance instanceof CodeSystem || instance instanceof ValueSet)) {
const knownSlices = determineKnownSlices(instanceSD, new Map([[path, { pathParts }]]), fisher);
setImpliedPropertiesOnInstance(instance, instanceSD, [path], [], fisher, knownSlices);
Expand Down
223 changes: 223 additions & 0 deletions test/export/StructureDefinitionExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8189,6 +8189,229 @@ describe('StructureDefinitionExporter R4', () => {
expect(ed.type[0]).toEqual(expectedType);
expect(loggerSpy.getAllMessages()).toHaveLength(0);
});

it('should apply CaretValueRules on the aggregation of a type and replace the parent values', () => {
// Profile: ParentObservation
// Parent: Observation
// * component ^type.aggregation[0] = #contained
// * component ^type.aggregation[1] = #referenced
const parentObservation = new Profile('ParentObservation');
parentObservation.parent = 'Observation';
const parentAggregationContained = new CaretValueRule('component');
parentAggregationContained.caretPath = 'type.aggregation[0]';
parentAggregationContained.value = new FshCode('contained');
const parentAggregationReferenced = new CaretValueRule('component');
parentAggregationReferenced.caretPath = 'type.aggregation[1]';
parentAggregationReferenced.value = new FshCode('referenced');
parentObservation.rules.push(parentAggregationContained, parentAggregationReferenced);
doc.profiles.set(parentObservation.name, parentObservation);
// Profile: ChildObservation
// Parent: ParentObservation
// * component ^type.aggregation[0] = #contained
const childObservation = new Profile('ChildObservation');
childObservation.parent = 'ParentObservation';
const childAggregationContained = new CaretValueRule('component');
childAggregationContained.caretPath = 'type.aggregation[0]';
childAggregationContained.value = new FshCode('contained');
childObservation.rules.push(childAggregationContained);
doc.profiles.set(childObservation.name, childObservation);

exporter.export();
const parentSd = pkg.profiles[0];
const parentComponent = parentSd.elements.find(el => el.id === 'Observation.component');
expect(parentComponent.type[0].aggregation).toEqual(['contained', 'referenced']);
const childSd = pkg.profiles[1];
const childComponent = childSd.elements.find(el => el.id === 'Observation.component');
expect(childComponent.type[0].aggregation).toEqual(['contained']);
expect(childComponent.calculateDiff().type[0].aggregation).toEqual(['contained']);
});

it('should apply CaretValueRules on elements within the aggregation of a type and replace the parent values', () => {
// Profile: ParentObservation
// Parent: Observation
// * component ^type.aggregation[0] = #contained
// * component ^type.aggregation[1] = #referenced
// * component ^type.aggregation[1].extension[0].url = "http://example.org"
// * component ^type.aggregation[1].extension[0].valueString = "parent value"
const parentObservation = new Profile('ParentObservation');
parentObservation.parent = 'Observation';
const parentAggregationContained = new CaretValueRule('component');
parentAggregationContained.caretPath = 'type.aggregation[0]';
parentAggregationContained.value = new FshCode('contained');
const parentAggregationReferenced = new CaretValueRule('component');
parentAggregationReferenced.caretPath = 'type.aggregation[1]';
parentAggregationReferenced.value = new FshCode('referenced');
const parentExtensionUrl = new CaretValueRule('component');
parentExtensionUrl.caretPath = 'type.aggregation[1].extension[0].url';
parentExtensionUrl.value = 'http://example.org';
const parentExtensionValue = new CaretValueRule('component');
parentExtensionValue.caretPath = 'type.aggregation[1].extension[0].valueString';
parentExtensionValue.value = 'parent value';
parentObservation.rules.push(
parentAggregationContained,
parentAggregationReferenced,
parentExtensionUrl,
parentExtensionValue
);
doc.profiles.set(parentObservation.name, parentObservation);
// Profile: ChildObservation
// Parent: ParentObservation
// * component ^type.aggregation[0].extension[0].url = "http://example.org"
// * component ^type.aggregation[0].extension[0].valueString = "child value"
// * component ^type.aggregation[0] = #contained
// * component ^type.aggregation[1] = #bundled
const childObservation = new Profile('ChildObservation');
childObservation.parent = 'ParentObservation';
const childExtensionUrl = new CaretValueRule('component');
childExtensionUrl.caretPath = 'type.aggregation[0].extension[0].url';
childExtensionUrl.value = 'http://example.org';
const childExtensionValue = new CaretValueRule('component');
childExtensionValue.caretPath = 'type.aggregation[0].extension[0].valueString';
childExtensionValue.value = 'child value';
const childAggregationContained = new CaretValueRule('component');
childAggregationContained.caretPath = 'type.aggregation[0]';
childAggregationContained.value = new FshCode('contained');
const childAggregationBundled = new CaretValueRule('component');
childAggregationBundled.caretPath = 'type.aggregation[1]';
childAggregationBundled.value = new FshCode('bundled');
childObservation.rules.push(
childExtensionUrl,
childExtensionValue,
childAggregationContained,
childAggregationBundled
);
doc.profiles.set(childObservation.name, childObservation);

exporter.export();
const parentSd = pkg.profiles[0];
const parentComponent = parentSd.elements.find(el => el.id === 'Observation.component');
expect(parentComponent.type[0].aggregation).toEqual(['contained', 'referenced']);
expect(parentComponent.type[0]._aggregation).toEqual([
null,
{
extension: [
{
url: 'http://example.org',
valueString: 'parent value'
}
]
}
]);
const childSd = pkg.profiles[1];
const childComponent = childSd.elements.find(el => el.id === 'Observation.component');
expect(childComponent.type[0].aggregation).toEqual(['contained', 'bundled']);
expect(childComponent.type[0]._aggregation).toEqual([
{
extension: [
{
url: 'http://example.org',
valueString: 'child value'
}
]
},
null
]);
const childComponentDiff = childComponent.calculateDiff();
expect(childComponentDiff.type[0].aggregation).toEqual(['contained', 'bundled']);
expect(childComponentDiff.type[0]._aggregation).toEqual([
{
extension: [
{
url: 'http://example.org',
valueString: 'child value'
}
]
},
null
]);
});

it('should apply CaretValueRules on elements within the aggregation of a type and replace the children of parent values when there is no parent value', () => {
// Profile: ParentObservation
// Parent: Observation
// * component ^type.aggregation[1].extension[0].url = "http://example.org"
// * component ^type.aggregation[1].extension[0].valueString = "parent value"
const parentObservation = new Profile('ParentObservation');
parentObservation.parent = 'Observation';
const parentExtensionUrl = new CaretValueRule('component');
parentExtensionUrl.caretPath = 'type.aggregation[1].extension[0].url';
parentExtensionUrl.value = 'http://example.org';
const parentExtensionValue = new CaretValueRule('component');
parentExtensionValue.caretPath = 'type.aggregation[1].extension[0].valueString';
parentExtensionValue.value = 'parent value';
parentObservation.rules.push(parentExtensionUrl, parentExtensionValue);
doc.profiles.set(parentObservation.name, parentObservation);
// Profile: ChildObservation
// Parent: ParentObservation
// * component ^type.aggregation[0].extension[0].url = "http://example.org"
// * component ^type.aggregation[0].extension[0].valueString = "child value"
// * component ^type.aggregation[0] = #contained
// * component ^type.aggregation[1] = #bundled
const childObservation = new Profile('ChildObservation');
childObservation.parent = 'ParentObservation';
const childExtensionUrl = new CaretValueRule('component');
childExtensionUrl.caretPath = 'type.aggregation[0].extension[0].url';
childExtensionUrl.value = 'http://example.org';
const childExtensionValue = new CaretValueRule('component');
childExtensionValue.caretPath = 'type.aggregation[0].extension[0].valueString';
childExtensionValue.value = 'child value';
const childAggregationContained = new CaretValueRule('component');
childAggregationContained.caretPath = 'type.aggregation[0]';
childAggregationContained.value = new FshCode('contained');
const childAggregationBundled = new CaretValueRule('component');
childAggregationBundled.caretPath = 'type.aggregation[1]';
childAggregationBundled.value = new FshCode('bundled');
childObservation.rules.push(
childExtensionUrl,
childExtensionValue,
childAggregationContained,
childAggregationBundled
);
doc.profiles.set(childObservation.name, childObservation);

exporter.export();
const parentSd = pkg.profiles[0];
const parentComponent = parentSd.elements.find(el => el.id === 'Observation.component');
expect(parentComponent.type[0].aggregation).toBeUndefined();
expect(parentComponent.type[0]._aggregation).toEqual([
null,
{
extension: [
{
url: 'http://example.org',
valueString: 'parent value'
}
]
}
]);
const childSd = pkg.profiles[1];
const childComponent = childSd.elements.find(el => el.id === 'Observation.component');
expect(childComponent.type[0].aggregation).toEqual(['contained', 'bundled']);
expect(childComponent.type[0]._aggregation).toEqual([
{
extension: [
{
url: 'http://example.org',
valueString: 'child value'
}
]
},
null
]);
const childComponentDiff = childComponent.calculateDiff();
expect(childComponentDiff.type[0].aggregation).toEqual(['contained', 'bundled']);
expect(childComponentDiff.type[0]._aggregation).toEqual([
{
extension: [
{
url: 'http://example.org',
valueString: 'child value'
}
]
},
null
]);
});
});

describe('#ObeysRule', () => {
Expand Down
Loading