modelable 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of modelable might be problematic. Click here for more details.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from modelable.compiler.workspace import Workspace
|
|
7
|
+
from modelable.emitters.base import EmittedArtifact, compute_content_hash
|
|
8
|
+
from modelable.parser.ir import (
|
|
9
|
+
AnnClassification,
|
|
10
|
+
AnnPii,
|
|
11
|
+
ArrayType,
|
|
12
|
+
DecimalType,
|
|
13
|
+
DirectMapping,
|
|
14
|
+
DomainDef,
|
|
15
|
+
EnumType,
|
|
16
|
+
FieldDef,
|
|
17
|
+
FieldType,
|
|
18
|
+
MdlFile,
|
|
19
|
+
NamedType,
|
|
20
|
+
ObjectType,
|
|
21
|
+
PrimitiveType,
|
|
22
|
+
ProjectionField,
|
|
23
|
+
ProjectionVersion,
|
|
24
|
+
RefType,
|
|
25
|
+
)
|
|
26
|
+
from modelable.registry.resolver import ResolvedModelRef, resolve_model_ref
|
|
27
|
+
|
|
28
|
+
FHIR_R4_VERSION = "4.0.1"
|
|
29
|
+
FHIR_STRUCTURE_DEFINITION_BASE = "http://hl7.org/fhir/StructureDefinition"
|
|
30
|
+
MODELABLE_STRUCTURE_DEFINITION_BASE = "http://modelable.io/fhir/StructureDefinition"
|
|
31
|
+
MODELABLE_VALUE_SET_BASE = "http://modelable.io/fhir/ValueSet"
|
|
32
|
+
SUPPORTED_BASE_RESOURCES = {"Encounter", "Observation", "Patient"}
|
|
33
|
+
|
|
34
|
+
_BASE_RESOURCE_ELEMENTS: dict[str, frozenset[str]] = {
|
|
35
|
+
"Patient": frozenset(
|
|
36
|
+
{
|
|
37
|
+
"id",
|
|
38
|
+
"meta",
|
|
39
|
+
"implicitRules",
|
|
40
|
+
"language",
|
|
41
|
+
"text",
|
|
42
|
+
"contained",
|
|
43
|
+
"extension",
|
|
44
|
+
"modifierExtension",
|
|
45
|
+
"identifier",
|
|
46
|
+
"active",
|
|
47
|
+
"name",
|
|
48
|
+
"telecom",
|
|
49
|
+
"gender",
|
|
50
|
+
"birthDate",
|
|
51
|
+
"deceased[x]",
|
|
52
|
+
"address",
|
|
53
|
+
"maritalStatus",
|
|
54
|
+
"multipleBirth[x]",
|
|
55
|
+
"photo",
|
|
56
|
+
"contact",
|
|
57
|
+
"communication",
|
|
58
|
+
"generalPractitioner",
|
|
59
|
+
"managingOrganization",
|
|
60
|
+
"link",
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
"Observation": frozenset(
|
|
64
|
+
{
|
|
65
|
+
"id",
|
|
66
|
+
"meta",
|
|
67
|
+
"implicitRules",
|
|
68
|
+
"language",
|
|
69
|
+
"text",
|
|
70
|
+
"contained",
|
|
71
|
+
"extension",
|
|
72
|
+
"modifierExtension",
|
|
73
|
+
"identifier",
|
|
74
|
+
"basedOn",
|
|
75
|
+
"partOf",
|
|
76
|
+
"status",
|
|
77
|
+
"category",
|
|
78
|
+
"code",
|
|
79
|
+
"subject",
|
|
80
|
+
"focus",
|
|
81
|
+
"encounter",
|
|
82
|
+
"effective[x]",
|
|
83
|
+
"issued",
|
|
84
|
+
"performer",
|
|
85
|
+
"value[x]",
|
|
86
|
+
"dataAbsentReason",
|
|
87
|
+
"interpretation",
|
|
88
|
+
"note",
|
|
89
|
+
"bodySite",
|
|
90
|
+
"method",
|
|
91
|
+
"specimen",
|
|
92
|
+
"device",
|
|
93
|
+
"referenceRange",
|
|
94
|
+
"hasMember",
|
|
95
|
+
"derivedFrom",
|
|
96
|
+
"component",
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
"Encounter": frozenset(
|
|
100
|
+
{
|
|
101
|
+
"id",
|
|
102
|
+
"meta",
|
|
103
|
+
"implicitRules",
|
|
104
|
+
"language",
|
|
105
|
+
"text",
|
|
106
|
+
"contained",
|
|
107
|
+
"extension",
|
|
108
|
+
"modifierExtension",
|
|
109
|
+
"identifier",
|
|
110
|
+
"status",
|
|
111
|
+
"statusHistory",
|
|
112
|
+
"class",
|
|
113
|
+
"classHistory",
|
|
114
|
+
"type",
|
|
115
|
+
"serviceType",
|
|
116
|
+
"priority",
|
|
117
|
+
"subject",
|
|
118
|
+
"episodeOfCare",
|
|
119
|
+
"basedOn",
|
|
120
|
+
"participant",
|
|
121
|
+
"appointment",
|
|
122
|
+
"period",
|
|
123
|
+
"length",
|
|
124
|
+
"reasonCode",
|
|
125
|
+
"reasonReference",
|
|
126
|
+
"diagnosis",
|
|
127
|
+
"account",
|
|
128
|
+
"hospitalization",
|
|
129
|
+
"location",
|
|
130
|
+
"serviceProvider",
|
|
131
|
+
"partOf",
|
|
132
|
+
}
|
|
133
|
+
),
|
|
134
|
+
"Basic": frozenset(
|
|
135
|
+
{
|
|
136
|
+
"id",
|
|
137
|
+
"meta",
|
|
138
|
+
"implicitRules",
|
|
139
|
+
"language",
|
|
140
|
+
"text",
|
|
141
|
+
"contained",
|
|
142
|
+
"extension",
|
|
143
|
+
"modifierExtension",
|
|
144
|
+
"identifier",
|
|
145
|
+
"code",
|
|
146
|
+
"subject",
|
|
147
|
+
"created",
|
|
148
|
+
"author",
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def emit_fhir_profile(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
|
|
155
|
+
"""Emit FHIR R4 StructureDefinition profiles and companion Extension SDs."""
|
|
156
|
+
artifacts: list[EmittedArtifact] = []
|
|
157
|
+
for domain in workspace.mdl.domains:
|
|
158
|
+
for projection_name, versions in domain.projections.items():
|
|
159
|
+
for version in versions:
|
|
160
|
+
result = _emit_projection(domain, projection_name, version, workspace.mdl, out_dir)
|
|
161
|
+
artifacts.append(result["profile"])
|
|
162
|
+
artifacts.extend(result["extensions"])
|
|
163
|
+
return artifacts
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _emit_projection(
|
|
167
|
+
domain: DomainDef,
|
|
168
|
+
projection_name: str,
|
|
169
|
+
version: ProjectionVersion,
|
|
170
|
+
mdl: MdlFile,
|
|
171
|
+
out_dir: Path,
|
|
172
|
+
) -> dict[str, object]:
|
|
173
|
+
artifact_id = _artifact_id(domain.name, projection_name, version.version)
|
|
174
|
+
source = _resolve_source(mdl, version)
|
|
175
|
+
base_resource, warnings = _base_resource(source)
|
|
176
|
+
|
|
177
|
+
ext_fields = [f for f in version.fields if _is_extension_field(base_resource, f)]
|
|
178
|
+
direct_fields = [f for f in version.fields if not _is_extension_field(base_resource, f)]
|
|
179
|
+
elements = _elements(domain, projection_name, version, source, base_resource, ext_fields, direct_fields)
|
|
180
|
+
|
|
181
|
+
struct_def: dict[str, object] = {
|
|
182
|
+
"resourceType": "StructureDefinition",
|
|
183
|
+
"url": f"{MODELABLE_STRUCTURE_DEFINITION_BASE}/{artifact_id}",
|
|
184
|
+
"version": str(version.version),
|
|
185
|
+
"name": projection_name,
|
|
186
|
+
"title": projection_name,
|
|
187
|
+
"status": "draft",
|
|
188
|
+
"fhirVersion": FHIR_R4_VERSION,
|
|
189
|
+
"kind": "resource",
|
|
190
|
+
"abstract": False,
|
|
191
|
+
"type": base_resource,
|
|
192
|
+
"baseDefinition": f"{FHIR_STRUCTURE_DEFINITION_BASE}/{base_resource}",
|
|
193
|
+
"derivation": "constraint",
|
|
194
|
+
"mapping": [
|
|
195
|
+
{
|
|
196
|
+
"identity": "modelable",
|
|
197
|
+
"uri": "https://github.com/ktjn/modelable",
|
|
198
|
+
"name": "Modelable",
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"snapshot": {"element": elements},
|
|
202
|
+
"differential": {"element": elements},
|
|
203
|
+
}
|
|
204
|
+
_add_domain_metadata(struct_def, domain)
|
|
205
|
+
|
|
206
|
+
content = json.dumps(struct_def, indent=2, ensure_ascii=False) + "\n"
|
|
207
|
+
profile = EmittedArtifact(
|
|
208
|
+
target="fhir-profile",
|
|
209
|
+
ref=f"{domain.name}.{projection_name}@{version.version}",
|
|
210
|
+
artifact_id=artifact_id,
|
|
211
|
+
path=out_dir / f"{artifact_id}.fhir.json",
|
|
212
|
+
content=content,
|
|
213
|
+
content_hash=compute_content_hash(content),
|
|
214
|
+
warnings=warnings,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
extension_artifacts = [
|
|
218
|
+
_emit_extension_sd(domain, projection_name, version.version, field, source, out_dir) for field in ext_fields
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
return {"profile": profile, "extensions": extension_artifacts}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _is_extension_field(base_resource: str, field: ProjectionField) -> bool:
|
|
225
|
+
elements = _BASE_RESOURCE_ELEMENTS.get(base_resource)
|
|
226
|
+
if elements is None:
|
|
227
|
+
return True
|
|
228
|
+
return field.name not in elements
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _artifact_id(domain: str, name: str, version: int) -> str:
|
|
232
|
+
return f"{domain}.{name}.v{version}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _add_domain_metadata(struct_def: dict[str, object], domain: DomainDef) -> None:
|
|
236
|
+
if domain.owner is not None:
|
|
237
|
+
struct_def["publisher"] = domain.owner
|
|
238
|
+
if domain.contact is not None:
|
|
239
|
+
struct_def["contact"] = [{"telecom": [{"system": "email", "value": domain.contact}]}]
|
|
240
|
+
if domain.description is not None:
|
|
241
|
+
struct_def["description"] = domain.description
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _resolve_source(mdl: MdlFile, projection: ProjectionVersion) -> ResolvedModelRef | None:
|
|
245
|
+
try:
|
|
246
|
+
return resolve_model_ref(mdl, projection.source.model, projection.source.version)
|
|
247
|
+
except LookupError:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _base_resource(source: ResolvedModelRef | None) -> tuple[str, list[str]]:
|
|
252
|
+
if source is None:
|
|
253
|
+
return "Basic", ["FHIR profile source model could not be resolved; using Basic as the base resource"]
|
|
254
|
+
resource = source.model_name
|
|
255
|
+
if resource in SUPPORTED_BASE_RESOURCES:
|
|
256
|
+
return resource, []
|
|
257
|
+
supported = ", ".join(sorted(SUPPORTED_BASE_RESOURCES))
|
|
258
|
+
return "Basic", [f"FHIR profile base resource '{resource}' is not in the supported R4 set: {supported}"]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _elements(
|
|
262
|
+
domain: DomainDef,
|
|
263
|
+
projection_name: str,
|
|
264
|
+
projection: ProjectionVersion,
|
|
265
|
+
source: ResolvedModelRef | None,
|
|
266
|
+
base_resource: str,
|
|
267
|
+
ext_fields: list[ProjectionField],
|
|
268
|
+
direct_fields: list[ProjectionField],
|
|
269
|
+
) -> list[dict[str, object]]:
|
|
270
|
+
root = {
|
|
271
|
+
"id": base_resource,
|
|
272
|
+
"path": base_resource,
|
|
273
|
+
"min": 0,
|
|
274
|
+
"max": "*",
|
|
275
|
+
"base": {"path": base_resource, "min": 0, "max": "*"},
|
|
276
|
+
"definition": (
|
|
277
|
+
f"Modelable projection {domain.name}.{projection_name}@{projection.version} constrained from "
|
|
278
|
+
f"{projection.source.model}@{_version_label(projection)}."
|
|
279
|
+
),
|
|
280
|
+
}
|
|
281
|
+
elements: list[dict[str, object]] = [root]
|
|
282
|
+
for field in direct_fields:
|
|
283
|
+
elements.append(_field_element(domain, projection_name, projection, field, source, base_resource))
|
|
284
|
+
if ext_fields:
|
|
285
|
+
elements.append(_extension_slicing_element(base_resource))
|
|
286
|
+
for field in ext_fields:
|
|
287
|
+
source_field = _source_field(field, source)
|
|
288
|
+
field_type = source_field.type if source_field is not None else PrimitiveType(kind="string")
|
|
289
|
+
elements.append(
|
|
290
|
+
_extension_slice_element(domain, projection_name, projection, field, source, base_resource, field_type)
|
|
291
|
+
)
|
|
292
|
+
if not isinstance(field_type, ObjectType):
|
|
293
|
+
elements.append(
|
|
294
|
+
_extension_value_element(
|
|
295
|
+
domain.name, projection_name, field, field_type, base_resource, source_field=source_field
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
return elements
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _field_element(
|
|
302
|
+
domain: DomainDef,
|
|
303
|
+
projection_name: str,
|
|
304
|
+
projection: ProjectionVersion,
|
|
305
|
+
field: ProjectionField,
|
|
306
|
+
source: ResolvedModelRef | None,
|
|
307
|
+
base_resource: str,
|
|
308
|
+
) -> dict[str, object]:
|
|
309
|
+
source_field = _source_field(field, source)
|
|
310
|
+
field_type = source_field.type if source_field is not None else PrimitiveType(kind="string")
|
|
311
|
+
path = f"{base_resource}.{field.name}"
|
|
312
|
+
max_occurs = _max_occurs(field_type)
|
|
313
|
+
element: dict[str, object] = {
|
|
314
|
+
"id": path,
|
|
315
|
+
"path": path,
|
|
316
|
+
"min": 0 if source_field is not None and source_field.optional else 1,
|
|
317
|
+
"max": max_occurs,
|
|
318
|
+
"base": {"path": path, "min": 0, "max": max_occurs},
|
|
319
|
+
"definition": f"Modelable field {field.name}.",
|
|
320
|
+
"type": _fhir_type(field_type, source_field=source_field),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
binding = _binding(domain.name, projection_name, field.name, field_type)
|
|
324
|
+
if binding is not None:
|
|
325
|
+
element["binding"] = binding
|
|
326
|
+
|
|
327
|
+
extensions = _extensions(field, source_field)
|
|
328
|
+
if extensions:
|
|
329
|
+
element["extension"] = extensions
|
|
330
|
+
|
|
331
|
+
lineage = _lineage_mapping(field, projection, source)
|
|
332
|
+
if lineage is not None:
|
|
333
|
+
element["mapping"] = [{"identity": "modelable", "map": lineage}]
|
|
334
|
+
|
|
335
|
+
return element
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _source_field(field: ProjectionField, source: ResolvedModelRef | None) -> FieldDef | None:
|
|
339
|
+
if source is None or not isinstance(field.mapping, DirectMapping):
|
|
340
|
+
return None
|
|
341
|
+
return next(
|
|
342
|
+
(source_field for source_field in source.version.fields if source_field.name == field.mapping.source_field),
|
|
343
|
+
None,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _fhir_type(field_type: FieldType, *, source_field: FieldDef | None = None) -> list[dict[str, object]]:
|
|
348
|
+
if source_field is not None:
|
|
349
|
+
wire_type = _wire_fhir_type_override(source_field)
|
|
350
|
+
if wire_type is not None:
|
|
351
|
+
return [{"code": wire_type}]
|
|
352
|
+
if isinstance(field_type, PrimitiveType):
|
|
353
|
+
return [{"code": _primitive_type(field_type.kind)}]
|
|
354
|
+
if isinstance(field_type, DecimalType):
|
|
355
|
+
return [{"code": "decimal"}]
|
|
356
|
+
if isinstance(field_type, EnumType):
|
|
357
|
+
return [{"code": "code"}]
|
|
358
|
+
if isinstance(field_type, RefType):
|
|
359
|
+
target = field_type.target.rsplit(".", 1)[-1]
|
|
360
|
+
return [
|
|
361
|
+
{
|
|
362
|
+
"code": "Reference",
|
|
363
|
+
"targetProfile": [f"{FHIR_STRUCTURE_DEFINITION_BASE}/{target}"],
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
if isinstance(field_type, ArrayType):
|
|
367
|
+
return _fhir_type(field_type.item)
|
|
368
|
+
if isinstance(field_type, (NamedType, ObjectType)):
|
|
369
|
+
return [{"code": "BackboneElement"}]
|
|
370
|
+
return [{"code": "string"}]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _wire_fhir_type_override(field: FieldDef) -> str | None:
|
|
374
|
+
wire = field.wire_targets().get("fhir")
|
|
375
|
+
if wire is not None:
|
|
376
|
+
if getattr(wire, "type", None) is not None:
|
|
377
|
+
return wire.type
|
|
378
|
+
encoding = getattr(wire, "encoding", None)
|
|
379
|
+
if encoding is not None:
|
|
380
|
+
_encoding_map = {
|
|
381
|
+
"string": "string",
|
|
382
|
+
"integer": "integer",
|
|
383
|
+
"boolean": "boolean",
|
|
384
|
+
"decimal": "decimal",
|
|
385
|
+
"dateTime": "dateTime",
|
|
386
|
+
"uri": "uri",
|
|
387
|
+
"code": "code",
|
|
388
|
+
}
|
|
389
|
+
return _encoding_map.get(encoding)
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _max_occurs(field_type: FieldType) -> str:
|
|
394
|
+
if isinstance(field_type, ArrayType):
|
|
395
|
+
return "*"
|
|
396
|
+
return "1"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _primitive_type(kind: str) -> str:
|
|
400
|
+
mapping = {
|
|
401
|
+
"binary": "base64Binary",
|
|
402
|
+
"bool": "boolean",
|
|
403
|
+
"date": "date",
|
|
404
|
+
"duration": "Duration",
|
|
405
|
+
"float": "decimal",
|
|
406
|
+
"int": "integer",
|
|
407
|
+
"json": "string",
|
|
408
|
+
"string": "string",
|
|
409
|
+
"time": "time",
|
|
410
|
+
"timestamp": "dateTime",
|
|
411
|
+
"uuid": "string",
|
|
412
|
+
}
|
|
413
|
+
return mapping.get(kind, "string")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _binding(domain_name: str, projection_name: str, field_name: str, field_type: FieldType) -> dict[str, str] | None:
|
|
417
|
+
if not isinstance(field_type, EnumType):
|
|
418
|
+
return None
|
|
419
|
+
return {
|
|
420
|
+
"strength": "required",
|
|
421
|
+
"valueSet": f"{MODELABLE_VALUE_SET_BASE}/{domain_name}.{projection_name}.{field_name}",
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _extensions(field: ProjectionField, source_field: FieldDef | None) -> list[dict[str, object]]:
|
|
426
|
+
annotations = [*(source_field.annotations if source_field is not None else []), *field.annotations]
|
|
427
|
+
classification_extensions: list[dict[str, object]] = []
|
|
428
|
+
pii_extensions: list[dict[str, object]] = []
|
|
429
|
+
for annotation in annotations:
|
|
430
|
+
if isinstance(annotation, AnnClassification):
|
|
431
|
+
classification_extensions.append(
|
|
432
|
+
{
|
|
433
|
+
"url": f"{MODELABLE_STRUCTURE_DEFINITION_BASE}/classification",
|
|
434
|
+
"valueCode": annotation.level,
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
elif isinstance(annotation, AnnPii):
|
|
438
|
+
pii_extensions.append(
|
|
439
|
+
{
|
|
440
|
+
"url": f"{MODELABLE_STRUCTURE_DEFINITION_BASE}/pii",
|
|
441
|
+
"valueBoolean": True,
|
|
442
|
+
}
|
|
443
|
+
)
|
|
444
|
+
return [*classification_extensions, *pii_extensions]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _lineage_mapping(
|
|
448
|
+
field: ProjectionField,
|
|
449
|
+
projection: ProjectionVersion,
|
|
450
|
+
source: ResolvedModelRef | None,
|
|
451
|
+
) -> str | None:
|
|
452
|
+
if source is None or not isinstance(field.mapping, DirectMapping):
|
|
453
|
+
return None
|
|
454
|
+
return f"{projection.source.model}@{source.version.version}.{field.mapping.source_field}"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _extension_artifact_id(domain_name: str, projection_name: str, version: int, field_name: str) -> str:
|
|
458
|
+
return f"{domain_name}.{projection_name}.v{version}.ext.{field_name}"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _extension_slicing_element(base_resource: str) -> dict[str, object]:
|
|
462
|
+
return {
|
|
463
|
+
"id": f"{base_resource}.extension",
|
|
464
|
+
"path": f"{base_resource}.extension",
|
|
465
|
+
"min": 0,
|
|
466
|
+
"max": "*",
|
|
467
|
+
"slicing": {
|
|
468
|
+
"discriminator": [{"type": "value", "path": "url"}],
|
|
469
|
+
"ordered": False,
|
|
470
|
+
"rules": "open",
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _extension_slice_element(
|
|
476
|
+
domain: DomainDef,
|
|
477
|
+
projection_name: str,
|
|
478
|
+
projection: ProjectionVersion,
|
|
479
|
+
field: ProjectionField,
|
|
480
|
+
source: ResolvedModelRef | None,
|
|
481
|
+
base_resource: str,
|
|
482
|
+
field_type: FieldType,
|
|
483
|
+
) -> dict[str, object]:
|
|
484
|
+
source_field = _source_field(field, source)
|
|
485
|
+
ext_id = _extension_artifact_id(domain.name, projection_name, projection.version, field.name)
|
|
486
|
+
max_occurs = _max_occurs(field_type)
|
|
487
|
+
element: dict[str, object] = {
|
|
488
|
+
"id": f"{base_resource}.extension:{field.name}",
|
|
489
|
+
"path": f"{base_resource}.extension",
|
|
490
|
+
"sliceName": field.name,
|
|
491
|
+
"min": 0 if source_field is not None and source_field.optional else 1,
|
|
492
|
+
"max": max_occurs,
|
|
493
|
+
"type": [
|
|
494
|
+
{
|
|
495
|
+
"code": "Extension",
|
|
496
|
+
"profile": [f"{MODELABLE_STRUCTURE_DEFINITION_BASE}/{ext_id}"],
|
|
497
|
+
}
|
|
498
|
+
],
|
|
499
|
+
"definition": f"Modelable field {field.name} (extension).",
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
extensions = _extensions(field, source_field)
|
|
503
|
+
if extensions:
|
|
504
|
+
element["extension"] = extensions
|
|
505
|
+
|
|
506
|
+
lineage = _lineage_mapping(field, projection, source)
|
|
507
|
+
if lineage is not None:
|
|
508
|
+
element["mapping"] = [{"identity": "modelable", "map": lineage}]
|
|
509
|
+
|
|
510
|
+
return element
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _extension_value_element(
|
|
514
|
+
domain_name: str,
|
|
515
|
+
projection_name: str,
|
|
516
|
+
field: ProjectionField,
|
|
517
|
+
field_type: FieldType,
|
|
518
|
+
base_resource: str,
|
|
519
|
+
*,
|
|
520
|
+
source_field: FieldDef | None = None,
|
|
521
|
+
) -> dict[str, object]:
|
|
522
|
+
max_occurs = _max_occurs(field_type)
|
|
523
|
+
element: dict[str, object] = {
|
|
524
|
+
"id": f"{base_resource}.extension:{field.name}.value[x]",
|
|
525
|
+
"path": f"{base_resource}.extension.value[x]",
|
|
526
|
+
"min": 1,
|
|
527
|
+
"max": max_occurs,
|
|
528
|
+
"type": _fhir_type(field_type, source_field=source_field),
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
element["definition"] = f"Modelable field {field.name} extension value."
|
|
532
|
+
|
|
533
|
+
binding = _binding(domain_name, projection_name, field.name, field_type)
|
|
534
|
+
if binding is not None:
|
|
535
|
+
element["binding"] = binding
|
|
536
|
+
|
|
537
|
+
return element
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _extension_sd_value_element(
|
|
541
|
+
domain_name: str,
|
|
542
|
+
projection_name: str,
|
|
543
|
+
field: ProjectionField,
|
|
544
|
+
field_type: FieldType,
|
|
545
|
+
*,
|
|
546
|
+
source_field: FieldDef | None = None,
|
|
547
|
+
) -> dict[str, object]:
|
|
548
|
+
max_occurs = _max_occurs(field_type)
|
|
549
|
+
element: dict[str, object] = {
|
|
550
|
+
"id": "Extension.value[x]",
|
|
551
|
+
"path": "Extension.value[x]",
|
|
552
|
+
"min": 1,
|
|
553
|
+
"max": max_occurs,
|
|
554
|
+
"type": _fhir_type(field_type, source_field=source_field),
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
element["definition"] = f"Modelable field {field.name} extension value."
|
|
558
|
+
|
|
559
|
+
binding = _binding(domain_name, projection_name, field.name, field_type)
|
|
560
|
+
if binding is not None:
|
|
561
|
+
element["binding"] = binding
|
|
562
|
+
|
|
563
|
+
return element
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _extension_sd_sub_extension_elements(
|
|
567
|
+
domain_name: str,
|
|
568
|
+
projection_name: str,
|
|
569
|
+
field: ProjectionField,
|
|
570
|
+
source_field: FieldDef | None,
|
|
571
|
+
ext_id: str,
|
|
572
|
+
parent_ext_url: str,
|
|
573
|
+
) -> list[dict[str, object]]:
|
|
574
|
+
if source_field is None or not isinstance(source_field.type, ObjectType):
|
|
575
|
+
field_type = source_field.type if source_field is not None else PrimitiveType(kind="string")
|
|
576
|
+
return [_extension_sd_value_element(domain_name, projection_name, field, field_type, source_field=source_field)]
|
|
577
|
+
|
|
578
|
+
obj_type = source_field.type
|
|
579
|
+
sub_elements: list[dict[str, object]] = [
|
|
580
|
+
{
|
|
581
|
+
"id": "Extension.extension",
|
|
582
|
+
"path": "Extension.extension",
|
|
583
|
+
"min": 0,
|
|
584
|
+
"max": "*",
|
|
585
|
+
"slicing": {
|
|
586
|
+
"discriminator": [{"type": "value", "path": "url"}],
|
|
587
|
+
"ordered": False,
|
|
588
|
+
"rules": "open",
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
]
|
|
592
|
+
for sub_field in obj_type.fields:
|
|
593
|
+
sub_url = f"{parent_ext_url}#{sub_field.name}"
|
|
594
|
+
sub_fhir_type = _fhir_type(sub_field.type, source_field=sub_field)
|
|
595
|
+
sub_max = _max_occurs(sub_field.type)
|
|
596
|
+
slice_id = f"Extension.extension:{sub_field.name}"
|
|
597
|
+
sub_elements.append(
|
|
598
|
+
{
|
|
599
|
+
"id": slice_id,
|
|
600
|
+
"path": "Extension.extension",
|
|
601
|
+
"sliceName": sub_field.name,
|
|
602
|
+
"min": 0 if sub_field.optional else 1,
|
|
603
|
+
"max": sub_max,
|
|
604
|
+
"type": [{"code": "Extension"}],
|
|
605
|
+
"definition": f"Modelable sub-field {field.name}.{sub_field.name}.",
|
|
606
|
+
}
|
|
607
|
+
)
|
|
608
|
+
sub_elements.append(
|
|
609
|
+
{
|
|
610
|
+
"id": f"{slice_id}.url",
|
|
611
|
+
"path": "Extension.extension.url",
|
|
612
|
+
"min": 1,
|
|
613
|
+
"max": "1",
|
|
614
|
+
"fixedUri": sub_url,
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
sub_elements.append(
|
|
618
|
+
{
|
|
619
|
+
"id": f"{slice_id}.value[x]",
|
|
620
|
+
"path": "Extension.extension.value[x]",
|
|
621
|
+
"min": 1,
|
|
622
|
+
"max": sub_max,
|
|
623
|
+
"type": sub_fhir_type,
|
|
624
|
+
"definition": f"Modelable sub-field {field.name}.{sub_field.name} value.",
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
return sub_elements
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _emit_extension_sd(
|
|
631
|
+
domain: DomainDef,
|
|
632
|
+
projection_name: str,
|
|
633
|
+
version_num: int,
|
|
634
|
+
field: ProjectionField,
|
|
635
|
+
source: ResolvedModelRef | None,
|
|
636
|
+
out_dir: Path,
|
|
637
|
+
) -> EmittedArtifact:
|
|
638
|
+
source_field = _source_field(field, source)
|
|
639
|
+
ext_id = _extension_artifact_id(domain.name, projection_name, version_num, field.name)
|
|
640
|
+
ext_url = f"{MODELABLE_STRUCTURE_DEFINITION_BASE}/{ext_id}"
|
|
641
|
+
sd_name = f"{projection_name}{field.name[0].upper() + field.name[1:] if field.name else field.name}"
|
|
642
|
+
|
|
643
|
+
elements: list[dict[str, object]] = [
|
|
644
|
+
{
|
|
645
|
+
"id": "Extension",
|
|
646
|
+
"path": "Extension",
|
|
647
|
+
"definition": f"Modelable extension for {domain.name}.{projection_name}.{field.name}.",
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
"id": "Extension.url",
|
|
651
|
+
"path": "Extension.url",
|
|
652
|
+
"min": 1,
|
|
653
|
+
"max": "1",
|
|
654
|
+
"fixedUri": ext_url,
|
|
655
|
+
},
|
|
656
|
+
]
|
|
657
|
+
elements.extend(
|
|
658
|
+
_extension_sd_sub_extension_elements(domain.name, projection_name, field, source_field, ext_id, ext_url)
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
struct_def: dict[str, object] = {
|
|
662
|
+
"resourceType": "StructureDefinition",
|
|
663
|
+
"url": ext_url,
|
|
664
|
+
"version": str(version_num),
|
|
665
|
+
"name": sd_name,
|
|
666
|
+
"title": f"{projection_name} {field.name}",
|
|
667
|
+
"status": "draft",
|
|
668
|
+
"fhirVersion": FHIR_R4_VERSION,
|
|
669
|
+
"kind": "complex-type",
|
|
670
|
+
"abstract": False,
|
|
671
|
+
"type": "Extension",
|
|
672
|
+
"derivation": "specialization",
|
|
673
|
+
"snapshot": {"element": elements},
|
|
674
|
+
"differential": {"element": elements},
|
|
675
|
+
}
|
|
676
|
+
_add_domain_metadata(struct_def, domain)
|
|
677
|
+
|
|
678
|
+
content = json.dumps(struct_def, indent=2, ensure_ascii=False) + "\n"
|
|
679
|
+
return EmittedArtifact(
|
|
680
|
+
target="fhir-extension",
|
|
681
|
+
ref=f"{domain.name}.{projection_name}@{version_num}.{field.name}",
|
|
682
|
+
artifact_id=ext_id,
|
|
683
|
+
path=out_dir / f"{ext_id}.fhir.json",
|
|
684
|
+
content=content,
|
|
685
|
+
content_hash=compute_content_hash(content),
|
|
686
|
+
warnings=[],
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _version_label(projection: ProjectionVersion) -> str:
|
|
691
|
+
version = projection.source.version
|
|
692
|
+
if hasattr(version, "version"):
|
|
693
|
+
return str(version.version)
|
|
694
|
+
return str(version)
|