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