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,36 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ from modelable.emitters.fhir import FHIR_R4_VERSION
9
+
10
+ RunCallable = Callable[..., subprocess.CompletedProcess[str]]
11
+
12
+
13
+ def validate_fhir_profile(
14
+ profile: Path,
15
+ validator_jar: Path,
16
+ *,
17
+ java: str = "java",
18
+ fhir_version: str = FHIR_R4_VERSION,
19
+ run: RunCallable = subprocess.run,
20
+ ) -> subprocess.CompletedProcess[str]:
21
+ command = [
22
+ java,
23
+ "-jar",
24
+ str(validator_jar),
25
+ str(profile),
26
+ "-version",
27
+ fhir_version,
28
+ ]
29
+ return run(
30
+ command,
31
+ check=False,
32
+ capture_output=True,
33
+ text=True,
34
+ encoding="utf-8",
35
+ env={**os.environ, "PYTHONIOENCODING": "utf-8"},
36
+ )
@@ -0,0 +1,334 @@
1
+ from __future__ import annotations
2
+
3
+ import re
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.emitters.diagnostics import type_loss
9
+ from modelable.emitters.shapes import TypeShape
10
+ from modelable.parser.ir import DirectMapping, DomainDef, ModelVersion, ProjectionVersion
11
+ from modelable.registry.resolver import resolve_model_ref
12
+
13
+
14
+ def emit_go(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
15
+ """Emit Go source files for every model and projection version."""
16
+ artifacts: list[EmittedArtifact] = []
17
+ for domain in workspace.mdl.domains:
18
+ for model_name, versions in domain.models.items():
19
+ for version in versions:
20
+ artifacts.append(_emit_model(domain, model_name, version, out_dir))
21
+ for projection_name, versions in domain.projections.items():
22
+ for version in versions:
23
+ artifacts.append(_emit_projection(domain, projection_name, version, out_dir, workspace.mdl))
24
+ return artifacts
25
+
26
+
27
+ def _artifact_id(domain: str, name: str, version: int) -> str:
28
+ return f"{domain}.{name}.v{version}"
29
+
30
+
31
+ def _pascalize(value: str) -> str:
32
+ parts = [part for part in re.split(r"[^A-Za-z0-9]+", value) if part]
33
+ return "".join(part[:1].upper() + part[1:] for part in parts) or "Generated"
34
+
35
+
36
+ def _snake_case(value: str) -> str:
37
+ text = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value)
38
+ text = re.sub(r"[^A-Za-z0-9]+", "_", text)
39
+ text = text.strip("_").lower()
40
+ return text or "generated"
41
+
42
+
43
+ def _stable_type_name(domain: str, name: str, version: int) -> str:
44
+ return f"{_pascalize(domain)}{_pascalize(name)}V{version}"
45
+
46
+
47
+ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
48
+ artifact_id = _artifact_id(domain.name, model_name, version.version)
49
+ type_name = _stable_type_name(domain.name, model_name, version.version)
50
+ nested_definitions: dict[str, list[str]] = {}
51
+ imports: set[str] = set()
52
+ field_specs = _field_specs_from_model_fields(
53
+ version.fields,
54
+ owner_type=type_name,
55
+ path=[],
56
+ definitions=nested_definitions,
57
+ imports=imports,
58
+ )
59
+
60
+ lines = _header_lines(_package_name(domain.name), imports)
61
+ lines.extend(_render_struct_definition(type_name, field_specs))
62
+ lines.extend(_render_nested_definitions(nested_definitions))
63
+
64
+ text = "\n".join(lines) + "\n"
65
+ return EmittedArtifact(
66
+ target="go",
67
+ ref=f"{domain.name}.{model_name}@{version.version}",
68
+ artifact_id=artifact_id,
69
+ path=out_dir / _module_path(domain.name, type_name),
70
+ content=text,
71
+ content_hash=compute_content_hash(text),
72
+ warnings=[],
73
+ )
74
+
75
+
76
+ def _emit_projection(
77
+ domain: DomainDef,
78
+ projection_name: str,
79
+ version: ProjectionVersion,
80
+ out_dir: Path,
81
+ mdl,
82
+ ) -> EmittedArtifact:
83
+ artifact_id = _artifact_id(domain.name, projection_name, version.version)
84
+ type_name = _stable_type_name(domain.name, projection_name, version.version)
85
+ nested_definitions: dict[str, list[str]] = {}
86
+ imports: set[str] = set()
87
+
88
+ field_specs: list[tuple[int, str, str, bool]] = []
89
+ warnings: list[str] = []
90
+ for index, field in enumerate(version.fields):
91
+ field_shape = _resolve_projection_field_shape(field, version, mdl)
92
+ if field_shape is None:
93
+ warnings.append(type_loss(f"{domain.name}.{projection_name}.{field.name}"))
94
+ field_specs.append((index, field.name, "any", False))
95
+ continue
96
+ annotation = _shape_annotation(
97
+ field_shape,
98
+ owner_type=type_name,
99
+ path=[field.name],
100
+ definitions=nested_definitions,
101
+ imports=imports,
102
+ )
103
+ optional = field_shape.optional or field_shape.nullable
104
+ field_specs.append((index, field.name, annotation, optional))
105
+
106
+ lines = _header_lines(_package_name(domain.name), imports)
107
+ lines.extend(_render_struct_definition(type_name, field_specs))
108
+ lines.extend(_render_nested_definitions(nested_definitions))
109
+
110
+ text = "\n".join(lines) + "\n"
111
+ return EmittedArtifact(
112
+ target="go",
113
+ ref=f"{domain.name}.{projection_name}@{version.version}",
114
+ artifact_id=artifact_id,
115
+ path=out_dir / _module_path(domain.name, type_name),
116
+ content=text,
117
+ content_hash=compute_content_hash(text),
118
+ warnings=warnings,
119
+ )
120
+
121
+
122
+ def _header_lines(package_name: str, imports: set[str]) -> list[str]:
123
+ lines = [
124
+ "// Code generated by Modelable; DO NOT EDIT.",
125
+ f"package {package_name}",
126
+ ]
127
+ if imports:
128
+ lines.append("")
129
+ lines.append("import (")
130
+ for item in sorted(imports):
131
+ lines.append(f' "{item}"')
132
+ lines.append(")")
133
+ lines.append("")
134
+ return lines
135
+
136
+
137
+ def _render_nested_definitions(definitions: dict[str, list[str]]) -> list[str]:
138
+ lines: list[str] = []
139
+ for definition in definitions.values():
140
+ lines.append("")
141
+ lines.extend(definition)
142
+ return lines
143
+
144
+
145
+ def _module_path(domain: str, type_name: str) -> Path:
146
+ return Path(_package_name(domain)) / _module_filename(type_name)
147
+
148
+
149
+ def _package_name(domain: str) -> str:
150
+ parts = [part.lower() for part in re.split(r"[^A-Za-z0-9]+", domain) if part]
151
+ return "_".join(parts) or "modelable"
152
+
153
+
154
+ def _module_filename(type_name: str) -> str:
155
+ return f"{_snake_case(type_name)}.go"
156
+
157
+
158
+ def _render_struct_definition(type_name: str, field_specs: list[tuple[int, str, str, bool]]) -> list[str]:
159
+ lines = [f"type {type_name} struct {{"]
160
+ if not field_specs:
161
+ lines.append("}")
162
+ return lines
163
+ for _, name, annotation, default_none in sorted(field_specs, key=lambda item: (item[3], item[0])):
164
+ field_name = _field_name(name)
165
+ tag = _json_tag(name, default_none)
166
+ lines.append(f" {field_name} {annotation} {tag}")
167
+ lines.append("}")
168
+ return lines
169
+
170
+
171
+ def _field_name(value: str) -> str:
172
+ parts = [part for part in re.split(r"[^A-Za-z0-9]+", value) if part]
173
+ return "".join(part[:1].upper() + part[1:] for part in parts) or "Field"
174
+
175
+
176
+ def _json_tag(value: str, optional: bool) -> str:
177
+ suffix = ",omitempty" if optional else ""
178
+ return f'`json:"{value}{suffix}"`'
179
+
180
+
181
+ def _field_specs_from_model_fields(
182
+ fields,
183
+ *,
184
+ owner_type: str,
185
+ path: list[str],
186
+ definitions: dict[str, list[str]],
187
+ imports: set[str],
188
+ ) -> list[tuple[int, str, str, bool]]:
189
+ specs: list[tuple[int, str, str, bool]] = []
190
+ for index, field in enumerate(fields):
191
+ shape = TypeShape.from_field_type(field.type, optional=field.optional)
192
+ annotation = _shape_annotation(
193
+ shape,
194
+ owner_type=owner_type,
195
+ path=[*path, field.name],
196
+ definitions=definitions,
197
+ imports=imports,
198
+ )
199
+ default_none = shape.optional or shape.nullable
200
+ specs.append((index, field.name, annotation, default_none))
201
+ return specs
202
+
203
+
204
+ def _field_specs_from_object_fields(
205
+ fields,
206
+ *,
207
+ owner_type: str,
208
+ path: list[str],
209
+ definitions: dict[str, list[str]],
210
+ imports: set[str],
211
+ ) -> list[tuple[int, str, str, bool]]:
212
+ specs: list[tuple[int, str, str, bool]] = []
213
+ for index, field in enumerate(fields):
214
+ annotation = _shape_annotation(
215
+ field.shape,
216
+ owner_type=owner_type,
217
+ path=[*path, field.name],
218
+ definitions=definitions,
219
+ imports=imports,
220
+ )
221
+ default_none = field.optional or field.shape.optional or field.shape.nullable
222
+ specs.append((index, field.name, annotation, default_none))
223
+ return specs
224
+
225
+
226
+ def _shape_annotation(
227
+ shape: TypeShape,
228
+ *,
229
+ owner_type: str,
230
+ path: list[str],
231
+ definitions: dict[str, list[str]],
232
+ imports: set[str],
233
+ ) -> str:
234
+ base = _shape_base_annotation(shape, owner_type=owner_type, path=path, definitions=definitions, imports=imports)
235
+ if shape.optional or shape.nullable:
236
+ return f"*{base}"
237
+ return base
238
+
239
+
240
+ def _shape_base_annotation(
241
+ shape: TypeShape,
242
+ *,
243
+ owner_type: str,
244
+ path: list[str],
245
+ definitions: dict[str, list[str]],
246
+ imports: set[str],
247
+ ) -> str:
248
+ if shape.kind == "primitive":
249
+ return _primitive_to_go(shape.ref or "string", imports=imports)
250
+ if shape.kind == "decimal":
251
+ return "string"
252
+ if shape.kind == "array":
253
+ element = shape.element or TypeShape(kind="primitive", ref="object")
254
+ element_type = _shape_annotation(
255
+ element,
256
+ owner_type=owner_type,
257
+ path=[*path, "Item"],
258
+ definitions=definitions,
259
+ imports=imports,
260
+ )
261
+ return f"[]{element_type}"
262
+ if shape.kind == "map":
263
+ value = shape.value or TypeShape(kind="primitive", ref="object")
264
+ value_type = _shape_annotation(
265
+ value,
266
+ owner_type=owner_type,
267
+ path=[*path, "Value"],
268
+ definitions=definitions,
269
+ imports=imports,
270
+ )
271
+ return f"map[string]{value_type}"
272
+ if shape.kind == "ref":
273
+ return "string"
274
+ if shape.kind == "enum":
275
+ return "string"
276
+ if shape.kind == "named":
277
+ return _pascalize(shape.ref or "Named")
278
+ if shape.kind == "object":
279
+ type_name = _nested_type_name(owner_type, path)
280
+ if type_name not in definitions:
281
+ definitions[type_name] = _render_struct_definition(
282
+ type_name,
283
+ _field_specs_from_object_fields(
284
+ shape.fields,
285
+ owner_type=owner_type,
286
+ path=path,
287
+ definitions=definitions,
288
+ imports=imports,
289
+ ),
290
+ )
291
+ return type_name
292
+ return "any"
293
+
294
+
295
+ def _primitive_to_go(kind: str, *, imports: set[str]) -> str:
296
+ mapping = {
297
+ "string": "string",
298
+ "bool": "bool",
299
+ "int": "int64",
300
+ "float": "float64",
301
+ "uuid": "string",
302
+ "timestamp": "time.Time",
303
+ "date": "time.Time",
304
+ "time": "time.Time",
305
+ "duration": "time.Duration",
306
+ "binary": "[]byte",
307
+ }
308
+ result = mapping.get(kind, "string")
309
+ if result.startswith("time."):
310
+ imports.add("time")
311
+ return result
312
+
313
+
314
+ def _nested_type_name(owner_type: str, path: list[str]) -> str:
315
+ suffix = "".join(_pascalize(part) for part in path)
316
+ return f"{owner_type}{suffix}" if suffix else owner_type
317
+
318
+
319
+ def _resolve_projection_field_shape(field, projection: ProjectionVersion, mdl):
320
+ if not isinstance(field.mapping, DirectMapping):
321
+ return None
322
+ try:
323
+ source_domain, source_model = projection.source.model.rsplit(".", 1)
324
+ except ValueError:
325
+ return None
326
+ try:
327
+ resolved = resolve_model_ref(mdl, f"{source_domain}.{source_model}", projection.source.version)
328
+ except LookupError:
329
+ return None
330
+ source_mv = resolved.version
331
+ for src_field in source_mv.fields:
332
+ if src_field.name == field.mapping.source_field:
333
+ return TypeShape.from_field_type(src_field.type, optional=src_field.optional)
334
+ return None
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ import re
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.emitters.diagnostics import type_loss
9
+ from modelable.emitters.shapes import TypeShape
10
+ from modelable.parser.ir import DirectMapping, DomainDef, ModelVersion, ProjectionVersion
11
+ from modelable.registry.resolver import resolve_model_ref
12
+
13
+
14
+ def emit_java(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
15
+ """Emit Java source files for every model and projection version."""
16
+ artifacts: list[EmittedArtifact] = []
17
+ for domain in workspace.mdl.domains:
18
+ for model_name, versions in domain.models.items():
19
+ for version in versions:
20
+ artifacts.append(_emit_model(domain, model_name, version, out_dir))
21
+ for projection_name, versions in domain.projections.items():
22
+ for version in versions:
23
+ artifacts.append(_emit_projection(domain, projection_name, version, out_dir, workspace.mdl))
24
+ return artifacts
25
+
26
+
27
+ def _artifact_id(domain: str, name: str, version: int) -> str:
28
+ return f"{domain}.{name}.v{version}"
29
+
30
+
31
+ def _package_name(domain: str) -> str:
32
+ parts = [part.lower() for part in re.split(r"[^A-Za-z0-9]+", domain) if part]
33
+ return ".".join(parts) or "modelable"
34
+
35
+
36
+ def _pascalize(value: str) -> str:
37
+ parts = [part for part in re.split(r"[^A-Za-z0-9]+", value) if part]
38
+ return "".join(part[:1].upper() + part[1:] for part in parts) or "Generated"
39
+
40
+
41
+ def _type_name(name: str, version: int) -> str:
42
+ return f"{_pascalize(name)}V{version}"
43
+
44
+
45
+ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path) -> EmittedArtifact:
46
+ artifact_id = _artifact_id(domain.name, model_name, version.version)
47
+ type_name = _type_name(model_name, version.version)
48
+ lines = _header_lines(_package_name(domain.name))
49
+ nested_definitions: dict[str, list[str]] = {}
50
+ warnings: list[str] = []
51
+
52
+ params: list[str] = []
53
+ for field in version.fields:
54
+ shape = TypeShape.from_field_type(field.type, optional=field.optional)
55
+ java_type = _shape_to_java(shape, owner_type=type_name, path=[field.name], definitions=nested_definitions)
56
+ params.append(f" {java_type} {_field_name(field.name)}")
57
+ lines.append(f"public record {type_name}(")
58
+ lines.append(",\n".join(params))
59
+ lines.append(") {")
60
+ lines.extend(_render_nested_definitions(nested_definitions))
61
+ lines.append("}")
62
+
63
+ text = "\n".join(lines) + "\n"
64
+ return EmittedArtifact(
65
+ target="java",
66
+ ref=f"{domain.name}.{model_name}@{version.version}",
67
+ artifact_id=artifact_id,
68
+ path=out_dir / _java_path(domain.name, type_name),
69
+ content=text,
70
+ content_hash=compute_content_hash(text),
71
+ warnings=warnings,
72
+ )
73
+
74
+
75
+ def _emit_projection(
76
+ domain: DomainDef,
77
+ projection_name: str,
78
+ version: ProjectionVersion,
79
+ out_dir: Path,
80
+ mdl,
81
+ ) -> EmittedArtifact:
82
+ artifact_id = _artifact_id(domain.name, projection_name, version.version)
83
+ type_name = _type_name(projection_name, version.version)
84
+ lines = _header_lines(_package_name(domain.name))
85
+ nested_definitions: dict[str, list[str]] = {}
86
+ warnings: list[str] = []
87
+
88
+ params: list[str] = []
89
+ for field in version.fields:
90
+ field_shape = _resolve_projection_field_shape(field, version, mdl)
91
+ if field_shape is None:
92
+ warnings.append(type_loss(f"{domain.name}.{projection_name}.{field.name}"))
93
+ java_type = "Object"
94
+ else:
95
+ java_type = _shape_to_java(
96
+ field_shape, owner_type=type_name, path=[field.name], definitions=nested_definitions
97
+ )
98
+ params.append(f" {java_type} {_field_name(field.name)}")
99
+ lines.append(f"public record {type_name}(")
100
+ lines.append(",\n".join(params))
101
+ lines.append(") {")
102
+ lines.extend(_render_nested_definitions(nested_definitions))
103
+ lines.append("}")
104
+
105
+ text = "\n".join(lines) + "\n"
106
+ return EmittedArtifact(
107
+ target="java",
108
+ ref=f"{domain.name}.{projection_name}@{version.version}",
109
+ artifact_id=artifact_id,
110
+ path=out_dir / _java_path(domain.name, type_name),
111
+ content=text,
112
+ content_hash=compute_content_hash(text),
113
+ warnings=warnings,
114
+ )
115
+
116
+
117
+ def _header_lines(package_name: str) -> list[str]:
118
+ return [
119
+ f"package {package_name};",
120
+ "",
121
+ "import java.math.BigDecimal;",
122
+ "import java.time.Duration;",
123
+ "import java.time.Instant;",
124
+ "import java.time.LocalDate;",
125
+ "import java.time.LocalTime;",
126
+ "import java.util.List;",
127
+ "import java.util.Map;",
128
+ "import java.util.Optional;",
129
+ "import java.util.UUID;",
130
+ "",
131
+ ]
132
+
133
+
134
+ def _render_nested_definitions(definitions: dict[str, list[str]]) -> list[str]:
135
+ lines: list[str] = []
136
+ for definition in definitions.values():
137
+ lines.append("")
138
+ lines.extend(definition)
139
+ return lines
140
+
141
+
142
+ def _field_name(value: str) -> str:
143
+ parts = [part for part in re.split(r"[^A-Za-z0-9]+", value) if part]
144
+ if not parts:
145
+ return "field"
146
+ first = parts[0][:1].lower() + parts[0][1:]
147
+ tail = "".join(part[:1].upper() + part[1:] for part in parts[1:])
148
+ return first + tail
149
+
150
+
151
+ def _shape_to_java(
152
+ shape: TypeShape,
153
+ *,
154
+ owner_type: str,
155
+ path: list[str],
156
+ definitions: dict[str, list[str]],
157
+ ) -> str:
158
+ base = _shape_base_to_java(shape, owner_type=owner_type, path=path, definitions=definitions)
159
+ if shape.optional or shape.nullable:
160
+ return f"Optional<{base}>"
161
+ return base
162
+
163
+
164
+ def _shape_base_to_java(
165
+ shape: TypeShape,
166
+ *,
167
+ owner_type: str,
168
+ path: list[str],
169
+ definitions: dict[str, list[str]],
170
+ ) -> str:
171
+ if shape.kind == "primitive":
172
+ return _primitive_to_java(shape.ref or "string")
173
+ if shape.kind == "decimal":
174
+ return "BigDecimal"
175
+ if shape.kind == "array":
176
+ element = shape.element or TypeShape(kind="primitive", ref="object")
177
+ return f"List<{_shape_to_java(element, owner_type=owner_type, path=[*path, 'Item'], definitions=definitions)}>"
178
+ if shape.kind == "map":
179
+ value = shape.value or TypeShape(kind="primitive", ref="object")
180
+ return f"Map<String, {_shape_to_java(value, owner_type=owner_type, path=[*path, 'Value'], definitions=definitions)}>"
181
+ if shape.kind == "ref":
182
+ return "String"
183
+ if shape.kind == "enum":
184
+ return "String"
185
+ if shape.kind == "named":
186
+ return _pascalize(shape.ref or "Named")
187
+ if shape.kind == "object":
188
+ type_name = _nested_type_name(path)
189
+ if type_name not in definitions:
190
+ definitions[type_name] = _build_record_definition(
191
+ type_name,
192
+ shape,
193
+ owner_type=owner_type,
194
+ path=path,
195
+ definitions=definitions,
196
+ )
197
+ return type_name
198
+ return "Object"
199
+
200
+
201
+ def _primitive_to_java(kind: str) -> str:
202
+ mapping = {
203
+ "string": "String",
204
+ "bool": "Boolean",
205
+ "int": "Long",
206
+ "float": "Double",
207
+ "uuid": "UUID",
208
+ "timestamp": "Instant",
209
+ "date": "LocalDate",
210
+ "time": "LocalTime",
211
+ "duration": "Duration",
212
+ "binary": "byte[]",
213
+ }
214
+ return mapping.get(kind, "String")
215
+
216
+
217
+ def _nested_type_name(path: list[str]) -> str:
218
+ return "".join(_pascalize(part) for part in path) or "Nested"
219
+
220
+
221
+ def _build_record_definition(
222
+ type_name: str,
223
+ shape: TypeShape,
224
+ *,
225
+ owner_type: str,
226
+ path: list[str],
227
+ definitions: dict[str, list[str]],
228
+ ) -> list[str]:
229
+ lines = [f" public record {type_name}("]
230
+ params: list[str] = []
231
+ for field in shape.fields:
232
+ child_shape = field.shape
233
+ child_type = _shape_to_java(
234
+ child_shape,
235
+ owner_type=owner_type,
236
+ path=[*path, field.name],
237
+ definitions=definitions,
238
+ )
239
+ params.append(f" {child_type} {_field_name(field.name)}")
240
+ lines.append(",\n".join(params))
241
+ lines.append(" ) {}")
242
+ return lines
243
+
244
+
245
+ def _resolve_projection_field_shape(field, projection: ProjectionVersion, mdl):
246
+ if not isinstance(field.mapping, DirectMapping):
247
+ return None
248
+ try:
249
+ source_domain, source_model = projection.source.model.rsplit(".", 1)
250
+ except ValueError:
251
+ return None
252
+ try:
253
+ resolved = resolve_model_ref(mdl, f"{source_domain}.{source_model}", projection.source.version)
254
+ except LookupError:
255
+ return None
256
+ source_mv = resolved.version
257
+ for src_field in source_mv.fields:
258
+ if src_field.name == field.mapping.source_field:
259
+ return TypeShape.from_field_type(src_field.type, optional=src_field.optional)
260
+ return None
261
+
262
+
263
+ def _java_path(domain: str, type_name: str) -> Path:
264
+ return Path(*_package_name(domain).split(".")) / f"{type_name}.java"