UNPKG

@synanetics/fhir-fml-convert

Version:

Converts StructureMaps written in FML to JSON ($convert operation)

273 lines 9.76 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StructureMapVisitor = void 0; const node_crypto_1 = require("node:crypto"); const lodash_1 = require("lodash"); const structureTypeModes = ['source', 'queried', 'target', 'produced']; const inputModes = ['source', 'target']; /** * The `StructureMapVisitor` class iterates over the token parsed from the FML and returns the * fragment of the StructureMap object to which the token pertains. It is an extension of the * automatically-generated FhirMapperVisitor. */ class StructureMapVisitor { METADATA_KEYS = [ 'contact', 'copyright', 'date', 'description', 'experimental', 'id', 'identifier', 'jurisdiction', 'name', 'publisher', 'purpose', 'status', 'title', 'url', 'useContext', 'version', ]; structureMapDefinition; fhirTypes; fhirVersion; visit(tree) { return tree.accept(this); } visitTerminal() { } visitErrorNode(node) { // dunno what this does yet... throw new Error(node.text); } visitStructureMap(ctx) { const metadata = {}; const metadataVisitResult = ctx.metadata().map((m) => m.accept(this)); metadataVisitResult.forEach((r) => { Object.assign(metadata, r); }); const mapId = ctx.mapId().accept(this); const structures = ctx.structure().map((s) => s.accept(this)); const groups = ctx.group().map((g) => g.accept(this)); return { resourceType: 'StructureMap', url: mapId.url, name: mapId.name, // "draft" is the default value (https://www.hl7.org/fhir/mapping-language.html#metadata) status: 'draft', structure: structures, group: groups, ...metadata, }; } /** * Returns a typed value from the inbound metadata field, according to the type expected by the * StructureMap FHIR profile * @param type The primitive type from the FHIR profile that this field is allowed to be. * @param parsedValue The value from the metadata field. * @returns */ getPrimitiveValue(type, parsedValue) { if (!parsedValue) return undefined; switch (type.code) { case 'boolean': if (parsedValue.text === 'false') return false; if (parsedValue.text === 'true') return true; return undefined; case 'string': case 'code': return this.sanitiseString((parsedValue.literal() || parsedValue.markdown())?.text); default: return undefined; } } visitMetadata(ctx) { const partialStructureDef = {}; const key = ctx.metadataKey().text; if (!this.METADATA_KEYS.includes(key.split('.')[0])) return partialStructureDef; const value = ctx.metadataValue(); const elemDef = this.structureMapDefinition.snapshot?.element.find((e) => e.path === `${this.structureMapDefinition.name}.${key}`); if (!value && !elemDef) return partialStructureDef; const targetTypes = this.fhirTypes .filter((ft) => elemDef?.type?.map((et) => et.code).includes(ft.id)); const typedValues = []; targetTypes.forEach((t) => { const currentElemDef = elemDef.type.find((type) => type.code === t.id); if (t.kind === 'primitive-type') { typedValues.push(this.getPrimitiveValue(currentElemDef, value)); } else if (t.kind === 'complex-type') { console.warn('Assigning FML metadata to complex FHIR types is not yet supported.'); } }); /** * None of the elements that are assignable outside of the main body of the can be of multiple * types, so this should always assign the correct value. */ if (typedValues.length === 1) (0, lodash_1.set)(partialStructureDef, key, typedValues[0]); return partialStructureDef; } visitMapId(ctx) { const url = ctx.url().text.replace(/"/g, ''); const alias = ctx.mapAlias()?.variableId().text.replace(/^"|"$/g, '') ?? 'Unnamed'; return { url, name: alias }; } visitStructure(ctx) { const mode = ctx.modelMode().text; if (!structureTypeModes.includes(mode)) { throw new Error(`Invalid structure type mode: ${mode}. Structure type mode must be one of: ${structureTypeModes.join(', ')}.`); } return { url: ctx.url().text.replace(/"/g, ''), alias: ctx.structureAlias()?.variableId()?.text, mode, }; } visitGroup(ctx) { const groupName = ctx.variableId().text; const inputs = ctx.parameters().parameter().map((p) => { const mode = p.inputMode().text; if (!inputModes.includes(mode)) { throw new Error(`Invalid input mode: ${mode}. Input mode must be one of: ${inputModes.join(', ')}.`); } const name = p.variableId().text; const type = p.type()?.variableId().text ?? undefined; return { name, mode, type }; }); const rules = ctx.rules().ruleDefinition().map((rule) => rule.accept(this)); let typeMode; const fmlTypeMode = ctx.typeMode()?.groupTypeMode().text; switch (fmlTypeMode) { case 'types': typeMode = 'types'; break; case 'type+': typeMode = 'type-and-types'; break; default: if (this.fhirVersion !== 'R5') { typeMode = 'none'; } break; } return { name: groupName, typeMode, input: inputs, rule: rules, }; } getRuleName(ctx, ruleSource) { const declaredName = ctx?.variableId().text.replace(/^"|"$/g, ''); if (declaredName) { return declaredName; } let constructedName; if (ruleSource) { constructedName = ruleSource.element; if (ruleSource.type) { constructedName = `${constructedName}-${ruleSource.type}`; } } return constructedName || (0, node_crypto_1.randomUUID)(); } visitRuleSources(ctx) { return ctx.ruleSource().map((src) => { const ruleCtx = src.ruleCtx().text.split('.'); const context = ruleCtx[0]; const element = ruleCtx[1] ?? undefined; const type = src.sourceType()?.variableId().text; const cardinalityMin = src.sourceCardinality()?.INTEGER().text; const min = cardinalityMin ? Number(cardinalityMin) : undefined; const max = src.sourceCardinality()?.upperBound().text; const condition = src.whereClause()?.fhirPath().text; const variable = src.alias()?.variableId().text; let defaultValue; let defaultKey; if (src.sourceDefault()) { ({ key: defaultKey, value: defaultValue } = this.getDefaultValue(src.sourceDefault())); } const ruleSource = { context, element, type, min, max, variable, condition, }; if (defaultValue) { // @ts-ignore the key will be valid, because of our implementation ruleSource[defaultKey] = defaultValue; } return ruleSource; }); } sanitiseString(source) { if (!source) return undefined; return source.replace(/^'|'$/g, ''); } /** * Converts the "literal" value from the FML to a JavaScript type and its accompanying named FHIR * Type * @param literal The parsed "literal" token from the FML * @returns The FHIR primitive type and the actual value from the token. */ getValueFromLiteral(literal) { if (literal.BOOL()) { return { type: 'boolean', value: Boolean(literal.BOOL().text).valueOf(), }; } if (literal.DATE()) { return { type: 'date', value: this.sanitiseString(literal.DATE().text), }; } if (literal.DATETIME()) { return { type: 'dateTime', value: this.sanitiseString(literal.DATETIME().text), }; } if (literal.INTEGER()) { return { type: 'integer', value: Number.parseInt(literal.INTEGER().text || 'NaN', 10), }; } if (literal.NUMBER()) { return { type: 'decimal', value: Number.parseInt(literal.NUMBER().text || 'NaN', 10), }; } if (literal.STRING()) { return { type: 'string', value: this.sanitiseString(literal.STRING().text), }; } if (literal.TIME()) { return { type: 'time', value: this.sanitiseString(literal.TIME().text), }; } return undefined; } // Default method fallback visitChildren() { } } exports.StructureMapVisitor = StructureMapVisitor; //# sourceMappingURL=StructureMapVisitor.js.map