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
|
@@ -174,7 +174,19 @@ export function buildForbiddenFieldConstraintEntries(fields) {
|
|
|
174
174
|
human: `${concretePath}: max allowed = 0, but found 1`,
|
|
175
175
|
severity: 'error',
|
|
176
176
|
},
|
|
177
|
-
|
|
177
|
+
// The full path, not the last segment. The expression above is written
|
|
178
|
+
// relative to the resource root, and the emitter wraps an expression in
|
|
179
|
+
// `${fieldPath}.all(...)` unless the expression already navigates from
|
|
180
|
+
// fieldPath. With a bare last segment it never did, so a max=0 on
|
|
181
|
+
// Medication.amount.numerator.value became
|
|
182
|
+
//
|
|
183
|
+
// value.all(amount.numerator.value.exists().not())
|
|
184
|
+
//
|
|
185
|
+
// and Medication has no `value` element: the collection is empty, FHIRPath
|
|
186
|
+
// all() on empty is true, and the constraint could never fire. Two of the
|
|
187
|
+
// three kbv-erp random-parity failures were this, with HL7 reporting the
|
|
188
|
+
// violation we had already written down and then scoped into oblivion.
|
|
189
|
+
fieldPath: concretePath,
|
|
178
190
|
isChoiceType: false,
|
|
179
191
|
});
|
|
180
192
|
});
|
|
@@ -262,95 +274,123 @@ function emitNestedCodingPattern(output, fields, pathParts, parentFieldName, pat
|
|
|
262
274
|
(f.name || '').replace(/^[^.]+\./, '').split(':')[0] === parentBasePath);
|
|
263
275
|
if (parentIsSliced)
|
|
264
276
|
return;
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Pattern constraint for ${humanRel}: every ${parentPath} element must include a coding matching
|
|
277
|
+
// Same split as the CodeableConcept case: identity here, pinned details below.
|
|
278
|
+
const match = codingIdentityPredicate(pattern);
|
|
279
|
+
const described = codingPatternDescription(pattern, true);
|
|
280
|
+
output.push(`
|
|
281
|
+
// Pattern constraint for ${humanRel}: every ${parentPath} element must include a coding matching ${described}
|
|
270
282
|
if (resource.${parentPath} && Array.isArray(resource.${parentPath})) {
|
|
271
283
|
for (const _item of resource.${parentPath}) {
|
|
272
|
-
const _hasMatch = _item
|
|
284
|
+
const _hasMatch = codings(_item).some((coding) => ${match});
|
|
273
285
|
if (!_hasMatch) {
|
|
274
|
-
errors.push("Pattern constraint violation: ${humanRel} requires
|
|
286
|
+
errors.push("Pattern constraint violation: ${humanRel} requires ${described}");
|
|
275
287
|
}
|
|
276
288
|
}
|
|
277
289
|
}`);
|
|
278
|
-
}
|
|
279
|
-
|
|
290
|
+
emitPinnedDetailChecks(output, `resource.${parentPath}`, pattern, parentPath.replace(/[^a-zA-Z0-9_]/g, '_'));
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Coding sub-elements a pattern can pin, in the order they read best in a message.
|
|
294
|
+
*
|
|
295
|
+
* `display` is deliberately absent: HL7 does not fail a pattern on a differing
|
|
296
|
+
* display, and enforcing it would make us the stricter outlier.
|
|
297
|
+
*/
|
|
298
|
+
const PATTERN_CODING_PARTS = ['system', 'version', 'code'];
|
|
299
|
+
/** The sub-elements this pattern actually pins, in message order. */
|
|
300
|
+
function pinnedParts(coding) {
|
|
301
|
+
return PATTERN_CODING_PARTS.filter(part => coding[part] !== undefined && coding[part] !== null && coding[part] !== '');
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Predicate matching only the parts that *identify* which coding the pattern is
|
|
305
|
+
* talking about, so a coding that is clearly the intended one but missing a pinned
|
|
306
|
+
* detail can be reported against that detail.
|
|
307
|
+
*/
|
|
308
|
+
function codingIdentityPredicate(coding, receiver = 'coding') {
|
|
309
|
+
const identity = pinnedParts(coding).filter(part => part === 'system' || part === 'code');
|
|
310
|
+
const comparisons = identity.map(part => `(${receiver}?.${part} as string) === "${String(coding[part])}"`);
|
|
311
|
+
return comparisons.length > 0 ? comparisons.join(' && ') : 'true';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Per-sub-element checks in HL7's own wording, for parts the pattern pins beyond
|
|
315
|
+
* system and code (in practice `version`).
|
|
316
|
+
*
|
|
317
|
+
* Reporting these separately is what makes the finding comparable: HL7 locates the
|
|
318
|
+
* intended coding and blames the specific absent sub-element —
|
|
319
|
+
* `Missing element 'version' - required by fixed value assigned in profile`, at
|
|
320
|
+
* location `ExplanationOfBenefit.type.coding.version`. A single message naming the
|
|
321
|
+
* parent element instead classifies as a `type` finding against HL7's `version`
|
|
322
|
+
* one, so the two never line up even though both validators agree the resource is
|
|
323
|
+
* wrong — which is precisely what left carin-bb at 7/15 after the comparison
|
|
324
|
+
* itself was fixed.
|
|
325
|
+
*/
|
|
326
|
+
function emitPinnedDetailChecks(output, accessor, coding, varName) {
|
|
327
|
+
const details = pinnedParts(coding).filter(part => part !== 'system' && part !== 'code');
|
|
328
|
+
if (details.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
const identity = codingIdentityPredicate(coding, 'c');
|
|
331
|
+
details.forEach((part, i) => {
|
|
332
|
+
const found = `_${varName}_${part}${i}`;
|
|
280
333
|
output.push(`
|
|
281
|
-
// Pattern
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
errors.push("Pattern constraint violation: ${humanRel} requires code='${code}'");
|
|
287
|
-
}
|
|
334
|
+
// Pattern on ${accessor} pins ${part}; reported the way HL7 does when it is absent
|
|
335
|
+
{
|
|
336
|
+
const ${found} = codings(${accessor}).find((c) => ${identity});
|
|
337
|
+
if (${found} && (${found}.${part} as string) !== "${String(coding[part])}") {
|
|
338
|
+
errors.push("Missing element '${part}' - required by fixed value assigned in profile");
|
|
288
339
|
}
|
|
289
340
|
}`);
|
|
290
|
-
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Human-readable form of the pattern, for the error message.
|
|
345
|
+
*
|
|
346
|
+
* `identityOnly` keeps it to the parts the headline check compares, so the message
|
|
347
|
+
* does not promise a comparison the emitted code no longer makes there.
|
|
348
|
+
*/
|
|
349
|
+
function codingPatternDescription(coding, identityOnly = false) {
|
|
350
|
+
return pinnedParts(coding)
|
|
351
|
+
.filter(part => !identityOnly || part === 'system' || part === 'code')
|
|
352
|
+
.map(part => `${part}='${String(coding[part])}'`)
|
|
353
|
+
.join(' and ');
|
|
291
354
|
}
|
|
292
355
|
function emitCodeableConceptPattern(output, field, rel, humanRel, pattern, fieldPathMap, _arrayFieldPaths) {
|
|
293
356
|
const requiredCodings = pattern.coding.filter((c) => c !== null && typeof c === 'object' && 'code' in c);
|
|
294
357
|
if (requiredCodings.length === 0)
|
|
295
358
|
return;
|
|
296
359
|
requiredCodings.forEach((requiredCoding, idx) => {
|
|
297
|
-
|
|
298
|
-
|
|
360
|
+
// The headline check asks only whether the *intended* coding is present, so it
|
|
361
|
+
// stays a `type` finding. Sub-elements the pattern additionally pins are
|
|
362
|
+
// reported separately by emitPinnedDetailChecks, which is how HL7 splits it —
|
|
363
|
+
// otherwise both fire and we report a `type` finding HL7 never raises.
|
|
364
|
+
const match = codingIdentityPredicate(requiredCoding);
|
|
365
|
+
const described = codingPatternDescription(requiredCoding, true);
|
|
299
366
|
const varName = rel.replace(/[^a-zA-Z0-9_]/g, '_') + `_pattern${idx}`;
|
|
300
367
|
const pathInfo = fieldPathMap.get(rel);
|
|
301
368
|
const isResourceFieldArray = field.isArray || (field.sliceName && pathInfo?.isNestedInArray !== true) || rel.includes('.');
|
|
302
369
|
if (isResourceFieldArray) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Pattern constraint for ${humanRel}: at least one element must include coding with system="${system}" and code="${code}"
|
|
306
|
-
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
307
|
-
const ${varName}Valid = resource.${rel}.some((item) =>
|
|
308
|
-
item?.coding?.some((coding) => (coding?.system as string) === "${system}" && (coding?.code as string) === "${code}")
|
|
309
|
-
);
|
|
310
|
-
if (!${varName}Valid) {
|
|
311
|
-
errors.push("${humanRel} must include at least one element with a coding with system '${system}' and code '${code}'");
|
|
312
|
-
}
|
|
313
|
-
}`);
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
output.push(`
|
|
317
|
-
// Pattern constraint for ${humanRel}: at least one element must include coding with code="${code}"
|
|
370
|
+
output.push(`
|
|
371
|
+
// Pattern constraint for ${humanRel}: at least one element must include a coding matching ${described}
|
|
318
372
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
319
|
-
const ${varName}Valid = resource.${rel}.some((item) =>
|
|
320
|
-
item
|
|
373
|
+
const ${varName}Valid = elements(resource.${rel}).some((item) =>
|
|
374
|
+
codings(item).some((coding) => ${match})
|
|
321
375
|
);
|
|
322
376
|
if (!${varName}Valid) {
|
|
323
|
-
errors.push("${humanRel} must include at least one element with a coding with
|
|
377
|
+
errors.push("${humanRel} must include at least one element with a coding with ${described}");
|
|
324
378
|
}
|
|
325
379
|
}`);
|
|
326
|
-
}
|
|
327
380
|
}
|
|
328
381
|
else {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// Pattern constraint for ${humanRel}: must include coding with system="${system}" and code="${code}"
|
|
332
|
-
if (resource.${rel}) {
|
|
333
|
-
const ${varName}Valid = resource.${rel}.coding?.some((coding) =>
|
|
334
|
-
(coding?.system as string) === "${system}" && (coding?.code as string) === "${code}"
|
|
335
|
-
);
|
|
336
|
-
if (!${varName}Valid) {
|
|
337
|
-
errors.push("${humanRel} must include a coding with system '${system}' and code '${code}'");
|
|
338
|
-
}
|
|
339
|
-
}`);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
output.push(`
|
|
343
|
-
// Pattern constraint for ${humanRel}: must include coding with code="${code}"
|
|
382
|
+
output.push(`
|
|
383
|
+
// Pattern constraint for ${humanRel}: must include a coding matching ${described}
|
|
344
384
|
if (resource.${rel}) {
|
|
345
|
-
const ${varName}Valid = resource.${rel}.
|
|
346
|
-
(coding?.code as string) === "${code}"
|
|
347
|
-
);
|
|
385
|
+
const ${varName}Valid = codings(resource.${rel}).some((coding) => ${match});
|
|
348
386
|
if (!${varName}Valid) {
|
|
349
|
-
errors.push("${humanRel} must include a coding with
|
|
387
|
+
errors.push("${humanRel} must include a coding with ${described}");
|
|
350
388
|
}
|
|
351
389
|
}`);
|
|
352
|
-
}
|
|
353
390
|
}
|
|
391
|
+
// Blame the specific pinned sub-element when the intended coding is present
|
|
392
|
+
// but incomplete, matching how HL7 reports it.
|
|
393
|
+
emitPinnedDetailChecks(output, `resource.${rel}`, requiredCoding, varName);
|
|
354
394
|
});
|
|
355
395
|
}
|
|
356
396
|
/**
|
|
@@ -396,8 +436,9 @@ export function buildChoiceTypeNarrowingValidations(fields, profileUrl) {
|
|
|
396
436
|
// Choice-type narrowing: ${relPath} allows only: ${allowedTypesStr}
|
|
397
437
|
{
|
|
398
438
|
const _ckAllowed = [${allowedKeys.join(', ')}];
|
|
399
|
-
|
|
400
|
-
|
|
439
|
+
const _ckNode = node(resource);
|
|
440
|
+
for (const _ckKey of Object.keys(_ckNode)) {
|
|
441
|
+
if (_ckKey.startsWith('${basePropName}') && _ckKey.length > ${baseLen} && _ckKey.charCodeAt(${baseLen}) >= 65 && _ckKey.charCodeAt(${baseLen}) <= 90 && !_ckAllowed.includes(_ckKey) && _ckNode[_ckKey] !== undefined) {
|
|
401
442
|
errors.push("The Profile '${escapedUrl}' definition allows for the type ${allowedTypesStr} but found type " + _ckKey.substring(${baseLen}));
|
|
402
443
|
}
|
|
403
444
|
}
|
|
@@ -413,7 +454,7 @@ function emitCodingPattern(output, field, rel, humanRel, pattern) {
|
|
|
413
454
|
output.push(`
|
|
414
455
|
// Pattern constraint for ${humanRel}: at least one element must have system="${system}" and code="${code}"
|
|
415
456
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
416
|
-
const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some((item) =>
|
|
457
|
+
const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = elements(resource.${rel}).some((item) =>
|
|
417
458
|
(item?.system as string) === "${system}" && (item?.code as string) === "${code}"
|
|
418
459
|
);
|
|
419
460
|
if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
|
|
@@ -425,7 +466,7 @@ function emitCodingPattern(output, field, rel, humanRel, pattern) {
|
|
|
425
466
|
output.push(`
|
|
426
467
|
// Pattern constraint for ${humanRel}: at least one element must have code="${code}"
|
|
427
468
|
if (resource.${rel} && Array.isArray(resource.${rel}) && resource.${rel}.length > 0) {
|
|
428
|
-
const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = resource.${rel}.some((item) =>
|
|
469
|
+
const ${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid = elements(resource.${rel}).some((item) =>
|
|
429
470
|
(item?.code as string) === "${code}"
|
|
430
471
|
);
|
|
431
472
|
if (!${rel.replace(/[^a-zA-Z0-9_]/g, '_')}_patternValid) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FHIRPath expression normalisation shared by the constraint emitters.
|
|
3
|
+
*
|
|
4
|
+
* Each function here exists because fhirpath.js holds the spec more strictly than
|
|
5
|
+
* HL7's Java engine does, or because a FHIR-supplied expression is not valid
|
|
6
|
+
* FHIRPath as written.
|
|
7
|
+
*/
|
|
8
|
+
const FHIRPATH_KEYWORDS = new Set([
|
|
9
|
+
'and', 'as', 'contains', 'day', 'days', 'div', 'false', 'hour', 'hours',
|
|
10
|
+
'implies', 'in', 'is', 'millisecond', 'milliseconds', 'minute', 'minutes',
|
|
11
|
+
'mod', 'month', 'months', 'not', 'or', 'second', 'seconds', 'true', 'week',
|
|
12
|
+
'weeks', 'xor', 'year', 'years',
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Delimit any keyword segment of a FHIRPath navigation path.
|
|
16
|
+
*
|
|
17
|
+
* Scoping a constraint to its field path used to emit the raw path, so a
|
|
18
|
+
* constraint on Narrative.div produced `text.div.all(htmlChecks())`, which
|
|
19
|
+
* fhirpath.js rejects outright ("mismatched input 'div'") and which threw out of
|
|
20
|
+
* validate() at runtime for every profile carrying txt-1.
|
|
21
|
+
*/
|
|
22
|
+
export function quoteFhirPathPath(path) {
|
|
23
|
+
return path
|
|
24
|
+
.split('.')
|
|
25
|
+
.map(segment => (FHIRPATH_KEYWORDS.has(segment) ? `\`${segment}\`` : segment))
|
|
26
|
+
.join('.');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite `as()` applied to a collection into the equivalent `ofType()`.
|
|
30
|
+
*
|
|
31
|
+
* FHIRPath defines `as` on a singleton and `ofType` as the collection filter, so
|
|
32
|
+
* `descendants().as(canonical)` is a spec violation. HL7's Java engine tolerates
|
|
33
|
+
* it, fhirpath.js rejects it outright ("Expected singleton on left side of 'as'")
|
|
34
|
+
* and the throw escaped validate(), losing every finding for the resource — dom-3
|
|
35
|
+
* is written this way. The two forms select the same items, so normalising keeps
|
|
36
|
+
* the invariant evaluated instead of dropped. `as()` applied per item inside
|
|
37
|
+
* `where()` is already a singleton and is left alone.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeCollectionTypeFilters(expression) {
|
|
40
|
+
return expression.replace(/\b(descendants\(\)|children\(\)|repeat\([^()]*\))\s*\.\s*as\s*\(/g, '$1.ofType(');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The two base FHIR element invariants whose evaluation diverges between
|
|
44
|
+
* fhirpath.js and HL7 once they are applied below the top level:
|
|
45
|
+
* ele-1: hasValue() or (children().count() > id.count())
|
|
46
|
+
* ext-1: extension.exists() != value.exists()
|
|
47
|
+
*
|
|
48
|
+
* They are dropped for nested field paths, and kept at a single level for
|
|
49
|
+
* recursive elements, rather than being walked through the whole recursion.
|
|
50
|
+
*/
|
|
51
|
+
export function isBaseElementInvariant(expression) {
|
|
52
|
+
return /^hasValue\(\)\s+or\s+\(children\(\)\.count\(\)\s*>\s*id\.count\(\)\)$/.test(expression)
|
|
53
|
+
|| /^extension\.exists\(\)\s*!=\s*value\.exists\(\)$/.test(expression);
|
|
54
|
+
}
|