babelfhir-ts 1.5.21 → 1.6.1

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 (57) hide show
  1. package/README.md +6 -3
  2. package/out/src/generator/core/constants.js +2 -1
  3. package/out/src/generator/core/extensionSdReader.js +4 -1
  4. package/out/src/generator/core/utils.js +91 -9
  5. package/out/src/generator/emitters/class/classGenerator.js +14 -8
  6. package/out/src/generator/emitters/class/classGeneratorHelpers.js +22 -9
  7. package/out/src/generator/emitters/class/classHeuristics.js +4 -2
  8. package/out/src/generator/emitters/class/fieldPatternExtractor.js +17 -13
  9. package/out/src/generator/emitters/class/sliceElementDefaults.js +21 -6
  10. package/out/src/generator/emitters/class/sliceElementGenerator.js +3 -3
  11. package/out/src/generator/emitters/interface/backboneSliceTyping.js +21 -13
  12. package/out/src/generator/emitters/interface/importManager.js +1 -1
  13. package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +15 -12
  14. package/out/src/generator/emitters/interface/interfaceFieldUtils.js +6 -3
  15. package/out/src/generator/emitters/interface/interfaceGenerator.js +14 -9
  16. package/out/src/generator/emitters/interface/postProcessExtensions.js +32 -16
  17. package/out/src/generator/emitters/interface/processFlatField.js +9 -7
  18. package/out/src/generator/emitters/interface/processNestedField.js +9 -10
  19. package/out/src/generator/emitters/namingsystem/namingSystemGenerator.js +136 -0
  20. package/out/src/generator/emitters/prefab/prefabEmitter.js +2 -15
  21. package/out/src/generator/emitters/prefab/prefabRenderer.js +2 -19
  22. package/out/src/generator/emitters/prefab/prefabTypeMapper.js +2 -0
  23. package/out/src/generator/emitters/validator/closedSlicingValidation.js +5 -2
  24. package/out/src/generator/emitters/validator/fhirpathStubInstaller.js +7 -1
  25. package/out/src/generator/emitters/validator/sliceBackboneValidation.js +27 -12
  26. package/out/src/generator/emitters/validator/sliceDelegation.js +8 -6
  27. package/out/src/generator/emitters/validator/sliceExtensionValidation.js +3 -3
  28. package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +41 -41
  29. package/out/src/generator/emitters/validator/sliceValidatorUtils.js +8 -3
  30. package/out/src/generator/emitters/validator/subExtensionScope.js +10 -2
  31. package/out/src/generator/emitters/validator/validatorBindingBuilder.js +4 -3
  32. package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +5 -5
  33. package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +8 -6
  34. package/out/src/generator/emitters/validator/validatorExpressions.js +44 -0
  35. package/out/src/generator/emitters/validator/validatorFieldBuilders.js +34 -17
  36. package/out/src/generator/emitters/validator/validatorGenerator.js +43 -47
  37. package/out/src/generator/emitters/validator/validatorRuntime.js +13 -2
  38. package/out/src/generator/emitters/validator/validatorTemplates.js +4 -1
  39. package/out/src/generator/emitters/valueset/valueSetGenerator.js +6 -10
  40. package/out/src/generator/emitters/zod/zodRefinementBuilder.js +48 -8
  41. package/out/src/generator/emitters/zod/zodSchemaGenerator.js +51 -24
  42. package/out/src/generator/fhir/corePackageResolver.js +2 -3
  43. package/out/src/generator/fhir/corePackageResolver.ts +2 -3
  44. package/out/src/generator/generationHelpers.js +59 -7
  45. package/out/src/generator/index.js +94 -49
  46. package/out/src/generator/parser/packageManager.js +4 -4
  47. package/out/src/generator/parser/packageParser.js +116 -64
  48. package/out/src/generator/parser/sdFetcher.js +5 -3
  49. package/out/src/generator/parser/sdParser.js +1 -1
  50. package/out/src/generator/parser/sliceAggregator.js +13 -10
  51. package/out/src/generator/parser/sliceAggregatorComposition.js +11 -2
  52. package/out/src/generator/parser/vsParser.js +6 -3
  53. package/out/src/generator/sdProcessor.js +30 -21
  54. package/out/src/generator/sdProcessorRequiredFields.js +3 -2
  55. package/out/src/main.js +5 -0
  56. package/package.json +13 -10
  57. package/parity-matrix.json +6 -0
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * BackboneElement and binding-codes slice validation strategies.
3
3
  */
4
+ import { firstSegment } from '../../core/utils.js';
4
5
  import { safeAccessor, safeList, sliceElseBranch, detectNestedArray, FHIR_PRIMITIVE_STRING_TYPES, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
5
6
  /** Collect required direct children of a BackboneElement slice for per-element validation. */
6
7
  export function collectRequiredSliceChildren(sliceElementId, fields, discriminatorProp, profileName, relPath) {
@@ -45,6 +46,11 @@ export function collectNestedRequiredSliceChildren(sliceElementId, fields, discr
45
46
  const parts = relativePath.split('.');
46
47
  if (parts.length !== 2)
47
48
  continue;
49
+ // Destructured so the two segments are values: the length check establishes
50
+ // there are exactly two, but indexing alone does not tell the compiler.
51
+ const [parentSegment, childSegment] = parts;
52
+ if (!parentSegment || !childSegment)
53
+ continue;
48
54
  if (relativePath.includes(':'))
49
55
  continue;
50
56
  if (f.max === 0)
@@ -52,20 +58,21 @@ export function collectNestedRequiredSliceChildren(sliceElementId, fields, discr
52
58
  const childMin = typeof f.min === 'number' ? f.min : (f.isOptional ? 0 : 1);
53
59
  if (childMin < 1)
54
60
  continue;
55
- const parentProp = parts[0].replace(/\[x\]$/, '');
61
+ const parentProp = parentSegment.replace(/\[x\]$/, '');
56
62
  if (parentProp === discriminatorProp)
57
63
  continue;
58
- const isChoiceType = parts[1].endsWith('[x]');
59
- const prop = parts[1].replace(/\[x\]$/, '');
64
+ const isChoiceType = childSegment.endsWith('[x]');
65
+ const prop = childSegment.replace(/\[x\]$/, '');
60
66
  const key = `${parentProp}.${prop}`;
61
67
  if (seen.has(key))
62
68
  continue;
63
69
  seen.add(key);
64
- const parentFieldId = `${sliceElementId}.${parts[0]}`;
70
+ const parentFieldId = `${sliceElementId}.${parentSegment}`;
65
71
  const parentField = fields.find(pf => (pf.elementId || pf.name) === parentFieldId);
66
72
  const isParentArray = parentField?.isArray ?? false;
67
- const baseParts = relPath.split('.');
68
- const lastPart = baseParts[baseParts.length - 1];
73
+ // relPath is non-empty, so split() yields a last segment; falling back to the
74
+ // whole path keeps the error path readable if it ever does not.
75
+ const lastPart = relPath.split('.').at(-1) ?? relPath;
69
76
  result.push({ parentProp, isParentArray, prop, errorPath: `${profileName}.${lastPart}.${parentProp}.${prop}`, isChoiceType });
70
77
  }
71
78
  return result;
@@ -87,7 +94,11 @@ export function collectFixedValueSliceChildren(sliceElementId, fields, discrimin
87
94
  // Handle choice type slicing: value[x]:valueString → prop "valueString"
88
95
  let prop = relativePath;
89
96
  if (prop.includes('[x]:')) {
90
- prop = prop.split(':')[1]; // "valueString", "valueCode", etc.
97
+ // The typed variant after the colon: "valueString", "valueCode", etc.
98
+ const typedVariant = prop.split(':')[1];
99
+ if (!typedVariant)
100
+ continue;
101
+ prop = typedVariant;
91
102
  }
92
103
  else if (prop.includes(':')) {
93
104
  continue; // Skip other slice references
@@ -117,13 +128,13 @@ export function generateBackboneElementValidation(slice, relPath, errorPath, sli
117
128
  const sliceId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
118
129
  let discriminatorProp = '';
119
130
  if (childFieldId.startsWith(sliceId + '.')) {
120
- discriminatorProp = childFieldId.substring(sliceId.length + 1).split('.')[0];
131
+ discriminatorProp = firstSegment(childFieldId.substring(sliceId.length + 1));
121
132
  }
122
133
  else {
123
134
  const childFieldName = discriminatorChild.name.replace(/^[^.]+\./, '');
124
135
  const sliceName = slice.name.replace(/^[^.]+\./, '');
125
136
  if (childFieldName.startsWith(sliceName + '.')) {
126
- discriminatorProp = childFieldName.substring(sliceName.length + 1).split('.')[0];
137
+ discriminatorProp = firstSegment(childFieldName.substring(sliceName.length + 1));
127
138
  }
128
139
  }
129
140
  if (discriminatorProp.includes('[x]')) {
@@ -167,7 +178,7 @@ export function generateBackboneElementValidation(slice, relPath, errorPath, sli
167
178
  }
168
179
  if (!checks)
169
180
  return;
170
- const resourceType = profileName || sliceElementId.split('.')[0];
181
+ const resourceType = profileName || firstSegment(sliceElementId);
171
182
  const requiredChildren = collectRequiredSliceChildren(sliceElementId, fields, discriminatorProp, resourceType, relPath);
172
183
  const nestedChildren = collectNestedRequiredSliceChildren(sliceElementId, fields, discriminatorProp, resourceType, relPath);
173
184
  const directChecksInner = requiredChildren.map(c => c.isChoiceType
@@ -257,12 +268,16 @@ export function generateBackboneElementValidation(slice, relPath, errorPath, sli
257
268
  */
258
269
  export function generateFixedValueSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, childFieldsWithFixedValue, out, profileName) {
259
270
  const sliceElementId = slice.elementId || slice.name;
271
+ // The caller only reaches here with a fixed-value child, but an empty list would
272
+ // otherwise read its discriminator off undefined.
260
273
  const discriminatorChild = childFieldsWithFixedValue[0];
274
+ if (!discriminatorChild)
275
+ return;
261
276
  const childFieldId = (discriminatorChild.elementId || discriminatorChild.name).replace(/^[^.]+\./, '');
262
277
  const sliceId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
263
278
  let discriminatorProp = '';
264
279
  if (childFieldId.startsWith(sliceId + '.')) {
265
- discriminatorProp = childFieldId.substring(sliceId.length + 1).split('.')[0];
280
+ discriminatorProp = firstSegment(childFieldId.substring(sliceId.length + 1));
266
281
  }
267
282
  if (!discriminatorProp)
268
283
  return;
@@ -270,7 +285,7 @@ export function generateFixedValueSliceValidation(slice, relPath, errorPath, sli
270
285
  ? `"${discriminatorChild.fixedValue}"`
271
286
  : String(discriminatorChild.fixedValue);
272
287
  const checks = `at(item, '${discriminatorProp}') === ${fixedVal}`;
273
- const resourceType = profileName || sliceElementId.split('.')[0];
288
+ const resourceType = profileName || firstSegment(sliceElementId);
274
289
  const requiredChildren = collectRequiredSliceChildren(sliceElementId, fields, discriminatorProp, resourceType, relPath);
275
290
  const nestedChildren = collectNestedRequiredSliceChildren(sliceElementId, fields, discriminatorProp, resourceType, relPath);
276
291
  const fixedChildren = collectFixedValueSliceChildren(sliceElementId, fields, discriminatorProp);
@@ -6,7 +6,7 @@
6
6
  * validator, which is what keeps findings inside a nested resource from being
7
7
  * lost. Split out of sliceValidatorGenerator, which had grown past the size limit.
8
8
  */
9
- import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
9
+ import { stripVersionFromCanonicalUrl, firstSegment } from '../../core/utils.js';
10
10
  import { safeAccessor, safeList, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
11
11
  export function generateProfiledChildDelegation(slice, relPath, childFieldsWithFixedValue, out, ctx) {
12
12
  if (!ctx.profileUrlToName || !ctx.validatorImports)
@@ -26,10 +26,12 @@ export function generateProfiledChildDelegation(slice, relPath, childFieldsWithF
26
26
  return;
27
27
  // Get discriminator info to build the filter expression
28
28
  const discriminatorChild = childFieldsWithFixedValue[0];
29
+ if (!discriminatorChild)
30
+ return;
29
31
  const childFieldId = (discriminatorChild.elementId || discriminatorChild.name).replace(/^[^.]+\./, '');
30
32
  let discriminatorProp = '';
31
33
  if (childFieldId.startsWith(sliceElemId + '.')) {
32
- discriminatorProp = childFieldId.substring(sliceElemId.length + 1).split('.')[0];
34
+ discriminatorProp = firstSegment(childFieldId.substring(sliceElemId.length + 1));
33
35
  }
34
36
  if (!discriminatorProp)
35
37
  return;
@@ -148,9 +150,9 @@ export function detectReferenceProfileDiscriminator(slice, profileUrlToType) {
148
150
  if (!slice.typeOptions?.length)
149
151
  return undefined;
150
152
  const refType = slice.typeOptions.find(t => t.code === 'Reference');
151
- if (!refType?.targetProfileUrls?.length)
153
+ const targetUrl = refType?.targetProfileUrls?.[0];
154
+ if (!targetUrl)
152
155
  return undefined;
153
- const targetUrl = refType.targetProfileUrls[0];
154
156
  // 1. Authoritative: resolve from profileUrlToType map
155
157
  if (profileUrlToType) {
156
158
  const resolved = profileUrlToType.get(targetUrl);
@@ -190,7 +192,7 @@ export function detectReferenceProfileDiscriminator(slice, profileUrlToType) {
190
192
  export function generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, targetResourceType, out, fields, baseResourceType) {
191
193
  // Build within-slice child max constraint checks (e.g., max=0 forbidden fields)
192
194
  const maxChildren = fields ? collectWithinSliceMaxChildren(slice, fields) : [];
193
- const resType = baseResourceType || (slice.elementId || slice.name).split('.')[0];
195
+ const resType = baseResourceType || firstSegment(slice.elementId || slice.name);
194
196
  const childMaxChecks = generateWithinSliceMaxChecks(maxChildren, `_${varName}Refs`, resType, relPath, sliceLabel);
195
197
  out.push(`
196
198
  // Required Reference-profile slice validation for ${relPath}:${sliceLabel} (target → ${targetResourceType})
@@ -333,7 +335,7 @@ export function detectProfileDiscriminator(slice, fields, profileUrlToType) {
333
335
  }
334
336
  // 2. Try to extract from profile URL last segment (core profiles)
335
337
  const profileUrl = profileUrls[0];
336
- const lastSegment = profileUrl.split('/').pop() || '';
338
+ const lastSegment = profileUrl?.split('/').pop() || '';
337
339
  if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
338
340
  return { resourceType: lastSegment, profileUrls };
339
341
  }
@@ -2,7 +2,7 @@
2
2
  * Extension and ValueSet slice validation strategies.
3
3
  */
4
4
  import { safeAccessor, safeList, sliceElseBranch, detectNestedArray, findArrayAncestor, isArrayPath, FHIR_PRIMITIVE_STRING_TYPES } from './sliceValidatorUtils.js';
5
- import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
5
+ import { stripVersionFromCanonicalUrl, stripSliceSuffix } from '../../core/utils.js';
6
6
  import { buildSubExtensionScope, renderScopeHops } from './subExtensionScope.js';
7
7
  /**
8
8
  * Read pre-resolved Extension SD metadata for a canonical.
@@ -267,7 +267,7 @@ export function generateExistsExtensionValidation(slice, relPath, errorPath, sli
267
267
  * profile URL that can serve as the extension URL discriminator.
268
268
  */
269
269
  export function detectExtensionUrlSlice(slice, relPath, fields) {
270
- const basePath = relPath.split(':')[0];
270
+ const basePath = stripSliceSuffix(relPath);
271
271
  if (!basePath.endsWith('extension') && !basePath.endsWith('modifierExtension'))
272
272
  return undefined;
273
273
  if (slice.profileUrls && slice.profileUrls.length > 0) {
@@ -335,7 +335,7 @@ export function generateExtensionUrlSliceValidation(slice, relPath, errorPath, s
335
335
  const scopedElemId = (slice.elementId || '').replace(/^[^.]+\./, '');
336
336
  const scopedErrorPath = scopedElemId || `${errorPath}:${sliceLabel}`;
337
337
  // The slot the slice lives in: its own last segment, minus the slice qualifier.
338
- const leafProp = scopedElemId.split('.').pop().split(':')[0];
338
+ const leafProp = stripSliceSuffix(scopedElemId.split('.').pop() ?? '');
339
339
  const body = renderScopeHops(subScope, (container, indent) => {
340
340
  const matches = `${varName}Matches`;
341
341
  let inner = `\n${indent}const ${matches} = elements(at(${container}, '${leafProp}')).filter((item) => item.url === "${extensionUrl}");`;
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import { addDiagnostic } from '../../core/sdDiagnostics.js';
7
7
  import { INLINE_RESOURCE_SLOTS } from './validatorFieldBuilders.js';
8
- import { safeAccessor, safeList, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, resolveNestedPatternChild } from './sliceValidatorUtils.js';
8
+ import { firstSegment, stripSliceSuffix } from '../../core/utils.js';
9
+ import { safeAccessor, safeList, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, resolveNestedPatternChild, collectWithinSliceMaxChildren, generateWithinSliceMaxChecks } from './sliceValidatorUtils.js';
9
10
  import { generateProfiledChildDelegation, detectReferenceProfileDiscriminator, generateReferenceProfileDiscriminatorValidation, generateOptionalProfiledSliceDelegation, detectProfileDiscriminator, generateProfileDiscriminatorValidation, detectSelfTypeDiscriminator, generateSelfTypeSliceValidation, generateDatatypeProfileSliceValidation, } from './sliceDelegation.js';
10
11
  import { generateBackboneElementValidation, generateBindingCodesValidation, generateFixedValueSliceValidation } from './sliceBackboneValidation.js';
11
12
  import { generateValueSetValidation, detectExistsExtensionDiscriminator, generateExistsExtensionValidation, detectExtensionUrlSlice, generateExtensionUrlSliceValidation, } from './sliceExtensionValidation.js';
@@ -144,7 +145,7 @@ export function generateSliceValidations(ctx) {
144
145
  return subMin > 0;
145
146
  }).map(f => {
146
147
  const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
147
- return fId.substring(sliceElemId.length + 1).split('.')[0];
148
+ return firstSegment(fId.substring(sliceElemId.length + 1));
148
149
  });
149
150
  generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, sliceValidations, requiredSubFields, ctx.baseResourceType, ctx.profileUrl);
150
151
  }
@@ -152,17 +153,21 @@ export function generateSliceValidations(ctx) {
152
153
  // For extension slices where the only fixedValue child is 'url', prefer the
153
154
  // extension URL validation path — it includes value[x] type checking from
154
155
  // resolved ExtensionMetadata, which generic fixedValue validation doesn't.
155
- const isExtensionUrlOnly = childFieldsWithFixedValue.length === 1 &&
156
- childFieldsWithFixedValue[0].fixedValue &&
157
- typeof childFieldsWithFixedValue[0].fixedValue === 'string' &&
156
+ // Read the sole child once rather than indexing it five times.
157
+ const onlyFixedChild = childFieldsWithFixedValue.length === 1
158
+ ? childFieldsWithFixedValue[0]
159
+ : undefined;
160
+ const extUrl = typeof onlyFixedChild?.fixedValue === 'string'
161
+ ? onlyFixedChild.fixedValue
162
+ : undefined;
163
+ const isExtensionUrlOnly = !!onlyFixedChild && extUrl !== undefined &&
158
164
  (() => {
159
- const fId = (childFieldsWithFixedValue[0].elementId || childFieldsWithFixedValue[0].name).replace(/^[^.]+\./, '');
165
+ const fId = (onlyFixedChild.elementId || onlyFixedChild.name).replace(/^[^.]+\./, '');
160
166
  const sliceEId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
161
167
  const relChild = fId.startsWith(sliceEId + '.') ? fId.substring(sliceEId.length + 1) : '';
162
168
  return relChild === 'url';
163
169
  })();
164
- if (isExtensionUrlOnly) {
165
- const extUrl = childFieldsWithFixedValue[0].fixedValue;
170
+ if (isExtensionUrlOnly && extUrl !== undefined) {
166
171
  generateExtensionUrlSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, extUrl, fieldPathMap, arrayFieldPaths, sliceValidations, ctx);
167
172
  }
168
173
  else {
@@ -194,6 +199,10 @@ export function generateSliceValidations(ctx) {
194
199
  generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, profileMatch, sliceValidations, ctx);
195
200
  }
196
201
  else {
202
+ const sliceProfileUrl = slice.profileUrls?.[0];
203
+ const datatypePin = sliceProfileUrl
204
+ ? ctx.datatypeProfilePins?.get(sliceProfileUrl)
205
+ : undefined;
197
206
  const refResourceType = detectReferenceProfileDiscriminator(slice, ctx.profileUrlToType);
198
207
  if (refResourceType) {
199
208
  generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, refResourceType, sliceValidations, fields, ctx.baseResourceType);
@@ -201,10 +210,11 @@ export function generateSliceValidations(ctx) {
201
210
  else if (slice.fixedValue !== undefined && !isChoiceTypeSlice) {
202
211
  generateSelfFixedValueSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, sliceValidations);
203
212
  }
204
- else if (slice.profileUrls?.length && ctx.datatypeProfilePins?.get(slice.profileUrls[0])) {
213
+ else if (datatypePin) {
205
214
  // A {profile, $this} slice against a datatype profile: the profile's
206
- // own pinned element is what distinguishes its members.
207
- generateDatatypeProfileSliceValidation(relPath, errorPath, sliceLabel, varName, min, ctx.datatypeProfilePins.get(slice.profileUrls[0]), sliceValidations);
215
+ // own pinned element is what distinguishes its members. Resolved once
216
+ // above, which also drops the non-null assertion on the second lookup.
217
+ generateDatatypeProfileSliceValidation(relPath, errorPath, sliceLabel, varName, min, datatypePin, sliceValidations);
208
218
  }
209
219
  else if (INLINE_RESOURCE_SLOTS.has(relPath) && detectSelfTypeDiscriminator(slice)) {
210
220
  // An inline-resource slot sliced by {type, $this}: the member is the
@@ -310,28 +320,12 @@ function generatePatternCodeableConceptValidation(slice, relPath, errorPath, sli
310
320
  const sliceFullId = slice.sliceName && !sliceElemId.includes(':')
311
321
  ? `${sliceElemId}:${slice.sliceName}`
312
322
  : sliceElemId;
313
- const resType = baseResourceType || sliceElemId.split('.')[0];
314
- const childMaxFields = fields.filter(f => {
315
- const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
316
- if (!fId.startsWith(sliceFullId + '.'))
317
- return false;
318
- const relChild = fId.substring(sliceFullId.length + 1);
319
- if (relChild.includes('.'))
320
- return false;
321
- return typeof f.max === 'number' && f.max < Infinity;
322
- });
323
- for (const child of childMaxFields) {
324
- const childFullId = (child.elementId || child.name).replace(/^[^.]+\./, '');
325
- const childProp = childFullId.substring(sliceFullId.length + 1);
326
- const maxVal = child.max;
327
- childMaxChecks += `
328
- for (const _wsElem of ${varName}Elements) {
329
- const _wsArr = at(_wsElem, '${childProp}');
330
- if (Array.isArray(_wsArr) && _wsArr.length > ${maxVal}) {
331
- errors.push("${resType}.${relPath}:${sliceLabel}.${childProp}: max allowed = ${maxVal}, but found " + _wsArr.length);
332
- }
333
- }`;
334
- }
323
+ const resType = baseResourceType || firstSegment(sliceElemId);
324
+ // Shared with the backbone and delegation slice validators. The inline copy
325
+ // this replaces was array-only, so `max=0` on a singleton child could never
326
+ // fire, and it rejected any dotted path, so a grandchild like
327
+ // `entity:patient.what.reference` was never reached.
328
+ childMaxChecks += generateWithinSliceMaxChecks(collectWithinSliceMaxChildren(slice, fields), `${varName}Elements`, resType, relPath, sliceLabel);
335
329
  // Build within-slice fixedValue checks on nested descendants.
336
330
  // E.g. category:labCategory.coding.system fixed to a specific URI.
337
331
  const fixedValueDescendants = fields.filter(f => {
@@ -371,11 +365,11 @@ function generatePatternCodeableConceptValidation(slice, relPath, errorPath, sli
371
365
  // pattern coding values as effectively fixed on each coding element
372
366
  // within matched slice members. The HL7 validator treats pattern values
373
367
  // as fixed when combined with max=1 cardinality.
374
- const codingChild = childMaxFields.find(f => {
368
+ const codingChild = fields.find(f => {
375
369
  const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
376
- return fId.substring(sliceFullId.length + 1) === 'coding';
370
+ return fId.startsWith(sliceFullId + '.') && fId.substring(sliceFullId.length + 1) === 'coding';
377
371
  });
378
- if (codingChild && typeof codingChild.max === 'number' && codingChild.max === 1) {
372
+ if (codingChild?.max === 1) {
379
373
  const pRef = profileUrl ? `${profileUrl}#${resType}.${relPath}:${sliceLabel}` : `${resType}.${relPath}:${sliceLabel}`;
380
374
  for (const pc of patternCodings) {
381
375
  const pSys = pc.system ? String(pc.system) : undefined;
@@ -462,7 +456,7 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
462
456
  // nothing where HL7 reports the mismatch. The CodeableConcept sibling has done
463
457
  // this for its descendants all along — the gap was only here.
464
458
  const sliceIdRel = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
465
- const resType = baseResourceType || sliceIdRel.split('.')[0];
459
+ const resType = baseResourceType || firstSegment(sliceIdRel);
466
460
  const fixedChildChecks = fields
467
461
  .filter(f => {
468
462
  if (typeof f.fixedValue !== 'string' && typeof f.fixedValue !== 'number' && f.fixedValue !== true)
@@ -508,10 +502,12 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
508
502
  const sliceIdParts = sliceElemId.split('.');
509
503
  for (let i = 0; i < sliceIdParts.length - 1; i++) {
510
504
  const part = sliceIdParts[i];
511
- if (part.includes(':')) {
505
+ if (part?.includes(':')) {
512
506
  const [basePath, sliceNameInId] = part.split(':');
507
+ if (!basePath)
508
+ continue;
513
509
  const parentSlice = fields.find(f => f.sliceName === sliceNameInId &&
514
- f.name.replace(/^[^.]+\./, '').split('.')[0] === basePath.split(':')[0]);
510
+ firstSegment(f.name.replace(/^[^.]+\./, '')) === stripSliceSuffix(basePath));
515
511
  if (parentSlice) {
516
512
  const parentMin = typeof parentSlice.min === 'number' ? parentSlice.min : (parentSlice.isOptional ? 0 : 1);
517
513
  if (parentMin === 0) {
@@ -522,9 +518,13 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
522
518
  }
523
519
  }
524
520
  const matchedChecks = subFieldChecks + fixedChildChecks;
525
- const subFieldBlock = matchedChecks ? `
521
+ // Within-slice max cardinality on this slice's own children, through the same
522
+ // helper the backbone and delegation validators use. This validator never ran
523
+ // them, so a `max=0` child of a Coding slice went unreported.
524
+ const maxChecks = generateWithinSliceMaxChecks(collectWithinSliceMaxChildren(slice, fields), `${varName}Elements`, resType, relPath, sliceLabel);
525
+ const subFieldBlock = (matchedChecks ? `
526
526
  for (const matched of ${varName}Elements) {${matchedChecks}
527
- }` : '';
527
+ }` : '') + maxChecks;
528
528
  if (isNestedInArray && arrayParentPath) {
529
529
  const remainingPath = relPath.substring(arrayParentPath.length + 1);
530
530
  const optionalPath = remainingPath.split('.').join('?.');
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Shared utility functions for slice validation code generation.
3
3
  */
4
+ import { stripSliceSuffix } from '../../core/utils.js';
4
5
  /** FHIR primitive type codes whose JSON extensions use _fieldName prefix. */
5
6
  export const FHIR_PRIMITIVES = new Set([
6
7
  'boolean', 'integer', 'string', 'decimal', 'uri', 'url', 'canonical',
@@ -56,7 +57,7 @@ export function detectNestedArray(relPath, fields) {
56
57
  const partialPath = pathParts.slice(0, i + 1).join('.');
57
58
  const parentField = fields.find(f => {
58
59
  const fieldPath = f.name.replace(/^[^.]+\./, '');
59
- return fieldPath === partialPath || fieldPath.split(':')[0] === partialPath;
60
+ return fieldPath === partialPath || stripSliceSuffix(fieldPath) === partialPath;
60
61
  });
61
62
  if (parentField && parentField.isArray) {
62
63
  return {
@@ -101,7 +102,7 @@ export function rewritePrimitiveExtensionPath(relPath, fields) {
101
102
  const sdSuffix = parts.slice(0, extIdx).join('.');
102
103
  const parentField = fields.find(f => {
103
104
  const fPath = f.name.replace(/^[^.]+\./, '');
104
- return fPath === sdSuffix || fPath.split(':')[0] === sdSuffix;
105
+ return fPath === sdSuffix || stripSliceSuffix(fPath) === sdSuffix;
105
106
  });
106
107
  if (parentField && FHIR_PRIMITIVES.has(parentField.type || '')) {
107
108
  parts[extIdx - 1] = `_${parentSegment}`;
@@ -209,7 +210,11 @@ export function collectWithinSliceMaxChildren(slice, fields) {
209
210
  .every((_, i) => isTraversableIntermediate(segments.slice(0, i + 1).join('.')));
210
211
  if (!intermediatesOk)
211
212
  continue;
212
- const leaf = segments[segments.length - 1];
213
+ const leaf = segments.at(-1);
214
+ // relChild is non-empty here, so split() always produced a leaf; the guard is
215
+ // what tells the compiler, and skipping is right if a path ever arrives empty.
216
+ if (!leaf)
217
+ continue;
213
218
  const isChoiceType = leaf.endsWith('[x]');
214
219
  const propPath = [...segments.slice(0, -1), isChoiceType ? leaf.replace(/\[x\]$/, '') : leaf];
215
220
  result.push({ propPath, fhirPath: relChild, maxVal: f.max, isArray: !!f.isArray, isChoiceType });
@@ -25,7 +25,8 @@ export function instanceUrlForSlice(slice, fields) {
25
25
  if (childId === sliceFullId + '.url' && typeof f.fixedValue === 'string')
26
26
  return f.fixedValue;
27
27
  }
28
- return slice.profileUrls?.length ? stripVersionFromCanonicalUrl(slice.profileUrls[0]) : undefined;
28
+ const firstProfile = slice.profileUrls?.[0];
29
+ return firstProfile ? stripVersionFromCanonicalUrl(firstProfile) : undefined;
29
30
  }
30
31
  /**
31
32
  * Build the walk down to the elements that *contain* a sub-extension slice.
@@ -54,7 +55,14 @@ export function buildSubExtensionScope(slice, fields) {
54
55
  return null;
55
56
  const hops = [];
56
57
  for (let i = 0; i < containerSegments.length; i++) {
57
- const [prop, sliceName] = containerSegments[i].split(':');
58
+ const segment = containerSegments[i];
59
+ if (segment === undefined)
60
+ return null;
61
+ const [prop, sliceName] = segment.split(':');
62
+ // split() on a non-empty segment always yields a first part; bailing keeps the
63
+ // "no unscoped walk" guarantee this function exists for.
64
+ if (!prop)
65
+ return null;
58
66
  let url;
59
67
  if (sliceName) {
60
68
  const ancestorId = containerSegments.slice(0, i + 1).join('.');
@@ -1,3 +1,4 @@
1
+ import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
1
2
  import { buildNestedLoopCode, emitSystemPresenceLeaf, emitSystemOnlyLeaf, emitResolvedLeaf, } from './validatorBindingLeafEmitters.js';
2
3
  /**
3
4
  * Emit a typed read of `rel` off the resource.
@@ -106,7 +107,7 @@ function emitSystemPresenceFallback(output, field, rel, isNested, arrayFieldPath
106
107
  const type = field.type || field.baseTypeCode;
107
108
  if (type !== 'CodeableConcept' && type !== 'Coding' && type !== 'Quantity')
108
109
  return;
109
- const vsUri = (field.binding.uri || '').split('|')[0];
110
+ const vsUri = stripVersionFromCanonicalUrl(field.binding.uri || '');
110
111
  const vsDisplayName = vsUri.split('/').pop() || vsUri;
111
112
  if (isNested) {
112
113
  const code = buildNestedLoopCode(field, rel, arrayFieldPaths, (leafAcc, indent, fType) => emitSystemPresenceLeaf(leafAcc, indent, fType, vsDisplayName, vsUri, field.isArray), `Required binding system-presence fallback for ${rel} (nested, ValueSet unresolved)`, type);
@@ -139,7 +140,7 @@ function emitSystemOnlyBinding(output, field, rel, isNested, vsInfo, arrayFieldP
139
140
  const type = field.type || field.baseTypeCode;
140
141
  if (type !== 'CodeableConcept' && type !== 'Coding' && type !== 'Quantity')
141
142
  return;
142
- const sysVsUri = (field.binding.uri || '').split('|')[0];
143
+ const sysVsUri = stripVersionFromCanonicalUrl(field.binding.uri || '');
143
144
  const systemsLiteral = JSON.stringify(vsInfo.systems);
144
145
  const vsDisplayName = vsInfo.displayName;
145
146
  if (isNested) {
@@ -186,7 +187,7 @@ function emitResolvedBinding(output, field, rel, isNested, vsInfo, arrayFieldPat
186
187
  const type = field.type || field.baseTypeCode;
187
188
  const vsName = vsInfo.sanitizedName;
188
189
  const vsDisplayName = vsInfo.displayName;
189
- const vsUri = (field.binding.uri || '').split('|')[0];
190
+ const vsUri = stripVersionFromCanonicalUrl(field.binding.uri || '');
190
191
  if (isNested) {
191
192
  const code = buildNestedLoopCode(field, rel, arrayFieldPaths, (leafAcc, indent, fType) => emitResolvedLeaf(leafAcc, indent, fType, vsName, vsDisplayName, vsUri, field.isArray), `Required ValueSet binding validation for ${rel} (nested)`, type);
192
193
  if (code)
@@ -1,3 +1,4 @@
1
+ import { stripSliceSuffix } from '../../core/utils.js';
1
2
  /**
2
3
  * Build nested loop code for iterating through array parents to reach a leaf field.
3
4
  * Shared by all three binding modes (system-presence, system-only, resolved).
@@ -6,10 +7,10 @@ export function buildNestedLoopCode(field, rel, arrayFieldPaths, emitLeaf, comme
6
7
  const parts = rel.split('.');
7
8
  const leafField = parts[parts.length - 1];
8
9
  const loopInfos = [];
9
- for (let i = 0; i < parts.length - 1; i++) {
10
+ for (const [i, segment] of parts.slice(0, -1).entries()) {
10
11
  const partialPath = parts.slice(0, i + 1).join('.');
11
- const isArr = arrayFieldPaths.has(partialPath) || arrayFieldPaths.has(partialPath.split(':')[0]);
12
- loopInfos.push({ segment: parts[i], isArray: isArr });
12
+ const isArr = arrayFieldPaths.has(partialPath) || arrayFieldPaths.has(stripSliceSuffix(partialPath));
13
+ loopInfos.push({ segment, isArray: isArr });
13
14
  }
14
15
  // A nested path with no array ancestor is still reachable: the loop below already
15
16
  // descends plain levels with an `if (… !== undefined)`. Requiring an array meant
@@ -21,8 +22,7 @@ export function buildNestedLoopCode(field, rel, arrayFieldPaths, emitLeaf, comme
21
22
  let code = `\n // ${comment}`;
22
23
  let indent = ' ';
23
24
  let currentVar = 'resource';
24
- for (let i = 0; i < loopInfos.length; i++) {
25
- const li = loopInfos[i];
25
+ for (const [i, li] of loopInfos.entries()) {
26
26
  // Every level goes through `at()`, including the first. Reading `resource.x`
27
27
  // directly assumed the emitted interface declares `x`, which a logical model
28
28
  // does not: SDCQuestionLibrary derives from Resource, so `resource.address`
@@ -1,3 +1,4 @@
1
+ import { stripSliceSuffix } from '../../core/utils.js';
1
2
  /**
2
3
  * Build required-field existence constraints.
3
4
  * Emits FHIRPath-style constraints for fields with min >= 1.
@@ -158,7 +159,8 @@ export function buildForbiddenFieldConstraintEntries(fields) {
158
159
  const eidRel = eid.replace(/^[^.]+\./, '');
159
160
  const concreteParts = eidRel.split('.').map(p => {
160
161
  if (p.includes(':')) {
161
- const [basePart, sliceName] = p.split(':', 2);
162
+ const basePart = stripSliceSuffix(p);
163
+ const sliceName = p.substring(p.indexOf(':') + 1);
162
164
  if (basePart.includes('[x]') && sliceName)
163
165
  return sliceName;
164
166
  return basePart;
@@ -216,7 +218,7 @@ export function buildPatternValidations(fields, arrayFieldPaths, fieldPathMap) {
216
218
  if (!isNestedInArray) {
217
219
  for (let i = 0; i < pathParts.length - 1; i++) {
218
220
  const parentPath = pathParts.slice(0, i + 1).join('.');
219
- if (arrayFieldPaths.has(parentPath) || arrayFieldPaths.has(parentPath.split(':')[0])) {
221
+ if (arrayFieldPaths.has(parentPath) || arrayFieldPaths.has(stripSliceSuffix(parentPath))) {
220
222
  isNestedInArray = true;
221
223
  break;
222
224
  }
@@ -224,7 +226,7 @@ export function buildPatternValidations(fields, arrayFieldPaths, fieldPathMap) {
224
226
  }
225
227
  const pattern = field.patternConstraint;
226
228
  const parentFieldName = pathParts.length === 2 ? pathParts[0] : '';
227
- const parentIsArray = parentFieldName && (arrayFieldPaths.has(parentFieldName) || arrayFieldPaths.has(parentFieldName.split(':')[0]));
229
+ const parentIsArray = parentFieldName && (arrayFieldPaths.has(parentFieldName) || arrayFieldPaths.has(stripSliceSuffix(parentFieldName)));
228
230
  const isNestedCodingPattern = pathParts.length === 2 && rel.endsWith('.coding')
229
231
  && parentIsArray
230
232
  && pattern && typeof pattern === 'object' && 'code' in pattern && !('coding' in pattern);
@@ -268,10 +270,10 @@ export function buildPatternValidations(fields, arrayFieldPaths, fieldPathMap) {
268
270
  return patternValidations;
269
271
  }
270
272
  function emitNestedCodingPattern(output, fields, pathParts, parentFieldName, pattern, humanRel) {
271
- const parentPath = pathParts[0];
272
- const parentBasePath = parentPath.split(':')[0];
273
+ const parentPath = pathParts[0] ?? '';
274
+ const parentBasePath = stripSliceSuffix(parentPath);
273
275
  const parentIsSliced = fields.some(f => f.sliceName &&
274
- (f.name || '').replace(/^[^.]+\./, '').split(':')[0] === parentBasePath);
276
+ stripSliceSuffix((f.name || '').replace(/^[^.]+\./, '')) === parentBasePath);
275
277
  if (parentIsSliced)
276
278
  return;
277
279
  // Same split as the CodeableConcept case: identity here, pinned details below.
@@ -48,6 +48,50 @@ export function normalizeCollectionTypeFilters(expression) {
48
48
  * They are dropped for nested field paths, and kept at a single level for
49
49
  * recursive elements, rather than being walked through the whole recursion.
50
50
  */
51
+ /**
52
+ * Escape a constraint expression for embedding in a double-quoted emitted string,
53
+ * and scope it to its element.
54
+ *
55
+ * Shared by the validator and the Zod emitter. Both evaluate the same FHIRPath
56
+ * against the same resource, so a second copy of this scoping would make the two
57
+ * disagree on the same profile — which parity would report as a real mismatch.
58
+ *
59
+ * If the expression does not already navigate from `fieldPath`, it is wrapped in
60
+ * `<path>.all(...)`. Only the START of the expression is checked: built
61
+ * constraints (e.g. "action.exists()") begin with the fieldPath, while
62
+ * field-level constraints are relative to the element and start with child
63
+ * names. Matching the fieldPath anywhere would false-positive on recursive
64
+ * elements, where "resource.exists() != action.exists()" on fieldPath "action"
65
+ * mentions the name only as a child reference.
66
+ *
67
+ * Resource-root constraints (fieldPath === baseResourceType) need the wrapping
68
+ * too: FHIRPath `implies` yields empty when the antecedent is missing, and
69
+ * `.all()` converts that to false, which is what HL7 and Firely report.
70
+ */
71
+ export function scopeConstraintExpression(expression, fieldPath, opts = {}) {
72
+ let escaped = normalizeCollectionTypeFilters(expression || '')
73
+ .replace(/\s*\n\s*/g, ' ')
74
+ .replace(/\\/g, '\\\\')
75
+ .replace(/"/g, '\\"');
76
+ if (!fieldPath)
77
+ return escaped;
78
+ const escapedPath = fieldPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ const navigationPattern = new RegExp(`^${escapedPath}\\s*\\.`);
80
+ const standalonePattern = new RegExp(`^${escapedPath}(?:[^.\\w]|$)`);
81
+ if (navigationPattern.test(escaped) || standalonePattern.test(escaped))
82
+ return escaped;
83
+ const resourceTypePattern = /^[A-Z][a-zA-Z]+\./;
84
+ if (resourceTypePattern.test(escaped))
85
+ return escaped;
86
+ const walkRecursion = !!opts.isRecursiveElement?.(fieldPath)
87
+ && !isBaseElementInvariant(expression || '');
88
+ const navigation = opts.sliceFilter
89
+ ? `${quoteFhirPathPath(fieldPath)}.where(${opts.sliceFilter})`
90
+ : quoteFhirPathPath(fieldPath);
91
+ const scope = walkRecursion ? `repeat(${navigation})` : navigation;
92
+ escaped = `${scope}.all(${escaped})`;
93
+ return escaped;
94
+ }
51
95
  export function isBaseElementInvariant(expression) {
52
96
  return /^hasValue\(\)\s+or\s+\(children\(\)\.count\(\)\s*>\s*id\.count\(\)\)$/.test(expression)
53
97
  || /^extension\.exists\(\)\s*!=\s*value\.exists\(\)$/.test(expression);