babelfhir-ts 1.5.19 → 1.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/out/src/generator/core/constants.js +19 -0
  2. package/out/src/generator/core/utils.js +40 -1
  3. package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
  4. package/out/src/generator/emitters/interface/backboneSliceTyping.js +13 -2
  5. package/out/src/generator/emitters/interface/importManager.js +28 -2
  6. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +21 -16
  7. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +16 -0
  8. package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
  9. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +37 -290
  10. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +89 -16
  11. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +113 -237
  12. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +13 -4
  13. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +91 -62
  14. package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
  15. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +275 -37
  16. package/out/src/generator/emitters/validator/validatorGenerator.js +167 -168
  17. package/out/src/generator/emitters/validator/validatorRuntime.js +269 -0
  18. package/out/src/generator/emitters/validator/validatorTemplates.js +186 -54
  19. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +29 -19
  20. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +16 -1
  21. package/out/src/generator/parser/packageParser.js +1 -8
  22. package/out/src/generator/sdProcessor.js +95 -10
  23. package/out/src/generator/sdProcessorHelpers.js +1 -1
  24. package/out/src/generator/sdProcessorRequiredFields.js +95 -0
  25. package/out/src/main.js +37 -11
  26. package/package.json +2 -1
  27. package/parity-matrix.json +18 -18
@@ -57,6 +57,25 @@ export const FETCH_TIMEOUT_MS = 15_000;
57
57
  export const TX_TIMEOUT_MS = 30_000;
58
58
  /** Max filename length for terminology expansion cache files */
59
59
  export const MAX_CACHE_FILENAME_LENGTH = 200;
60
+ /**
61
+ * FHIR package registries in priority order.
62
+ *
63
+ * packages.fhir.org is the canonical source; packages.simplifier.net is the
64
+ * fallback and carries some national IGs the canonical registry does not mirror.
65
+ * Both speak the npm metadata shape, so `GET <registry>/<packageId>` returns
66
+ * `dist-tags` and `versions`.
67
+ */
68
+ /**
69
+ * Attempts per registry when downloading a package tarball.
70
+ *
71
+ * Registry blips are the single largest cause of spurious parity failures: a run
72
+ * pulls ~30 packages and a lone `fetch failed` aborted the whole generation.
73
+ */
74
+ export const PACKAGE_DOWNLOAD_ATTEMPTS = 3;
75
+ export const FHIR_REGISTRIES = [
76
+ 'https://packages.fhir.org',
77
+ 'https://packages.simplifier.net',
78
+ ];
60
79
  // ─── FHIR StructureDefinition URL prefixes ───────────────────────────────────
61
80
  export const FHIR_SD_URL_PREFIX = 'http://hl7.org/fhir/StructureDefinition/';
62
81
  export const FHIR_SD_URL_PREFIX_HTTPS = 'https://hl7.org/fhir/StructureDefinition/';
@@ -24,6 +24,38 @@ export function capitalize(str) {
24
24
  return str;
25
25
  return str.charAt(0).toUpperCase() + str.slice(1);
26
26
  }
27
+ /**
28
+ * The property name a choice element takes for one of its permitted types.
29
+ *
30
+ * A choice element has no property of its own: FHIR declares one property per
31
+ * permitted type, named for the type code capitalized (`value[x]` as a
32
+ * SampledData is `valueSampledData`, as a `code` primitive it is `valueCode`).
33
+ *
34
+ * Pass the *type code*, never a profile name. R4 declares
35
+ * `Coverage.costToBeneficiary.value[x]` as a Quantity profiled to SimpleQuantity,
36
+ * and the property is `valueQuantity` — `valueSimpleQuantity` does not exist on
37
+ * any resource, so emitting it produces data every validator rejects.
38
+ *
39
+ * @param baseName the choice element's name with `[x]` already removed
40
+ */
41
+ export function choicePropertyName(baseName, typeCode) {
42
+ return baseName + capitalize(typeCode);
43
+ }
44
+ /**
45
+ * The single type a choice element has been narrowed to, or undefined while it
46
+ * still permits more than one.
47
+ *
48
+ * A still-open choice has no one property name, so callers must leave the base
49
+ * definition's expanded properties in force rather than pick a type.
50
+ */
51
+ export function narrowedChoiceType(field) {
52
+ if (field.typeOptions && field.typeOptions.length > 1)
53
+ return undefined;
54
+ const type = field.typeOptions?.[0]?.code || field.type;
55
+ if (!type || type === 'any' || type === 'unknown')
56
+ return undefined;
57
+ return type;
58
+ }
27
59
  /**
28
60
  * Extract base FHIR types from all interface declarations in a file.
29
61
  * Handles:
@@ -220,9 +252,16 @@ function caseAwareWriteFile(filePath, content) {
220
252
  }
221
253
  /**
222
254
  * Extracts the base resource name from a baseDefinition URL.
255
+ *
256
+ * The version suffix has to go first. A canonical may be pinned
257
+ * (`http://hl7.org/fhir/StructureDefinition/Base|4.0.1`, as IPS 2.0.1's logical
258
+ * models write it), and keeping it yielded the base name `Base_4_0_1` after
259
+ * sanitising. That defeated the `baseResource === 'Base'` check that suppresses
260
+ * extending the abstract root, so the emitter produced
261
+ * `export type Document = Base_4_0_1` plus an import of a type nothing generates.
223
262
  */
224
263
  export function getBaseResource(baseDefinition) {
225
- return baseDefinition?.split("/").pop() || "Resource";
264
+ return stripVersionFromCanonicalUrl(baseDefinition ?? '').split('/').pop() || 'Resource';
226
265
  }
227
266
  /**
228
267
  * Sanitizes a string so it can be used as a valid TypeScript identifier.
@@ -221,7 +221,9 @@ export function buildNestedRequirementsObject(nestedRequirements) {
221
221
  if (!nestedRequirements || nestedRequirements.length === 0) {
222
222
  return null;
223
223
  }
224
- // Collect all array segments from all requirements to know which paths need array wrapping
224
+ // Relative paths that are arrays, so nesting knows where to wrap. Keyed by the
225
+ // full relative path rather than by segment name: `name.part` is 0..* while the
226
+ // `part` string inside it is 1..1, and one shared verdict wrapped the string too.
225
227
  const allArraySegments = new Set();
226
228
  for (const req of nestedRequirements) {
227
229
  if (req.arraySegments) {
@@ -250,7 +252,7 @@ export function buildNestedRequirementsObject(nestedRequirements) {
250
252
  // Navigate/create nested objects for all but the last part
251
253
  for (let i = 0; i < parts.length - 1; i++) {
252
254
  const part = parts[i];
253
- const isArraySegment = allArraySegments.has(part);
255
+ const isArraySegment = allArraySegments.has(parts.slice(0, i + 1).join('.'));
254
256
  if (!(part in current)) {
255
257
  // Create either an array with one object or a plain object
256
258
  if (isArraySegment) {
@@ -346,7 +348,7 @@ export function buildNestedRequirementsObject(nestedRequirements) {
346
348
  }
347
349
  // If the final segment is an array segment, wrap the value in an array.
348
350
  // Also wrap if the field is inherently an array in FHIR base types
349
- const shouldWrap = (allArraySegments.has(lastPart) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
351
+ const shouldWrap = (allArraySegments.has(req.path) || FHIR_BASE_ARRAY_FIELDS.has(lastPart))
350
352
  && !Array.isArray(value);
351
353
  if (shouldWrap) {
352
354
  current[lastPart] = [value];
@@ -18,7 +18,7 @@
18
18
  * }
19
19
  * ```
20
20
  */
21
- import { capitalize, sanitizeIdentifier } from '../../core/utils.js';
21
+ import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier } from '../../core/utils.js';
22
22
  import { getRules } from '../../fhir/versionContext.js';
23
23
  import { logger } from '../../../logger.js';
24
24
  const log = logger.withTag('backbone-slices');
@@ -263,7 +263,18 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
263
263
  // metadata and their inherited type (e.g. Quantity) is misleading
264
264
  if (choiceTypeParents.has(relativePath))
265
265
  continue;
266
- const propName = relativePath.replace(/\[x\]$/, '');
266
+ // A choice element has no property of its own, so it is named for the single
267
+ // type the slice narrowed it to. Stripping `[x]` produced `value`, `effective`
268
+ // and the like — properties no resource carries, so the slice demanded a
269
+ // property FHIR has no such thing as while leaving the real `valueSampledData`
270
+ // unconstrained. A choice still open here names nothing and is skipped.
271
+ let propName = relativePath;
272
+ if (propName.endsWith('[x]')) {
273
+ const narrowedTo = narrowedChoiceType(child);
274
+ if (!narrowedTo)
275
+ continue;
276
+ propName = choicePropertyName(propName.slice(0, -'[x]'.length), narrowedTo);
277
+ }
267
278
  if (child.fixedValue !== undefined) {
268
279
  // Fixed value constraint
269
280
  const fixedLiteral = JSON.stringify(child.fixedValue);
@@ -191,6 +191,33 @@ export class ImportManager {
191
191
  }
192
192
  return statements.join('\n');
193
193
  }
194
+ /**
195
+ * Whether `name` is referenced in `body`, counting the profile ids that
196
+ * resolve to it as references too.
197
+ *
198
+ * An external profile can be reached under more than one name: the id taken
199
+ * from its canonical URL (`AuDosage` from `.../au-dosage`) and the interface
200
+ * name it is finally generated under (`AUBaseDosage`, from the SD's name).
201
+ * `generateImportStatements` deduplicates those to the resolved name, but the
202
+ * interface body is still written with the id at that point — a later pass
203
+ * rewrites it. Matching only the resolved name therefore pruned an import that
204
+ * the rewritten body goes on to need, leaving a dangling reference (TS2304 on
205
+ * `AUBaseDosage` in au-core 2.0.0's AUCoreMedicationStatement, and the same
206
+ * shape in davinci-pas and davinci-dtr).
207
+ */
208
+ isReferencedAllowingAliases(name, body) {
209
+ if (new RegExp(`\\b${name}\\b`).test(body))
210
+ return true;
211
+ if (!this.profileIdToName)
212
+ return false;
213
+ for (const [alias, resolved] of this.profileIdToName) {
214
+ if (resolved !== name || alias === name)
215
+ continue;
216
+ if (new RegExp(`\\b${alias}\\b`).test(body))
217
+ return true;
218
+ }
219
+ return false;
220
+ }
194
221
  /**
195
222
  * Prunes unused imports from generated code.
196
223
  * Removes import statements for types that aren't actually used.
@@ -213,8 +240,7 @@ export class ImportManager {
213
240
  const used = [];
214
241
  const specifiers = inside.split(',').map((s) => s.trim()).filter(Boolean);
215
242
  for (const name of specifiers) {
216
- const re = new RegExp(`\\b${name}\\b`);
217
- if (re.test(output.replace(full, ''))) {
243
+ if (this.isReferencedAllowingAliases(name, output.replace(full, ''))) {
218
244
  used.push(name);
219
245
  }
220
246
  }
@@ -1,4 +1,4 @@
1
- import { capitalize, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
1
+ import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, toPascalCase, stripVersionFromCanonicalUrl } from '../../core/utils.js';
2
2
  import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
3
3
  import { logger } from '../../../logger.js';
4
4
  import { getRules } from '../../fhir/versionContext.js';
@@ -214,25 +214,30 @@ export function processFields(ctx, fields, parentInterfaceName, parentFieldType,
214
214
  });
215
215
  return;
216
216
  }
217
- if (fieldName === "value[x]")
218
- fieldName = "value";
219
- if (fieldName === "event[x]" && field.type === "any" && field.fixedValue === undefined)
220
- return;
221
- if (fieldName === "event[x]")
222
- fieldName = "event";
223
- if (fieldName === "deceased[x]")
224
- fieldName = "deceased";
217
+ // `extension[x]` is a parser artifact rather than a FHIR choice — Extension.extension
218
+ // is a plain repeating element — so it keeps its name.
225
219
  if (fieldName === "extension[x]")
226
220
  fieldName = "extension";
227
- if (fieldName === "multipleBirth[x]")
228
- fieldName = "multipleBirth";
229
221
  // Helper to retrieve original last segment before local renaming
230
222
  function fieldNameOriginal(fullName) { return fullName.split('.').pop() || fullName; }
231
- // Generic handling: for choice elements (ending in [x]) that we do NOT explicitly map above,
232
- // we skip emitting them so the base resource's expanded choice properties remain intact (e.g., onsetDateTime, onsetAge,...).
233
- if (/\[x\]$/.test(fieldNameOriginal(field.name)) && !['value[x]', 'event[x]', 'deceased[x]', 'extension[x]', 'multipleBirth[x]'].includes(fieldNameOriginal(field.name))) {
234
- debug('skip choice element to inherit base choice fields', field.name);
235
- return;
223
+ // Choice elements carry no property of their own: FHIR declares one property
224
+ // per permitted type. A profile that narrows the choice to a single type names
225
+ // that property, so the constraint lands on the property the resource actually
226
+ // carries; anything still open is skipped, leaving the base definition's
227
+ // expanded choice properties in force (onsetDateTime, onsetAge, …).
228
+ //
229
+ // value[x], event[x], deceased[x] and multipleBirth[x] used to be renamed to
230
+ // value/event/deceased/multipleBirth, none of which exist on any resource.
231
+ // A slice narrowing Observation.component.value[x] to SampledData declared
232
+ // `value: SampledData`, so a caller writing correct FHIR failed to typecheck
233
+ // for a missing `value` while `valueSampledData` went unconstrained.
234
+ if (/\[x\]$/.test(fieldNameOriginal(field.name))) {
235
+ const narrowedTo = narrowedChoiceType(field);
236
+ if (!narrowedTo) {
237
+ debug('skip choice element to inherit base choice fields', field.name);
238
+ return;
239
+ }
240
+ fieldName = choicePropertyName(fieldName.replace(/\[x\]$/, ''), narrowedTo);
236
241
  }
237
242
  if (isFhirType(capitalize(fieldName)) && field.type === "any" && field.fixedValue === undefined && !(fieldName === 'extension' && hasSamePathExtensionSlices)) {
238
243
  if (isAaaCodesContext)
@@ -13,6 +13,22 @@ export function canonicalToTypeName(name, profileUrlToName) {
13
13
  }
14
14
  t = t.split('/').pop() || t;
15
15
  }
16
+ // A bare profile id also has a registered interface name, and it is not
17
+ // derivable from the id: davinci-pas 2.2.1's `profile-identifier` is generated
18
+ // as `PASIdentifier` (from the SD's `name`), so PascalCasing the id produced
19
+ // `ProfileIdentifier` — a type that is referenced and imported but never
20
+ // emitted (TS2307/TS2304 across seven files). Resolve through the registry by
21
+ // matching the URL's last segment, and only when it is unambiguous.
22
+ if (profileUrlToName && !/^https?:\/\//.test(t)) {
23
+ const matches = new Set();
24
+ for (const [url, generatedName] of profileUrlToName) {
25
+ if ((url.split('/').pop() || '').toLowerCase() === t.toLowerCase()) {
26
+ matches.add(generatedName);
27
+ }
28
+ }
29
+ if (matches.size === 1)
30
+ return [...matches][0];
31
+ }
16
32
  // if already a valid identifier with capitalization, return as-is
17
33
  if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(t) && /[A-Z]/.test(t[0]))
18
34
  return t;