babelfhir-ts 1.5.20 → 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/extensionSdReader.js +99 -0
- package/out/src/generator/core/utils.js +57 -2
- package/out/src/generator/emitters/class/classTemplate.js +8 -2
- 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 +2 -2
- 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/sliceExtensionValidation.js +200 -35
- package/out/src/generator/emitters/validator/sliceValidatorGenerator.js +98 -14
- package/out/src/generator/emitters/validator/subExtensionScope.js +100 -0
- package/out/src/generator/emitters/validator/validatorConstraintBuilders.js +13 -1
- package/out/src/generator/emitters/validator/validatorFieldBuilders.js +218 -10
- package/out/src/generator/emitters/validator/validatorGenerator.js +4 -2
- package/out/src/generator/emitters/validator/validatorRuntime.js +17 -7
- package/out/src/generator/index.js +21 -11
- package/out/src/generator/parser/sdParser.js +1 -0
- package/out/src/generator/sdProcessor.js +209 -21
- package/out/src/generator/sdProcessorRequiredFields.js +32 -7
- package/package.json +2 -1
|
@@ -149,6 +149,38 @@ export function buildInlineResourceTypeValidations(fields, profileUrl) {
|
|
|
149
149
|
* Build deeply nested required field validations.
|
|
150
150
|
* For fields like component.value[x].data (min=1) where the parent is an array.
|
|
151
151
|
*/
|
|
152
|
+
/**
|
|
153
|
+
* Rewrite an element id whose only slice qualifiers name choice-type variants into
|
|
154
|
+
* the concrete JSON path, or undefined when any qualifier discriminates a subset.
|
|
155
|
+
*
|
|
156
|
+
* `ingredient.item[x]:itemCodeableConcept.text` → `ingredient.itemCodeableConcept.text`.
|
|
157
|
+
* A qualifier is only accepted when its base ends in `[x]` and the slice name starts
|
|
158
|
+
* with the base property, which is the convention that makes the slice name the
|
|
159
|
+
* property name. `identifier:NPI.value` fails both tests and is still skipped: NPI
|
|
160
|
+
* is one identifier among several, so requiring its children of every identifier
|
|
161
|
+
* would report a conformant resource.
|
|
162
|
+
*/
|
|
163
|
+
function leafFieldName(parts) {
|
|
164
|
+
return parts[parts.length - 1];
|
|
165
|
+
}
|
|
166
|
+
function resolveChoiceSliceSegments(elementIdRel) {
|
|
167
|
+
const segments = elementIdRel.split('.');
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const segment of segments) {
|
|
170
|
+
if (!segment.includes(':')) {
|
|
171
|
+
out.push(segment.replace(/\[x\]$/, ''));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const [base, sliceName] = segment.split(':');
|
|
175
|
+
if (!base.endsWith('[x]') || !sliceName)
|
|
176
|
+
return undefined;
|
|
177
|
+
const baseProp = base.slice(0, -'[x]'.length);
|
|
178
|
+
if (!sliceName.startsWith(baseProp) || sliceName === baseProp)
|
|
179
|
+
return undefined;
|
|
180
|
+
out.push(sliceName);
|
|
181
|
+
}
|
|
182
|
+
return out.join('.');
|
|
183
|
+
}
|
|
152
184
|
export function buildNestedRequiredValidations(fields, arrayFieldPaths) {
|
|
153
185
|
const result = [];
|
|
154
186
|
fields
|
|
@@ -162,15 +194,34 @@ export function buildNestedRequiredValidations(fields, arrayFieldPaths) {
|
|
|
162
194
|
let rel = relFull.replace(/^[^.]+\./, '').replace(/\[x\]/g, '');
|
|
163
195
|
if (!rel)
|
|
164
196
|
return;
|
|
165
|
-
// Skip slice-specific fields
|
|
197
|
+
// Skip slice-specific fields: a slice's requirements bind only its own
|
|
198
|
+
// members, and this builder emits an unconditional check.
|
|
199
|
+
//
|
|
200
|
+
// One exception, and it is not really a subset. A choice-type slice names a
|
|
201
|
+
// JSON property rather than discriminating within one: FHIR writes
|
|
202
|
+
// `item[x]:itemCodeableConcept` for the CodeableConcept variant of `item[x]`,
|
|
203
|
+
// and an instance carrying that variant writes `itemCodeableConcept`. So the
|
|
204
|
+
// slice *is* the property, and its required children apply whenever the
|
|
205
|
+
// property is present — which is what HL7 reports as
|
|
206
|
+
// "Medication.ingredient.item[x]:itemCodeableConcept.text: minimum required = 1"
|
|
207
|
+
// where we said nothing, because `field.name` flattens `item[x]` to `item`
|
|
208
|
+
// and loses which variant was meant.
|
|
166
209
|
const elementId = field.elementId || relFull;
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
let choiceSliceResolved = false;
|
|
211
|
+
if (elementId.includes(':')) {
|
|
212
|
+
const resolved = resolveChoiceSliceSegments(elementId.replace(/^[^.]+\./, ''));
|
|
213
|
+
if (!resolved)
|
|
214
|
+
return;
|
|
215
|
+
rel = resolved;
|
|
216
|
+
choiceSliceResolved = true;
|
|
217
|
+
}
|
|
169
218
|
const pathParts = rel.split('.');
|
|
170
219
|
if (pathParts.length < 2)
|
|
171
220
|
return;
|
|
172
|
-
// Resolve choice type in path
|
|
173
|
-
|
|
221
|
+
// Resolve choice type in path. Skipped when the element id already named the
|
|
222
|
+
// variant: this block reads the *leaf's* own type, which for a nested leaf
|
|
223
|
+
// (…item[x]:itemCodeableConcept.text) is the text's type, not the choice's.
|
|
224
|
+
if (relFull.includes('[x]') && !choiceSliceResolved) {
|
|
174
225
|
// The JSON property name comes from the element's *own* type, not its parent's.
|
|
175
226
|
// Reading the parent discarded the check whenever that parent was a container:
|
|
176
227
|
// Claim.item.location[x] is typed CodeableConcept, so the property is
|
|
@@ -203,8 +254,27 @@ export function buildNestedRequiredValidations(fields, arrayFieldPaths) {
|
|
|
203
254
|
// segment to be an array meant a path whose only array sits deeper
|
|
204
255
|
// (Observation.code.coding.code — code is 0..1, coding is the array) bailed
|
|
205
256
|
// out and emitted nothing, leaving HL7 to report the missing element alone.
|
|
206
|
-
if (!topIsArray && !hasIntermediateArray)
|
|
257
|
+
if (!topIsArray && !hasIntermediateArray) {
|
|
258
|
+
// A choice-slice path is emitted even when no level is a known array.
|
|
259
|
+
// `Medication.ingredient` is 0..* in the base but the KBV differential
|
|
260
|
+
// constrains only its `min`, so the merged field carries no max and
|
|
261
|
+
// `isArray` reads false — the check bailed and HL7 reported
|
|
262
|
+
// "…itemCodeableConcept.text: minimum required = 1" alone. `elementsAt`
|
|
263
|
+
// flattens an array level and passes a single object through unchanged,
|
|
264
|
+
// so it is right either way and does not depend on that flag.
|
|
265
|
+
if (!choiceSliceResolved)
|
|
266
|
+
return;
|
|
267
|
+
const walk = resolvedParts.slice(0, -1).map(p => `'${p}'`).join(', ');
|
|
268
|
+
result.push(`
|
|
269
|
+
// Nested required field: ${rel} (min=1)
|
|
270
|
+
for (const _nrEl of elementsAt(resource, [${walk}])) {
|
|
271
|
+
const _nrLeaf = at(_nrEl, '${leafFieldName(resolvedParts)}');
|
|
272
|
+
if (_nrLeaf === undefined || _nrLeaf === null) {
|
|
273
|
+
errors.push("Missing required member: '${leafFieldName(resolvedParts)}'");
|
|
274
|
+
}
|
|
275
|
+
}`);
|
|
207
276
|
return;
|
|
277
|
+
}
|
|
208
278
|
const leafField = resolvedParts[resolvedParts.length - 1];
|
|
209
279
|
// Check if the leaf field itself is an array — if so, don't flag Array values as absent
|
|
210
280
|
const fullLeafPath = resolvedParts.join('.');
|
|
@@ -450,6 +520,20 @@ export function buildProhibitedFieldValidations(fields, arrayFieldPaths, fieldPa
|
|
|
450
520
|
// Skip if ANY segment has a slice qualifier — the constraint is
|
|
451
521
|
// slice-scoped (e.g. name:Geburtsname.given) and must not be emitted
|
|
452
522
|
// as a global prohibition.
|
|
523
|
+
//
|
|
524
|
+
// TODO(parity): this correctly avoids a global prohibition but emits nothing
|
|
525
|
+
// at all, so slice-scoped max=0 goes unchecked. A complex extension sets its
|
|
526
|
+
// own value[x] to max=0 — sub-extensions or a value, never both — and we miss
|
|
527
|
+
// it: on DTRStdQuestionnaire, HL7 reports "Extension.value[x]: max allowed =
|
|
528
|
+
// 0, but found 1" and "Extension.extension: minimum required = 2, but only
|
|
529
|
+
// found 0" for item.extension:contextExpression while we report neither.
|
|
530
|
+
//
|
|
531
|
+
// Fixing it needs a slice-scoped emission, not a relaxed guard: iterate the
|
|
532
|
+
// extensions matching the slice's url and assert the absence of any value*
|
|
533
|
+
// property. It cannot name one property, because a forbidden choice narrows
|
|
534
|
+
// to no type — which is why the choice resolution elsewhere in this file
|
|
535
|
+
// skips it too. Removing this guard instead would reintroduce the regression
|
|
536
|
+
// where AuditEvent.agent:user.requestor was demanded of every agent.
|
|
453
537
|
const segments = field.elementId.split('.');
|
|
454
538
|
if (segments.some(seg => seg.includes(':')))
|
|
455
539
|
return false;
|
|
@@ -570,6 +654,16 @@ const FHIR_PRIMITIVE_TYPES = new Set([
|
|
|
570
654
|
'time', 'instant', 'code', 'id', 'oid', 'markdown', 'base64Binary',
|
|
571
655
|
'uuid', 'positiveInt', 'unsignedInt', 'integer64', 'xhtml',
|
|
572
656
|
]);
|
|
657
|
+
/**
|
|
658
|
+
* FHIR primitives that serialize as a JSON number.
|
|
659
|
+
*
|
|
660
|
+
* integer64 is deliberately absent: R5 serializes it as a *string* to survive
|
|
661
|
+
* JavaScript's 53-bit safe integer range, so requiring a number would reject
|
|
662
|
+
* conformant data.
|
|
663
|
+
*/
|
|
664
|
+
const NUMERIC_PRIMITIVES = new Set([
|
|
665
|
+
'decimal', 'integer', 'positiveInt', 'unsignedInt',
|
|
666
|
+
]);
|
|
573
667
|
/** Check whether a FHIR type serializes as a JSON primitive (string/number/bool). */
|
|
574
668
|
function isFhirPrimitive(fieldType) {
|
|
575
669
|
if (FHIR_PRIMITIVE_TYPES.has(fieldType))
|
|
@@ -595,9 +689,72 @@ const CONTAINER_TYPES = new Set([
|
|
|
595
689
|
*
|
|
596
690
|
* Only top-level (depth-1) non-sliced fields are checked.
|
|
597
691
|
*/
|
|
692
|
+
/**
|
|
693
|
+
* A numeric or boolean primitive below a repeating element must still hold the right
|
|
694
|
+
* JSON kind.
|
|
695
|
+
*
|
|
696
|
+
* The flat pass deliberately skips any path with an array ancestor, because a path
|
|
697
|
+
* walk cannot descend one — so `ingredient.strength.numerator.value` went unchecked
|
|
698
|
+
* and HL7 reported "The value 'Example value' is not a valid decimal" alone. This
|
|
699
|
+
* emits the loop that pass declined to guess at.
|
|
700
|
+
*
|
|
701
|
+
* Both of HL7's headings are pushed: the value wording classifies as `unclassified`
|
|
702
|
+
* and the parse wording as `constraint`, so emitting only the first leaves
|
|
703
|
+
* `constraint` looking like an HL7-only finding.
|
|
704
|
+
*/
|
|
705
|
+
function buildArrayNestedPrimitiveKindValidations(fields, arrayFieldPaths, result) {
|
|
706
|
+
const isArrayPath = (p) => arrayFieldPaths.has(p) || arrayFieldPaths.has(p.split(':')[0]);
|
|
707
|
+
const seen = new Set();
|
|
708
|
+
for (const field of fields) {
|
|
709
|
+
if (field.sliceName || field.isArray)
|
|
710
|
+
continue;
|
|
711
|
+
const rel = (field.name || '').replace(/^[^.]+\./, '');
|
|
712
|
+
if (!rel || !rel.includes('.') || rel.includes('[x]') || seen.has(rel))
|
|
713
|
+
continue;
|
|
714
|
+
const fieldType = field.type || field.baseTypeCode;
|
|
715
|
+
const jsonKind = NUMERIC_PRIMITIVES.has(fieldType || '') ? 'number'
|
|
716
|
+
: fieldType === 'boolean' ? 'boolean'
|
|
717
|
+
: undefined;
|
|
718
|
+
if (!jsonKind)
|
|
719
|
+
continue;
|
|
720
|
+
const segments = rel.split('.');
|
|
721
|
+
const parents = segments.slice(0, -1);
|
|
722
|
+
const leaf = segments[segments.length - 1];
|
|
723
|
+
// The flat pass already covers a path with no array on the way down.
|
|
724
|
+
if (!parents.some((_p, i) => isArrayPath(parents.slice(0, i + 1).join('.'))))
|
|
725
|
+
continue;
|
|
726
|
+
seen.add(rel);
|
|
727
|
+
let expr = 'resource';
|
|
728
|
+
const open = [];
|
|
729
|
+
let indent = ' ';
|
|
730
|
+
parents.forEach((segment, i) => {
|
|
731
|
+
if (isArrayPath(parents.slice(0, i + 1).join('.'))) {
|
|
732
|
+
const loopVar = `_pkEl${i}`;
|
|
733
|
+
open.push(`${indent}for (const ${loopVar} of arr(at(${expr}, '${segment}'))) {`);
|
|
734
|
+
indent += ' ';
|
|
735
|
+
expr = loopVar;
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
expr = `at(${expr}, '${segment}')`;
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
const closes = open.map((_l, i) => ' '.repeat(open.length - i + 1) + '}').join('\n');
|
|
742
|
+
const body = `${indent}const _pkValue = at(${expr}, '${leaf}');
|
|
743
|
+
${indent}if (_pkValue !== undefined && _pkValue !== null && typeof _pkValue !== '${jsonKind}') {
|
|
744
|
+
${indent} errors.push("The value '" + String(_pkValue) + "' is not a valid ${fieldType}");
|
|
745
|
+
${indent} errors.push("Error parsing JSON: the primitive value must be a ${jsonKind}");
|
|
746
|
+
${indent}}`;
|
|
747
|
+
result.push(`
|
|
748
|
+
// Structural type check: ${rel} must be a JSON ${jsonKind} (${fieldType})
|
|
749
|
+
{
|
|
750
|
+
${[...open, body, closes].filter(Boolean).join('\n')}
|
|
751
|
+
}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
598
754
|
export function buildStructuralTypeValidations(fields, arrayFieldPaths) {
|
|
599
755
|
const result = [];
|
|
600
756
|
buildNestedObjectTypeValidations(fields, arrayFieldPaths, result);
|
|
757
|
+
buildArrayNestedPrimitiveKindValidations(fields, arrayFieldPaths, result);
|
|
601
758
|
for (const field of fields) {
|
|
602
759
|
// Skip sliced fields — handled by slice validator
|
|
603
760
|
if (field.sliceName)
|
|
@@ -610,13 +767,34 @@ export function buildStructuralTypeValidations(fields, arrayFieldPaths) {
|
|
|
610
767
|
if (fullPath.includes('[x]'))
|
|
611
768
|
continue;
|
|
612
769
|
const rel = fullPath.replace(/^[^.]+\./, '');
|
|
613
|
-
if (!rel
|
|
614
|
-
continue;
|
|
770
|
+
if (!rel)
|
|
771
|
+
continue;
|
|
772
|
+
// A nested primitive is still worth checking, and only the primitive kind check
|
|
773
|
+
// below handles one: HL7 reports "The value 'Example value' is not a valid
|
|
774
|
+
// decimal" on Medication.amount.numerator.value, three levels down, where we
|
|
775
|
+
// reported nothing. The array and object checks stay top-level only, since both
|
|
776
|
+
// would need iteration to descend correctly.
|
|
777
|
+
const isNested = rel.includes('.');
|
|
778
|
+
if (isNested) {
|
|
779
|
+
// Any array on the way down needs a loop, not a path walk. Skipped rather than
|
|
780
|
+
// guessed at, so a repeating ancestor never yields a check against the array
|
|
781
|
+
// object itself.
|
|
782
|
+
// arrayFieldPaths is keyed on the *relative* path. Building the key from the
|
|
783
|
+
// full path never matched, so Bundle.entry.search.score was emitted as
|
|
784
|
+
// at(resource, 'entry', 'search', 'score') — a walk into an array, which
|
|
785
|
+
// yields undefined and checks nothing.
|
|
786
|
+
const segments = rel.split('.');
|
|
787
|
+
const hasArrayAncestor = segments.some((_seg, i) => arrayFieldPaths.has(segments.slice(0, i + 1).join('.')));
|
|
788
|
+
if (hasArrayAncestor)
|
|
789
|
+
continue;
|
|
790
|
+
if (!NUMERIC_PRIMITIVES.has(field.type || '') && field.type !== 'boolean')
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
615
793
|
const fieldType = field.type || field.baseTypeCode;
|
|
616
794
|
if (!fieldType)
|
|
617
795
|
continue;
|
|
618
796
|
// ── Array structural check ─────────────────────────────────────────
|
|
619
|
-
if (field.isArray) {
|
|
797
|
+
if (field.isArray && !isNested) {
|
|
620
798
|
result.push(`
|
|
621
799
|
// Structural type check: ${rel} must be a JSON Array
|
|
622
800
|
{
|
|
@@ -625,10 +803,40 @@ export function buildStructuralTypeValidations(fields, arrayFieldPaths) {
|
|
|
625
803
|
const _stType = at(resource, 'resourceType');
|
|
626
804
|
errors.push("The property ${rel} must be a JSON Array, not an Object (at " + (typeof _stType === 'string' ? _stType : "Resource") + ")");
|
|
627
805
|
}
|
|
806
|
+
}`);
|
|
807
|
+
}
|
|
808
|
+
// ── Primitive JSON kind check ──────────────────────────────────────
|
|
809
|
+
// A numeric or boolean FHIR primitive must not arrive as a string. We checked
|
|
810
|
+
// that complex types are objects but never that a decimal holds a number, so
|
|
811
|
+
// HL7 reported "The value 'Example value' is not a valid decimal" and "the
|
|
812
|
+
// primitive value must be a number" where we said nothing.
|
|
813
|
+
//
|
|
814
|
+
// Both headings, as HL7 reports them: the value wording classifies as
|
|
815
|
+
// `unclassified` and the parse wording as `constraint`, so emitting only the
|
|
816
|
+
// first left `constraint` attributed to HL7 alone for a defect we had detected.
|
|
817
|
+
//
|
|
818
|
+
// Only the kinds JSON distinguishes are checked. Date, code and the rest all
|
|
819
|
+
// serialize as strings, and their formats already have dedicated checks
|
|
820
|
+
// elsewhere; asserting `typeof === 'string'` on them would add nothing.
|
|
821
|
+
const jsonKind = NUMERIC_PRIMITIVES.has(fieldType) ? 'number'
|
|
822
|
+
: fieldType === 'boolean' ? 'boolean'
|
|
823
|
+
: undefined;
|
|
824
|
+
if (jsonKind && !field.isArray) {
|
|
825
|
+
// Segments, not a dotted key: at() walks its varargs, so at(resource, 'a.b')
|
|
826
|
+
// would look for a property literally named "a.b" and always find nothing.
|
|
827
|
+
const accessorArgs = rel.split('.').map(seg => `'${seg}'`).join(', ');
|
|
828
|
+
result.push(`
|
|
829
|
+
// Structural type check: ${rel} must be a JSON ${jsonKind} (${fieldType})
|
|
830
|
+
{
|
|
831
|
+
const _stValue = at(resource, ${accessorArgs});
|
|
832
|
+
if (_stValue !== undefined && _stValue !== null && typeof _stValue !== '${jsonKind}') {
|
|
833
|
+
errors.push("The value '" + String(_stValue) + "' is not a valid ${fieldType}");
|
|
834
|
+
errors.push("Error parsing JSON: the primitive value must be a ${jsonKind}");
|
|
835
|
+
}
|
|
628
836
|
}`);
|
|
629
837
|
}
|
|
630
838
|
// ── Complex type as Object check ───────────────────────────────────
|
|
631
|
-
if (!isFhirPrimitive(fieldType) && !CONTAINER_TYPES.has(fieldType)) {
|
|
839
|
+
if (!isNested && !isFhirPrimitive(fieldType) && !CONTAINER_TYPES.has(fieldType)) {
|
|
632
840
|
// Complex types must be objects when present — not primitive strings/numbers
|
|
633
841
|
if (!field.isArray) {
|
|
634
842
|
result.push(`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from '../../../logger.js';
|
|
2
2
|
import { sanitizeValueSetName } from '../valueset/valueSetGenerator.js';
|
|
3
3
|
import { generateSliceValidations } from './sliceValidatorGenerator.js';
|
|
4
|
+
import { generateClosedSlicingValidations } from './closedSlicingValidation.js';
|
|
4
5
|
import { fhirpathModelSlug } from '../../fhir/versionContext.js';
|
|
5
6
|
import { buildRequiredConstraints, buildMaxCardinalityConstraints, buildForbiddenFieldConstraintEntries, buildPatternValidations, buildChoiceTypeNarrowingValidations, } from './validatorConstraintBuilders.js';
|
|
6
7
|
import { buildNestedRequiredValidations, buildFixedValueValidations, buildProhibitedFieldValidations, buildPrimitiveFormatValidations, buildStructuralTypeValidations, buildInlineResourceTypeValidations, INLINE_RESOURCE_SLOTS, } from './validatorFieldBuilders.js';
|
|
@@ -241,6 +242,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
241
242
|
valueSetImports, profileUrl, profileName: interfaceName, baseResourceType,
|
|
242
243
|
profileUrlToName, validatorImports, extensionMetadata, profileUrlToType, datatypeProfilePins,
|
|
243
244
|
});
|
|
245
|
+
const closedSlicingValidations = generateClosedSlicingValidations(fields, profileUrl, baseResourceType);
|
|
244
246
|
const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
|
|
245
247
|
const structuralTypeValidations = [
|
|
246
248
|
...buildStructuralTypeValidations(fields, arrayFieldPaths),
|
|
@@ -479,7 +481,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
479
481
|
})
|
|
480
482
|
.join("\n");
|
|
481
483
|
// ── Assemble final output ───────────────────────────────────────────────
|
|
482
|
-
const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0
|
|
484
|
+
const hasConstraints = filteredConstraints.length > 0 || sliceValidations.length > 0 || closedSlicingValidations.length > 0
|
|
483
485
|
|| patternValidations.length > 0 || fixedValueValidations.length > 0
|
|
484
486
|
|| bindingValidations.length > 0 || prohibitedFieldValidations.length > 0
|
|
485
487
|
|| bundleRefValidation.length > 0 || primitiveFormatValidations.length > 0
|
|
@@ -561,7 +563,7 @@ ${body}
|
|
|
561
563
|
const errors: string[] = [];
|
|
562
564
|
const warnings: string[] = [];
|
|
563
565
|
${fhirpathOptionsBlock}
|
|
564
|
-
${bundleEntryResolution}${referenceTargetTypeValidation}${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${structuralTypeValidations.join('')}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${choiceNarrowingValidations.join('')}${sliceValidations.join('')}${datatypeProfileDelegations.join('')}${nonDomainTextCheck}${extensionStructuralValidation}${containedRefValidation}${bundledAggregationValidation}${bundleRefValidation}
|
|
566
|
+
${bundleEntryResolution}${referenceTargetTypeValidation}${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${structuralTypeValidations.join('')}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${choiceNarrowingValidations.join('')}${sliceValidations.join('')}${closedSlicingValidations.join('')}${datatypeProfileDelegations.join('')}${nonDomainTextCheck}${extensionStructuralValidation}${containedRefValidation}${bundledAggregationValidation}${bundleRefValidation}
|
|
565
567
|
return { errors, warnings };
|
|
566
568
|
}`,
|
|
567
569
|
valueSetImports
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Runtime helpers every generated validator may import from ValidatorOptions.
|
|
9
9
|
* Single source of truth for the emitted import list.
|
|
10
10
|
*/
|
|
11
|
-
export const HELPER_NAMES = ['at', 'arr', 'node', 'elements', 'coding', 'codings', 'asResource', 'refsAt'];
|
|
11
|
+
export const HELPER_NAMES = ['at', 'arr', 'node', 'elements', 'elementsAt', 'coding', 'codings', 'asResource', 'refsAt'];
|
|
12
12
|
/**
|
|
13
13
|
* Returns the content of ValidatorOptions.ts — the shared runtime options type
|
|
14
14
|
* plus the element-probing helpers that every generated validateX() imports.
|
|
@@ -131,13 +131,14 @@ export function elements(value: unknown): readonly ElementShape[] {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Collect
|
|
134
|
+
* Collect every element sitting at a property path, flattening arrays at each hop.
|
|
135
135
|
*
|
|
136
|
-
*
|
|
137
|
-
* (Composition.author is 1..*, Composition.subject 0..1)
|
|
138
|
-
*
|
|
136
|
+
* One path therefore covers both single and repeating elements
|
|
137
|
+
* (Composition.author is 1..*, Composition.subject 0..1) and any mix of the two,
|
|
138
|
+
* which is what \`at()\` cannot do — it stops the moment the path meets an array.
|
|
139
|
+
* Absent and null intermediates simply contribute nothing.
|
|
139
140
|
*/
|
|
140
|
-
export function
|
|
141
|
+
export function elementsAt(value: unknown, path: readonly string[]): unknown[] {
|
|
141
142
|
let current: unknown[] = [value];
|
|
142
143
|
for (const key of path) {
|
|
143
144
|
const next: unknown[] = [];
|
|
@@ -148,9 +149,18 @@ export function refsAt(value: unknown, path: readonly string[]): string[] {
|
|
|
148
149
|
}
|
|
149
150
|
current = next;
|
|
150
151
|
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
151
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Collect the literal references found at a property path.
|
|
157
|
+
*
|
|
158
|
+
* Only string \`reference\` values are returned; a Reference carrying just an
|
|
159
|
+
* identifier names no target.
|
|
160
|
+
*/
|
|
161
|
+
export function refsAt(value: unknown, path: readonly string[]): string[] {
|
|
152
162
|
const found: string[] = [];
|
|
153
|
-
for (const item of
|
|
163
|
+
for (const item of elementsAt(value, path)) {
|
|
154
164
|
const reference = at(item, 'reference');
|
|
155
165
|
if (typeof reference === 'string') found.push(reference);
|
|
156
166
|
}
|
|
@@ -223,16 +223,18 @@ async function generateIndexFile(dir) {
|
|
|
223
223
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
224
224
|
const exports = [];
|
|
225
225
|
const exportedNames = new Set();
|
|
226
|
-
// First pass: collect all files and their exported names
|
|
226
|
+
// First pass: collect all files and their exported names, keeping the
|
|
227
|
+
// declaration kind — `verbatimModuleSyntax` needs a re-exported type to go
|
|
228
|
+
// through `export type { … }`, so the barrel has to know which is which.
|
|
227
229
|
const fileExports = new Map();
|
|
228
230
|
for (const file of files) {
|
|
229
231
|
if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts') && file.name !== 'index.ts' && file.name !== 'ValidatorOptions.ts') {
|
|
230
232
|
const filePath = path.join(dir, file.name);
|
|
231
233
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
232
|
-
const names = new
|
|
233
|
-
const exportMatches = content.matchAll(/export\s+(
|
|
234
|
+
const names = new Map();
|
|
235
|
+
const exportMatches = content.matchAll(/export\s+(interface|class|type|const|function)\s+(\w+)/g);
|
|
234
236
|
for (const match of exportMatches) {
|
|
235
|
-
names.
|
|
237
|
+
names.set(match[2], match[1] === 'interface' || match[1] === 'type' ? 'type' : 'value');
|
|
236
238
|
}
|
|
237
239
|
fileExports.set(file.name, names);
|
|
238
240
|
}
|
|
@@ -241,11 +243,11 @@ async function generateIndexFile(dir) {
|
|
|
241
243
|
for (const file of files) {
|
|
242
244
|
if (file.isFile() && file.name.endsWith('.ts') && !file.name.endsWith('.d.ts') && file.name !== 'index.ts' && file.name !== 'ValidatorOptions.ts') {
|
|
243
245
|
const baseName = file.name.replace('.ts', '');
|
|
244
|
-
const fileNames = fileExports.get(file.name) || new
|
|
246
|
+
const fileNames = fileExports.get(file.name) || new Map();
|
|
245
247
|
// Find which names from this file conflict with already-exported names
|
|
246
248
|
const conflicting = new Set();
|
|
247
249
|
const unique = new Set();
|
|
248
|
-
for (const name of fileNames) {
|
|
250
|
+
for (const name of fileNames.keys()) {
|
|
249
251
|
if (exportedNames.has(name)) {
|
|
250
252
|
conflicting.add(name);
|
|
251
253
|
}
|
|
@@ -254,17 +256,25 @@ async function generateIndexFile(dir) {
|
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
if (conflicting.size === 0) {
|
|
257
|
-
// No conflicts — use simple wildcard export
|
|
259
|
+
// No conflicts — use simple wildcard export. `export *` carries types
|
|
260
|
+
// without needing the `type` modifier.
|
|
258
261
|
exports.push(`export * from './${baseName}.js';`);
|
|
259
262
|
}
|
|
260
263
|
else if (unique.size > 0) {
|
|
261
|
-
// Has conflicts but also has unique names — use selective named
|
|
262
|
-
|
|
263
|
-
|
|
264
|
+
// Has conflicts but also has unique names — use selective named exports,
|
|
265
|
+
// split by kind so re-exported types carry the `type` modifier.
|
|
266
|
+
const typeNames = [...unique].filter(n => fileNames.get(n) === 'type').sort();
|
|
267
|
+
const valueNames = [...unique].filter(n => fileNames.get(n) !== 'type').sort();
|
|
268
|
+
if (valueNames.length > 0) {
|
|
269
|
+
exports.push(`export { ${valueNames.join(', ')} } from './${baseName}.js';`);
|
|
270
|
+
}
|
|
271
|
+
if (typeNames.length > 0) {
|
|
272
|
+
exports.push(`export type { ${typeNames.join(', ')} } from './${baseName}.js';`);
|
|
273
|
+
}
|
|
264
274
|
}
|
|
265
275
|
// If all names conflict (unique.size === 0), skip entirely
|
|
266
276
|
// Track all names from this file as exported
|
|
267
|
-
for (const name of fileNames) {
|
|
277
|
+
for (const name of fileNames.keys()) {
|
|
268
278
|
exportedNames.add(name);
|
|
269
279
|
}
|
|
270
280
|
}
|
|
@@ -442,6 +442,7 @@ export async function parseStructureDefinition(structureDefinition, fhirServerUr
|
|
|
442
442
|
type,
|
|
443
443
|
baseTypeCode, // Raw FHIR type code (e.g., "HumanName" even when type is "humanname-de-basis")
|
|
444
444
|
sliceName: element.sliceName,
|
|
445
|
+
slicingRules: element.slicing?.rules,
|
|
445
446
|
profileUrls: (element.type || []).flatMap(t => t.profile || []),
|
|
446
447
|
binding: bindingMeta,
|
|
447
448
|
isProfiled: hasProfile,
|