/** * Shared FhirRules factory. * * Builds a version-agnostic {@link FhirRules} implementation from derived * data. All version-specific differences come from the data itself (which * was parsed from the core package's base StructureDefinitions), not from * hardcoded tables. * * The FHIR primitive → TypeScript type map and the "known-primitives" set * are genuinely identical across R4 / R4B / R5 so they live here as shared * constants. */ import type { FhirRules } from './types.js'; import type { DerivedVersionData } from './corePackageResolver.js'; // ── Shared constants (version-independent) ────────────────────────────────── /** FHIR primitive type → TypeScript type mapping (same for R4/R4B/R5). */ const PRIMITIVE_MAP: Record = { any: 'string', base64Binary: 'string', canonical: 'string', code: 'string', date: 'string', dateTime: 'string', decimal: 'number', integer: 'number', id: 'string', instant: 'string', integer64: 'number', markdown: 'string', oid: 'string', positiveInt: 'number', unsignedInt: 'number', time: 'string', uri: 'string', url: 'string', uuid: 'string', }; /** Set of all type names considered "primitive" by the TS emitter. */ const PRIMITIVES = new Set([ 'any', 'string', 'boolean', 'never', 'undefined', 'number', 'integer', 'decimal', 'url', 'uri', 'canonical', 'date', 'dateTime', 'time', 'instant', 'code', 'id', 'oid', 'markdown', 'base64Binary', 'uuid', 'positiveInt', 'unsignedInt', 'integer64', ]); /** ValueSet size thresholds (tuning knobs, not version-specific). */ export const VALUESET_THRESHOLDS = { UNION_TYPE: 20, STORE_CODES: 50, } as const; // ── Cardinality / child-type helpers ──────────────────────────────────────── /** * Fields whose `use` / `type` properties are enum codes that should *not* * be overridden to `string` by the child-type fallback. Keyed by parent * type, value is the set of field names to omit from the fallback map. */ const ENUM_FIELD_EXCLUSIONS: Record> = { Identifier: new Set(['use']), HumanName: new Set(['use']), Address: new Set(['use', 'type']), ContactPoint: new Set(['system', 'use']), }; /** * Build KNOWN_ARRAYS / KNOWN_SINGULAR tables from derived dataTypes. */ function buildCardinalityTables(dataTypes: DerivedVersionData['dataTypes']): { knownArrays: Record>; knownSingular: Record>; } { const knownArrays: Record> = {}; const knownSingular: Record> = {}; for (const [typeName, fields] of Object.entries(dataTypes)) { const arrays = new Set(); const singulars = new Set(); for (const [fieldName, info] of Object.entries(fields)) { if (info.isArray) arrays.add(fieldName); else singulars.add(fieldName); } if (arrays.size > 0) knownArrays[typeName] = arrays; if (singulars.size > 0) knownSingular[typeName] = singulars; } return { knownArrays, knownSingular }; } /** * Build CHILD_TYPE_FALLBACK from derived dataTypes, excluding enum fields * that should not be overridden to `string`. */ function buildChildTypeFallback( dataTypes: DerivedVersionData['dataTypes'], ): Record> { const fallback: Record> = {}; for (const [typeName, fields] of Object.entries(dataTypes)) { const exclusions = ENUM_FIELD_EXCLUSIONS[typeName]; const map: Record = {}; for (const [fieldName, info] of Object.entries(fields)) { if (exclusions?.has(fieldName)) continue; // Map FHIR primitive types to their TS equivalents map[fieldName] = PRIMITIVE_MAP[info.type] ?? info.type; } if (Object.keys(map).length > 0) fallback[typeName] = map; } return fallback; } // ── Public API ────────────────────────────────────────────────────────────── /** * Create a {@link FhirRules} instance from derived version data. * The returned object is fully functional and version-agnostic — all * version-specific behaviour comes from the `data` parameter. */ export function createRules(data: DerivedVersionData): FhirRules { // Merge data type and resource field maps for cardinality / fallback tables const allFields: DerivedVersionData['dataTypes'] = { ...data.dataTypes, ...data.resourceFields, }; const { knownArrays, knownSingular } = buildCardinalityTables(allFields); const childTypeFallback = buildChildTypeFallback(data.dataTypes); const allNames = new Set([...data.interfaceNames, ...data.resourceNames]); const coreResourceBases = new Set(['Resource', 'DomainResource', ...data.resourceNames]); const rules: FhirRules = { isFhirType: (name: string) => name === 'FhirResource' || allNames.has(name), mapTypeToTS: (t: string) => { if (t === 'http://hl7.org/fhirpath/System.String') return 'string'; return PRIMITIVE_MAP[t] ?? t; }, isPrimitiveType: (t: string) => PRIMITIVES.has(t) || /['"]/.test(t), isPrimitiveLike: (t?: string) => { if (!t) return true; if (PRIMITIVES.has(t)) return true; return /['"]/.test(t) && /\|/.test(t); }, isFhirPrimitiveType: (t: string) => data.primitiveTypes.has(t), isCoreResource: (name: string) => coreResourceBases.has(name), isFhirFieldArray: (parentType: string, fieldName: string) => { const singles = knownSingular[parentType]; if (singles?.has(fieldName)) return false; const arrays = knownArrays[parentType]; if (!arrays) return undefined; return arrays.has(fieldName); }, inferFhirChildType: ( parentType: string, fieldName: string, fhirChildTypeMap?: Map>, ) => { const parentSan = parentType.replace(/[^A-Za-z0-9_]/g, '_'); // Data-driven map takes priority (from ctx().dataTypes) const fromMap = fhirChildTypeMap?.get(parentSan)?.get(fieldName)?.type; if (fromMap) return rules.mapTypeToTS(fromMap); // Fallback to derived table return childTypeFallback[parentSan]?.[fieldName]; }, }; return rules; }