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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- 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)
|