fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
252 lines (238 loc) • 11.9 kB
text/typescript
/**
* © Copyright Outburn Ltd. 2022-2024 All Rights Reserved
* Project name: FUME-COMMUNITY
*/
import _ from 'lodash';
import uuidByString from 'uuid-by-string';
import config from '../../config';
import { getFhirPackageIndex } from '../conformance';
import expressions from '../jsonataExpression';
import { getStructureDefinition } from '../jsonataFunctions';
import { getLogger } from '../logger';
import thrower from '../thrower';
export interface CastToFhirOptions {
name: string // name of the element
path: string // the path of the element - needed for error messages
baseType: string // the type.code from eDef. Would be a FHIR type name or a primitive's System URI
kind?: string // the kind of element as defined in the type's StructureDefinition
jsonPrimitiveProfile?: string // when baseType is a System primitive, the fhirtype to take contraints from
fixed?: any // the fixed[x] or pattern[x] from edef
vsDictionary?: any[] // codes for required bindings
};
const dev = process.env.NODE_ENV === 'dev';
const primitiveParsers = {}; // cache for primitive value testing functions
const getPrimitiveParser = async (typeName: string): Promise<Function | undefined> => {
// returns a function that tests primitives agaist their regex
// returned functions are cached in memory so they are only compiled once
if (primitiveParsers[typeName]) {
// exists in cache, return from there
return primitiveParsers[typeName];
} else {
// generate and compile the function
let resFn: Function;
const sDef = await getStructureDefinition(typeName, config.getFhirVersion(), getFhirPackageIndex(), getLogger());
if (sDef === undefined) return thrower.throwRuntimeError(`error fetching structure definition for type ${typeName}`);
const valueElementDef = sDef?.snapshot?.element[3]; // 4th element in a primitive's structdef is always the actual primitive value
// get regular expression string from the standard extension
const regexStr: string = valueElementDef?.type[0]?.extension?.filter((ext: any) => ext?.url === 'http://hl7.org/fhir/StructureDefinition/regex')[0]?.valueString;
if (regexStr) {
// found regex, compile it
const fn = new RegExp(`^${regexStr}$`);
resFn = (value: string): boolean => fn.test(value);
} else {
// no regex - function will just test for empty strings
resFn = (value: string): boolean => value.trim() !== '';
}
primitiveParsers[typeName] = resFn; // cache the function
return resFn;
}
};
const testCodingAgainstVS = async (coding: any, vs: any[]): Promise<string | string[] | undefined> => {
return await expressions.testCodingAgainstVS.evaluate({}, { coding, vs });
};
export const castToFhir = async (options: CastToFhirOptions, input: any) => {
// this is the function that is called from parsed flash rules. it only runs
// at runtime - when the native jsonata build of the flash script is evaluated
if (dev) console.log({ castToFhir }, options, input);
if (typeof input === 'undefined') return {};
const baseType: string = options?.baseType;
const kind: string | undefined = options?.kind; // primitive-type | complex-type | resource
const elementName: string = options?.name;
const fixed: any = options?.fixed;
const res: { // container object for the key-value and _key/children pairs
[k: string]: any
} = {};
// handle arrays
const isInputAnArray = Array.isArray(input);
if (isInputAnArray) {
const resArray: any[] = [];
for (let i = 0; i < input.length; i++) {
const iterationRes: any = await castToFhir(options, input[i]);
resArray.push(iterationRes[elementName]);
};
res[elementName] = resArray;
return res;
};
if (baseType.startsWith('http://hl7.org/fhirpath/System.')) {
// this is a true (System) primitive
const value = input?.__value; // it is only assined inline in a rule - no children are possible
let testValue: string; // will be passed to the tester function
if (typeof value === 'string') {
testValue = value;
} else {
testValue = JSON.stringify(value); // any value other than string is strigified
};
const primitiveProfile: string | undefined = options?.jsonPrimitiveProfile; // the fhirtype to take the regex from
// throw error if empty
if (baseType.endsWith('String') && typeof testValue === 'string' && testValue.trim() === '' && !fixed) return thrower.throwRuntimeError(`got forbidden empty value in ${options.path}! (type: ${primitiveProfile})`);
// prepare to validate and parse the value
if (primitiveProfile) {
// fetch a tester function
const parser = await getPrimitiveParser(primitiveProfile);
// perform test
if (testValue && parser && !await parser(testValue)) {
// failed the test, throw error
return thrower.throwRuntimeError(`value '${testValue}' is invalid according to RegEx defined for type ${primitiveProfile} (${options.path})`);
}
};
// parse to the correct primitive type
if (['decimal', 'integer', 'positiveInt', 'integer64', 'unsignedInt'].includes(baseType)) {
// numeric primitive - cast as number.
// it should not fail since regex has already been tested
// TODO: how to retain decimal percision in js?? not sure it's possible...
res[elementName] = fixed ?? Number(value);
} else if (baseType === 'boolean') {
if (typeof value === 'boolean') res[elementName] = (fixed ?? value); // already boolean, take as-is
if (typeof value === 'string') res[elementName] = (fixed ?? (value === 'true' ? true : value === 'false' ? false : undefined)); // literal string booleans are accepted
} else {
// the base type is some specialization of string (date, uri, code etc.)
// regex test has already passed, just take the stringified version if not already a string
if (typeof value === 'string') {
res[elementName] = fixed ?? value; // already a string
} else {
res[elementName] = fixed ?? testValue; // stringified version of value
}
}
} else if (kind === 'primitive-type') {
// it's a fhir primitive, so it might have children
const ruleValue = input?.__value; // a value assigned as part of an assignment rule
const literalValue = input?.value; // a value that's been assigned directly to the 'value' property under a primitive's path
const id = input?.id; // an element id
let extension;
try {
// @ts-expect-error
extension = input?.extension || input[Object.keys(input).find(key => key.includes('extension'))]; // any element extensions
} catch (e) {}
if (literalValue || !_.isNil(ruleValue)) { // has primitive value assigned in expression
// fetch tester function
const parser = await getPrimitiveParser(baseType);
let passToParser: string;
// value to pass to the tester
// values assigned explicitly to the "value" element override inline assignment values
// e.g., in:
//
// * code = 'a'
// * value = 'b'
//
// => 'b' will override 'a' as the primitive value of the 'code' element
let value = literalValue ?? ruleValue;
if (typeof value === 'string') {
if (!isNaN(new Date(value).getTime()) && options.baseType === 'date') {
value = value.split('T')[0];
}
passToParser = value; // pass as-is, remove time if Datetime
} else {
// pass a stringified version of value
passToParser = JSON.stringify(value);
}
if (parser && await parser(passToParser)) {
// passed the test
if (options.vsDictionary) {
// it's a primitive that has binding
const vsTest: string = await expressions.testCodeAgainstVS.evaluate({}, { value: passToParser, vs: options.vsDictionary });
if (!vsTest) {
return thrower.throwRuntimeError(`value '${passToParser}' is invalid for element ${options?.path}. This code is not in the required value set`);
}
}
let resValue: string | number | boolean; // only possible primitive types
if (['decimal', 'integer', 'positiveInt', 'integer64', 'unsignedInt'].includes(baseType)) {
// numeric in the json
resValue = fixed ?? Number(value);
} else if (baseType === 'boolean') {
// boolean in the json
if (typeof value === 'boolean') resValue = fixed ?? value;
if (typeof value === 'string') resValue = fixed ?? value === 'true';
} else {
// string in the json
if (typeof value === 'string') {
resValue = fixed ?? value;
} else {
resValue = fixed ?? passToParser;
}
};
res[elementName] = resValue!; // set value to a key with the element's name
} else {
// failed the test, throw error
return thrower.throwRuntimeError(`value '${passToParser}' failed RegEx defined for type ${baseType} (${options?.path})`);
}
};
if (fixed) {
// has fixed primitive value
res[elementName] = fixed;
}
if (id || extension) {
// this primitive has children, they should be placed in an
// object with the same key, but prefixed with underscore
res['_' + elementName] = {
id,
extension
};
}
} else {
// this is a complex type (or a resource?), so it's an object
// TODO: what happens when it's an inline resource? (contained, bundle)
let resObj = { ...input }; // copy all keys
// value assined inline to the object is currently ignored
// TODO: find a way to handle objects assigned this way (they might contain illegal elements)
if (kind === 'resource' && Object.prototype.hasOwnProperty.call(resObj, '__value') && typeof resObj.__value === 'object' && !Array.isArray(resObj.__value)) {
resObj = { ...resObj.__value };
if (options?.path.startsWith('Bundle')) {
// this is a resource inside a bundle
// need to add a fullUrl
const seed: string = JSON.stringify(resObj);
// logger.info(seed);
res.fullUrl = 'urn:uuid:' + uuidByString(seed);
}
};
delete resObj.__value; // remove the internal __value key
if ((fixed && Object.keys(fixed).length > 0) || Object.keys(resObj).length > 0) {
if (fixed) {
res[elementName] = { ...fixed, ...resObj };
} else {
res[elementName] = { ...resObj };
}
};
if (options.vsDictionary && options.baseType === 'CodeableConcept') {
// required bindings on CodeableConcept
if (Object.keys(res[elementName]).filter((k: string) => k.startsWith('coding')).length > 0) {
const vsTest = await expressions.testCodeableAgainstVS.evaluate({}, { codeable: res[elementName], vs: options.vsDictionary, testCodingAgainstVS });
if (!vsTest) {
return thrower.throwRuntimeError(`Element ${options?.path} is invalid since none of the codings provided are in the required value set`);
}
}
};
if (options.vsDictionary && (options.baseType === 'Quantity' || options.baseType === 'Coding')) {
if (dev) console.log(`(castToFhir): Generating ${options.baseType}`, { path: options?.path, name: options?.name });
// required bindings on Quantity or Coding
if (res[elementName]?.system || res[elementName]?.code) {
const vsTest = await testCodingAgainstVS(res[elementName], options.vsDictionary);
if (!vsTest) {
const system: string = res[elementName]?.system ?? '';
const code: string = res[elementName]?.code ?? '';
return thrower.throwRuntimeError(`The code '${system}#${code}' is invalid for element ${options?.path}. This code is not in the required value set`);
}
}
}
};
return res;
};