UNPKG

fume-fhir-converter

Version:

FHIR-Utilized Mapping Engine - Community

925 lines (819 loc) 42.3 kB
/** * © 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) }); };