Skip to main content
Glama
parse.ts15 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { ConceptMap, StructureMap, StructureMapGroup, StructureMapGroupInput, StructureMapGroupRule, StructureMapGroupRuleDependent, StructureMapGroupRuleSource, StructureMapGroupRuleTarget, StructureMapGroupRuleTargetParameter, StructureMapStructure, } from '@medplum/fhirtypes'; import type { Atom, Parser } from '../fhirlexer/parse'; import { FunctionAtom, LiteralAtom, SymbolAtom } from '../fhirpath/atoms'; import { OperatorPrecedence, initFhirPathParserBuilder } from '../fhirpath/parse'; import { tokenize } from './tokenize'; /** * Mapping from FHIR Mapping Language equivalence operators to FHIR ConceptMap equivalence codes. * * See: https://build.fhir.org/mapping.g4 for FHIR Mapping Language operators. * * See: https://hl7.org/fhir/r4/valueset-concept-map-equivalence.html for ConceptMap equivalence codes. * * @internal */ const CONCEPT_MAP_EQUIVALENCE: Record<string, string> = { '-': 'disjoint', '==': 'equal', }; class StructureMapParser { readonly parser: Parser; readonly structureMap: Partial<StructureMap> = { resourceType: 'StructureMap', status: 'active', }; constructor(parser: Parser) { this.parser = parser; } parse(): StructureMap { while (this.parser.hasMore()) { const next = this.parser.peek()?.value; switch (next) { case 'map': this.parseMap(); break; case 'uses': this.parseUses(); break; case 'imports': this.parseImport(); break; case 'group': this.parseGroup(); break; case 'conceptmap': this.parseConceptMap(); break; default: throw new Error(`Unexpected token: ${next}`); } } return this.structureMap as StructureMap; } private parseMap(): void { // 'map' url '=' identifier // map "http://hl7.org/fhir/StructureMap/tutorial" = tutorial this.parser.consume('Symbol', 'map'); this.structureMap.url = this.parser.consume('String').value; this.parser.consume('='); this.structureMap.name = this.parser.consume().value; } private parseUses(): void { // 'uses' url structureAlias? 'as' modelMode // uses "http://hl7.org/fhir/StructureDefinition/tutorial-left" as source this.parser.consume('Symbol', 'uses'); const result: Partial<StructureMapStructure> = {}; result.url = this.parser.consume('String').value; if (this.parser.peek()?.value === 'alias') { this.parser.consume('Symbol', 'alias'); result.alias = this.parser.consume('Symbol').value; } this.parser.consume('Symbol', 'as'); result.mode = this.parser.consume().value as 'source' | 'queried' | 'target' | 'produced'; if (!this.structureMap.structure) { this.structureMap.structure = []; } this.structureMap.structure.push(result as StructureMapStructure); } private parseImport(): void { this.parser.consume('Symbol', 'imports'); if (!this.structureMap.import) { this.structureMap.import = []; } this.structureMap.import.push(this.parser.consume('String').value); } private parseGroup(): void { // 'group' identifier parameters extends? typeMode? rules // group tutorial(source src : TLeft, target tgt : TRight) { const result: Partial<StructureMapGroup> = {}; this.parser.consume('Symbol', 'group'); result.name = this.parser.consume('Symbol').value; result.input = this.parseParameters(); if (this.parser.peek()?.value === 'extends') { this.parser.consume('Symbol', 'extends'); result.extends = this.parser.consume('Symbol').value; } if (this.parser.peek()?.value === '<<') { this.parser.consume('<<'); result.typeMode = this.parser.consume().value as 'none' | 'types' | 'type-and-types'; if (this.parser.peek()?.value === '+') { this.parser.consume('+'); result.typeMode = 'type-and-types'; } this.parser.consume('>>'); } else { result.typeMode = 'none'; } result.rule = this.parseRules(); if (!this.structureMap.group) { this.structureMap.group = []; } this.structureMap.group.push(result as StructureMapGroup); } private parseParameters(): StructureMapGroupInput[] { const parameters: StructureMapGroupInput[] = []; this.parser.consume('('); while (this.parser.hasMore() && this.parser.peek()?.value !== ')') { parameters.push(this.parseParameter()); if (this.parser.peek()?.value === ',') { this.parser.consume(','); } } this.parser.consume(')'); return parameters; } private parseParameter(): StructureMapGroupInput { // inputMode identifier type? // ':' identifier // source src : TLeft const result: Partial<StructureMapGroupInput> = {}; result.mode = this.parser.consume().value as 'source' | 'target'; result.name = this.parser.consume('Symbol').value; if (this.parser.peek()?.value === ':') { this.parser.consume(':'); result.type = this.parser.consume('Symbol').value; } return result as StructureMapGroupInput; } private parseRules(): StructureMapGroupRule[] { const rules = []; this.parser.consume('{'); while (this.parser.hasMore() && this.parser.peek()?.value !== '}') { rules.push(this.parseRule()); } this.parser.consume('}'); return rules; } private parseRule(): StructureMapGroupRule { const result: Partial<StructureMapGroupRule> = { source: this.parseRuleSources(), }; if (this.parser.peek()?.value === '->') { this.parser.consume('->'); result.target = this.parseRuleTargets(); } if (this.parser.peek()?.value === 'then') { this.parser.consume('Symbol', 'then'); if (this.parser.peek()?.id === '{') { result.rule = this.parseRules(); } else { result.dependent = this.parseRuleDependents(); } } if (this.parser.peek()?.id === 'String') { result.name = this.parser.consume().value; } else { result.name = result.source?.[0]?.element; } this.parser.consume(';'); return result as StructureMapGroupRule; } private parseRuleSources(): StructureMapGroupRuleSource[] { if (this.parser.hasMore() && this.parser.peek()?.value === 'for') { // The "for" keyword is optional // It is not in the official grammar: https://build.fhir.org/mapping.g4 // But it is used in the examples: https://build.fhir.org/mapping-tutorial.html this.parser.consume('Symbol', 'for'); } const sources = [this.parseRuleSource()]; while (this.parser.hasMore() && this.parser.peek()?.value === ',') { this.parser.consume(','); sources.push(this.parseRuleSource()); } return sources; } private parseRuleSource(): StructureMapGroupRuleSource { const result: Partial<StructureMapGroupRuleSource> = {}; const context = this.parseRuleContext(); const parts = context.split('.'); result.context = parts[0]; result.element = parts[1]; if (this.parser.hasMore() && this.parser.peek()?.value === ':') { this.parser.consume(':'); result.type = this.parser.consume().value; } if (this.parser.hasMore() && this.parser.peek()?.value === 'default') { this.parser.consume('Symbol', 'default'); result.defaultValueString = this.parser.consume('String').value; } if ( this.parser.peek()?.value === 'first' || this.parser.peek()?.value === 'not_first' || this.parser.peek()?.value === 'last' || this.parser.peek()?.value === 'not_last' || this.parser.peek()?.value === 'only_one' ) { result.listMode = this.parser.consume().value as 'first' | 'not_first' | 'last' | 'not_last' | 'only_one'; } if (this.parser.peek()?.value === 'as') { this.parser.consume('Symbol', 'as'); result.variable = this.parser.consume().value; } if (this.parser.peek()?.value === 'log') { this.parser.consume('Symbol', 'log'); result.logMessage = this.parser.consume().value; } if (this.parser.peek()?.value === 'where') { this.parser.consume('Symbol', 'where'); const whereFhirPath = this.parser.consumeAndParse(OperatorPrecedence.Arrow); result.condition = whereFhirPath.toString(); } if (this.parser.peek()?.value === 'check') { this.parser.consume('Symbol', 'check'); const checkFhirPath = this.parser.consumeAndParse(OperatorPrecedence.Arrow); result.check = checkFhirPath.toString(); } return result as StructureMapGroupRuleSource; } private parseRuleTargets(): StructureMapGroupRuleTarget[] { const targets = [this.parseRuleTarget()]; while (this.parser.hasMore() && this.parser.peek()?.value === ',') { this.parser.consume(','); targets.push(this.parseRuleTarget()); } return targets; } private parseRuleTarget(): StructureMapGroupRuleTarget { const result: StructureMapGroupRuleTarget = {}; const context = this.parseRuleContext(); const parts = context.split('.'); result.contextType = 'variable'; result.context = parts[0]; result.element = parts[1]; if (this.parser.peek()?.value === '=') { this.parser.consume('='); this.parseRuleTargetTransform(result); } if (this.parser.peek()?.value === 'as') { this.parser.consume('Symbol', 'as'); result.variable = this.parser.consume().value; } if (this.parser.peek()?.value === 'share') { this.parser.consume('Symbol', 'share'); result.listMode = ['share']; this.parser.consume('Symbol'); // NB: Not in the spec, but used by FHIRCH maps } if ( this.parser.peek()?.value === 'first' || this.parser.peek()?.value === 'last' || this.parser.peek()?.value === 'collate' ) { result.listMode = [this.parser.consume().value as 'first' | 'last' | 'collate']; } return result; } private parseRuleTargetTransform(result: StructureMapGroupRuleTarget): void { const transformAtom = this.parser.consumeAndParse(OperatorPrecedence.As); if (transformAtom instanceof FunctionAtom) { result.transform = transformAtom.name as 'append' | 'truncate'; result.parameter = transformAtom.args?.map(atomToParameter); } else if (transformAtom instanceof LiteralAtom || transformAtom instanceof SymbolAtom) { result.transform = 'copy'; result.parameter = [atomToParameter(transformAtom)]; } else { result.transform = 'evaluate'; result.parameter = [{ valueString: transformAtom.toString() }]; } } private parseRuleContext(): string { let identifier = this.parser.consume().value; while (this.parser.peek()?.value === '.') { this.parser.consume('.'); identifier += '.' + this.parser.consume().value; } return identifier; } private parseRuleDependents(): StructureMapGroupRuleDependent[] | undefined { const atom = this.parser.consumeAndParse(OperatorPrecedence.Arrow) as FunctionAtom; return [ { name: atom.name, variable: atom.args.map((arg) => (arg as SymbolAtom).name), }, ]; } private parseConceptMap(): void { this.parser.consume('Symbol', 'conceptmap'); const conceptMap: ConceptMap = { resourceType: 'ConceptMap', status: 'active', url: '#' + this.parser.consume('String').value, }; this.parser.consume('{'); const prefixes: Record<string, string> = {}; let next = this.parser.peek()?.value; while (next !== '}') { if (next === 'prefix') { this.parseConceptMapPrefix(prefixes); } else { this.parseConceptMapRule(conceptMap, prefixes); } next = this.parser.peek()?.value; } this.parser.consume('}'); if (!this.structureMap.contained) { this.structureMap.contained = []; } this.structureMap.contained.push(conceptMap as ConceptMap); } private parseConceptMapPrefix(prefixes: Record<string, string>): void { this.parser.consume('Symbol', 'prefix'); const prefix = this.parser.consume().value; this.parser.consume('='); const uri = this.parser.consume().value; prefixes[prefix] = uri; } private parseConceptMapRule(conceptMap: Partial<ConceptMap>, prefixes: Record<string, string>): void { const sourcePrefix = this.parser.consume().value; const sourceSystem = prefixes[sourcePrefix]; this.parser.consume(':'); const sourceCode = this.parser.consume().value; const equivalence = CONCEPT_MAP_EQUIVALENCE[this.parser.consume().value] as 'relatedto'; const targetPrefix = this.parser.consume().value; const targetSystem = prefixes[targetPrefix]; this.parser.consume(':'); const targetCode = this.parser.consume().value; let group = conceptMap?.group?.find((g) => g.source === sourceSystem && g.target === targetSystem); if (!group) { group = { source: sourceSystem, target: targetSystem, element: [] }; if (!conceptMap.group) { conceptMap.group = []; } conceptMap.group.push(group); } if (!group.element) { group.element = []; } group.element.push({ code: sourceCode, target: [{ code: targetCode, equivalence }], }); } } function atomToParameter(atom: Atom): StructureMapGroupRuleTargetParameter { if (atom instanceof SymbolAtom) { return { valueId: atom.name }; } if (atom instanceof LiteralAtom) { return literalToParameter(atom); } throw new Error(`Unknown parameter atom type: ${atom.constructor.name} (${atom.toString()})`); } function literalToParameter(literalAtom: LiteralAtom): StructureMapGroupRuleTargetParameter { switch (literalAtom.value.type) { case 'boolean': return { valueBoolean: literalAtom.value.value as boolean }; case 'decimal': return { valueDecimal: literalAtom.value.value as number }; case 'integer': return { valueInteger: literalAtom.value.value as number }; case 'dateTime': case 'string': return { valueString: literalAtom.value.value as string }; default: throw new Error('Unknown target literal type: ' + literalAtom.value.type); } } const fhirPathParserBuilder = initFhirPathParserBuilder() .registerInfix('->', { precedence: OperatorPrecedence.Arrow }) .registerInfix(';', { precedence: OperatorPrecedence.Semicolon }); /** * Parses a FHIR Mapping Language document into an AST. * @param input - The FHIR Mapping Language document to parse. * @returns The AST representing the document. */ export function parseMappingLanguage(input: string): StructureMap { const parser = fhirPathParserBuilder.construct(tokenize(input)); parser.removeComments(); return new StructureMapParser(parser).parse(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server