@synanetics/fhir-fml-convert
Version:
Converts StructureMaps written in FML to JSON ($convert operation)
273 lines • 9.76 kB
JavaScript
;
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