@atomic-ehr/codegen 0.0.16 → 0.0.17

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/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - [Quick Start](#quick-start)
17
17
  - [Usage Examples](#usage-examples)
18
18
  - [Architecture](#architecture)
19
+ - [Generator Options](#generator-options)
19
20
  - [Input - FHIR packages & resolves canonicals](#input---fhir-packages--resolves-canonicals)
20
21
  - [Load Local StructureDefinitions & TGZ Archives](#load-local-structuredefinitions--tgz-archives)
21
22
  - [Intermediate - Type Schema](#intermediate---type-schema)
@@ -188,6 +189,44 @@ const builder = new APIBuilder()
188
189
 
189
190
  Each method returns the builder instance, allowing method chaining. The `generate()` method executes the pipeline and returns a report with success status and generated file details.
190
191
 
192
+ ### Generator Options
193
+
194
+ Each language generator accepts its own option object. All options are optional; the tables below list the defaults.
195
+
196
+ **TypeScript** — `.typescript({ ... })`
197
+
198
+ | Option | Type | Default | Description |
199
+ |--------|------|---------|-------------|
200
+ | `generateProfile` | `boolean` | `true` | Generate profile wrapper classes (factories, typed slice/extension accessors, `validate()`). |
201
+ | `primitiveTypeExtension` | `boolean` | `true` | Emit sibling `_field` properties for [primitive element extensions](https://www.hl7.org/fhir/element.html#json). |
202
+ | `openResourceTypeSet` | `boolean` | `false` | For resource families (`Resource`, `DomainResource`), keep the `resourceType` union open by adding a `string` fallback instead of a closed literal union. |
203
+ | `extensionGetterDefault` | `"flat" \| "profile" \| "raw"` | `"flat"` | Default return shape for generated extension getters. |
204
+ | `sliceGetterDefault` | `"flat" \| "raw"` | `"flat"` | Default return shape for generated slice getters (`flat` strips discriminators, `raw` returns the full FHIR element). |
205
+ | `lineWidth` | `number` | `120` | Maximum line width before wrapping. |
206
+ | `withDebugComment` | `boolean` | `false` | Emit comments tracing each generated type back to its source schema. |
207
+
208
+ **Python** — `.python({ ... })`
209
+
210
+ | Option | Type | Default | Description |
211
+ |--------|------|---------|-------------|
212
+ | `client` | `"fhirpy" \| "none"` | `"fhirpy"` | Client integration baked into the models: `"fhirpy"` makes models extend `FhirpyBaseModel` for the fhirpy async client; `"none"` emits plain Pydantic models. |
213
+ | `fieldFormat` | `"camelCase" \| "snake_case" \| "PascalCase"` | `"camelCase"` | Naming convention for generated model fields. |
214
+ | `generateProfile` | `boolean` | `false` | Generate profile wrapper classes (`create()`, typed accessors, `validate()`) around the Pydantic models. |
215
+ | `primitiveTypeExtension` | `boolean` | `false` | Emit primitive element extension fields. |
216
+ | `allowExtraFields` | `boolean` | `false` | Allow fields not present in the schema on generated models (Pydantic `extra`). |
217
+ | `rootPackageName` | `string` | `"fhir_types"` | Root Python package name for the generated module tree. |
218
+ | `withDebugComment` | `boolean` | `false` | Emit comments tracing each generated type back to its source schema. |
219
+
220
+ > `fhirpyClient?: boolean` is **deprecated** — use `client` instead (`true` → `"fhirpy"`, `false` → `"none"`).
221
+
222
+ **C#** — `.csharp({ ... })`
223
+
224
+ | Option | Type | Default | Description |
225
+ |--------|------|---------|-------------|
226
+ | `rootNamespace` | `string` | `"Fhir.Types"` | Root namespace for generated classes. |
227
+ | `staticSourceDir` | `string` | — | Directory of static `.cs` source files copied verbatim into the output. |
228
+ | `withDebugComment` | `boolean` | `false` | Emit comments tracing each generated type back to its source schema. |
229
+
191
230
  ### Input - FHIR packages & resolves canonicals
192
231
 
193
232
  The input stage leverages [Canonical Manager](https://github.com/atomic-ehr/canonical-manager) to handle FHIR package management and dependency resolution. It processes FHIR packages from multiple sources (registry, local files, TGZ archives) and resolves all canonical URLs to their concrete definitions, ensuring all references between resources are properly linked before transformation.
@@ -1,5 +1,6 @@
1
1
  from typing import Any, Union, Optional, Iterator, Tuple, Dict
2
2
  from pydantic import BaseModel, Field
3
+ from pydantic_core import PydanticUndefined
3
4
  from typing import Protocol
4
5
 
5
6
 
@@ -22,7 +23,10 @@ class FhirpyBaseModel(BaseModel):
22
23
  def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
23
24
  super().__pydantic_init_subclass__(**kwargs)
24
25
  field = cls.model_fields.get("resource_type") or cls.model_fields.get("resourceType")
25
- if field is not None and field.default is not None:
26
+ # Only concrete resources carry a default resourceType. Abstract/family base types
27
+ # (Resource, DomainResource) leave it unset, so we skip them to avoid registering a
28
+ # class attribute that concrete subclasses would shadow.
29
+ if field is not None and field.default is not None and field.default is not PydanticUndefined:
26
30
  type.__setattr__(cls, "resourceType", str(field.default))
27
31
 
28
32
  def __iter__(self) -> Iterator[Tuple[str, Any]]: # type: ignore[override]
@@ -1,5 +1,6 @@
1
1
  from typing import Any, Union, Optional, Iterator, Tuple, Dict
2
2
  from pydantic import BaseModel, Field
3
+ from pydantic_core import PydanticUndefined
3
4
  from typing import Protocol
4
5
 
5
6
 
@@ -21,7 +22,10 @@ class FhirpyBaseModel(BaseModel):
21
22
  def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
22
23
  super().__pydantic_init_subclass__(**kwargs)
23
24
  field = cls.model_fields.get("resource_type") or cls.model_fields.get("resourceType")
24
- if field is not None and field.default is not None:
25
+ # Only concrete resources carry a default resourceType. Abstract/family base types
26
+ # (Resource, DomainResource) leave it unset, so we skip them to avoid registering a
27
+ # class attribute that concrete subclasses would shadow.
28
+ if field is not None and field.default is not None and field.default is not PydanticUndefined:
25
29
  type.__setattr__(cls, "resourceType", str(field.default))
26
30
 
27
31
  def __iter__(self) -> Iterator[Tuple[str, Any]]: # type: ignore[override]
@@ -26,6 +26,8 @@ from __future__ import annotations
26
26
  import copy
27
27
  from typing import Any, Iterable, Mapping, MutableMapping, MutableSequence, Sequence, TypeVar
28
28
 
29
+ from typing_extensions import TypeGuard
30
+
29
31
  T = TypeVar("T")
30
32
 
31
33
  # ---------------------------------------------------------------------------
@@ -33,9 +35,9 @@ T = TypeVar("T")
33
35
  # ---------------------------------------------------------------------------
34
36
 
35
37
 
36
- def is_record(value: Any) -> bool:
38
+ def is_record(value: Any) -> TypeGuard[MutableMapping[str, Any]]:
37
39
  """True when ``value`` is a non-None mapping (dict-like, not a list)."""
38
- return isinstance(value, Mapping)
40
+ return isinstance(value, MutableMapping)
39
41
 
40
42
 
41
43
  def ensure_path(root: MutableMapping[str, Any], path: Sequence[str]) -> MutableMapping[str, Any]:
package/dist/index.js CHANGED
@@ -1459,7 +1459,16 @@ var generateExtensionMethods = (w, tsIndex, flatProfile, className, extensionBas
1459
1459
  const valueField = pyValueFieldName(valueType, w.nameFormatFunction);
1460
1460
  const pyType = pyTypeFromIdentifier(valueType);
1461
1461
  generateSingleValueExtensionGetter(w, ext, baseName, targetPath, valueField, pyType, extProfileInfo);
1462
- generateSingleValueExtensionSetter(w, ext, className, baseName, targetPath, valueField, extProfileInfo);
1462
+ generateSingleValueExtensionSetter(
1463
+ w,
1464
+ ext,
1465
+ className,
1466
+ baseName,
1467
+ targetPath,
1468
+ valueField,
1469
+ pyType,
1470
+ extProfileInfo
1471
+ );
1463
1472
  } else {
1464
1473
  generateGenericExtensionGetter(w, ext, baseName, targetPath, extProfileInfo);
1465
1474
  generateGenericExtensionSetter(w, ext, className, baseName, targetPath, extProfileInfo);
@@ -1566,6 +1575,7 @@ var generateExtensionSetter = (w, ext, className, baseName, flatParamType, targe
1566
1575
  };
1567
1576
  var generateComplexExtensionSetter = (w, ext, className, baseName, targetPath, extProfileInfo) => {
1568
1577
  generateExtensionSetter(w, ext, className, baseName, "dict[str, Any]", targetPath, extProfileInfo, () => {
1578
+ w.line("assert is_record(value)");
1569
1579
  w.line("sub_extensions = []");
1570
1580
  for (const sub of ext.subExtensions ?? []) {
1571
1581
  const valueField = sub.valueFieldType ? pyValueFieldName(sub.valueFieldType, w.nameFormatFunction) : "value";
@@ -1594,8 +1604,9 @@ var generateSingleValueExtensionGetter = (w, ext, baseName, targetPath, valueFie
1594
1604
  w.line(`return cast('${pyType} | None', get_extension_value(ext, ${JSON.stringify(valueField)}))`);
1595
1605
  });
1596
1606
  };
1597
- var generateSingleValueExtensionSetter = (w, ext, className, baseName, targetPath, valueField, extProfileInfo) => {
1598
- generateExtensionSetter(w, ext, className, baseName, "Any", targetPath, extProfileInfo, () => {
1607
+ var generateSingleValueExtensionSetter = (w, ext, className, baseName, targetPath, valueField, pyType, extProfileInfo) => {
1608
+ generateExtensionSetter(w, ext, className, baseName, pyType, targetPath, extProfileInfo, () => {
1609
+ w.line("assert not isinstance(value, Extension)");
1599
1610
  emitExtPush(w, targetPath, `Extension(url=${JSON.stringify(ext.url)}, ${valueField}=value)`);
1600
1611
  });
1601
1612
  };
@@ -1608,6 +1619,7 @@ var generateGenericExtensionGetter = (w, ext, baseName, targetPath, extProfileIn
1608
1619
  };
1609
1620
  var generateGenericExtensionSetter = (w, ext, className, baseName, targetPath, extProfileInfo) => {
1610
1621
  generateExtensionSetter(w, ext, className, baseName, "dict[str, Any]", targetPath, extProfileInfo, () => {
1622
+ w.line("assert is_record(value)");
1611
1623
  emitExtPush(w, targetPath, `{"url": ${JSON.stringify(ext.url)}, **value}`);
1612
1624
  });
1613
1625
  };
@@ -1808,15 +1820,17 @@ var generateSliceSetters = (w, className, sliceDefs, sliceBaseNames) => {
1808
1820
  } else {
1809
1821
  w.line(`merged = apply_slice_match(${inputExpr}, match)`);
1810
1822
  }
1823
+ let elementExpr = "merged";
1811
1824
  if (sliceDef.elementTypeName) {
1812
- w.line(`merged = ${sliceDef.elementTypeName}(**merged)`);
1825
+ w.line(`element = ${sliceDef.elementTypeName}(**merged)`);
1826
+ elementExpr = "element";
1813
1827
  }
1814
1828
  if (sliceDef.array) {
1815
1829
  w.line(`items = getattr(self._resource, ${JSON.stringify(fieldName)}, None) or []`);
1816
- w.line("set_array_slice(items, match, merged)");
1830
+ w.line(`set_array_slice(items, match, ${elementExpr})`);
1817
1831
  w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, items)`);
1818
1832
  } else {
1819
- w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, merged)`);
1833
+ w.line(`setattr(self._resource, ${JSON.stringify(fieldName)}, ${elementExpr})`);
1820
1834
  }
1821
1835
  w.line("return self");
1822
1836
  });
@@ -2188,6 +2202,10 @@ var collectHelperImports = (isResourceBase, factoryInfo, sliceDefs, extensions,
2188
2202
  imports.push("_get_key", "is_extension", "get_extension_value", "push_extension");
2189
2203
  if (extensions.some((ext) => ext.isComplex && ext.subExtensions)) imports.push("extract_complex_extension");
2190
2204
  if (extensions.some((ext) => ext.path.split(".").some((s) => s !== "extension"))) imports.push("ensure_path");
2205
+ const hasDictFormSetter = extensions.some(
2206
+ (ext) => ext.isComplex && ext.subExtensions || !(ext.valueFieldTypes?.length === 1 && ext.valueFieldTypes[0])
2207
+ );
2208
+ if (hasDictFormSetter) imports.push("is_record");
2191
2209
  }
2192
2210
  imports.push(...validationHelpers);
2193
2211
  imports.sort();
@@ -2789,13 +2807,9 @@ var Python = class extends Writer {
2789
2807
  }
2790
2808
  generateResourceTypeField(schema) {
2791
2809
  const hasChildren = (schema.typeFamily?.resources?.length ?? 0) > 0;
2792
- if (hasChildren) {
2793
- this.line(`${this.nameFormatFunction("resourceType")}: str = Field(`);
2794
- } else {
2795
- this.line(`${this.nameFormatFunction("resourceType")}: Literal['${schema.identifier.name}'] = Field(`);
2796
- }
2810
+ this.line(`${this.nameFormatFunction("resourceType")}: str = Field(`);
2797
2811
  this.indentBlock(() => {
2798
- this.line(`default='${schema.identifier.name}',`);
2812
+ if (!hasChildren) this.line(`default='${schema.identifier.name}',`);
2799
2813
  this.line(`alias='resourceType',`);
2800
2814
  this.line(`serialization_alias='resourceType',`);
2801
2815
  if (!this.forFhirpyClient) {