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.

Files changed (122) hide show
  1. modelable/__init__.py +1 -0
  2. modelable/__main__.py +3 -0
  3. modelable/_pydantic_py314_compat.py +31 -0
  4. modelable/cli.py +41 -0
  5. modelable/commands/__init__.py +1 -0
  6. modelable/commands/apicurio.py +84 -0
  7. modelable/commands/codegen.py +241 -0
  8. modelable/commands/common.py +43 -0
  9. modelable/commands/compile.py +237 -0
  10. modelable/commands/create.py +164 -0
  11. modelable/commands/diff.py +82 -0
  12. modelable/commands/graph.py +53 -0
  13. modelable/commands/llm.py +564 -0
  14. modelable/commands/lsp.py +15 -0
  15. modelable/commands/runtime.py +37 -0
  16. modelable/commands/scenario.py +104 -0
  17. modelable/commands/spec.py +197 -0
  18. modelable/commands/workspace.py +240 -0
  19. modelable/compat/__init__.py +11 -0
  20. modelable/compat/checker.py +179 -0
  21. modelable/compat/diff.py +169 -0
  22. modelable/compiler/__init__.py +3 -0
  23. modelable/compiler/compiler.py +19 -0
  24. modelable/compiler/workspace.py +346 -0
  25. modelable/diagnostics/__init__.py +3 -0
  26. modelable/diagnostics/model.py +27 -0
  27. modelable/emitters/__init__.py +0 -0
  28. modelable/emitters/base.py +22 -0
  29. modelable/emitters/csharp.py +245 -0
  30. modelable/emitters/dbt_yaml.py +290 -0
  31. modelable/emitters/diagnostics.py +25 -0
  32. modelable/emitters/fhir.py +694 -0
  33. modelable/emitters/fhir_validator.py +36 -0
  34. modelable/emitters/go.py +334 -0
  35. modelable/emitters/java.py +264 -0
  36. modelable/emitters/json_schema.py +458 -0
  37. modelable/emitters/markdown.py +252 -0
  38. modelable/emitters/odcs.py +355 -0
  39. modelable/emitters/openlineage.py +315 -0
  40. modelable/emitters/openmetadata.py +258 -0
  41. modelable/emitters/python.py +282 -0
  42. modelable/emitters/rust.py +643 -0
  43. modelable/emitters/shapes.py +261 -0
  44. modelable/emitters/sql.py +266 -0
  45. modelable/emitters/targets.py +141 -0
  46. modelable/emitters/typescript.py +352 -0
  47. modelable/expressions/__init__.py +0 -0
  48. modelable/expressions/cel.py +547 -0
  49. modelable/governance/__init__.py +3 -0
  50. modelable/governance/checker.py +271 -0
  51. modelable/governance/por.py +46 -0
  52. modelable/grammar/__init__.py +1 -0
  53. modelable/grammar/modelable.lark +257 -0
  54. modelable/graph/__init__.py +5 -0
  55. modelable/graph/export.py +442 -0
  56. modelable/llm/__init__.py +43 -0
  57. modelable/llm/chat.py +255 -0
  58. modelable/llm/config.py +87 -0
  59. modelable/llm/context.py +194 -0
  60. modelable/llm/engine.py +976 -0
  61. modelable/llm/importers.py +1077 -0
  62. modelable/llm/provenance.py +84 -0
  63. modelable/llm/providers.py +182 -0
  64. modelable/llm/qa.py +126 -0
  65. modelable/llm/recommendations.py +33 -0
  66. modelable/llm/redaction.py +19 -0
  67. modelable/llm/render.py +279 -0
  68. modelable/llm/update_plan.py +101 -0
  69. modelable/llm/validation_help.py +10 -0
  70. modelable/lsp/__init__.py +3 -0
  71. modelable/lsp/__main__.py +4 -0
  72. modelable/lsp/code_actions.py +210 -0
  73. modelable/lsp/completion.py +480 -0
  74. modelable/lsp/definition.py +343 -0
  75. modelable/lsp/diagnostics.py +31 -0
  76. modelable/lsp/document_symbols.py +197 -0
  77. modelable/lsp/federation.py +261 -0
  78. modelable/lsp/folding.py +33 -0
  79. modelable/lsp/formatting.py +64 -0
  80. modelable/lsp/highlight.py +30 -0
  81. modelable/lsp/hover.py +370 -0
  82. modelable/lsp/inlay_hints.py +158 -0
  83. modelable/lsp/references.py +511 -0
  84. modelable/lsp/rename.py +564 -0
  85. modelable/lsp/semantic_tokens.py +412 -0
  86. modelable/lsp/server.py +370 -0
  87. modelable/lsp/workspace.py +83 -0
  88. modelable/lsp/workspace_symbols.py +104 -0
  89. modelable/parser/__init__.py +94 -0
  90. modelable/parser/ir.py +451 -0
  91. modelable/parser/parse.py +47 -0
  92. modelable/parser/transformer.py +798 -0
  93. modelable/parser/wire.py +68 -0
  94. modelable/planner/__init__.py +0 -0
  95. modelable/planner/lineage.py +91 -0
  96. modelable/planner/planner.py +134 -0
  97. modelable/planner/plans.py +122 -0
  98. modelable/py.typed +0 -0
  99. modelable/registry/__init__.py +9 -0
  100. modelable/registry/apicurio.py +166 -0
  101. modelable/registry/base.py +18 -0
  102. modelable/registry/factory.py +18 -0
  103. modelable/registry/index.py +419 -0
  104. modelable/registry/local.py +26 -0
  105. modelable/registry/oci.py +22 -0
  106. modelable/registry/resolver.py +213 -0
  107. modelable/registry/schema.sql +119 -0
  108. modelable/registry/signature.py +26 -0
  109. modelable/release.py +125 -0
  110. modelable/runtime/__init__.py +5 -0
  111. modelable/runtime/adapter/__init__.py +17 -0
  112. modelable/runtime/adapter/base.py +18 -0
  113. modelable/runtime/adapter/postgres.py +82 -0
  114. modelable/specs/__init__.py +23 -0
  115. modelable/specs/tracking.py +220 -0
  116. modelable/validation/__init__.py +3 -0
  117. modelable/validation/semantic.py +659 -0
  118. modelable-1.0.0.dist-info/METADATA +61 -0
  119. modelable-1.0.0.dist-info/RECORD +122 -0
  120. modelable-1.0.0.dist-info/WHEEL +4 -0
  121. modelable-1.0.0.dist-info/entry_points.txt +2 -0
  122. 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)