@atomic-ehr/codegen 0.0.16 → 0.0.17-canary.20260702213544.39af049
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 +39 -0
- package/assets/api/writer-generator/python/fhirpy_base_model.py +5 -1
- package/assets/api/writer-generator/python/fhirpy_base_model_camel_case.py +5 -1
- package/assets/api/writer-generator/python/profile_helpers.py +4 -2
- package/dist/index.js +26 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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) ->
|
|
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,
|
|
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(
|
|
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,
|
|
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(`
|
|
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(
|
|
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)},
|
|
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
|
-
|
|
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) {
|