fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
925 lines (819 loc) • 42.3 kB
text/typescript
/**
* © Copyright Outburn Ltd. 2022-2024 All Rights Reserved
* Project name: FUME-COMMUNITY
*/
import jsonata from 'jsonata';
import { ILogger } from '../types';
import { IFhirPackageIndex } from './conformance';
import { getStructureDefinition as getRawDefinition } from './jsonataFunctions';
import { omitKeys } from './objectFunctions';
import parser from './parser';
import { endsWith, startsWith } from './stringFunctions';
const expressions = {
polyNameWithoutX: jsonata(`(
/* take last node name and remove the [x] part */
/* $polyId is the element id */
($polyId.$split('.')[-1]) ~> $substringBefore('[x]');
)`),
fixMonoPolySlicesIterator: jsonata(`
(
/* @param: $depth (num) */
/* @param: $diff (array of elements)*/
/* find all polymorphics with a single type in this depth level */
$monoPolyHeads := [$diff[$endsWith(id, '[x]') and $count(type) = 1 and $count($split(id,'.')) = $depth+1]];
/* if non found, return $diff unchanged */
$count($monoPolyHeads) = 0 ? $diff : (
/* for each monopoly head, find slice entries */
$monoPolySliceHeads := $monoPolyHeads#$i.(
$head := $;
$typeName := $head.type[0].code;
$titleCaseType := $toTitleCase($typeName);
$nodeName := $polyNameWithoutX($head.id);
$sliceName := $nodeName & $titleCaseType;
$sliceHead := $diff[id = $head.id & ':' & $sliceName];
$exists($sliceHead) ? {
'__head': $head,
'__headChildren': $diff[$startsWith(id, $head.id & '.')].$merge([$,{'__relativeId': $substringAfter(id, $head.id & '.')}]),
'__slice': $sliceHead,
'__sliceChildren': $diff[$startsWith(id, $sliceHead.id & '.')].$merge([$,{'__relativeId': $substringAfter(id, $sliceHead.id & '.')}])
}
);
/* if no slice entries found, return $diff unchanged */
$count($monoPolySliceHeads) = 0 ? $diff : (
/* create an array with corrected elements that can be used later to fix $diff */
$correctionArray := $monoPolySliceHeads.(
/* for each slice head: */
/* 1. merge into monopoly head, preserving monopoly id and removing slice attributes */
/* 2. merge children into monopoly children, preserving monopoly id's */
/* 3. fix remaining children id's */
$mergedHead := ($mergeElementDefinition([__head, __slice, {'id': __head.id}])) ~> $omitKeys(['slicing', 'sliceName']);
$headChildren := __headChildren;
$sliceChildren := __sliceChildren;
$headId := __head.id;
$sliceId := __slice.id;
$mergedHeadChildren := $headChildren.(
$headChild := $;
$sliceChild := $sliceChildren[__relativeId = $headChild.__relativeId];
$mergeElementDefinition(
[
$headChild,
$sliceChild,
{'id': $headChild.id, '__sliceChildId': $sliceChild.id}
]
)
);
$remainingSliceChildren := $sliceChildren[$not(__relativeId in $headChildren.__relativeId)].$merge([
$,
{
'id': $headId & '.' & __relativeId,
'__sliceChildId': id
}
]);
{
'__headId': $headId,
'__sliceId': $sliceId,
'__mergedHead': $mergedHead,
'__mergedChildren': [$mergedHeadChildren, $remainingSliceChildren]
}
);
/* then go through $diff, rearrange it and return it */
$headMap := $correctionArray{__headId: __mergedHead};
$headOmitMap := $correctionArray{__sliceId: {}};
$childMap := [($correctionArray.__mergedChildren)]{id: $};
$childOmitMap := [($correctionArray.__mergedChildren)]{__sliceChildId: {}};
$diff.(
/* check if in headMap, if so - return it */
$headOverride := $lookup($headMap, id);
$exists($headOverride) ? $headOverride : (
/* check if in headOmit, if so - omit it */
$headOmit := $lookup($headOmitMap, id);
$exists($headOmit) ? undefined : (
/* check in child map, if there - return it */
$childOverride := $lookup($childMap, id);
$exists($childOverride) ? $childOverride : (
/* check in child omit, if there - omit it */
$childOmit := $lookup($childOmitMap, id);
$exists($childOmit) ? undefined : (
/* element not affected - return unchanged */
$
)
)
)
)
)
)
)
)
`),
fixMonoPolySlicesDiff: jsonata(`
(
/* $def is the raw StructureDefinition resource */
$def.derivation = 'constraint' ? (
$originalDiff := $def.differential.element[];
$maxDepth := $max($originalDiff.($count($split(id, '.')))) - 1;
$newDiff := $reduce(
[1..$maxDepth],
$fixMonoPolySlicesIterator,
$originalDiff
);
$merge([$def, {'differential': {'element': [$newDiff]}}]);
) : (
/* derivation is not 'constraint', this is a base type or logical model. */
/* returning $def unchanged */
$def
);
)
`),
fixMonoPolySlicesSnap: jsonata(`
(
/* $def is the raw StructureDefinition resource */
$def.derivation = 'constraint' ? (
$originalSnap := $def.snapshot.element[];
$maxDepth := $max($originalSnap.($count($split(id, '.')))) - 1;
$newSnap := $reduce(
[1..$maxDepth],
$fixMonoPolySlicesIterator,
$originalSnap
);
$merge([$def, {'snapshot': {'element': [$newSnap]}}]);
) : (
/* derivation is not 'constraint', this is a base type or logical model. */
/* returning $def unchanged */
$def
);
)
`),
toNamedMonoPoly: jsonata(`
(
/* this function gets an element definition of a mono-poly element */
/* and returns its id in the shortcut (named type) form: */
/* valueCode, deceasedBoolean etc. */
$typeName := $element.type.code[0];
$titleCaseType := $uppercase($substring($typeName, 0, 1)) & $substring($typeName, 1);
$idWithoutX := $substring($element.id, 0, $length($element.id)-3);
$idWithoutX & $titleCaseType;
)
`),
polySliceToNamedMonoPoly: jsonata(`
(
/* take an element in the form Xx.value[x]:valueQuantity */
/* and return it's id in the named form: Xx.valueQuantity */
$endsWith($element.id, ':' & $element.sliceName) ? (
$nodes := $split($element.id, '.');
$pathNodes := $nodes#$i[$i < ($count($nodes)-1)];
$newIdNodes := [$pathNodes, $element.sliceName];
$join($newIdNodes, '.');
)
)
`),
toPolySlice: jsonata(`
(
/* this function gets an element definition of a mono-poly element */
/* and returns its id in the long (sliced) form: */
/* value[x]:valueCode, deceased[x]:deceasedBoolean etc. */
$namedPolyId := $toNamedMonoPoly($element);
$sliceName := $split($namedPolyId, '.')[-1];
$element.id & ':' & $sliceName;
)
`),
isMonoPolyMatch: jsonata(`
(
/* returns true if the diff element matches the snapshot one */
/* according to the mono-poly matching rules */
$isMonoPoly($snapElement) and (
$diffElement.id in [$toNamedMonoPoly($snapElement), $toPolySlice($snapElement)]
)
)
`),
polyToMonoPolies: jsonata(`
(
/* takes a single polymorphic element and returns an array of monopolies. */
/* each monopoly returned has only a single type */
$types := $polyElement.type;
$types@$thisType.(
/* for each type, return the element with this type only */
$merge([$polyElement, {'type': [$thisType]}])
)
)
`),
isExplicitSlice: jsonata(`
(
/* checks if a diff is an explicit slice entry on the snapshot element */
/* slice entries should have a sliceName and an id that ends with :sliceName */
$exists($snapElement) and $exists($diffElement) ? (
$startsWith($diffElement.id, $snapElement.id & ':')
and $substringAfter($diffElement.id, $snapElement.id & ':') = $diffElement.sliceName
)
)
`),
implicitToExplicitSlice: jsonata(`
(
/* checks if a diff is an implicit slice on the snapshot element */
/* and if it is, returns it with the correct id, type and sliceName. */
/* if it isn't, returns undefined */
/* this is only relevant for polymorphic snapshot elements */
$isPoly($snapElement) and $count($snapElement.type) > 1 ? (
$monopolies := $polyToMonoPolies($snapElement);
$monopolies@$this.(
$namedMonoPoly := $toNamedMonoPoly($this);
$diffElement.id = $namedMonoPoly ? (
$correctId := $toPolySlice($this);
$sliceName := $split($correctId, ':')[-1];
$correctionObj := {
'id': $correctId,
'type': [$this.type],
'sliceName': $sliceName,
'_diffId': $diffElement.id
};
$head := $omitKeys($snapElement, ['slicing']);
$merge([$head, $diffElement, $correctionObj])
)
)
)
)
`),
mergeElementDefinition: jsonata(`
(
/* first append all constraints */
$constraints := $arrayOfElements.constraint;
$constraintIds := $distinct($constraints.key);
$constraintArray := $constraintIds@$thisKey.(
$merge($constraints[key=$thisKey]);
);
$constraintsObj := {
'constraint': $count($constraintArray) > 0 ? [$constraintArray]
};
$conditions := $arrayOfElements.condition;
$conditionsObj := {
'condition': $count($conditions) > 0 ? [$distinct($conditions)]
};
/* then simply merge everything */
$res := $merge([$arrayOfElements, $conditionsObj, $constraintsObj]);
$res;
)
`),
fetchSliceEntries: jsonata(`
(
/* if there are slice entries for this element, return them */
/* this includes: */
/* 1. explicit slices */
/* 2. implicit slices on type($this) under polymorphics */
$slicingHead := $omitKeys($snapElement, ['slicing']);
$expSliceEntries := $diffArray[$isExplicitSlice($snapElement, $)];
$expSliceEntries := $expSliceEntries@$thisSlice.(
/* merge each slice entry with head element */
/* (but removing the 'slicing' attribute) */
/* and add the '_diffId' attribute */
$mergeElementDefinition([$slicingHead, $thisSlice, { '_diffId': $thisSlice.id }]);
);
$impSliceEntries := $diffArray@$this.(
$implicitToExplicitSlice($snapElement, $this)
);
[$expSliceEntries, $impSliceEntries];
)
`),
fixDiffIds: jsonata(`
(
$changedIds := $inputObject.wipSnapshot[id != _diffId].{
'id': id,
'_diffId': _diffId
};
$fixedDiff := $inputObject.wipDifferential@$thisDiff.(
/* check if this diff's id starts with one of the changed ones */
$idToChange := ($changedIds@$changed.(
$startsWith($thisDiff.id, $changed._diffId & '.') ? $changed;
))[-1];
$exists($idToChange) ? (
$merge([$thisDiff, {'id': $idToChange.id & $substringAfter($thisDiff.id, $idToChange._diffId)}]);
) : $thisDiff;
);
{
'wipDifferential': $fixedDiff,
'profileSnap': $inputObject.profileSnap,
'wipSnapshot': $inputObject.wipSnapshot
}
)
`),
fetchDiffElement: jsonata(`
(
/* if there's a diff that should be applied to this element - returns it */
$diffElement := ($diffArray[id = $snapElement.id or $isMonoPolyMatch($snapElement, $) or id = $polySliceToNamedMonoPoly($snapElement)]);
$allSlicesId := $getLastAllSlicesId($snapElement.id);
$diffAllSlicesElement := $diffArray[id = $allSlicesId];
$diffAllSlicesElement := $exists($diffAllSlicesElement) ? (
$omitKeys($diffAllSlicesElement, ['id', 'path'])
);
/* return the diff element without the id and path, but add _diffId attribute */
/* to retain the relationship to the original diff element id */
$res := $diffElement@$this.(
$newThis := $merge([$omitKeys($this, ['id', 'path']), {'_diffId': $this.id}]);
$exists($diffAllSlicesElement) ? $mergeElementDefinition([$diffAllSlicesElement, $newThis]) : $newThis;
);
$res;
)
`),
removeSliceFromId: jsonata(`
(
$exists($element.sliceName) ? (
$endsWith($element.id, ':' & $element.sliceName) ? (
$substring($element.id, 0, $length($element.id) - $length($element.sliceName) - 1)
) : $element.id
) : $element.id
)
`),
getLastAllSlicesId: jsonata(`
(
/* if element id has a slice in the path, remove the last slice */
/* to get the id of the same element in the "all slices" section */
$parts := $split($elementId, ':');
$count($parts) > 1 ? (
$lastPart := $parts[-1];
$unsliced := $substringAfter($lastPart,'.');
$partsBefore := $join($parts#$i[$i < ($count($parts) - 1)], ':');
$partsBefore & '.' & $unsliced;
);
)
`),
changeRoot: jsonata(`
(
/* gets an element list and changes the root id and path */
/* it also merges the root element with the new one */
$oldRootElement := $elementArray[0];
$elementArray@$thisElement.(
$thisElement.id = $oldRootElement.id ? (
/* first element */
$merge([$thisElement, $newRootElement])
) : (
/* children */
$newIdAndPath := {
'id': $newRootElement.id & '.' & $substringAfter($thisElement.id, $oldRootElement.id & '.'),
'path': $newRootElement.path & '.' & $substringAfter($thisElement.path, $oldRootElement.path & '.')
};
$merge([$thisElement, $newIdAndPath])
)
)
)
`),
getSliceEntriesId: jsonata(`
(
/* from an array of elements, return those that are slice entries */
$res := $elements[$exists(sliceName) and $endsWith(id, ':' & sliceName)].id;
[$res];
)
`),
noDuplicateElements: jsonata(`
(
$res := $count(($elementArray{id: $count($distinct($))}.*)[$>1])=0;
$res = false ? (
(($elementArray{id: $count($distinct($))} ~> $spread)[*>1] ~> $keys)@$id.(
$e := $elementArray[id = $id];
$info($e.id);
$info($e);
)
);
$res;
)
`),
repositionSlices: jsonata(`
(
/* @params: $wipSnapshot, $originalBase, $level */
/* reorder slice entries so original ones that exist in the snapshot come first */
$assert($wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 2)');
/* get all id's of original slices of this level from original snapshot */
$originalSlicesId := ($getSliceEntriesId($originalBase))[$getLevel($) = $level];
/* get all non-slice entries of this level from the wip snapshot */
$nonSliceEntries := $wipSnapshot[$getLevel($) = $level and ($exists(sliceName) = false or $endsWith(id, '[x]'))];
$res := ($wipSnapshot[
/* filter wip snapshot so only elements that aren't slices or aren't at this level remain */
$getLevel($) != $level
or id in $nonSliceEntries.id
])@$this.(
/* for each element return itself plus its slices in the right order */
$getLevel($this) = $level ? (
/* first slices are those that exist in original snapshot */
$firstBlockOfSlices := $wipSnapshot[$getLevel($) = $level and $startsWith(id, $this.id & ':') and id in $originalSlicesId];
/* then the slices introduced by the diff */
$secondBlockOfSlices := $wipSnapshot[
$getLevel($) = $level
and $startsWith(id, $this.id & ':')
and $endsWith(id, ':' & sliceName)
and $not(id in $firstBlockOfSlices.id)
];
[$this, $firstBlockOfSlices, $secondBlockOfSlices]
) : (
/* if it's not at this level return $this only */
$this
)
);
$res;
)
`),
generateSnapshot: jsonata(`
(
$generateSnapshot := function($profileId) {(
$applyDifferential := function($base, $diff, $profileSnapshot){(
/* applies all diffs on the base */
/* optional: profileSnapshot - allows bringing implicit rules that */
/* the target profile has in the snapshot but not in the diff */
/* e.g. bp profile's constraint on the types of value[x] */
$assert($count($base) > 0 and $count($diff) > 0, 'To apply differential both base and diff arrays must have entries.');
$assert($base ~> $noDuplicateElements,'Duplicate elements! (tag: 0)');
$applyDiffLevel := function($inputObject, $level, $originalBase){
/* for each element in the requested level in the snapshot, apply diff */
(
$assert($inputObject.wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 1)');
/* re-create wip array with ancestors only, merged with any applicable diffs */
$wipSnapshot := ($inputObject.wipSnapshot[$getLevel($) <= $level])@$snapElement.(
/* check if element's depth matches the level */
$currentDepth := $getLevel($snapElement);
$currentDepth = $level ? (
/* if it matches, fetch diff element */
$diffElement := $fetchDiffElement($snapElement, $inputObject.wipDifferential);
/* apply implicit constraints on poly element that come from the original profile's snapshot. */
/* this is mainly done because of hl7's broken vitalsigns derived profiles (e.g. bp) */
$profileSnapElement := (
/* if original profile def has a snapshot, take the type of */
/* the corresponding element (only if it's polymorphic). */
$exists($inputObject.profileSnap) and $isPoly($snapElement) ? (
$inputObject.profileSnap[id=$snapElement.id].{ 'type': type }
)
);
/* merge all into a single element definition */
$applied := $mergeElementDefinition([$snapElement, $profileSnapElement, $diffElement]);
/* get slices of this element from diff */
$slices := $fetchSliceEntries($applied, $inputObject.wipDifferential);
/* filter out slices that already exist in snapshot */
$slices := $slices[$not(id in $inputObject.wipSnapshot.id)];
/* return applied element followed by slices */
$res := [$applied, $slices];
$res;
) : (
/* elements not in the requested level are returned unchanged */
$snapElement
);
);
/* remove applied diffs from diff array */
$remainingDiff := $inputObject.wipDifferential[(id in $wipSnapshot._diffId) = false];
/* reposition slices - parent's first, new slices after */
$assert($wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 3)');
$wipSnapshot := $repositionSlices($wipSnapshot, $originalBase, $level);
$assert($wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 4)');
/* put children back in the wip array */
$snapChildren := $inputObject.wipSnapshot[$getLevel($) > $level];
$wipSnapshot := (
$wipSnapshot@$this.(
$getLevel($this) < $level ? (
/* if level of this parent is less then current level (grand-ancsestor), return it as-is */
$this
) :
/* if it's at the current level, return it plus any applicable children */
(
/* if it has children in the snapshot, return those */
$thisSnapChildren := ($count($snapChildren) > 0 ? $snapChildren[$startsWith(id, $this.id & '.')]);
$count($thisSnapChildren) > 0 ? (
/* but apply profile diff, if there is a profile on $this that has been introduced by diff */
$thisDiff := $inputObject.wipDifferential[id=$this.id];
$thisProfileUrl := $count($thisDiff.type) = 1 and $exists($thisDiff.type.profile) ? $thisDiff.type[0].profile[0];
$thisProfileDef := $exists($thisProfileDef) ? $getStructureDefinition($thisProfileUrl);
$thisProfileDiff := $exists($thisProfileDef) ? $thisProfileDef.differential.element;
$newRootElement := $merge([$this, {'id': $thisProfileDef.type, 'path': $thisProfileDef.type}]);
$thisSnapChildren := $exists($thisProfileDiff) ? (
$base := $changeRoot([$this, $thisSnapChildren], $newRootElement);
$assert($base ~> $noDuplicateElements,'Duplicate elements! (tag: 5)');
($applyDifferential($base, $thisProfileDiff) ~> $changeRoot($this))[id != $this.id];
) : $thisSnapChildren;
[$this, $thisSnapChildren]
) : (
/* if it doesn't have children in the snapshot: */
/* A. fetch children of all slices from snapshot. if there are any, */
/* it means we are in a new slice, and there are rules that apply on all slices' children. */
/* + Another option that should behave just like "all slices" is contenReference */
$allSlicesId := (
$unslicedId := $removeSliceFromId($this);
$this.id = $unslicedId ? (
/* it's not a slice. check for contentReference */
$exists($this.contentReference) ? (
/* there is a contentReference, return the referenced id (skip the #) */
$this.contentReference ~> $substring(1);
) : (
/* no contentReference, return $this.id */
$this.id;
)
) : (
/* it's a slice, return all slices' id */
$unslicedId;
)
);
$allSlicesHead := $inputObject.wipSnapshot[id=$allSlicesId];
$thisAllSlicesChildren := $exists($allSlicesId) ? [$inputObject.wipSnapshot[$startsWith(id, $allSlicesId & '.')]] : [];
/* B. expansion of $this by type/profile - the type/profile should now be up-to-date, and will define the children */
/* If $this is poly, expand using the definition of Element (will only add id and extension).*/
$thisProfile := (
$count($this.type) > 1 ? (
/* poly */
'Element'
) : (
/* mono or contentReference */
$exists($this.contentReference) ? $this.contentReference : (
$exists($this.type[0].profile) ? (
$this.type[0].profile[0]
) : $this.type[0].code
)
)
);
$diffChildren := (
$remainingDiff[$getLevel($) > $level and ($startsWith(id, $this.id & '.') or ($exists($allSlicesId) and $startsWith(id, $allSlicesId & '.')))]
);
$childrenToAdd := $count($diffChildren) > 0 ? (
$count($thisAllSlicesChildren) = 0 and $exists($thisProfile) and $not($startsWith($thisProfile, 'http://hl7.org/fhirpath/System.')) ? (
/* when A is empty, B should be done using simple snapshot generation. */
$thisProfileSnapshot := $getSnapshot($thisProfile).snapshot.element;
($changeRoot($thisProfileSnapshot, $this))[id != $this.id];
) : (
/* when A is not empty:*/
/* if $this is poly or a Backbone, take A without any changes.*/
$thisProfile in ['Element', 'BackboneElement'] ? (
($changeRoot([$allSlicesHead,$thisAllSlicesChildren], $this))[id != $this.id]
) : (
/* $this is mono */
/* if A's head is poly: */
$count($allSlicesHead.type) > 1 ? (
/* add the specialization of the type and apply the diff of the profile */
$exists($thisProfile) ? (
$baseType := $this.type.code;
/* get the base datatype differential */
$baseTypeDifferential := ($getStructureDefinition($baseType).differential.element);
/* change its root to $this's id */
$baseTypeDifferential := $changeRoot($baseTypeDifferential, $this);
/* change all slice's children's root to this's id */
$thisAllSlicesChildrenNewRoot := $changeRoot([$allSlicesHead,$thisAllSlicesChildren], $this);
/* append so all slices' children come first and then the children of the base type */
$baseBeforeRootChange := [$thisAllSlicesChildrenNewRoot, $baseTypeDifferential[id != $this.id]];
$base := $changeRoot($baseBeforeRootChange, {'id': $baseType, 'path': $baseType});
$assert($base ~> $noDuplicateElements,'Duplicate elements! (tag: 6)');
($applyDifferential($base, $getStructureDefinition($thisProfile).differential.element) ~> $changeRoot($this))[id != $this.id];
) : (
($changeRoot([$allSlicesHead,$thisAllSlicesChildren], $this))[id != $this.id]
)
) : (
/* if A's head is mono */
$exists($thisProfile) and $not($thisProfile.$startsWith($, 'http://hl7.org/fhirpath/System.')) ? (
/* apply the diff of the profile */
$thisProfileDef := $getStructureDefinition($thisProfile);
$thisProfileDef.derivation = 'constraint' ? (
$thisProfileDiff := $thisProfileDef.differential.element;
$base := ([
$this,
$changeRoot(
[
$allSlicesHead,
$thisAllSlicesChildren
],
$this
)[id != $this.id]
]~>$changeRoot({'id': $thisProfileDef.type, 'path': $thisProfileDef.type}));
$assert($base ~> $noDuplicateElements,'Duplicate elements! (tag: 7)');
($applyDifferential($base, $thisProfileDiff)~>$changeRoot($this))[id != $this.id];
) : (
$changeRoot(
[
$allSlicesHead,
$thisAllSlicesChildren
],
$this
)[id != $this.id]
)
) : (
($changeRoot([$allSlicesHead, $thisAllSlicesChildren], $this))[id != $this.id];
)
)
/* TODO: when applying profile diffs, it should be done serially with the whole chain of profiles */
)
)
) : [];
[$this, $childrenToAdd];
)
)
)
);
$assert($wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 8)');
/* generate an updated wip object */
$updatedObj := {
'wipDifferential': $remainingDiff,
'profileSnap': $inputObject.profileSnap,
'wipSnapshot': $wipSnapshot
};
/* fix changed id's in diff */
$updatedObj := $fixDiffIds($updatedObj);
/* check if any diffs were applied in this iteration */
$count($updatedObj.wipDifferential) = $count($inputObject.wipDifferential) ? (
/* no diffs applied, return results */
$updatedObj
) : (
/* diffs were applied, run this level again */
$applyDiffLevel($updatedObj, $level, $originalBase);
)
)
};
$iterate := function($obj, $step, $maxDepth, $base) {(
$assert($obj.wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 9)');
$nextObj := $applyDiffLevel($obj, $step, $base);
$assert($nextObj.wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 10)');
$nextObj.($step >= $maxDepth and ($count(wipDifferential) in [0, $count($obj.wipDifferential)] or $exists(wipDifferential)=false)) ? (
/* when all diffs were applied, or no change in diffs and we have reached max depth */
/* throw warning if not all diffs could be applid */
$remainingDiffs := $nextObj.wipDifferential[id in $diff.id];
$count($remainingDiffs) > 0 ? (
$warning('Snapshot generation incomplete, there are differentials that cannot be applied! (' & $join($remainingDiffs.id, ', ') & '). ProfileId: "' & $profileId & '"');
);
/* and return snapshot */
$nextObj.wipSnapshot
) : (
/* there are still diffs to apply, */
/* iterate to the next depth level */
$nextStep := $step + 1;
$assert($nextObj.wipSnapshot ~> $noDuplicateElements,'Duplicate elements! (tag: 11)');
$iterate($nextObj, $nextStep, $maxDepth, $base);
)
)};
$assert($base ~> $noDuplicateElements,'Duplicate elements! (tag: 12)');
$wipObject := {
'wipSnapshot': $base,
'wipDifferential': $diff,
'profileSnap': $profileSnapshot
};
$maxDepth := $max($diff.($getLevel($)));
$res := $iterate($wipObject, 1, $maxDepth, $base);
/* remove contentReference if there's a type */
$res := $res ~> |$[$exists(type) and $exists(contentReference)]|{},['contentReference']|;
)};
$profileDef := $getStructureDefinition($profileId);
$profileDef.derivation = 'constraint' ? (
/* it's a profile */
$parentUrl := $profileDef.baseDefinition;
$parentDef := $getStructureDefinition($parentUrl);
$parentDef := $getSnapshot($parentUrl);
$info("Generating snapshot for '" & $profileId & "'");
$profileSnapshot := (
/* if it's an HL7 profile, use its existing snapshot for hints about implicit type constraints */
$startsWith($profileDef.url, 'http://hl7.org/fhir/') ?
$profileDef.snapshot.element
);
$snapshot := $applyDifferential($parentDef.snapshot.element, $profileDef.differential.element, $profileSnapshot);
$merge([$profileDef, { 'snapshot': { 'element': [$snapshot] } }]);
) : (
/* it's not a profile - it's a base type - return definition as-is */
$profileDef;
)
)};
$ss := $generateSnapshot($profileId);
$ss := (/* remove internal proccessing tags from results */
$ss
~> |*.element|{},['_diffId', '__fromDefinition', '__rootChildren', '__sliceChildren']|
~> $fixMonoPolySlicesSnap);
$merge([$ss, {'snapshot': { 'element': [$distinct($ss.snapshot.element)]}}]);
)
`)
};
const toTitleCase = (str: string) => {
const firstChar: string = str.charAt(0).toUpperCase();
const rest: string = str.slice(1);
const res: string = firstChar + rest;
return res;
};
const polyNameWithoutX = async (polyId: string) => {
const res: string = await expressions.polyNameWithoutX.evaluate({}, { polyId });
return res;
};
const fixMonoPolySlicesIterator = async (diff: any[], depth: number, logger: ILogger) => {
/* fix a single depth level - used in the $reduce function */
return await expressions.fixMonoPolySlicesIterator.evaluate({}, { diff, depth, endsWith, polyNameWithoutX, startsWith, mergeElementDefinition, toTitleCase, omitKeys, info: (msg: any) => logger.info(msg) });
};
const fixMonoPolySlicesDiff = async (def: any, logger: ILogger) => {
/* for every polymorphoic with a single type, */
/* merge the diff head element with the slice element */
return await expressions.fixMonoPolySlicesDiff.evaluate({}, { def, fixMonoPolySlicesIterator: async (diff: any[], depth: number) => await fixMonoPolySlicesIterator(diff, depth, logger) });
};
const fixMonoPolySlicesSnap = async (def: any, logger: ILogger) => {
/* for every polymorphoic with a single type, */
/* merge the diff head element with the slice element */
return await expressions.fixMonoPolySlicesSnap.evaluate({}, { def, fixMonoPolySlicesIterator: async (diff: any[], depth: number) => await fixMonoPolySlicesIterator(diff, depth, logger) });
};
const getStructureDefinition = async (definitionId: string, fhirVersion: string, fhirPackageIndex: IFhirPackageIndex, logger: ILogger) => {
const rawDef = getRawDefinition(definitionId, fhirVersion, fhirPackageIndex, logger);
return await fixMonoPolySlicesDiff(rawDef, logger);
};
const isPoly = (element: any) => {
/* check if an element is polymorphic at its base */
return element?.id.endsWith('[x]');
};
const polySliceToNamedMonoPoly = async (element: any) => {
return await expressions.polySliceToNamedMonoPoly.evaluate({}, { element, endsWith });
};
const isMonoPoly = (element: any) => {
/* this function checks if this element is a polymorphic */
/* base element that has has been limited to a single type */
return isPoly(element) && element?.type?.length === 1;
};
const toNamedMonoPoly = async (element: any) => {
/* this function gets an element definition of a mono-poly element */
/* and returns its id in the shortcut (named type) form: */
/* valueCode, deceasedBoolean etc. */
return await expressions.toNamedMonoPoly.evaluate({}, { element });
};
const toPolySlice = async (element: any) => {
/* this function gets an element definition of a mono-poly element */
/* and returns its id in the long (sliced) form: */
/* value[x]:valueCode, deceased[x]:deceasedBoolean etc. */
return await expressions.toPolySlice.evaluate({}, { element, toNamedMonoPoly });
};
const isMonoPolyMatch = async (snapElement: any, diffElement: any) => {
/* returns true if the diff element matches the snapshot one */
/* according to the mono-poly matching rules */
return await expressions.isMonoPolyMatch.evaluate({}, { snapElement, diffElement, isMonoPoly, toNamedMonoPoly, toPolySlice });
};
const polyToMonoPolies = async (polyElement: any) => {
/* takes a single polymorphic element and returns an array of monopolies. */
/* each monopoly returned has only a single type */
return await expressions.polyToMonoPolies.evaluate({}, { polyElement });
};
const isExplicitSlice = async (snapElement: any, diffElement: any) => {
/* checks if a diff is an explicit slice entry on the snapshot element */
/* slice entries should have a sliceName and an id that ends with :sliceName */
return await expressions.isExplicitSlice.evaluate({}, { snapElement, diffElement, startsWith });
};
const implicitToExplicitSlice = async (snapElement: any, diffElement: any, logger: ILogger) => {
/* checks if a diff is an implicit slice on the snapshot element */
/* and if it is, returns it with the correct id, type and sliceName. */
/* if it isn't, returns undefined */
/* this is only relevant for polymorphic snapshot elements */
return await expressions.implicitToExplicitSlice.evaluate({}, { snapElement, diffElement, isPoly, polyToMonoPolies, toNamedMonoPoly, toPolySlice, omitKeys, info: (msg: any) => logger.info(msg) });
};
const mergeElementDefinition = async (arrayOfElements: any | any[], logger: ILogger) => {
return await expressions.mergeElementDefinition.evaluate({}, { arrayOfElements, info: (msg: any) => logger.info(msg) });
};
const fetchSliceEntries = async (snapElement: any, diffArray: any | any[], logger: ILogger) => {
/* if there are slice entries for this element, return them */
/* this includes: */
/* 1. explicit slices */
/* 2. implicit slices on type($this) under polymorphics */
const res = await expressions.fetchSliceEntries.evaluate({}, { snapElement, diffArray, omitKeys, isExplicitSlice, mergeElementDefinition, implicitToExplicitSlice: async (snapElement: any, diffElement: any) => await implicitToExplicitSlice(snapElement, diffElement, logger), info: (msg: any) => logger.info(msg) });
return res;
};
const fixDiffIds = async (inputObject: any) => {
return await expressions.fixDiffIds.evaluate({}, { inputObject, startsWith });
};
const fetchDiffElement = async (snapElement: any, diffArray: any | any[], logger: ILogger) => {
return await expressions.fetchDiffElement.evaluate({}, { snapElement, diffArray, isMonoPolyMatch, omitKeys, info: (msg: any) => logger.info(msg), getLastAllSlicesId, mergeElementDefinition, polySliceToNamedMonoPoly });
};
const getLevel = (element: string | any) => {
const elementId: string = typeof element === 'string' ? element : element?.id;
if (typeof elementId === 'undefined') {
return undefined;
} else {
return elementId.split('.').length;
}
};
const removeSliceFromId = async (element: any) => {
return await expressions.removeSliceFromId.evaluate({}, { element, endsWith });
};
const getLastAllSlicesId = async (elementId: string) => {
return await expressions.getLastAllSlicesId.evaluate({}, { elementId });
};
const changeRoot = async (elementArray: any | any[], newRootElement: any) => {
return await expressions.changeRoot.evaluate({}, { elementArray, newRootElement });
};
const noDuplicateElements = async (elementArray: any | any[], logger: ILogger) => {
return await expressions.noDuplicateElements.evaluate({}, { elementArray, info: (msg: any) => logger.info(msg) });
};
const repositionSlices = async (wipSnapshot: any | any[], originalBase: any | any[], level: number, logger: ILogger) => {
return await expressions.repositionSlices.evaluate({}, { wipSnapshot, originalBase, level, noDuplicateElements: async (elementArray: any | any[]) => await noDuplicateElements(elementArray, logger), getSliceEntriesId, getLevel, startsWith, endsWith, info: (msg: any) => logger.info(msg) });
};
const getSliceEntriesId = async (elements: any | any[]) => {
const res = await expressions.getSliceEntriesId.evaluate({}, { elements, endsWith });
return res;
};
export const generateSnapshot = async (profileId: string, fhirVersion: string, fhirPackageIndex: IFhirPackageIndex, logger: ILogger) => {
return await expressions.generateSnapshot.evaluate({},
{
profileId,
getSliceEntriesId,
info: (msg: any) => logger.info(msg),
warning: (msg: any) => logger.warn(msg),
startsWith,
endsWith,
fetchDiffElement: async (snapElement: any, diffArray: any | any[]) => await fetchDiffElement(snapElement, diffArray, logger),
isPoly,
mergeElementDefinition,
fetchSliceEntries: async (snapElement: any, diffArray: any | any[]) => await fetchSliceEntries(snapElement, diffArray, logger),
getStructureDefinition: async (profileId: string) => await getStructureDefinition(profileId, fhirVersion, fhirPackageIndex, logger),
changeRoot,
removeSliceFromId,
getLevel,
fixDiffIds,
noDuplicateElements: async (elementArray: any | any[]) => await noDuplicateElements(elementArray, logger),
repositionSlices: async (wipSnapshot: any | any[], originalBase: any | any[], level: number) => await repositionSlices(wipSnapshot, originalBase, level, logger),
getSnapshot: async (profileId: string) => await parser.getSnapshot(profileId, fhirVersion, fhirPackageIndex, logger),
fixMonoPolySlicesSnap: async (def: any) => await fixMonoPolySlicesSnap(def, logger)
});
};