babelfhir-ts 1.5.4 → 1.5.5

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.
@@ -70,7 +70,7 @@ export function generateBackboneSliceTypes(ctx) {
70
70
  return childId.startsWith(sliceElementId + '.');
71
71
  });
72
72
  // Build interface body lines from slice children and the slice's own constraints
73
- const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType);
73
+ const { lines: bodyLines, referencedFhirTypes } = buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, backboneType, baseFields, parentField.name);
74
74
  if (bodyLines.length === 0) {
75
75
  // No constrainable properties — skip this slice interface
76
76
  log.debug(`No constraints for slice ${sliceName} of ${parentField.name}, skipping`);
@@ -197,7 +197,7 @@ function gatherChildFields(fields, path) {
197
197
  /**
198
198
  * Build the body lines for a slice-specific interface.
199
199
  */
200
- function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType) {
200
+ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _backboneType, baseFields, parentPath) {
201
201
  const lines = [];
202
202
  const referencedFhirTypes = [];
203
203
  // Add pattern constraint from the slice field itself (discriminator value)
@@ -211,16 +211,58 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
211
211
  }
212
212
  }
213
213
  }
214
- // Process direct children of the slice
214
+ // Pre-scan: identify choice-type parent elements (value[x]) that have
215
+ // sub-slices (value[x]:valueQuantity, etc.). These parents carry only slicing
216
+ // metadata and should not be emitted as typed properties.
217
+ // Match only direct choice-type slices like "value[x]:valueString" — NOT deeper
218
+ // elements like "value[x].extension:questionnaireDisplay" where the colon is
219
+ // on a different segment.
220
+ const choiceTypeParents = new Set();
221
+ const choiceTypeSlices = [];
222
+ const choiceSlicePattern = /^([^.]+\[x]):([^.]+)$/; // e.g. "value[x]:valueString"
215
223
  for (const child of sliceChildren) {
216
224
  const childId = child.elementId || child.name;
217
225
  const relativePath = childId.substring(sliceElementId.length + 1);
218
- // Only handle direct children (no dots in relative path)
219
- if (relativePath.includes('.'))
226
+ const match = choiceSlicePattern.exec(relativePath);
227
+ if (match) {
228
+ choiceTypeSlices.push(child);
229
+ choiceTypeParents.add(match[1]); // e.g. "value[x]"
230
+ }
231
+ }
232
+ // Collect deeply nested fixed values for object literal synthesis.
233
+ // e.g. code.coding.code = "confidence" → code: { coding: [{ code: "confidence" }] }
234
+ const nestedFixedByProp = new Map();
235
+ // Process children of the slice
236
+ for (const child of sliceChildren) {
237
+ const childId = child.elementId || child.name;
238
+ const relativePath = childId.substring(sliceElementId.length + 1);
239
+ // Nested children (dots in relative path) — collect fixed values for synthesis
240
+ if (relativePath.includes('.')) {
241
+ if (child.fixedValue !== undefined) {
242
+ const segments = relativePath.split('.');
243
+ const rawPropName = segments[0];
244
+ const propName = rawPropName.replace(/\[x\]$/, '');
245
+ // Skip nested values under:
246
+ // - choice-type elements ([x]) — handled by direct child or sub-slices
247
+ // - slice notation (part:LineItem.name) — deeper-level constraints
248
+ // - extension/modifierExtension — handled by postProcessExtensions
249
+ if (!rawPropName.includes('[x]') && !rawPropName.includes(':')
250
+ && propName !== 'extension' && propName !== 'modifierExtension') {
251
+ const nestedPath = segments.slice(1).join('.');
252
+ if (!nestedFixedByProp.has(propName))
253
+ nestedFixedByProp.set(propName, new Map());
254
+ nestedFixedByProp.get(propName).set(nestedPath, String(child.fixedValue));
255
+ }
256
+ }
220
257
  continue;
221
- // Skip slice notation in child paths
258
+ }
259
+ // Choice-type sub-slices (value[x]:valueXxx) are processed separately below
222
260
  if (relativePath.includes(':'))
223
261
  continue;
262
+ // Skip choice-type parents that have sub-slices — they carry only slicing
263
+ // metadata and their inherited type (e.g. Quantity) is misleading
264
+ if (choiceTypeParents.has(relativePath))
265
+ continue;
224
266
  const propName = relativePath.replace(/\[x\]$/, '');
225
267
  if (child.fixedValue !== undefined) {
226
268
  // Fixed value constraint
@@ -270,6 +312,43 @@ function buildSliceInterfaceBody(sliceField, sliceChildren, sliceElementId, _bac
270
312
  }
271
313
  }
272
314
  }
315
+ // Emit choice-type sub-slice properties (e.g. value[x]:valueQuantity → valueQuantity: Quantity)
316
+ for (const child of choiceTypeSlices) {
317
+ if (!child.sliceName || !child.type)
318
+ continue;
319
+ if (child.type === 'BackboneElement' || child.type === 'Element' || child.type === 'any')
320
+ continue;
321
+ const rawType = getRules().mapTypeToTS(child.type);
322
+ const mappedType = sanitizeIdentifier(rawType);
323
+ if (mappedType !== rawType && child.isProfiled)
324
+ continue;
325
+ const optMark = child.isOptional ? '?' : '';
326
+ lines.push(`${child.sliceName}${optMark}: ${mappedType};`);
327
+ if (getRules().isFhirType(mappedType)) {
328
+ referencedFhirTypes.push(mappedType);
329
+ }
330
+ }
331
+ // Emit synthesized nested object literals from deeply nested fixed values.
332
+ // Skip properties already emitted by the direct child or choice-type loops
333
+ // to prevent TS2717 (duplicate property) and TS2430 (incompatible extends).
334
+ const emittedProps = new Set(lines.map(l => l.match(/^(\w+)[?]?:/)?.[1]).filter(Boolean));
335
+ for (const [propName, paths] of nestedFixedByProp) {
336
+ if (emittedProps.has(propName))
337
+ continue;
338
+ // Check if this property is an array — look in both slice children and
339
+ // base fields. Synthesized literals for array properties need wrapping
340
+ // in [] to avoid TS2430 (incompatible extends).
341
+ const propFullPath = `${parentPath}.${propName}`;
342
+ const isArrayProp = sliceChildren.some(c => {
343
+ const cId = c.elementId || c.name;
344
+ const rel = cId.substring(sliceElementId.length + 1);
345
+ return rel === propName && c.isArray;
346
+ }) || baseFields.some(f => f.name === propFullPath && f.isArray);
347
+ const literal = synthesizeNestedLiteral(paths);
348
+ if (literal) {
349
+ lines.push(`${propName}: ${isArrayProp ? `[${literal}]` : literal};`);
350
+ }
351
+ }
273
352
  return { lines, referencedFhirTypes };
274
353
  }
275
354
  /**
@@ -294,6 +373,39 @@ function formatCodingLiteral(codings) {
294
373
  return null;
295
374
  return `[${entries.join(', ')}]`;
296
375
  }
376
+ /**
377
+ * Synthesize a nested object literal from deeply nested fixed values.
378
+ *
379
+ * Given a map of nested paths → values (e.g. { "coding.code" → "confidence" }),
380
+ * produces an object literal like `{ coding: [{ code: "confidence" }] }`.
381
+ *
382
+ * Special handling: `coding` segments are wrapped in array brackets because
383
+ * `CodeableConcept.coding` is always an array in FHIR.
384
+ */
385
+ function synthesizeNestedLiteral(paths) {
386
+ const codingFields = new Map();
387
+ const scalarFields = [];
388
+ for (const [path, value] of paths) {
389
+ if (path.startsWith('coding.')) {
390
+ codingFields.set(path.substring('coding.'.length), value);
391
+ }
392
+ else if (!path.includes('.')) {
393
+ scalarFields.push([path, value]);
394
+ }
395
+ // Deeper nesting (3+ levels) is uncommon; skip gracefully
396
+ }
397
+ const parts = [];
398
+ if (codingFields.size > 0) {
399
+ const codingParts = [...codingFields.entries()].map(([k, v]) => `${k}: "${v}"`);
400
+ parts.push(`coding: [{ ${codingParts.join('; ')} }]`);
401
+ }
402
+ for (const [field, value] of scalarFields) {
403
+ parts.push(`${field}: "${value}"`);
404
+ }
405
+ if (parts.length === 0)
406
+ return null;
407
+ return `{ ${parts.join('; ')} }`;
408
+ }
297
409
  /**
298
410
  * Update the root interface to use the union type for a sliced backbone field.
299
411
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B/R5 StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",