metaobjects 0.9.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.
Files changed (181) hide show
  1. metaobjects/__init__.py +75 -0
  2. metaobjects/agent_context/__init__.py +55 -0
  3. metaobjects/agent_context/_content/README.md +14 -0
  4. metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
  5. metaobjects/agent_context/_content/servers/java.meta.json +5 -0
  6. metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
  7. metaobjects/agent_context/_content/servers/python.meta.json +5 -0
  8. metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
  9. metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
  10. metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
  11. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
  12. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
  13. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
  14. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
  15. metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
  16. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
  17. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
  18. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
  19. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
  20. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
  21. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  22. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
  23. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  24. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
  25. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  26. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  27. metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
  28. metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
  29. metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
  30. metaobjects/agent_context/assemble.py +133 -0
  31. metaobjects/agent_context/content_root.py +54 -0
  32. metaobjects/agent_context/scaffold.py +191 -0
  33. metaobjects/agent_context/types.py +44 -0
  34. metaobjects/attr_class_map.py +23 -0
  35. metaobjects/cli.py +696 -0
  36. metaobjects/codegen/__init__.py +0 -0
  37. metaobjects/codegen/config.py +11 -0
  38. metaobjects/codegen/constants.py +13 -0
  39. metaobjects/codegen/extract_delegate_emitter.py +384 -0
  40. metaobjects/codegen/extract_schema_emitter.py +139 -0
  41. metaobjects/codegen/format.py +31 -0
  42. metaobjects/codegen/fr010_field_mapping.py +220 -0
  43. metaobjects/codegen/generator.py +62 -0
  44. metaobjects/codegen/generator_registry.py +163 -0
  45. metaobjects/codegen/generators/__init__.py +0 -0
  46. metaobjects/codegen/generators/entity_model.py +263 -0
  47. metaobjects/codegen/generators/extractor_generator.py +317 -0
  48. metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
  49. metaobjects/codegen/generators/m2m_codegen.py +192 -0
  50. metaobjects/codegen/generators/output_parser_generator.py +272 -0
  51. metaobjects/codegen/generators/output_prompt_generator.py +192 -0
  52. metaobjects/codegen/generators/payload_vo_generator.py +672 -0
  53. metaobjects/codegen/generators/render_helper_generator.py +451 -0
  54. metaobjects/codegen/generators/router_generator.py +635 -0
  55. metaobjects/codegen/generators/template_generator.py +70 -0
  56. metaobjects/codegen/generators/tph_plan.py +120 -0
  57. metaobjects/codegen/generators/trace_helper_generator.py +336 -0
  58. metaobjects/codegen/instance_artifacts.py +15 -0
  59. metaobjects/codegen/output_format_spec_emitter.py +79 -0
  60. metaobjects/codegen/overwrite_policy.py +27 -0
  61. metaobjects/codegen/runner.py +110 -0
  62. metaobjects/codegen/runtime/__init__.py +6 -0
  63. metaobjects/codegen/runtime/filter_parser.py +193 -0
  64. metaobjects/codegen/type_map.py +84 -0
  65. metaobjects/core_types.py +809 -0
  66. metaobjects/datatype.py +19 -0
  67. metaobjects/documentation/__init__.py +28 -0
  68. metaobjects/documentation/doc_constants.py +20 -0
  69. metaobjects/documentation/doc_provider.py +20 -0
  70. metaobjects/documentation/doc_schema.py +24 -0
  71. metaobjects/errors.py +124 -0
  72. metaobjects/loader/__init__.py +0 -0
  73. metaobjects/loader/merge.py +287 -0
  74. metaobjects/loader/meta_data_loader.py +245 -0
  75. metaobjects/loader/sources/__init__.py +24 -0
  76. metaobjects/loader/sources/directory_source.py +50 -0
  77. metaobjects/loader/sources/file_source.py +41 -0
  78. metaobjects/loader/sources/meta_data_source.py +67 -0
  79. metaobjects/loader/sources/uri_source.py +56 -0
  80. metaobjects/loader/validate_discriminator.py +181 -0
  81. metaobjects/loader/validate_field_readonly.py +146 -0
  82. metaobjects/loader/validate_source_parameter_ref.py +159 -0
  83. metaobjects/loader/validate_source_physical_names.py +140 -0
  84. metaobjects/loader/validation_passes.py +1513 -0
  85. metaobjects/meta/__init__.py +1 -0
  86. metaobjects/meta/core/__init__.py +0 -0
  87. metaobjects/meta/core/attr/__init__.py +0 -0
  88. metaobjects/meta/core/attr/attr_constants.py +31 -0
  89. metaobjects/meta/core/attr/meta_attr.py +136 -0
  90. metaobjects/meta/core/field/__init__.py +0 -0
  91. metaobjects/meta/core/field/field_constants.py +105 -0
  92. metaobjects/meta/core/field/meta_field.py +76 -0
  93. metaobjects/meta/core/identity/__init__.py +0 -0
  94. metaobjects/meta/core/identity/identity_constants.py +19 -0
  95. metaobjects/meta/core/identity/meta_identity.py +8 -0
  96. metaobjects/meta/core/object/__init__.py +0 -0
  97. metaobjects/meta/core/object/meta_object.py +65 -0
  98. metaobjects/meta/core/object/meta_object_aware.py +43 -0
  99. metaobjects/meta/core/object/object_class_registry.py +56 -0
  100. metaobjects/meta/core/object/object_constants.py +13 -0
  101. metaobjects/meta/core/object/object_extract.py +400 -0
  102. metaobjects/meta/core/object/value_object.py +70 -0
  103. metaobjects/meta/core/relationship/__init__.py +0 -0
  104. metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
  105. metaobjects/meta/core/relationship/meta_relationship.py +54 -0
  106. metaobjects/meta/core/relationship/relationship_constants.py +51 -0
  107. metaobjects/meta/core/validator/__init__.py +0 -0
  108. metaobjects/meta/core/validator/validator_constants.py +18 -0
  109. metaobjects/meta/meta_data.py +206 -0
  110. metaobjects/meta/meta_root.py +8 -0
  111. metaobjects/meta/persistence/__init__.py +0 -0
  112. metaobjects/meta/persistence/db/__init__.py +1 -0
  113. metaobjects/meta/persistence/db/db_constants.py +41 -0
  114. metaobjects/meta/persistence/db/db_provider.py +60 -0
  115. metaobjects/meta/persistence/origin/__init__.py +0 -0
  116. metaobjects/meta/persistence/origin/meta_origin.py +8 -0
  117. metaobjects/meta/persistence/origin/origin_constants.py +20 -0
  118. metaobjects/meta/persistence/source/__init__.py +0 -0
  119. metaobjects/meta/persistence/source/meta_source.py +137 -0
  120. metaobjects/meta/persistence/source/source_constants.py +115 -0
  121. metaobjects/meta/presentation/__init__.py +0 -0
  122. metaobjects/meta/presentation/layout/__init__.py +0 -0
  123. metaobjects/meta/presentation/layout/layout_constants.py +13 -0
  124. metaobjects/meta/presentation/layout/meta_layout.py +8 -0
  125. metaobjects/meta/presentation/view/__init__.py +0 -0
  126. metaobjects/meta/presentation/view/meta_view.py +8 -0
  127. metaobjects/meta/presentation/view/view_constants.py +22 -0
  128. metaobjects/meta/template/__init__.py +0 -0
  129. metaobjects/meta/template/meta_template.py +46 -0
  130. metaobjects/meta/template/template_constants.py +112 -0
  131. metaobjects/meta/template/template_provider.py +43 -0
  132. metaobjects/parser.py +380 -0
  133. metaobjects/parser_yaml.py +82 -0
  134. metaobjects/provider.py +111 -0
  135. metaobjects/py.typed +0 -0
  136. metaobjects/registry.py +210 -0
  137. metaobjects/registry_manifest.py +223 -0
  138. metaobjects/render/__init__.py +74 -0
  139. metaobjects/render/email_document.py +14 -0
  140. metaobjects/render/escapers.py +109 -0
  141. metaobjects/render/extract/__init__.py +59 -0
  142. metaobjects/render/extract/coerce.py +279 -0
  143. metaobjects/render/extract/extract.py +211 -0
  144. metaobjects/render/extract/extract_map.py +61 -0
  145. metaobjects/render/extract/json_forgiving_reader.py +203 -0
  146. metaobjects/render/extract/locate.py +65 -0
  147. metaobjects/render/extract/normalize.py +96 -0
  148. metaobjects/render/extract/strip.py +20 -0
  149. metaobjects/render/extract/types.py +332 -0
  150. metaobjects/render/extract/xml_forgiving_reader.py +162 -0
  151. metaobjects/render/filesystem_provider.py +51 -0
  152. metaobjects/render/prompt/__init__.py +32 -0
  153. metaobjects/render/prompt/output_format_renderer.py +340 -0
  154. metaobjects/render/prompt/output_format_spec.py +28 -0
  155. metaobjects/render/prompt/prompt_field.py +29 -0
  156. metaobjects/render/prompt/prompt_overrides.py +29 -0
  157. metaobjects/render/prompt/prompt_style.py +38 -0
  158. metaobjects/render/renderer.py +358 -0
  159. metaobjects/render/verify.py +266 -0
  160. metaobjects/runtime/__init__.py +39 -0
  161. metaobjects/runtime/llm_recorder.py +210 -0
  162. metaobjects/runtime/n2m_resolver.py +155 -0
  163. metaobjects/runtime/object_manager.py +715 -0
  164. metaobjects/runtime/tph.py +50 -0
  165. metaobjects/serializer_json.py +172 -0
  166. metaobjects/shared/__init__.py +0 -0
  167. metaobjects/shared/base_types.py +16 -0
  168. metaobjects/shared/separators.py +4 -0
  169. metaobjects/shared/structural.py +9 -0
  170. metaobjects/source/__init__.py +79 -0
  171. metaobjects/source/error_source.py +266 -0
  172. metaobjects/source/json_path.py +106 -0
  173. metaobjects/source/semantic_diff.py +98 -0
  174. metaobjects/source/yaml_positions.py +174 -0
  175. metaobjects/super_resolve.py +128 -0
  176. metaobjects/yaml_desugar.py +481 -0
  177. metaobjects-0.9.0.dist-info/METADATA +97 -0
  178. metaobjects-0.9.0.dist-info/RECORD +181 -0
  179. metaobjects-0.9.0.dist-info/WHEEL +4 -0
  180. metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
  181. metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
@@ -0,0 +1,11 @@
1
+ """Codegen run configuration (the run_gen surface)."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class GenConfig:
9
+ out_dir: str
10
+ output_layout: str = "flat" # "flat" only in sub-project A
11
+ emit_abstract_shapes: bool = True # Python concretes subclass the abstract base model
@@ -0,0 +1,13 @@
1
+ """Codegen-wide constants. The @generated marker drives the overwrite policy."""
2
+ from __future__ import annotations
3
+
4
+ GENERATED_MARKER = "@generated by metaobjects"
5
+
6
+
7
+ def generated_header(entity_name: str, fqn: str) -> str:
8
+ """Line-1 header for every emitted module. Detected by the overwrite policy."""
9
+ return (
10
+ f"# {GENERATED_MARKER} — DO NOT EDIT.\n"
11
+ f"# Source metadata: {entity_name} ({fqn})\n"
12
+ f"# Customize via {entity_name}_extra.py in this directory.\n"
13
+ )
@@ -0,0 +1,384 @@
1
+ """FR-010 nested-codegen-gap — the runtime-DELEGATING extract emitter (Python).
2
+
3
+ The self-contained ``extract_<name>(text)`` path (``extract_schema_emitter`` + the
4
+ baked ``ExtractSchema``) covers scalars / enums / scalar-arrays but leaves
5
+ nested-object and array-of-object components ``None`` — the historical FR-010 codegen
6
+ gap. This module emits the additive *delegating* entry that CLOSES that gap by
7
+ wrapping the runtime extract:
8
+
9
+ extract_<name>_with_loader(root, text, opts=None) -> ExtractionResult[<Name>Extracted]
10
+
11
+ It resolves this payload's ``MetaObject`` from the supplied loaded ``MetaRoot`` by its
12
+ baked simple name (``PAYLOAD_NAME``), delegates to ``extract_object`` in
13
+ :mod:`metaobjects.meta.core.object.object_extract` (which assembles the FULL nested
14
+ object graph reflection-free via the Phase A object model — ``MetaObject.new_instance()``
15
+ + the ``MetaField`` set-by-name SPI), then maps the assembled ``ValueObject`` graph into
16
+ the typed nullable mirror graph via generated ``_from_<vo>_extracted(o)`` mapper
17
+ functions (payload + every reachable nested VO, deduped).
18
+
19
+ This is the codegen-wrapping-runtime pattern (a generated DAO calling the
20
+ dynamic-metadata runtime), mirroring the Java / Kotlin / TS pilots. The generated
21
+ mappers read the assembled graph through a tiny ``_read_prop`` helper that mirrors the
22
+ ``MetaField`` get SPI (``ValueObject.get(name)`` else plain-attribute access), so the
23
+ emitted code stays self-sufficient and reflection-free.
24
+
25
+ Bounded by the cross-port ``MAX_NEST_DEPTH`` via the runtime — codegen here only mirrors
26
+ the runtime's resolved object graph, so depth/cycle guarding lives in ``object_extract``.
27
+ The emitter dedupes mirrors/mappers by VO simple name (cycle-safe).
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from metaobjects.codegen import fr010_field_mapping as fm
32
+ from metaobjects.meta.core.field import field_constants as fc
33
+ from metaobjects.meta.meta_data import MetaData
34
+ from metaobjects.shared.base_types import TYPE_OBJECT
35
+ from metaobjects.shared.separators import PACKAGE_SEP
36
+
37
+
38
+ def _find_object(root: MetaData, name: str) -> MetaData | None:
39
+ """The own-child ``object.*`` node named *name*, or ``None``."""
40
+ for c in root.own_children():
41
+ if c.type == TYPE_OBJECT and c.name == name:
42
+ return c
43
+ return None
44
+
45
+
46
+ def ref_vo(field: MetaData, root: MetaData) -> MetaData | None:
47
+ """The ``@objectRef`` target VO for a nested-object field, or ``None`` when
48
+ unresolvable. Matches first on the full ref, then the trailing simple-name
49
+ segment (mirrors the runtime ``_resolve_object_ref`` short-name fallback)."""
50
+ ref = field.attr(fc.FIELD_ATTR_OBJECT_REF)
51
+ if not isinstance(ref, str) or not ref:
52
+ return None
53
+ direct = _find_object(root, ref)
54
+ if direct is not None:
55
+ return direct
56
+ if PACKAGE_SEP in ref:
57
+ return _find_object(root, ref.rsplit(PACKAGE_SEP, 1)[-1])
58
+ return None
59
+
60
+
61
+ def _is_object_field(field: MetaData) -> bool:
62
+ """True iff the field is a nested object reference (``field.object`` — distinct
63
+ from the string-backed ``field.enum``, treated as a scalar)."""
64
+ return field.sub_type == fc.FIELD_SUBTYPE_OBJECT
65
+
66
+
67
+ def mirror_name(vo: MetaData) -> str:
68
+ """The extracted-mirror dataclass name for a value-object (``<Name>Extracted``)."""
69
+ return f"{vo.name}Extracted"
70
+
71
+
72
+ def _mapper_name(vo: MetaData) -> str:
73
+ """The mapper function name for a value-object (``_from_<name>_extracted``)."""
74
+ return f"_from_{_snake(vo.name)}_extracted"
75
+
76
+
77
+ def root_mapper_name(template_name: str) -> str:
78
+ """The root mapper's name — derived from the TEMPLATE (so it returns the
79
+ canonically-named ``<Template>Extracted`` mirror)."""
80
+ return f"_from_{_snake(template_name)}_extracted"
81
+
82
+
83
+ def _snake(name: str) -> str:
84
+ """``NpcResponseOutput`` → ``npc_response_output`` (matches the cross-generator
85
+ ``_snake_case`` convention; no acronym handling)."""
86
+ out: list[str] = []
87
+ for i, ch in enumerate(name):
88
+ if ch.isupper() and i > 0:
89
+ out.append("_")
90
+ out.append(ch.lower())
91
+ return "".join(out)
92
+
93
+
94
+ # =============================================================================
95
+ # Nested-aware mirror type (recurses into nested mirror names)
96
+ # =============================================================================
97
+
98
+
99
+ def _nested_mirror_type(field: MetaData, root: MetaData) -> str:
100
+ """The nullable mirror annotation for one field — nested-aware (nested objects
101
+ become ``<Nested>Extracted``; array-of-objects become ``list[...]``)."""
102
+ if _is_object_field(field):
103
+ target = ref_vo(field, root)
104
+ base = f'"{mirror_name(target)}"' if target is not None else "object"
105
+ elem = f"{base} | None"
106
+ return f"list[{elem}] | None" if fm.is_array(field) else elem
107
+ if fm.is_array(field):
108
+ return "list[str | None] | None"
109
+ if field.sub_type == fc.FIELD_SUBTYPE_ENUM:
110
+ return "str | None"
111
+ kind = fm.scalar_kind(field.sub_type)
112
+ if kind in ("INT", "LONG"):
113
+ return "int | None"
114
+ if kind == "DOUBLE":
115
+ return "float | None"
116
+ if kind == "BOOLEAN":
117
+ return "bool | None"
118
+ return "str | None"
119
+
120
+
121
+ # =============================================================================
122
+ # Reachability
123
+ # =============================================================================
124
+
125
+
126
+ def reachable_vos(vo: MetaData, root: MetaData) -> list[MetaData]:
127
+ """``vo`` + every value-object reachable through nested ``@objectRef`` fields, in
128
+ stable BFS order, deduped by simple name (cycle-safe)."""
129
+ out: list[MetaData] = []
130
+ seen: set[str] = set()
131
+ queue: list[MetaData] = [vo]
132
+ while queue:
133
+ cur = queue.pop(0)
134
+ if cur.name in seen:
135
+ continue
136
+ seen.add(cur.name)
137
+ out.append(cur)
138
+ for f in fm.fields(cur):
139
+ if _is_object_field(f):
140
+ target = ref_vo(f, root)
141
+ if target is not None and target.name not in seen:
142
+ queue.append(target)
143
+ return out
144
+
145
+
146
+ def has_nested(vo: MetaData, root: MetaData) -> bool:
147
+ """True iff ``vo`` (or any reachable nested VO) has a nested-object field."""
148
+ for cur in reachable_vos(vo, root):
149
+ if any(_is_object_field(f) for f in fm.fields(cur)):
150
+ return True
151
+ return False
152
+
153
+
154
+ # =============================================================================
155
+ # Nested-aware mirror dataclasses
156
+ # =============================================================================
157
+
158
+
159
+ def nested_mirror_dataclasses(
160
+ vo: MetaData, root: MetaData, payload_mirror: str
161
+ ) -> list[str]:
162
+ """Emit the nested-aware mirror dataclass for ``vo`` and every reachable nested VO
163
+ (deduped). The payload mirror keeps the canonical ``<Template>Extracted`` name
164
+ (``payload_mirror``) so the existing self-contained ``extract_<name>()`` initializer
165
+ and the delegating path share ONE mirror type. The nested mirrors carry their own
166
+ ``<VO>Extracted`` name. Returns source lines (blank-line separated)."""
167
+ lines: list[str] = []
168
+ for i, cur in enumerate(reachable_vos(vo, root)):
169
+ if i > 0:
170
+ lines.append("")
171
+ lines.append("")
172
+ name = payload_mirror if i == 0 else mirror_name(cur)
173
+ lines.extend(_one_mirror(cur, root, name))
174
+ return lines
175
+
176
+
177
+ def _one_mirror(vo: MetaData, root: MetaData, record_name: str) -> list[str]:
178
+ base = (
179
+ record_name[: -len("Extracted")]
180
+ if record_name.endswith("Extracted")
181
+ else record_name
182
+ )
183
+ lines: list[str] = [
184
+ "@dataclass(frozen=True, slots=True)",
185
+ f"class {record_name}:",
186
+ f' """Best-effort extracted twin of ``{base}`` — every field nullable',
187
+ ' (``None`` where the value was lost or malformed)."""',
188
+ ]
189
+ field_lines = [
190
+ f" {f.name}: {_nested_mirror_type(f, root)} = None" for f in fm.fields(vo)
191
+ ]
192
+ lines.extend(field_lines or [" pass"])
193
+ return lines
194
+
195
+
196
+ # =============================================================================
197
+ # Mapper functions (assembled ValueObject graph -> typed nullable mirror graph)
198
+ # =============================================================================
199
+
200
+
201
+ def nested_mappers(
202
+ vo: MetaData, root: MetaData, root_mapper_fn: str, root_mirror: str
203
+ ) -> list[str]:
204
+ """Emit one ``_from_<vo>_extracted(o)`` mapper per reachable VO (payload + nested,
205
+ deduped). The ROOT mapper is overridden to the template-derived ``root_mapper_fn`` /
206
+ ``root_mirror`` so it returns the canonically-named root mirror. Returns source
207
+ lines (blank-line separated)."""
208
+ lines: list[str] = []
209
+ vos = reachable_vos(vo, root)
210
+ for i, cur in enumerate(vos):
211
+ if i > 0:
212
+ lines.append("")
213
+ lines.append("")
214
+ fn = root_mapper_fn if i == 0 else _mapper_name(cur)
215
+ mir = root_mirror if i == 0 else mirror_name(cur)
216
+ lines.extend(_one_mapper(cur, root, fn, mir))
217
+ return lines
218
+
219
+
220
+ def _one_mapper(
221
+ vo: MetaData, root: MetaData, fn: str, mirror: str
222
+ ) -> list[str]:
223
+ lines: list[str] = [
224
+ f'def {fn}(o: object | None) -> "{mirror} | None":',
225
+ f" \"\"\"Map an assembled ValueObject graph into a typed ``{mirror}`` mirror;"
226
+ " null-tolerant.\"\"\"",
227
+ " if o is None:",
228
+ " return None",
229
+ f" return {mirror}(",
230
+ ]
231
+ for f in fm.fields(vo):
232
+ lines.append(f" {f.name}={_mapper_arg(f, root)},")
233
+ lines.append(" )")
234
+ return lines
235
+
236
+
237
+ def _mapper_arg(field: MetaData, root: MetaData) -> str:
238
+ """The mirror-field initializer that reads ``field`` from the assembled object ``o``."""
239
+ key = f'"{field.name}"'
240
+ if _is_object_field(field):
241
+ target = ref_vo(field, root)
242
+ if target is None:
243
+ return "None # unresolved @objectRef"
244
+ fn = _mapper_name(target)
245
+ if fm.is_array(field):
246
+ return f"_map_object_list(_read_prop(o, {key}), {fn})"
247
+ return f"{fn}(_read_prop(o, {key}))"
248
+
249
+ # Enum / scalar / scalar-array: the runtime already coerced; read + light-coerce to
250
+ # the mirror's nullable shape via the locally-defined _dlg_* readers.
251
+ # ARRAY is checked BEFORE the scalar-enum branch: an enum ARRAY must route through
252
+ # the string-LIST reader, not the scalar enum reader — otherwise the list collapses
253
+ # to a single stringified scalar (the cross-port "enum-before-isArray" ordering bug).
254
+ if fm.is_array(field):
255
+ return f"_dlg_str_list(_read_prop(o, {key}))"
256
+ if field.sub_type == fc.FIELD_SUBTYPE_ENUM:
257
+ return f"_dlg_str(_read_prop(o, {key}))"
258
+ kind = fm.scalar_kind(field.sub_type)
259
+ if kind in ("INT", "LONG"):
260
+ return f"_dlg_int(_read_prop(o, {key}))"
261
+ if kind == "DOUBLE":
262
+ return f"_dlg_float(_read_prop(o, {key}))"
263
+ if kind == "BOOLEAN":
264
+ return f"_dlg_bool(_read_prop(o, {key}))"
265
+ return f"_dlg_str(_read_prop(o, {key}))"
266
+
267
+
268
+ # =============================================================================
269
+ # Used-helper scoping + the shared helper block
270
+ # =============================================================================
271
+
272
+
273
+ def used_helpers(vo: MetaData, root: MetaData) -> set[str]:
274
+ """The generated-helper names the mappers for ``vo`` (+ reachable nested VOs)
275
+ actually reference. ``_read_prop`` is always present once any mapper is emitted."""
276
+ used: set[str] = {"_read_prop"}
277
+ for cur in reachable_vos(vo, root):
278
+ for f in fm.fields(cur):
279
+ if _is_object_field(f):
280
+ if fm.is_array(f):
281
+ used.add("_map_object_list")
282
+ continue
283
+ if fm.is_array(f):
284
+ # ARRAY before scalar-enum — an enum array uses the string-LIST reader
285
+ # (mirrors the _mapper_arg ordering; avoids the collapse-to-scalar bug).
286
+ used.add("_dlg_str_list")
287
+ elif f.sub_type == fc.FIELD_SUBTYPE_ENUM:
288
+ used.add("_dlg_str")
289
+ else:
290
+ kind = fm.scalar_kind(f.sub_type)
291
+ if kind in ("INT", "LONG"):
292
+ used.add("_dlg_int")
293
+ elif kind == "DOUBLE":
294
+ used.add("_dlg_float")
295
+ elif kind == "BOOLEAN":
296
+ used.add("_dlg_bool")
297
+ else:
298
+ used.add("_dlg_str")
299
+ return used
300
+
301
+
302
+ # Each helper: name -> source block (lines).
303
+ _HELPER_BLOCKS: dict[str, list[str]] = {
304
+ "_read_prop": [
305
+ "def _read_prop(o: object | None, name: str) -> object | None:",
306
+ ' """Read a property from an assembled backing object, mirroring the',
307
+ " MetaField get SPI (``ValueObject.get(name)`` else plain-attribute access).",
308
+ ' Keeps the mappers reflection-free + backing-agnostic."""',
309
+ " if o is None:",
310
+ " return None",
311
+ ' getter = getattr(o, "get", None)',
312
+ " if callable(getter):",
313
+ " return getter(name)",
314
+ " return getattr(o, name, None)",
315
+ ],
316
+ "_map_object_list": [
317
+ "def _map_object_list(v: object | None, fn) -> list | None:",
318
+ ' """Map each element of an assembled array via ``fn``; non-list -> None."""',
319
+ " if not isinstance(v, list):",
320
+ " return None",
321
+ " return [fn(e) for e in v]",
322
+ ],
323
+ "_dlg_str": [
324
+ "def _dlg_str(v: object | None) -> str | None:",
325
+ " return None if v is None else str(v)",
326
+ ],
327
+ "_dlg_int": [
328
+ "def _dlg_int(v: object | None) -> int | None:",
329
+ " if v is None:",
330
+ " return None",
331
+ " try:",
332
+ " return int(v) # type: ignore[arg-type, call-overload]",
333
+ " except (TypeError, ValueError):",
334
+ " return None",
335
+ ],
336
+ "_dlg_float": [
337
+ "def _dlg_float(v: object | None) -> float | None:",
338
+ " if v is None:",
339
+ " return None",
340
+ " try:",
341
+ " return float(v) # type: ignore[arg-type]",
342
+ " except (TypeError, ValueError):",
343
+ " return None",
344
+ ],
345
+ "_dlg_bool": [
346
+ "def _dlg_bool(v: object | None) -> bool | None:",
347
+ " if v is None:",
348
+ " return None",
349
+ " if isinstance(v, bool):",
350
+ " return v",
351
+ ' return str(v).lower() == "true"',
352
+ ],
353
+ "_dlg_str_list": [
354
+ "def _dlg_str_list(v: object | None) -> list | None:",
355
+ " if not isinstance(v, list):",
356
+ " return None",
357
+ " return [None if e is None else str(e) for e in v]",
358
+ ],
359
+ }
360
+
361
+ # Stable emission order (deterministic output).
362
+ _HELPER_ORDER: tuple[str, ...] = (
363
+ "_read_prop",
364
+ "_map_object_list",
365
+ "_dlg_str",
366
+ "_dlg_int",
367
+ "_dlg_float",
368
+ "_dlg_bool",
369
+ "_dlg_str_list",
370
+ )
371
+
372
+
373
+ def delegate_helpers(used: set[str]) -> list[str]:
374
+ """The shared helper block the generated mappers rely on, scoped to ``used`` and
375
+ emitted in stable order. Returns source lines (blank-line separated)."""
376
+ lines: list[str] = [
377
+ "# ---- runtime-delegating extract helpers (generated) ----"
378
+ ]
379
+ for name in _HELPER_ORDER:
380
+ if name in used:
381
+ lines.append("")
382
+ lines.append("")
383
+ lines.extend(_HELPER_BLOCKS[name])
384
+ return lines
@@ -0,0 +1,139 @@
1
+ """Turns a payload value-object into Python source fragments for the FR-010 extract
2
+ codegen:
3
+
4
+ * :func:`schema_literal` — a ``ExtractSchema(Format.X, "root", [FieldSpec(...), …])``
5
+ baked descriptor for the emitted parser module.
6
+ * :func:`mirror_dataclass` — an all-nullable mirror dataclass ``<Payload>Extracted``
7
+ (every field ``X | None = None``; the extract entry
8
+ returns this nullable twin rather than the strict
9
+ Pydantic payload — same reasoning as the Kotlin / C#
10
+ nullable mirror).
11
+ * :func:`mirror_initializer` — ``<Name>Extracted(field=as_string(d, "field"), …)``.
12
+
13
+ Mirrors the C# / Java ``ExtractSchemaEmitter`` adapted to Python syntax + the
14
+ nullable-mirror shape. Bounded scope: scalar / enum / scalar-array. Nested object +
15
+ array-of-enum deferred.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from metaobjects.codegen import fr010_field_mapping as fm
20
+ from metaobjects.meta.core.field import field_constants as fc
21
+ from metaobjects.meta.meta_data import MetaData
22
+
23
+ # The extract_map accessors a generated module may import (sorted, deduped subset).
24
+ ALL_EXTRACT_MAP_HELPERS: tuple[str, ...] = (
25
+ "as_bool",
26
+ "as_double",
27
+ "as_int",
28
+ "as_long",
29
+ "as_string",
30
+ "as_string_list",
31
+ )
32
+
33
+
34
+ def _format_enum(fmt: str) -> str:
35
+ return "Format.XML" if fmt.lower() == "xml" else "Format.JSON"
36
+
37
+
38
+ def _field_spec_literal(field: MetaData, owner: MetaData) -> str:
39
+ name = field.name
40
+ required = fm.is_required(field)
41
+ req = "True" if required else "False"
42
+
43
+ # An enum ARRAY is checked BEFORE the scalar-enum branch below: it is baked as a
44
+ # string-list scalar slot (read via as_string_list), NOT a scalar enum_field —
45
+ # otherwise the runtime collapses the list to one stringified value (the cross-port
46
+ # "enum-before-isArray" ordering bug). The mirror type / extract_map helper already
47
+ # route arrays first; this keeps the baked schema spec consistent with them. Scoped
48
+ # to the enum case so NON-enum scalar arrays keep their real element FieldKind below
49
+ # (the self-contained baked path drops arrays as MALFORMED before kind is read, so
50
+ # the kind is inert either way — but emitting the true kind is correct + clearer).
51
+ if fm.is_array(field) and field.sub_type == fc.FIELD_SUBTYPE_ENUM:
52
+ return f'FieldSpec.scalar("{name}", FieldKind.STRING, {req})'
53
+
54
+ if field.sub_type == fc.FIELD_SUBTYPE_ENUM:
55
+ values_lit = fm.string_list_literal(fm.enum_values(field))
56
+ alias_lit = fm.properties_map_literal(field.attr(fc.FIELD_ATTR_ENUM_ALIAS))
57
+ # FR-011: resolve the three new enum args (field → object.value → "strip" for
58
+ # normalize). Keep the back-compat 4-arg form when nothing is set; otherwise
59
+ # emit the 7-arg form (..., coerce_default, normalize, default_value).
60
+ coerce_default = fm.coerce_default(field)
61
+ default_value = fm.default_value(field)
62
+ normalize = fm.resolve_normalize(field, owner)
63
+ if (
64
+ coerce_default is None
65
+ and default_value is None
66
+ and normalize == fc.NORMALIZE_DEFAULT
67
+ ):
68
+ return f'FieldSpec.enum_field("{name}", {req}, {values_lit}, {alias_lit})'
69
+ cd_lit = "None" if coerce_default is None else fm.py_string_literal(coerce_default)
70
+ norm_lit = fm.py_string_literal(normalize)
71
+ dv_lit = "None" if default_value is None else fm.py_string_literal(default_value)
72
+ return (
73
+ f'FieldSpec.enum_field("{name}", {req}, {values_lit}, {alias_lit}, '
74
+ f"{cd_lit}, {norm_lit}, {dv_lit})"
75
+ )
76
+
77
+ if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
78
+ # Self-contained path models a nested object as a plain (opaque) STRING scalar
79
+ # slot — it does not recurse. The runtime-delegating ``*_with_loader`` entry
80
+ # populates the real nested graph. (No trailing inline comment: this literal is
81
+ # joined into a ``[...]`` argument list, where a ``#`` comment would be illegal.)
82
+ return f'FieldSpec.scalar("{name}", FieldKind.STRING, {req})'
83
+
84
+ kind = fm.scalar_kind(field.sub_type) or "STRING"
85
+ # @xmlText: a scalar field marked to receive its element's XML text content
86
+ # (JAXB @XmlValue / Jackson @JacksonXmlText / .NET [XmlText]). Mirrors the TS
87
+ # extract-schema-emitter textContentField branch. No effect for JSON.
88
+ if fm.xml_text(field):
89
+ return f'FieldSpec.text_content_field("{name}", FieldKind.{kind}, {req})'
90
+ return f'FieldSpec.scalar("{name}", FieldKind.{kind}, {req})'
91
+
92
+
93
+ def schema_literal(vo: MetaData, fmt: str, root_name: str) -> str:
94
+ """Emit ``ExtractSchema(Format.X, "rootName", [FieldSpec(...), …])``."""
95
+ format_enum = _format_enum(fmt)
96
+ specs = [_field_spec_literal(f, vo) for f in fm.fields(vo)]
97
+ if not specs:
98
+ return f'ExtractSchema({format_enum}, "{root_name}", [])'
99
+ body = ", ".join(specs)
100
+ return f'ExtractSchema({format_enum}, "{root_name}", [{body}])'
101
+
102
+
103
+ def mirror_dataclass(vo: MetaData, record_name: str) -> list[str]:
104
+ """Emit the all-nullable mirror dataclass declaration (source lines)."""
105
+ base = (
106
+ record_name[: -len("Extracted")]
107
+ if record_name.endswith("Extracted")
108
+ else record_name
109
+ )
110
+ lines: list[str] = [
111
+ "@dataclass(frozen=True, slots=True)",
112
+ f"class {record_name}:",
113
+ f' """Best-effort extracted twin of ``{base}`` — every field nullable',
114
+ ' (``None`` where the value was lost or malformed)."""',
115
+ ]
116
+ field_lines = [
117
+ f" {f.name}: {fm.mirror_type(f)} = None" for f in fm.fields(vo)
118
+ ]
119
+ if field_lines:
120
+ lines.extend(field_lines)
121
+ else:
122
+ lines.append(" pass")
123
+ return lines
124
+
125
+
126
+ def mirror_initializer(vo: MetaData, record_name: str) -> str:
127
+ """Emit ``<recordName>(field=as_string(d, "field"), …)``."""
128
+ assigns = [f"{f.name}={fm.extract_map_call(f)}" for f in fm.fields(vo)]
129
+ return f"{record_name}({', '.join(assigns)})"
130
+
131
+
132
+ def extract_map_imports(vo: MetaData) -> list[str]:
133
+ """The sorted, deduped ``extract_map`` accessor names the mirror initializer needs."""
134
+ used: set[str] = set()
135
+ for f in fm.fields(vo):
136
+ h = fm.extract_map_helper(f)
137
+ if h is not None:
138
+ used.add(h)
139
+ return [h for h in ALL_EXTRACT_MAP_HELPERS if h in used]
@@ -0,0 +1,31 @@
1
+ """Canonicalize emitted source via ruff: sort imports (isort), then format.
2
+
3
+ Both passes read stdin / write stdout, so generated code is import-sorted AND
4
+ formatted — i.e. `ruff check`- and `ruff format`-clean for downstream consumers."""
5
+ from __future__ import annotations
6
+
7
+ import subprocess
8
+ import sys
9
+
10
+ _STDIN_NAME = "generated.py"
11
+
12
+
13
+ def _run_ruff(args: list[str], source: str) -> str:
14
+ proc = subprocess.run(
15
+ [sys.executable, "-m", "ruff", *args],
16
+ input=source,
17
+ capture_output=True,
18
+ text=True,
19
+ )
20
+ if proc.returncode != 0:
21
+ raise RuntimeError(f"ruff {args[0]} failed: {proc.stderr.strip()}")
22
+ return proc.stdout
23
+
24
+
25
+ def ruff_format(source: str) -> str:
26
+ """Sort imports then format *source*; return the canonical text.
27
+ Raises RuntimeError if ruff fails (e.g. a syntax error in emitted code)."""
28
+ sorted_src = _run_ruff(
29
+ ["check", "--select", "I", "--fix", "--stdin-filename", _STDIN_NAME, "-"], source
30
+ )
31
+ return _run_ruff(["format", "-"], sorted_src)