fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
455 lines • 26.9 kB
JavaScript
;
/**
* © Copyright Outburn Ltd. 2022-2024 All Rights Reserved
* Project name: FUME-COMMUNITY
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toJsonataString = void 0;
const tslib_1 = require("tslib");
const config_1 = tslib_1.__importDefault(require("../../config"));
const conformance_1 = require("../conformance");
const conformance_2 = require("../conformance/conformance");
const jsonataExpression_1 = tslib_1.__importDefault(require("../jsonataExpression"));
const jsonataFuncs_1 = require("../jsonataFuncs");
const jsonataFunctions_1 = require("../jsonataFunctions");
const logger_1 = require("../logger");
const stringFunctions_1 = require("../stringFunctions");
const thrower_1 = tslib_1.__importDefault(require("../thrower"));
const getElementDefinition_1 = require("./getElementDefinition");
const removeComments_1 = require("./removeComments");
const dev = process.env.NODE_ENV === 'dev';
const toJsonataString = async (inExpr) => {
if (dev)
console.log(exports.toJsonataString);
let res;
// decrlare FLASH block variables:
let rootIndentOffset; // the padding of the first declerations in current flash block
let prevRuleDepth; // the number of indentations the last rule had
let prevRuleNodes; // the number of chained nodes the last rule had (no chain is 1)
let rootStructDef; // the current flash block's StructureDefinition resource
let instanceDeclerationFlag = false; // there was an Instance decleration somewhere in the current flash block
let instanceOfDeclerationFlag = false; // there was an InstanceOf decleration somewhere in the current flash block
let ruleClosureString = ''; // add this string to the end of the flash block - it closes all open rules
let flashBlockHanging = false; // true if we are inside a flash block
let currentFshPath = ''; // the absolute path relative to root base type
// declare expression global variables:
let currentBracketDepth = 0; // count global hanging open parenthesis
let expressionHasFlash = false; // there is some FLASH inside the expression
const lineParser = async (line, index, allLines) => {
// line by line parsing into native jsonata expressions
const trimmedLine = line.trim(); // trim line
// check if line is an Instance decleration
const isInstanceDeclaration = trimmedLine.startsWith('Instance:');
// check if line is InstanceOf decleration
const isInstanceOfDeclaration = trimmedLine.startsWith('InstanceOf:');
// raise expressionHasFlash flag if Instance[Of] was declared
expressionHasFlash = expressionHasFlash || isInstanceDeclaration || isInstanceOfDeclaration;
// check if line is a FLASH rule
const isRule = await isLineFlashRule(trimmedLine);
if (isInstanceDeclaration) {
// this line starts with 'Instance:'
if (instanceOfDeclerationFlag) {
// this instance decleration comes after an InstanceOf
return thrower_1.default.throwParseError(`'Instance:' decleration must come *before* the 'InstanceOf:' decleration in a FLASH block: '${trimmedLine}'`);
}
;
if (instanceDeclerationFlag) {
// this is the second instance decleration in current flash block
return thrower_1.default.throwParseError(`only a single 'Instance:' decleration is allowed in each FLASH block. Found: '${trimmedLine}'`);
}
;
// raise flag so we can trace Instance[Of] parse errors in the current flash block
instanceDeclerationFlag = true;
// register open flash block
flashBlockHanging = true;
// extract the expression that comes after Instance:
const after = (0, stringFunctions_1.substringAfter)(trimmedLine, 'Instance:');
// the indentation in Instance: is the root indentation
rootIndentOffset = (0, stringFunctions_1.substringBefore)(line, 'Instance:');
// set this so indentations of following rules can be validated
prevRuleDepth = 0;
prevRuleNodes = [];
// return the native jsonata corresponding to the "Instance:" decleration
return `${rootIndentOffset}$__flashInstanceId := ${after}`;
}
;
if (isInstanceOfDeclaration) {
// current line is an "InstanceOf:" declaration
if (instanceOfDeclerationFlag) {
// this is the second InstanceOf decleration in current flash block
return thrower_1.default.throwParseError(`only a single 'InstanceOf:' decleration is allowed in each FLASH block. Found: '${trimmedLine}'`);
}
;
// raise this flag so we can trace Instance[Of] parsing errors in the current flash block:
instanceOfDeclerationFlag = true;
// register that we are in an open flash block
flashBlockHanging = true;
// extract the InstanceOf: *** part
const rootTypeId = ((0, stringFunctions_1.substringAfter)(line, 'InstanceOf:') || '').trim();
// make sure it's a potentially valid FHIR type
if (!(await jsonataFuncs_1.funcs.isTypeNameValid(rootTypeId)))
return thrower_1.default.throwParseError(`value after "InstanceOf:" must be a valid type name, id or URL, and cannot be an expression. Found: "${rootTypeId}"`);
// try to fetch the type's StructureDefinition resource
rootStructDef = await (0, jsonataFunctions_1.getStructureDefinition)(rootTypeId, config_1.default.getFhirVersion(), (0, conformance_1.getFhirPackageIndex)(), (0, logger_1.getLogger)());
// throw error if StructureDefinition can't be fetched
if (rootStructDef === undefined)
return thrower_1.default.throwParseError(`can't find definition of ${rootTypeId}!`);
// currentFshPath = rootStructDef.type;
currentFshPath = '';
// the indentation of InstanceOf:
const indent = (0, stringFunctions_1.substringBefore)(line, 'InstanceOf:');
// might need to put a semicolon at the beginning of this line, this var will hold it
let prefix;
if (rootIndentOffset === undefined) {
// this means it's an InstanceOf declaration without a preceding
// "Instance:", so there is no need for a semicolon at the start
prefix = '';
}
else {
// there's a preceding Instance: declaration, so we may need to terminate it with a semicolon
const prevline = allLines[index - 1];
const isSingleLineExpression = prevline === line;
const isPrevLineTerminated = prevline.trimEnd()[-1] === ';';
prefix = !(isSingleLineExpression || isPrevLineTerminated) ? ';' : '';
}
;
if (rootIndentOffset !== undefined && indent !== rootIndentOffset) {
// different indetations for Instance: vs. InstanceOf
return thrower_1.default.throwParseError('indentation of "InstanceOf:" must be the same as "Instance:"!');
}
;
// if rootIndent was already set than it's the same as current indent.
// if it wasn't set, we need to set it now:
rootIndentOffset = indent;
prevRuleDepth = 0;
prevRuleNodes = [];
// set resourceType if it's an instance of a resource or resource profile
const resourceType = rootStructDef?.kind === 'resource' ? `'${rootStructDef?.type}'` : 'undefined';
// set meta with profile tag if it's a resource profile
const meta = rootStructDef?.kind === 'resource' && rootStructDef?.derivation === 'constraint' ? `{'profile': ['${rootStructDef?.url}']}` : 'undefined';
// create a regex test expression for the id if it's a resource
const testId = resourceType === 'undefined' ? '$__flashInstanceId' : '$__checkResourceId($__flashInstanceId)';
// mandatory fixed elements
const mandatoryElementsObj = await jsonataFuncs_1.funcs.getMandatoriesOfStructure(rootTypeId);
const mandatoriesAsString = typeof mandatoryElementsObj === 'object' ? JSON.stringify(mandatoryElementsObj) : '{}';
const res = `${prefix}${indent}$__flashInstance := $merge([{'resourceType': ${resourceType}},{'id': ${testId}, 'meta': ${meta}},${mandatoriesAsString}]);`;
return res;
}
;
if (flashBlockHanging && isRule) {
// logger.info('flashBlockHanging && isRule');
// this line is a flash rule
if (!instanceOfDeclerationFlag)
return thrower_1.default.throwParseError(`rules cannot appear before an "InstaceOf:" declaration. Rule: "${trimmedLine}"`);
// check indentation
//
// root indentation (Instance & InstanceOf) is regarded as zero
// any root rule is 2
// when indentation is lower than previous one, need to close previous rules - depending on the diff
// when indentation is higher than previous one it can only be by 2 spaces - no need to close previous rule
// when indentation is the same - close previous rule
const indent = (0, stringFunctions_1.substringBefore)(line, '* '); // the indentation of this rule in actual spaces
const ruleDepth = (indent.length - rootIndentOffset.length) + 2; // any root rule is 2, so all rules are shifted by 2
if (ruleDepth < 2) {
return thrower_1.default.throwParseError(`expected indentation of at least ${rootIndentOffset.length} spaces, got ${indent.length} instead. Rule: ${line}`);
}
if (ruleDepth > prevRuleDepth + 2) {
return thrower_1.default.throwParseError(`expected indentation of max ${rootIndentOffset.length + prevRuleDepth} spaces, got ${indent.length} instead. Rule: ${line}`);
}
if (ruleDepth % 2 !== 0) {
return thrower_1.default.throwParseError(`rule indentation must be in increments of two spaces. Rule: ${line}`);
}
// indentation correct
let rulesToClose; // how many rules we need to close at the beginning of this line
if (ruleDepth < prevRuleDepth) {
const steps = ((prevRuleDepth - ruleDepth) / 2);
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
rulesToClose = steps + sumCarriedNodes(steps, true) + 1; // add carried nodes from previous chained paths
}
else if (ruleDepth === prevRuleDepth) {
const carried = sumCarriedNodes(1, false);
rulesToClose = carried + 1;
}
else {
rulesToClose = 0;
prevRuleNodes.push(0);
}
;
// this will prefix the jsonata line
ruleClosureString = rulesToClose === 0 ? '' : await (0, stringFunctions_1.duplicate)(')));', rulesToClose);
// update current path
currentFshPath = climbFshPath(currentFshPath, rulesToClose);
// update prevRuleDepth to current value
prevRuleDepth = ruleDepth;
// check if rule starts with a (context) expression
const isContextStart = ((0, stringFunctions_1.substringAfter)(trimmedLine, '* ') || '').trim().charAt(0) === '(';
if (isContextStart) {
const openingIndex = line.indexOf('(');
// check if context ends on this line
const closingIndex = jsonataFuncs_1.funcs.findClosingBrackets(line, openingIndex + 1, 1);
if (closingIndex > -1) {
// this is a single-lined context - opens and closes on this line
const contextPart = line.substring(openingIndex, closingIndex + 1);
const restOfLine = line.substring(closingIndex + 1);
// check a period comes after closing bracket
if (restOfLine.trimStart()[0] !== '.')
return thrower_1.default.throwParseError(`expected a period between the context and the path. Rule: '${line}'`);
const afterPeriod = ((0, stringFunctions_1.substringAfter)(restOfLine, '.') || '').trim();
const pathOnly = ((0, stringFunctions_1.substringBefore)(afterPeriod, '=') || '').trim();
let valueOnly = ((0, stringFunctions_1.substringAfter)(afterPeriod, '=') || '');
// when there is no assigned value, the result of valueOnly will be equal to the whole line after the period
valueOnly = valueOnly === afterPeriod ? '' : valueOnly.trim();
const res = await constructLine(ruleClosureString, pathOnly, valueOnly, contextPart);
return res;
}
else {
// no closing parenthesis - syntax error or multiline context
// multiline context. TODO: make it work
return thrower_1.default.throwParseError(`Expected ')' to close the context part. Rule: '${line}'`);
}
}
else {
// regular rule without context
const pathPart = ((0, stringFunctions_1.substringBefore)((0, stringFunctions_1.substringAfter)(line, '* ') || '', '=') || '').trim();
let valuePart = ((0, stringFunctions_1.substringAfter)(line, '=') || '');
valuePart = valuePart === line ? '' : valuePart.trim();
// trace hanging parenthesis
const hangingBracketDiff = await jsonataFuncs_1.funcs.calcBracketDiff(trimmedLine);
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
currentBracketDepth = currentBracketDepth + hangingBracketDiff;
const res = await constructLine(ruleClosureString, pathPart, valuePart);
return res;
}
;
}
;
const endOfFlashBlock = jsonataFuncs_1.funcs.findClosingBrackets(line, 0, 1);
if (expressionHasFlash && (endOfFlashBlock >= 0 || (flashBlockHanging && index === (allLines.length - 1)))) {
// this terminates a flash block
const lhs = line.substring(0, endOfFlashBlock);
const rhs = line.substring(endOfFlashBlock);
const hangingBracketDiff = (await jsonataFuncs_1.funcs.calcBracketDiff(rhs));
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const steps = (prevRuleDepth / 2);
const rulesToClose = steps + sumCarriedNodes(steps, true);
// logger.info({ rulesToClose, steps });
let closure = ''; // = (await duplicate(')));', rulesToClose)) + '$__flashInstance := $__finalize($__flashInstance);';
if (rulesToClose !== 0) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
closure = (await (0, stringFunctions_1.duplicate)(')));', rulesToClose)) + '$__flashInstance := $__finalize($__flashInstance);';
}
// closure = closure + '$__flashInstance := $__finalize($__flashInstance);';
// logger.info('calling resetVariable()');
resetVariables();
currentBracketDepth = currentBracketDepth - hangingBracketDiff;
let res;
if (endOfFlashBlock >= 0) {
res = `${closure}${lhs}${rhs}`;
}
else {
res = `${line}${closure}`;
}
;
return res;
}
;
// no special handling required for this line - return it as-is
// trace hanging parenthesis
const hangingBracketDiff = await jsonataFuncs_1.funcs.calcBracketDiff(trimmedLine);
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
currentBracketDepth = currentBracketDepth + hangingBracketDiff;
return line;
};
const resetVariables = () => {
currentFshPath = ruleClosureString = '';
flashBlockHanging = instanceDeclerationFlag = instanceOfDeclerationFlag = false;
rootIndentOffset = rootStructDef = undefined;
prevRuleDepth = 0;
prevRuleNodes = [];
};
const climbFshPath = (path, steps) => {
const startPath = path === '' ? currentFshPath : path;
// goes up in path <steps> levels
if (steps === 0)
return startPath; // no change
const nodes = startPath.split('.'); // split to path nodes
nodes.splice(nodes.length - steps, steps); // remove top levels
const newPath = nodes.join('.'); // concatenate back into a path chain
currentFshPath = newPath; // update currentFshPath
return newPath;
};
const isLineFlashRule = async (line) => {
// check if this line should be parsed as a FLASH rule
return expressionHasFlash && // if this is false,
// it couldn't be a FLASH rule since those are only
// present in expressions that have FLASH scripts in them.
flashBlockHanging && // Even if previous condition is true, the line must
// be inside a FLASH block otherwise it's probably
// just a potentially valid part of a jsontata expression
line.startsWith('*'); // a rule line starts with an asterisk
};
const parseFumeExpression = async (expr) => {
const bindings = {
expr,
splitLineFunc: stringFunctions_1.splitToLines,
lineParser,
startsWith: stringFunctions_1.startsWith,
endsWith: stringFunctions_1.endsWith
};
const parsed = await jsonataExpression_1.default.parseFumeExpression.evaluate({}, bindings);
// logger.info({ parsed });
return parsed;
};
const sumCarriedNodes = (steps, climb) => {
// hard to explain...
// TODO: explain...
// logger.info('sumCarriedNodes');
if (climb)
prevRuleNodes = prevRuleNodes.filter((item) => item > 0);
if (prevRuleNodes.length === 0)
return 0;
const sum = prevRuleNodes.slice(-steps).reduce((acc, val) => acc + val);
prevRuleNodes = prevRuleNodes.slice(0, prevRuleNodes.length - steps);
return sum;
};
const constructLine = async (prefix, path, value, context, chainNode) => {
// take the parts of a flash rule and return a jsonata equivalent line
// wrap context in parenthesis and add a period
const contextPart = context && context !== '' ? `${context}.` : '';
// take value into __value or create an empty object
const valuePart = value && value.trim() !== '' ? `$__flashInstance := {'__value': ${value}}` : '$__flashInstance := {}';
// split path to separate nodes
const nodes = path.split('.');
if (nodes.length === 1) {
// this is a single node iteration (may still be a step in a chain)
// add node to accumulating FSH path
currentFshPath = currentFshPath === '' ? path : `${currentFshPath}.${path}`;
if (chainNode) { // this is an internal node in a chain of nodes (but not the last one)
// increase node counter
prevRuleNodes[prevRuleNodes.length - 1] += 1;
}
;
// fetch element definition
if (dev)
console.log('(toJsonataString): getElementDefinition()', { originPath: currentFshPath, newPath: currentFshPath });
let eDef = await (0, getElementDefinition_1.getElementDefinition)(rootStructDef.id, { originPath: currentFshPath, newPath: currentFshPath });
if (eDef) {
const eDefPath = eDef?.path;
const jsonName = await jsonataFuncs_1.funcs.getJsonElementName(eDef, path);
let chosenTypeEntry;
const isPoly = eDefPath.endsWith('[x]');
if (isPoly) {
// it's a polymorfic element (e.g. value[x])
// extract type name
if (eDef?.type.length === 1) {
// if constrained to a single type, just take it
chosenTypeEntry = eDef?.type[0];
}
else {
// get type from end of fsh path
const typeByFshPath = path.split(eDefPath.split('.').slice(-1)[0].split('[x]')[0]).slice(-1)[0];
const compareTypes = (typeFromDef) => {
// this function compares types by lowercasing their names
const typeCode = typeFromDef?.code;
return typeCode.toLowerCase() === typeByFshPath.toLowerCase();
};
// select from types using comparison function
chosenTypeEntry = eDef?.type?.filter(compareTypes)[0];
try {
// if there's an explicit type slice, it overrules the polymorphic head element
const eDefIDNoRoot = eDef.id.split('.').slice(1).join('.');
const typeSliceElementId = `${eDefIDNoRoot}:${jsonName}`;
// try to fetch the explicit type slice
if (dev)
console.log('(toJsonataString): getElementDefinition()', { 'rootStructDef.id': rootStructDef.id }, { originPath: typeSliceElementId, newPath: typeSliceElementId });
const typeSlice = await (0, getElementDefinition_1.getElementDefinition)(rootStructDef.id, { originPath: typeSliceElementId, newPath: typeSliceElementId });
if (typeSlice) {
// replace head element with type slice element
eDef = typeSlice;
}
}
catch { }
;
}
}
else {
// not polymorphic, there can only be a single type in the array
chosenTypeEntry = eDef?.type[0];
}
;
const baseType = chosenTypeEntry?.code;
let kind = '';
if (!baseType.startsWith('http://hl7.org/fhirpath/System.')) {
// fetch StructureDefintion of type
const typeStructDef = await (0, jsonataFunctions_1.getStructureDefinition)(baseType, config_1.default.getFhirVersion(), (0, conformance_1.getFhirPackageIndex)(), (0, logger_1.getLogger)());
kind = typeStructDef?.kind;
}
;
const jsonPrimitiveProfile = chosenTypeEntry?.extension?.filter((ext) => ext?.url === 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type')[0]?.valueUrl;
const mandatoryObj = await jsonataFuncs_1.funcs.getMandatoriesOfElement(rootStructDef.id, currentFshPath);
const typeForFixed = jsonPrimitiveProfile ?? baseType;
const fixed = (eDef['fixed' + (0, stringFunctions_1.initCapOnce)(typeForFixed)] ?? eDef['pattern' + (0, stringFunctions_1.initCapOnce)(typeForFixed)]) ?? mandatoryObj;
const bindingUrl = eDef?.binding?.strength === 'required' ? eDef.binding.valueSet : '';
const vsDictionary = bindingUrl !== '' ? await (0, conformance_2.valueSetExpandDictionary)(bindingUrl) : undefined;
let pathForCardinality = currentFshPath;
if (eDefPath.split('.').length > currentFshPath.split('.').length) {
pathForCardinality = eDefPath.split('.')[0] + '.' + currentFshPath;
}
// create cast options object
const castOptions = {
name: jsonName,
path: pathForCardinality,
baseType,
kind,
jsonPrimitiveProfile,
fixed,
vsDictionary
};
const mergeOptions = {
key: jsonName,
baseMax: eDef?.base?.max ?? '1',
max: eDef?.max ?? '1',
min: eDef?.min ?? 0,
path: pathForCardinality,
eDefPath,
kind
};
// construct line
const line = `${prefix}$__context := ${contextPart}($);$__flashInstance := $__flashMerge($merge([${JSON.stringify(mergeOptions)},{'mandatory': $count($__context)>0 ? ${JSON.stringify(mandatoryObj)} : undefined }]),$__flashInstance,$__castToFhir(${JSON.stringify(castOptions)}, ${contextPart}(${valuePart};`;
return line;
}
;
}
else if (nodes.length > 1) {
// chained path, iterate through each one
prevRuleNodes.push(0);
const bindings = {
nodes,
construct: constructLine,
prefix,
value: value ?? '',
context: context ?? ''
};
const line = await jsonataExpression_1.default.constructLineIterator.evaluate({}, bindings);
return line;
}
;
// didn't find any edef
return thrower_1.default.throwParseError(`Invalid path: '${currentFshPath}'`);
};
const withoutComments = (0, removeComments_1.removeComments)(inExpr);
let parsed = await parseFumeExpression(withoutComments);
if (!expressionHasFlash) {
res = withoutComments;
}
else {
// check if expression starts with "("
const isEnclosed = parsed.trimStart().startsWith('(') && parsed.trimEnd().endsWith('(');
// parsed = parsed.includes('$__finalize') ? parsed : `${parsed}$__flashInstance := $__finalize($__flashInstance);`
parsed = !isEnclosed ? `(${parsed})` : parsed; // enclose in () if needed
res = parsed.includes('$__finalize') ? parsed : parsed + '~>$__finalize';
}
return res;
};
exports.toJsonataString = toJsonataString;
//# sourceMappingURL=toJsonataString.js.map