babelfhir-ts 1.5.19 → 1.5.21
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.
- package/out/src/cli/updateCommand.js +19 -12
- package/out/src/generator/core/constants.js +19 -0
- package/out/src/generator/core/extensionSdReader.js +99 -0
- package/out/src/generator/core/utils.js +97 -3
- package/out/src/generator/emitters/class/classTemplate.js +8 -2
- package/out/src/generator/emitters/class/sliceElementDefaults.js +5 -3
- package/out/src/generator/emitters/client/clientGenerator.js +22 -6
- package/out/src/generator/emitters/client/smartAuthGenerator.js +4 -1
- package/out/src/generator/emitters/interface/backboneSliceTyping.js +15 -4
- package/out/src/generator/emitters/interface/importManager.js +28 -2
- package/out/src/generator/emitters/interface/interfaceFieldProcessor.js +21 -16
- package/out/src/generator/emitters/interface/interfaceFieldUtils.js +16 -0
- package/out/src/generator/emitters/interface/postProcessExtensions.js +1 -0
- package/out/src/generator/emitters/validator/closedSlicingValidation.js +146 -0
- package/out/src/generator/emitters/validator/sliceDelegation.js +450 -0
- package/out/src/generator/emitters/validator/sliceExtensionValidation.js +200 -35
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +130 -299
- package/out/src/generator/emitters/validator/sliceValidatorUtils.js +89 -16
- package/out/src/generator/emitters/validator/subExtensionScope.js +100 -0
- package/out/src/generator/emitters/validator/validatorBindingBuilder.js +113 -237
- package/out/src/generator/emitters/validator/validatorBindingLeafEmitters.js +13 -4
- package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +104 -63
- package/out/src/generator/emitters/validator/validatorExpressions.js +54 -0
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +492 -46
- package/out/src/generator/emitters/validator/validatorGenerator.js +170 -169
- package/out/src/generator/emitters/validator/validatorRuntime.js +279 -0
- package/out/src/generator/emitters/validator/validatorTemplates.js +186 -54
- package/out/src/generator/emitters/zod/zodRefinementBuilder.js +29 -19
- package/out/src/generator/emitters/zod/zodSchemaGenerator.js +16 -1
- package/out/src/generator/index.js +21 -11
- package/out/src/generator/parser/packageParser.js +1 -8
- package/out/src/generator/parser/sdParser.js +1 -0
- package/out/src/generator/sdProcessor.js +303 -30
- package/out/src/generator/sdProcessorHelpers.js +1 -1
- package/out/src/generator/sdProcessorRequiredFields.js +120 -0
- package/out/src/main.js +37 -11
- package/package.json +3 -1
- package/parity-matrix.json +18 -18
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* (patternCodeableConcept, patternCoding, BackboneElement, binding, ValueSet).
|
|
5
5
|
*/
|
|
6
6
|
import { addDiagnostic } from '../../core/sdDiagnostics.js';
|
|
7
|
-
import {
|
|
7
|
+
import { INLINE_RESOURCE_SLOTS } from './validatorFieldBuilders.js';
|
|
8
|
+
import { safeAccessor, safeList, sliceElseBranch, rewritePrimitiveExtensionPath, isArrayPath, resolveNestedPatternChild } from './sliceValidatorUtils.js';
|
|
9
|
+
import { generateProfiledChildDelegation, detectReferenceProfileDiscriminator, generateReferenceProfileDiscriminatorValidation, generateOptionalProfiledSliceDelegation, detectProfileDiscriminator, generateProfileDiscriminatorValidation, detectSelfTypeDiscriminator, generateSelfTypeSliceValidation, generateDatatypeProfileSliceValidation, } from './sliceDelegation.js';
|
|
8
10
|
import { generateBackboneElementValidation, generateBindingCodesValidation, generateFixedValueSliceValidation } from './sliceBackboneValidation.js';
|
|
9
11
|
import { generateValueSetValidation, detectExistsExtensionDiscriminator, generateExistsExtensionValidation, detectExtensionUrlSlice, generateExtensionUrlSliceValidation, } from './sliceExtensionValidation.js';
|
|
10
12
|
/**
|
|
@@ -29,8 +31,37 @@ export function generateSliceValidations(ctx) {
|
|
|
29
31
|
slicesByBase.forEach((slices, baseName) => {
|
|
30
32
|
slices.forEach(slice => {
|
|
31
33
|
const min = typeof slice.min === "number" ? slice.min : (slice.isOptional ? 0 : 1);
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
+
// An optional extension slice is still validated when an instance is present.
|
|
35
|
+
// Only this shape is admitted below at min=0, mirroring the isExtensionUrlOnly
|
|
36
|
+
// test inside the body, so every other slice kind keeps its existing gate and
|
|
37
|
+
// no other emitter is reached with a count it cannot assert.
|
|
38
|
+
const isOptionalExtensionUrlSlice = min === 0 && isUrlDiscriminatedExtensionSlice(slice, baseName, fields);
|
|
39
|
+
// An optional slice still needs its profiled resource validated. HL7 checks a
|
|
40
|
+
// present entry whether or not the slice itself is required, so skipping the
|
|
41
|
+
// whole slice on min=0 lost every finding inside it — DTR's qpackage output
|
|
42
|
+
// Parameters pins parameter:packagebundle.resource to a Bundle profile with
|
|
43
|
+
// min=0, and none of that Bundle's requirements were being checked.
|
|
44
|
+
if (min === 0) {
|
|
45
|
+
const emittedBefore = sliceValidations.length;
|
|
46
|
+
generateOptionalProfiledSliceDelegation(slice, baseName, fields, sliceValidations, ctx);
|
|
47
|
+
const alreadyDelegated = sliceValidations.length > emittedBefore;
|
|
48
|
+
// An extension slice carries on into the body, where the url discriminator
|
|
49
|
+
// branch emits its value[x] type and max=0 checks. Returning here left
|
|
50
|
+
// DTRStdQuestionnaire's item.extension:contextExpression unvalidated: it is
|
|
51
|
+
// min=0, so nothing about a present instance was ever checked.
|
|
52
|
+
//
|
|
53
|
+
// Only when the delegation emitted nothing, though. A top-level extension
|
|
54
|
+
// slice naming a profile gets a discriminator validation from the call above,
|
|
55
|
+
// and continuing into the body added a second check over the same slice — so
|
|
56
|
+
// one finding was reported twice and parity broke on both empty and random
|
|
57
|
+
// resources across us-core, mcode, ndh and pas. Falling through is for the
|
|
58
|
+
// slices the delegation declines, which is why the earlier attempt looked
|
|
59
|
+
// safe on the profiles I sampled: theirs are nested, and
|
|
60
|
+
// generateOptionalProfiledSliceDelegation returns early on a dotted path.
|
|
61
|
+
if (!isOptionalExtensionUrlSlice || alreadyDelegated)
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (min > 0 || isOptionalExtensionUrlSlice) {
|
|
34
65
|
let relPath = baseName.replace(/^[^.]+\./, "");
|
|
35
66
|
// Skip value[x] slices that are nested within parent elements.
|
|
36
67
|
if (relPath.includes('.') && relPath.includes('value[x]')) {
|
|
@@ -68,7 +99,10 @@ export function generateSliceValidations(ctx) {
|
|
|
68
99
|
('system' in slice.patternConstraint || 'code' in slice.patternConstraint) &&
|
|
69
100
|
!('coding' in slice.patternConstraint);
|
|
70
101
|
const sliceElementId = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
|
|
71
|
-
|
|
102
|
+
// A slice pinning a coded child discriminates on that pin, which takes
|
|
103
|
+
// precedence over the child's own (wider) binding.
|
|
104
|
+
const nestedPatternChild = resolveNestedPatternChild(slice);
|
|
105
|
+
const childFieldsWithPattern = nestedPatternChild ? [nestedPatternChild] : fields.filter(f => {
|
|
72
106
|
if (!f.elementId)
|
|
73
107
|
return false;
|
|
74
108
|
const hasPattern = !!f.patternConstraint;
|
|
@@ -112,7 +146,7 @@ export function generateSliceValidations(ctx) {
|
|
|
112
146
|
const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
113
147
|
return fId.substring(sliceElemId.length + 1).split('.')[0];
|
|
114
148
|
});
|
|
115
|
-
generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, sliceValidations, requiredSubFields);
|
|
149
|
+
generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, sliceValidations, requiredSubFields, ctx.baseResourceType, ctx.profileUrl);
|
|
116
150
|
}
|
|
117
151
|
else if (childFieldsWithFixedValue.length > 0) {
|
|
118
152
|
// For extension slices where the only fixedValue child is 'url', prefer the
|
|
@@ -164,6 +198,19 @@ export function generateSliceValidations(ctx) {
|
|
|
164
198
|
if (refResourceType) {
|
|
165
199
|
generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, refResourceType, sliceValidations, fields, ctx.baseResourceType);
|
|
166
200
|
}
|
|
201
|
+
else if (slice.fixedValue !== undefined && !isChoiceTypeSlice) {
|
|
202
|
+
generateSelfFixedValueSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, sliceValidations);
|
|
203
|
+
}
|
|
204
|
+
else if (slice.profileUrls?.length && ctx.datatypeProfilePins?.get(slice.profileUrls[0])) {
|
|
205
|
+
// 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);
|
|
208
|
+
}
|
|
209
|
+
else if (INLINE_RESOURCE_SLOTS.has(relPath) && detectSelfTypeDiscriminator(slice)) {
|
|
210
|
+
// An inline-resource slot sliced by {type, $this}: the member is the
|
|
211
|
+
// resource, so its resourceType decides membership.
|
|
212
|
+
generateSelfTypeSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, detectSelfTypeDiscriminator(slice), sliceValidations);
|
|
213
|
+
}
|
|
167
214
|
else {
|
|
168
215
|
sliceValidations.push(`
|
|
169
216
|
// Slice validation for ${relPath}:${sliceLabel} skipped: no pattern/value discriminator found
|
|
@@ -234,16 +281,19 @@ function generateCrossSliceExclusivityValidations(slicesByBase, fields, fieldPat
|
|
|
234
281
|
return;
|
|
235
282
|
if (!isArrayPath(relPath, fieldPathMap, arrayFieldPaths))
|
|
236
283
|
return;
|
|
284
|
+
// `elements()`, not the raw accessor: a multi-segment path compiles to
|
|
285
|
+
// `at(resource, …)`, which is `unknown`, and an enclosing `Array.isArray()`
|
|
286
|
+
// does not narrow a fresh call expression — so `for…of` on it fails to
|
|
287
|
+
// typecheck. The matchers read `_el.coding`, so the list must stay element-
|
|
288
|
+
// typed rather than going through `arr()`.
|
|
237
289
|
let code = `\n // Cross-slice exclusivity validation for ${relPath}`;
|
|
238
|
-
code += `\n
|
|
239
|
-
code += `\n
|
|
240
|
-
code += `\n const _matchedSlices: string[] = [];`;
|
|
290
|
+
code += `\n for (const _el of ${safeList(relPath)}) {`;
|
|
291
|
+
code += `\n const _matchedSlices: string[] = [];`;
|
|
241
292
|
for (const m of matchers) {
|
|
242
|
-
code += `\n
|
|
293
|
+
code += `\n if (${m.matchExpr}) _matchedSlices.push("${m.label}");`;
|
|
243
294
|
}
|
|
244
|
-
code += `\n
|
|
245
|
-
code += `\n
|
|
246
|
-
code += `\n }`;
|
|
295
|
+
code += `\n if (_matchedSlices.length > 1) {`;
|
|
296
|
+
code += `\n errors.push("Element matches more than one slice - " + _matchedSlices.join(", "));`;
|
|
247
297
|
code += `\n }`;
|
|
248
298
|
code += `\n }`;
|
|
249
299
|
out.push(code);
|
|
@@ -384,7 +434,7 @@ function generatePatternCodeableConceptValidation(slice, relPath, errorPath, sli
|
|
|
384
434
|
}`);
|
|
385
435
|
}
|
|
386
436
|
}
|
|
387
|
-
function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, out, requiredSubFields) {
|
|
437
|
+
function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel, varName, min, fields, out, requiredSubFields, baseResourceType, profileUrl) {
|
|
388
438
|
const pattern = slice.patternConstraint;
|
|
389
439
|
const hasCode = 'code' in pattern;
|
|
390
440
|
const hasSystem = 'system' in pattern;
|
|
@@ -404,6 +454,40 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
|
|
|
404
454
|
}`)
|
|
405
455
|
.join('')
|
|
406
456
|
: '';
|
|
457
|
+
// Fixed values pinned on the slice's own children.
|
|
458
|
+
//
|
|
459
|
+
// This validator only ever checked that a required child was *present*, so a
|
|
460
|
+
// pinned value went unenforced: KBV_PR_ERP_Medication_Compounding fixes
|
|
461
|
+
// code.coding:verordnungskategorieCode.code to 'rezeptur' and we reported
|
|
462
|
+
// nothing where HL7 reports the mismatch. The CodeableConcept sibling has done
|
|
463
|
+
// this for its descendants all along — the gap was only here.
|
|
464
|
+
const sliceIdRel = (slice.elementId || slice.name).replace(/^[^.]+\./, '');
|
|
465
|
+
const resType = baseResourceType || sliceIdRel.split('.')[0];
|
|
466
|
+
const fixedChildChecks = fields
|
|
467
|
+
.filter(f => {
|
|
468
|
+
if (typeof f.fixedValue !== 'string' && typeof f.fixedValue !== 'number' && f.fixedValue !== true)
|
|
469
|
+
return false;
|
|
470
|
+
const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
471
|
+
if (!fId.startsWith(sliceIdRel + '.'))
|
|
472
|
+
return false;
|
|
473
|
+
const rel = fId.substring(sliceIdRel.length + 1);
|
|
474
|
+
// Direct children only; a deeper pin needs per-level descent this loop
|
|
475
|
+
// does not do, and a sub-slice has its own validator.
|
|
476
|
+
return !rel.includes('.') && !rel.includes(':');
|
|
477
|
+
})
|
|
478
|
+
.map(f => {
|
|
479
|
+
const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
480
|
+
const child = fId.substring(sliceIdRel.length + 1);
|
|
481
|
+
const expected = String(f.fixedValue).replace(/'/g, "\\'");
|
|
482
|
+
// The fragment must end at the pinned child so parity attributes this to
|
|
483
|
+
// the same field HL7 does.
|
|
484
|
+
const profileRef = `${profileUrl ? `${profileUrl}#` : ''}${resType}.${sliceIdRel}.${child}`;
|
|
485
|
+
return `
|
|
486
|
+
if ((matched as Record<string, unknown>).${child} !== undefined && String((matched as Record<string, unknown>).${child}) !== '${expected}') {
|
|
487
|
+
errors.push("Value is '" + (matched as Record<string, unknown>).${child} + "' but is fixed to '${expected}' in the profile ${profileRef}");
|
|
488
|
+
}`;
|
|
489
|
+
})
|
|
490
|
+
.join('');
|
|
407
491
|
const pathParts = relPath.split('.');
|
|
408
492
|
let isNestedInArray = false;
|
|
409
493
|
let arrayParentPath = '';
|
|
@@ -437,8 +521,9 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
|
|
|
437
521
|
}
|
|
438
522
|
}
|
|
439
523
|
}
|
|
440
|
-
const
|
|
441
|
-
|
|
524
|
+
const matchedChecks = subFieldChecks + fixedChildChecks;
|
|
525
|
+
const subFieldBlock = matchedChecks ? `
|
|
526
|
+
for (const matched of ${varName}Elements) {${matchedChecks}
|
|
442
527
|
}` : '';
|
|
443
528
|
if (isNestedInArray && arrayParentPath) {
|
|
444
529
|
const remainingPath = relPath.substring(arrayParentPath.length + 1);
|
|
@@ -522,296 +607,42 @@ function generatePatternCodingValidation(slice, relPath, errorPath, sliceLabel,
|
|
|
522
607
|
* generates code that delegates validation of that sub-resource to the profiled
|
|
523
608
|
* validator, similar to how generateProfileDiscriminatorValidation handles Bundle entries.
|
|
524
609
|
*/
|
|
525
|
-
function
|
|
526
|
-
|
|
610
|
+
function generateSelfFixedValueSliceValidation(slice, relPath, errorPath, sliceLabel, varName, min, out) {
|
|
611
|
+
const fixed = slice.fixedValue;
|
|
612
|
+
if (fixed === undefined)
|
|
527
613
|
return;
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
const
|
|
531
|
-
const fId = (f.elementId || f.name).replace(/^[^.]+\./, '');
|
|
532
|
-
if (!fId.startsWith(sliceElemId + '.'))
|
|
533
|
-
return false;
|
|
534
|
-
const relativePath = fId.substring(sliceElemId.length + 1);
|
|
535
|
-
if (relativePath.includes('.') || relativePath.includes(':'))
|
|
536
|
-
return false;
|
|
537
|
-
return f.profileUrls && f.profileUrls.length > 0;
|
|
538
|
-
});
|
|
539
|
-
if (profiledChildren.length === 0)
|
|
540
|
-
return;
|
|
541
|
-
// Get discriminator info to build the filter expression
|
|
542
|
-
const discriminatorChild = childFieldsWithFixedValue[0];
|
|
543
|
-
const childFieldId = (discriminatorChild.elementId || discriminatorChild.name).replace(/^[^.]+\./, '');
|
|
544
|
-
let discriminatorProp = '';
|
|
545
|
-
if (childFieldId.startsWith(sliceElemId + '.')) {
|
|
546
|
-
discriminatorProp = childFieldId.substring(sliceElemId.length + 1).split('.')[0];
|
|
547
|
-
}
|
|
548
|
-
if (!discriminatorProp)
|
|
549
|
-
return;
|
|
550
|
-
const fixedVal = typeof discriminatorChild.fixedValue === 'string'
|
|
551
|
-
? `"${discriminatorChild.fixedValue}"`
|
|
552
|
-
: String(discriminatorChild.fixedValue);
|
|
553
|
-
const sliceLabel = slice.sliceName || 'slice';
|
|
554
|
-
for (const profiledChild of profiledChildren) {
|
|
555
|
-
const childRelPath = ((profiledChild.elementId || profiledChild.name).replace(/^[^.]+\./, '')).substring(sliceElemId.length + 1);
|
|
556
|
-
// Skip choice-type children (value[x]) — these are datatype values (e.g., Signature),
|
|
557
|
-
// not nested FHIR resources. Delegation only applies to concrete resource children.
|
|
558
|
-
if (childRelPath.includes('[x]'))
|
|
559
|
-
continue;
|
|
560
|
-
for (const pUrl of profiledChild.profileUrls) {
|
|
561
|
-
const delegateName = ctx.profileUrlToName.get(pUrl);
|
|
562
|
-
if (!delegateName || delegateName === ctx.profileName)
|
|
563
|
-
continue;
|
|
564
|
-
ctx.validatorImports.set(delegateName, `./${delegateName}.js`);
|
|
565
|
-
const varName = (slice.sliceName || 'slice').replace(/[^a-zA-Z0-9_]/g, '_');
|
|
566
|
-
// Derive expected resourceType for the profiled child. When the child type is
|
|
567
|
-
// 'Resource' (generic), try the profile URL last segment or delegate name suffix.
|
|
568
|
-
const childType = profiledChild.baseTypeCode || profiledChild.type;
|
|
569
|
-
let expectedResType;
|
|
570
|
-
if (childType && childType !== 'Resource' && /^[A-Z][a-zA-Z]+$/.test(childType)) {
|
|
571
|
-
expectedResType = childType;
|
|
572
|
-
}
|
|
573
|
-
else if (ctx.profileUrlToType) {
|
|
574
|
-
// Use the profileUrlToType map to get the expected resource type from the profile
|
|
575
|
-
expectedResType = ctx.profileUrlToType.get(pUrl);
|
|
576
|
-
}
|
|
577
|
-
if (!expectedResType) {
|
|
578
|
-
const seg = pUrl.split('/').pop() || '';
|
|
579
|
-
if (/^[A-Z][a-zA-Z]+$/.test(seg))
|
|
580
|
-
expectedResType = seg;
|
|
581
|
-
}
|
|
582
|
-
// Build a resourceType guard to avoid false positives on wrong types
|
|
583
|
-
const rtGuard = expectedResType
|
|
584
|
-
? ` && at(_p, '${childRelPath}', 'resourceType') === "${expectedResType}"`
|
|
585
|
-
: '';
|
|
586
|
-
// When resource type doesn't match, emit errors matching HL7 behavior:
|
|
587
|
-
// - Type mismatch error → field: resource
|
|
588
|
-
// - For Parameters: unknown resource type → field: unclassified
|
|
589
|
-
// - For Parameters: inv-1 (parameter must have value/resource/part) → field: constraint
|
|
590
|
-
const isParameters = ctx.baseResourceType === 'Parameters';
|
|
591
|
-
let rtMismatchCheck = '';
|
|
592
|
-
if (expectedResType) {
|
|
593
|
-
const mismatchErrors = [
|
|
594
|
-
`errors.push("Specified profile type was '${expectedResType}' in profile '${pUrl}', but found type '" + (at(_p, '${childRelPath}', 'resourceType') || "Resource") + "'");`,
|
|
595
|
-
];
|
|
596
|
-
if (isParameters) {
|
|
597
|
-
mismatchErrors.push(`errors.push("Contained resource does not appear to be a FHIR resource (unknown name '" + at(_p, '${childRelPath}', 'resourceType') + "')");`, `errors.push("A parameter must have one and only one of (value, resource, part)");`);
|
|
598
|
-
}
|
|
599
|
-
rtMismatchCheck = `\n if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}') && at(_p, '${childRelPath}', 'resourceType') !== "${expectedResType}") {\n ${mismatchErrors.join('\n ')}\n }`;
|
|
600
|
-
}
|
|
601
|
-
// Detect if sliced path goes through an array ancestor (e.g., parameter.part where parameter is array)
|
|
602
|
-
const pathParts = relPath.split('.');
|
|
603
|
-
let arrayParentPath = '';
|
|
604
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
605
|
-
const partialPath = pathParts.slice(0, i + 1).join('.');
|
|
606
|
-
const parentField = ctx.fields.find(f => {
|
|
607
|
-
const fRelPath = f.name.replace(/^[^.]+\./, '');
|
|
608
|
-
return fRelPath === partialPath;
|
|
609
|
-
});
|
|
610
|
-
if (parentField && parentField.isArray) {
|
|
611
|
-
arrayParentPath = partialPath;
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (arrayParentPath) {
|
|
616
|
-
const remainingPath = relPath.substring(arrayParentPath.length + 1);
|
|
617
|
-
out.push(`
|
|
618
|
-
// Profiled sub-resource delegation for ${relPath}:${sliceLabel}.${childRelPath} → ${delegateName}
|
|
619
|
-
{
|
|
620
|
-
if (resource.${arrayParentPath} && Array.isArray(resource.${arrayParentPath})) {
|
|
621
|
-
for (const _parentItem of resource.${arrayParentPath}) {
|
|
622
|
-
const _pi = node(_parentItem);
|
|
623
|
-
const _nestedArr = _pi?.${remainingPath};
|
|
624
|
-
if (_nestedArr && Array.isArray(_nestedArr)) {
|
|
625
|
-
for (const _p of _nestedArr) {
|
|
626
|
-
if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}')${rtGuard}) {
|
|
627
|
-
const _sub = await validate${delegateName}(asResource(at(_p, '${childRelPath}')), options);
|
|
628
|
-
errors.push(..._sub.errors);
|
|
629
|
-
warnings.push(..._sub.warnings);
|
|
630
|
-
}${rtMismatchCheck ? rtMismatchCheck.replace(/\n {8}/g, '\n ') : ''}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}`);
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
out.push(`
|
|
639
|
-
// Profiled sub-resource delegation for ${relPath}:${sliceLabel}.${childRelPath} → ${delegateName}
|
|
640
|
-
{
|
|
641
|
-
const _${varName}Params = elements(${safeAccessor(relPath)});
|
|
642
|
-
for (const _p of _${varName}Params) {
|
|
643
|
-
if (at(_p, '${discriminatorProp}') === ${fixedVal} && at(_p, '${childRelPath}')${rtGuard}) {
|
|
644
|
-
const _sub = await validate${delegateName}(asResource(at(_p, '${childRelPath}')), options);
|
|
645
|
-
errors.push(..._sub.errors);
|
|
646
|
-
warnings.push(..._sub.warnings);
|
|
647
|
-
}${rtMismatchCheck}
|
|
648
|
-
}
|
|
649
|
-
}`);
|
|
650
|
-
}
|
|
651
|
-
break; // Only delegate to first resolved profile
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
// ── Reference profile discriminator support (Reference.type matching) ─────
|
|
656
|
-
/**
|
|
657
|
-
* Detect whether a slice on a Reference-typed field uses a `profile` discriminator
|
|
658
|
-
* on `$this.resolve()`. Returns the target resource type if discoverable.
|
|
659
|
-
* E.g., MessageHeader.focus:admit-notification with targetProfile → Encounter.
|
|
660
|
-
*/
|
|
661
|
-
function detectReferenceProfileDiscriminator(slice, profileUrlToType) {
|
|
662
|
-
if (!slice.typeOptions?.length)
|
|
663
|
-
return undefined;
|
|
664
|
-
const refType = slice.typeOptions.find(t => t.code === 'Reference');
|
|
665
|
-
if (!refType?.targetProfileUrls?.length)
|
|
666
|
-
return undefined;
|
|
667
|
-
const targetUrl = refType.targetProfileUrls[0];
|
|
668
|
-
// 1. Authoritative: resolve from profileUrlToType map
|
|
669
|
-
if (profileUrlToType) {
|
|
670
|
-
const resolved = profileUrlToType.get(targetUrl);
|
|
671
|
-
if (resolved && /^[A-Z][a-zA-Z]+$/.test(resolved))
|
|
672
|
-
return resolved;
|
|
673
|
-
}
|
|
674
|
-
// 2. Heuristic: last URL segment if it looks like a resource type
|
|
675
|
-
const lastSeg = targetUrl.split('/').pop() || '';
|
|
676
|
-
if (/^[A-Z][a-zA-Z]+$/.test(lastSeg))
|
|
677
|
-
return lastSeg;
|
|
678
|
-
return undefined;
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Generate slice validation for Reference-typed slices discriminated by target profile.
|
|
682
|
-
* Matches references by `Reference.type` field or reference string path prefix.
|
|
683
|
-
*/
|
|
684
|
-
function generateReferenceProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, targetResourceType, out, fields, baseResourceType) {
|
|
685
|
-
// Build within-slice child max constraint checks (e.g., max=0 forbidden fields)
|
|
686
|
-
const maxChildren = fields ? collectWithinSliceMaxChildren(slice, fields) : [];
|
|
687
|
-
const resType = baseResourceType || (slice.elementId || slice.name).split('.')[0];
|
|
688
|
-
const childMaxChecks = generateWithinSliceMaxChecks(maxChildren, `_${varName}Refs`, resType, relPath, sliceLabel);
|
|
614
|
+
// The sliced element is a primitive here ($this is the value), so a plain
|
|
615
|
+
// equality count over arr() is the whole check — no element shape to inspect.
|
|
616
|
+
const literal = JSON.stringify(fixed);
|
|
689
617
|
out.push(`
|
|
690
|
-
// Required
|
|
691
|
-
if (
|
|
692
|
-
const _${varName}Refs = ${safeList(relPath)}.filter((e) =>
|
|
693
|
-
e?.type === '${targetResourceType}' ||
|
|
694
|
-
(typeof e?.reference === 'string' && (e.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(e.reference)))
|
|
695
|
-
);
|
|
696
|
-
if (_${varName}Refs.length < ${min}) {
|
|
697
|
-
errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
|
|
698
|
-
}${childMaxChecks}
|
|
699
|
-
} else if (${safeAccessor(relPath)} && !Array.isArray(${safeAccessor(relPath)})) {
|
|
700
|
-
const _ref = ${safeAccessor(relPath)} as { type?: string; reference?: string } | undefined;
|
|
701
|
-
const _isMatch = _ref?.type === '${targetResourceType}' ||
|
|
702
|
-
(typeof _ref?.reference === 'string' && (_ref.reference.startsWith('${targetResourceType}/') || new RegExp('/${targetResourceType}/').test(_ref.reference)));
|
|
703
|
-
if (!_isMatch) {
|
|
704
|
-
errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
|
|
705
|
-
}
|
|
706
|
-
} else {
|
|
618
|
+
// Required slice validation for ${relPath}:${sliceLabel} ($this fixed value)
|
|
619
|
+
if (arr(${safeAccessor(relPath)}).filter((item) => item === ${literal}).length < ${min}) {
|
|
707
620
|
errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
|
|
708
621
|
}`);
|
|
709
622
|
}
|
|
710
623
|
/**
|
|
711
|
-
*
|
|
712
|
-
*
|
|
624
|
+
* Whether a slice's only pinned direct child is `url` — the shape of an extension
|
|
625
|
+
* slice discriminated by its canonical.
|
|
713
626
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
627
|
+
* Deliberately identical to the isExtensionUrlOnly test inside the generator body,
|
|
628
|
+
* so that admitting a min=0 slice past the required-slice gate admits exactly the
|
|
629
|
+
* ones that then take the extension-url branch. Any other slice kind reaching a
|
|
630
|
+
* count-based emitter with min=0 would produce an assertion that cannot fire.
|
|
718
631
|
*/
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
// 2. Try to extract from profile URL last segment (core profiles)
|
|
738
|
-
const profileUrl = profileUrls[0];
|
|
739
|
-
const lastSegment = profileUrl.split('/').pop() || '';
|
|
740
|
-
if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
|
|
741
|
-
return { resourceType: lastSegment, profileUrls };
|
|
742
|
-
}
|
|
743
|
-
// 3. Slice name if it looks like a resource type
|
|
744
|
-
const sliceName = slice.sliceName || '';
|
|
745
|
-
if (/^[A-Z][a-zA-Z]+$/.test(sliceName)) {
|
|
746
|
-
return { resourceType: sliceName, profileUrls };
|
|
747
|
-
}
|
|
748
|
-
// 4. Fallback: derive from the child's type code or typeOptions
|
|
749
|
-
const childType = resourceChild.baseTypeCode || resourceChild.type;
|
|
750
|
-
if (childType && /^[A-Z][a-zA-Z]+$/.test(childType)) {
|
|
751
|
-
return { resourceType: childType, profileUrls };
|
|
752
|
-
}
|
|
753
|
-
const typeOpt = resourceChild.typeOptions?.find(t => /^[A-Z][a-zA-Z]+$/.test(t.code));
|
|
754
|
-
if (typeOpt) {
|
|
755
|
-
return { resourceType: typeOpt.code, profileUrls };
|
|
756
|
-
}
|
|
757
|
-
return undefined;
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Generate slice validation for profile-discriminated entry slices.
|
|
761
|
-
* Matches Bundle entries by `entry[i].resource.resourceType`, enforces min/max,
|
|
762
|
-
* and delegates to the profiled validator when the profile URL is in profileUrlToName.
|
|
763
|
-
*/
|
|
764
|
-
function generateProfileDiscriminatorValidation(slice, relPath, errorPath, sliceLabel, varName, min, match, out, ctx) {
|
|
765
|
-
const { resourceType, profileUrls } = match;
|
|
766
|
-
const max = typeof slice.max === 'number' && slice.max !== Infinity ? slice.max : undefined;
|
|
767
|
-
const parentPath = relPath.includes('.') ? relPath.split('.').slice(0, -1).join('.') : '';
|
|
768
|
-
const accessor = parentPath ? `at(resource, ${parentPath.split('.').map(s => `'${s}'`).join(', ')})` : `resource.${relPath}`;
|
|
769
|
-
// For top-level entry slices, access resource.entry directly
|
|
770
|
-
const arrayAccessor = relPath === 'entry' || !relPath.includes('.') ? `resource.${relPath}` : accessor;
|
|
771
|
-
// Resolve profiled validator for delegation
|
|
772
|
-
let delegateInterfaceName;
|
|
773
|
-
if (ctx.profileUrlToName && ctx.validatorImports) {
|
|
774
|
-
for (const pUrl of profileUrls) {
|
|
775
|
-
const name = ctx.profileUrlToName.get(pUrl);
|
|
776
|
-
if (name && name !== ctx.profileName) {
|
|
777
|
-
delegateInterfaceName = name;
|
|
778
|
-
ctx.validatorImports.set(name, `./${name}.js`);
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
// When delegating, annotate the array so .resource is accessible without casts
|
|
784
|
-
const entriesType = '';
|
|
785
|
-
let code = `
|
|
786
|
-
// Required slice validation for ${relPath}:${sliceLabel} (profile discriminator → ${resourceType})
|
|
787
|
-
{
|
|
788
|
-
const _${varName}Entries${entriesType} = elements(${arrayAccessor}).filter(
|
|
789
|
-
(e) => at(e, 'resource', 'resourceType') === '${resourceType}'
|
|
790
|
-
);`;
|
|
791
|
-
if (min > 0) {
|
|
792
|
-
code += `
|
|
793
|
-
if (_${varName}Entries.length < ${min}) {
|
|
794
|
-
errors.push("Slice '${errorPath}:${sliceLabel}': a matching slice is required, but not found");
|
|
795
|
-
}`;
|
|
796
|
-
}
|
|
797
|
-
if (max !== undefined) {
|
|
798
|
-
code += `
|
|
799
|
-
if (_${varName}Entries.length > ${max}) {
|
|
800
|
-
errors.push("${errorPath}:${sliceLabel}: max allowed = ${max}, but found " + _${varName}Entries.length);
|
|
801
|
-
}`;
|
|
802
|
-
}
|
|
803
|
-
// Delegate to profiled validator for each matched entry
|
|
804
|
-
if (delegateInterfaceName) {
|
|
805
|
-
code += `
|
|
806
|
-
for (const _e of _${varName}Entries) {
|
|
807
|
-
if (at(_e, 'resource')) {
|
|
808
|
-
const _sub = await validate${delegateInterfaceName}(asResource(at(_e, 'resource')), options);
|
|
809
|
-
errors.push(..._sub.errors);
|
|
810
|
-
warnings.push(..._sub.warnings);
|
|
811
|
-
}
|
|
812
|
-
}`;
|
|
813
|
-
}
|
|
814
|
-
code += `
|
|
815
|
-
}`;
|
|
816
|
-
out.push(code);
|
|
632
|
+
function isUrlDiscriminatedExtensionSlice(slice, baseName, fields) {
|
|
633
|
+
// The body tests hasPatternCodeableConcept and hasPatternCoding *before* any
|
|
634
|
+
// extension branch, so a slice carrying a pattern reaches a pattern branch and
|
|
635
|
+
// would emit pattern checks under a minimum of zero. Two regressions came from
|
|
636
|
+
// admitting those.
|
|
637
|
+
if (slice.patternConstraint !== undefined)
|
|
638
|
+
return false;
|
|
639
|
+
if (slice.binding?.codes?.length)
|
|
640
|
+
return false;
|
|
641
|
+
// The body detector, not a hand-rolled copy. My first version required a pinned
|
|
642
|
+
// `url` child, which DTRStdQuestionnaire's item.extension:contextExpression does
|
|
643
|
+
// not have — it names its canonical through type.profile, so detectExtensionUrlSlice
|
|
644
|
+
// reads it from profileUrls. The predicate said no, the slice bailed, and nothing
|
|
645
|
+
// about a present instance was checked.
|
|
646
|
+
const relPath = baseName.replace(/^[^.]+\./, '');
|
|
647
|
+
return detectExtensionUrlSlice(slice, relPath, fields) !== undefined;
|
|
817
648
|
}
|