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,672 @@
|
|
|
1
|
+
"""Payload-VO codegen — one ``<template_name_snake>_payload.py`` per declared
|
|
2
|
+
``template.*`` (prompt / output / toolcall).
|
|
3
|
+
|
|
4
|
+
FR-006 (Python) — cross-port parity with the Kotlin ``KotlinPayloadGenerator``
|
|
5
|
+
(server/java/codegen-kotlin), C# ``MetaObjects.Codegen.PayloadVoGenerator``, and
|
|
6
|
+
TS payload-VO emit. The Python output-parser generator (FR-006) was originally
|
|
7
|
+
self-contained — it embedded its Pydantic model inline pending a payload-VO
|
|
8
|
+
generator. With this module shipping, the parser generator switches to an
|
|
9
|
+
import-style emit so a single payload class is reused by both prompt rendering
|
|
10
|
+
and output parsing (matches the Java payload-VO ↔ Java output-parser handoff).
|
|
11
|
+
|
|
12
|
+
Each generated file declares a Pydantic v2 ``BaseModel`` per template. Field
|
|
13
|
+
types are origin-aware: a payload-VO field may carry an ``origin.*`` child that
|
|
14
|
+
declares how the value is derived, and the field's annotation is resolved as:
|
|
15
|
+
|
|
16
|
+
* ``origin.passthrough`` (``@from "Entity.field"``) — type of the source field.
|
|
17
|
+
* ``origin.aggregate`` (``@agg count``) — ``int``.
|
|
18
|
+
(``@agg avg``) — ``float``.
|
|
19
|
+
(``@agg sum``/``min``/``max``) — type of ``@of`` field.
|
|
20
|
+
* ``origin.collection`` (``@via "Parent.relName"``) — ``list[<TargetShortName>Payload]``,
|
|
21
|
+
and the nested ``<TargetShortName>Payload`` is emitted into the SAME file
|
|
22
|
+
(so callers ``from .<template>_payload import …`` once). Within one file,
|
|
23
|
+
the nested class is emitted exactly once even if multiple fields reference
|
|
24
|
+
the same target (per-file dedupe — see the Dedupe note below).
|
|
25
|
+
* No origin child — fall back to ``type_map.py_type_for(field)``.
|
|
26
|
+
|
|
27
|
+
Generated file naming mirrors the output-parser convention:
|
|
28
|
+
``<snake_case(template_name)>_payload.py`` and the public model class is
|
|
29
|
+
``<template_name>Payload``. Resolution of ``@payloadRef`` to the underlying
|
|
30
|
+
``object.value`` is short-name based (same contract as the Kotlin reference).
|
|
31
|
+
|
|
32
|
+
Dedupe note: the nested-payload dedupe is **per-file**, not per-run. Each
|
|
33
|
+
template's payload module is self-contained, so when two templates reference
|
|
34
|
+
the same `origin.collection` target, both files emit `PostPayload`. This
|
|
35
|
+
differs from Kotlin's cross-run dedupe — Kotlin emits each class to its OWN
|
|
36
|
+
`.kt` file (one-class-per-file via KotlinPoet), so a single `PostPayload.kt`
|
|
37
|
+
is enough; subsequent templates merely import it. Python's per-template
|
|
38
|
+
file emit makes cross-run dedupe a footgun (the second template would
|
|
39
|
+
reference an undefined `PostPayload` class), so each file owns its full
|
|
40
|
+
class graph.
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from collections.abc import Callable
|
|
45
|
+
|
|
46
|
+
from metaobjects.codegen.constants import generated_header
|
|
47
|
+
from metaobjects.codegen.format import ruff_format
|
|
48
|
+
from metaobjects.codegen import type_map
|
|
49
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
|
|
50
|
+
from metaobjects.codegen.type_map import py_type_for
|
|
51
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
52
|
+
from metaobjects.meta.core.field.meta_field import MetaField
|
|
53
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
54
|
+
from metaobjects.meta.core.object.object_constants import OBJECT_SUBTYPE_VALUE
|
|
55
|
+
from metaobjects.meta.core.relationship.meta_relationship import MetaRelationship
|
|
56
|
+
from metaobjects.meta.meta_data import MetaData
|
|
57
|
+
from metaobjects.meta.persistence.origin.meta_origin import MetaOrigin
|
|
58
|
+
from metaobjects.meta.persistence.origin.origin_constants import (
|
|
59
|
+
ORIGIN_ATTR_AGG,
|
|
60
|
+
ORIGIN_ATTR_FROM,
|
|
61
|
+
ORIGIN_ATTR_OF,
|
|
62
|
+
ORIGIN_ATTR_VIA,
|
|
63
|
+
)
|
|
64
|
+
from metaobjects.meta.template import template_constants as tc
|
|
65
|
+
from metaobjects.meta.template.meta_template import MetaTemplate
|
|
66
|
+
from metaobjects.shared.base_types import TYPE_OBJECT, TYPE_TEMPLATE
|
|
67
|
+
from metaobjects.shared.separators import PACKAGE_SEP
|
|
68
|
+
|
|
69
|
+
_GENERATOR_NAME = "payload-vo-generator"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Naming helpers (snake_case mirrors the router/output-parser local helpers).
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _snake_case(name: str) -> str:
|
|
78
|
+
"""``NpcResponseOutput`` → ``npc_response_output``. PascalCase → snake_case
|
|
79
|
+
with no acronym handling — matches the convention used by sibling generators."""
|
|
80
|
+
out: list[str] = []
|
|
81
|
+
for i, ch in enumerate(name):
|
|
82
|
+
if ch.isupper() and i > 0:
|
|
83
|
+
out.append("_")
|
|
84
|
+
out.append(ch.lower())
|
|
85
|
+
return "".join(out)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def payload_class_name(template_name: str) -> str:
|
|
89
|
+
"""``NpcResponseOutput`` → ``NpcResponseOutputPayload``.
|
|
90
|
+
|
|
91
|
+
Mirrors Kotlin's ``templateShort + "Payload"`` and gives the output-parser
|
|
92
|
+
generator a single, stable class name to import."""
|
|
93
|
+
return f"{template_name}Payload"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def payload_module_name(template_name: str) -> str:
|
|
97
|
+
"""``NpcResponseOutput`` → ``npc_response_output_payload``. The module name
|
|
98
|
+
used in the emitted file path AND the import statement from the parser."""
|
|
99
|
+
return f"{_snake_case(template_name)}_payload"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Resolution helpers (lookup by short-name OR FQN — same contract as Kotlin).
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _resolve_object_by_short_or_fqn(root: MetaData, ref: str) -> MetaObject | None:
|
|
108
|
+
"""Find a ``MetaObject`` child by short-name match.
|
|
109
|
+
|
|
110
|
+
Equivalent contract to ``KotlinGenUtil.resolveObjectByShortOrFqn``, but
|
|
111
|
+
simpler in Python: ``MetaData.name`` only ever holds the short name (the
|
|
112
|
+
package lives on ``MetaData.package`` and ``fqn()`` builds the dotted
|
|
113
|
+
form on demand). So a plain ``child.name == ref`` is enough.
|
|
114
|
+
|
|
115
|
+
``ref`` is the value passed in ``@payloadRef`` / ``@objectRef`` /
|
|
116
|
+
``origin.@from``, which by spec is the short name of the target object."""
|
|
117
|
+
for child in root.own_children():
|
|
118
|
+
if child.type != TYPE_OBJECT or not isinstance(child, MetaObject):
|
|
119
|
+
continue
|
|
120
|
+
if child.name == ref:
|
|
121
|
+
return child
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resolve_payload_vo(root: MetaData, ref: str) -> MetaObject | None:
|
|
126
|
+
"""Resolve a ``@payloadRef`` to its ``object.value``. Rejects entities —
|
|
127
|
+
payloads MUST be value-objects (same contract as Kotlin)."""
|
|
128
|
+
obj = _resolve_object_by_short_or_fqn(root, ref)
|
|
129
|
+
if obj is None or obj.sub_type != OBJECT_SUBTYPE_VALUE:
|
|
130
|
+
return None
|
|
131
|
+
return obj
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _split_dotted_ref(ref: str) -> tuple[str, str] | None:
|
|
135
|
+
"""``"Entity.field"`` → ``("Entity", "field")``. Returns ``None`` for
|
|
136
|
+
no-dot / leading-dot / trailing-dot — same contract as Kotlin."""
|
|
137
|
+
dot = ref.find(".")
|
|
138
|
+
if dot <= 0 or dot >= len(ref) - 1:
|
|
139
|
+
return None
|
|
140
|
+
return ref[:dot], ref[dot + 1 :]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _resolve_dotted_field_ref(root: MetaData, dotted_ref: str) -> MetaField | None:
|
|
144
|
+
"""Resolve ``"Entity.field"`` to the ``MetaField`` on ``Entity`` (by short
|
|
145
|
+
name OR FQN-trailing-segment match). Returns ``None`` if either half fails."""
|
|
146
|
+
parts = _split_dotted_ref(dotted_ref)
|
|
147
|
+
if parts is None:
|
|
148
|
+
return None
|
|
149
|
+
entity_name, field_name = parts
|
|
150
|
+
obj = _resolve_object_by_short_or_fqn(root, entity_name)
|
|
151
|
+
if obj is None:
|
|
152
|
+
return None
|
|
153
|
+
for f in obj.fields():
|
|
154
|
+
if not isinstance(f, MetaField):
|
|
155
|
+
continue
|
|
156
|
+
if f.name == field_name:
|
|
157
|
+
return f
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_field_required(field: MetaField) -> bool:
|
|
162
|
+
"""The single required-ness predicate the payload model uses to decide a field's
|
|
163
|
+
optionality (``T`` vs ``T | None = None``). A field is required iff its OWN
|
|
164
|
+
``@required`` attr is the boolean ``True`` — matching the TS payload-codegen
|
|
165
|
+
``isFieldRequired`` (``ownAttr === true``) so the extract-tier mapper that
|
|
166
|
+
constructs this payload can rely on the same boundary (no skew). A
|
|
167
|
+
``@required: "true"`` string therefore types optional in BOTH the payload and the
|
|
168
|
+
mapper. The extractor generator imports THIS predicate.
|
|
169
|
+
|
|
170
|
+
Note: this intentionally accepts ONLY the boolean ``True`` (matching the TS
|
|
171
|
+
payload-codegen predicate), which DELIBERATELY differs from the runtime
|
|
172
|
+
``object_extract._is_required`` / ``fr010_field_mapping.is_required``, both of which
|
|
173
|
+
additionally treat the string ``"true"`` as required. The payload type's optionality
|
|
174
|
+
and the extractor mapper's None-guarding are kept in lockstep by sharing THIS
|
|
175
|
+
predicate, so do not "reconcile" it with the runtime predicate."""
|
|
176
|
+
return field.attr(fc.FIELD_ATTR_REQUIRED) is True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _resolve_object_field_type(
|
|
180
|
+
field: MetaField,
|
|
181
|
+
root: MetaData,
|
|
182
|
+
nested_emit_queue: list[tuple[MetaObject, str]],
|
|
183
|
+
emitted_nested_fqns: set[str],
|
|
184
|
+
) -> tuple[str, set[str]]:
|
|
185
|
+
"""A plain ``field.object`` (``@objectRef``, no origin child) — resolve to the
|
|
186
|
+
nested ``<TargetShortName>Payload`` (single) or ``list[<TargetShortName>Payload]``
|
|
187
|
+
(array). The target VO is scheduled for in-file emission (per-file dedupe, same
|
|
188
|
+
mechanism as ``origin.collection``). Falls back to the bare type-map form when the
|
|
189
|
+
``@objectRef`` can't be resolved (defensive — loader validation gates it first)."""
|
|
190
|
+
ref = field.attr(fc.FIELD_ATTR_OBJECT_REF)
|
|
191
|
+
if not isinstance(ref, str) or not ref:
|
|
192
|
+
return _fallback_type(field)
|
|
193
|
+
target = _resolve_object_by_short_or_fqn(root, ref)
|
|
194
|
+
if target is None and PACKAGE_SEP in ref:
|
|
195
|
+
target = _resolve_object_by_short_or_fqn(root, ref.rsplit(PACKAGE_SEP, 1)[-1])
|
|
196
|
+
if target is None:
|
|
197
|
+
return _fallback_type(field)
|
|
198
|
+
nested_class = payload_class_name(target.name)
|
|
199
|
+
target_fqn = target.fqn()
|
|
200
|
+
if target_fqn not in emitted_nested_fqns:
|
|
201
|
+
emitted_nested_fqns.add(target_fqn)
|
|
202
|
+
nested_emit_queue.append((target, nested_class))
|
|
203
|
+
if type_map.field_is_array(field):
|
|
204
|
+
return f"list[{nested_class}]", set()
|
|
205
|
+
return nested_class, set()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _pascal(name: str) -> str:
|
|
209
|
+
"""``priority`` → ``Priority``; ``order_priority`` is left as-is segment-wise
|
|
210
|
+
(only the leading char is upper-cased), matching the cross-port naming rule which
|
|
211
|
+
PascalCases the bare field / super name (no snake-splitting)."""
|
|
212
|
+
return name[:1].upper() + name[1:] if name else name
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _shared_enum_super(field: MetaField) -> MetaData | None:
|
|
216
|
+
"""The abstract ``field.enum`` super a field extends, or ``None``. A field whose
|
|
217
|
+
``@values`` is inherited from an abstract base enum collapses (cross-port) to a
|
|
218
|
+
NAMED module alias keyed on the SUPER's name, so multiple fields sharing one
|
|
219
|
+
abstract enum reuse a single ``<Super> = Literal[...]`` alias. An inline enum
|
|
220
|
+
(no super) types inline."""
|
|
221
|
+
sup = field.super_data
|
|
222
|
+
if sup is not None and sup.sub_type == fc.FIELD_SUBTYPE_ENUM:
|
|
223
|
+
return sup
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _enum_field_type(
|
|
228
|
+
field: MetaField, enum_aliases: dict[str, str]
|
|
229
|
+
) -> tuple[str, set[str]] | None:
|
|
230
|
+
"""Resolve a ``field.enum`` annotation. Returns ``None`` for a non-enum field (the
|
|
231
|
+
caller falls through to the generic path).
|
|
232
|
+
|
|
233
|
+
* SHARED (extends an abstract ``field.enum``) → emit/reuse a module-level
|
|
234
|
+
``<Pascal(super.name)> = Literal[...]`` alias (deduped in *enum_aliases*) and
|
|
235
|
+
reference it (``<Alias>`` / ``list[<Alias>]``).
|
|
236
|
+
* INLINE (no super) → inline ``Literal[...]`` via ``py_type_for`` (no alias).
|
|
237
|
+
* No effective ``@values`` → fall through to ``py_type_for`` (bare ``str``).
|
|
238
|
+
"""
|
|
239
|
+
if field.sub_type != fc.FIELD_SUBTYPE_ENUM:
|
|
240
|
+
return None
|
|
241
|
+
values = type_map.effective_enum_values(field)
|
|
242
|
+
if not values:
|
|
243
|
+
pt = py_type_for(field)
|
|
244
|
+
return pt.expr, set(pt.imports)
|
|
245
|
+
sup = _shared_enum_super(field)
|
|
246
|
+
if sup is None:
|
|
247
|
+
# Inline enum — let py_type_for emit the inline Literal[...] (+ the import).
|
|
248
|
+
pt = py_type_for(field)
|
|
249
|
+
return pt.expr, set(pt.imports)
|
|
250
|
+
alias = _pascal(sup.name)
|
|
251
|
+
if alias not in enum_aliases:
|
|
252
|
+
members = ", ".join(type_map._py_str_literal(v) for v in values)
|
|
253
|
+
enum_aliases[alias] = f"Literal[{members}]"
|
|
254
|
+
ref = f"list[{alias}]" if type_map.field_is_array(field) else alias
|
|
255
|
+
return ref, {"from typing import Literal"}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _find_origin_child(field: MetaField) -> MetaOrigin | None:
|
|
259
|
+
"""First ``origin.*`` child of *field* (own children only — origins are
|
|
260
|
+
declared inline; there's no inheritance contract for them today)."""
|
|
261
|
+
for c in field.own_children():
|
|
262
|
+
if isinstance(c, MetaOrigin):
|
|
263
|
+
return c
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _find_relationship_on(obj: MetaObject, rel_name: str) -> MetaRelationship | None:
|
|
268
|
+
"""Resolve a relationship by name on *obj*. Walks effective children so
|
|
269
|
+
inherited relationships are visible."""
|
|
270
|
+
for c in obj.children():
|
|
271
|
+
if isinstance(c, MetaRelationship) and c.name == rel_name:
|
|
272
|
+
return c
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
# Origin-aware field-type resolution.
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _resolve_passthrough_type(
|
|
282
|
+
origin: MetaOrigin, root: MetaData, fallback: MetaField
|
|
283
|
+
) -> tuple[str, set[str]]:
|
|
284
|
+
"""``origin.passthrough @from "Entity.field"`` — resolve to source field's
|
|
285
|
+
Python type. Falls back to the payload field's own type when the dotted
|
|
286
|
+
ref can't be resolved (defensive — loader validation already gates ``@from``)."""
|
|
287
|
+
from_ref = origin.attr(ORIGIN_ATTR_FROM)
|
|
288
|
+
if not isinstance(from_ref, str) or not from_ref:
|
|
289
|
+
return _fallback_type(fallback)
|
|
290
|
+
source = _resolve_dotted_field_ref(root, from_ref)
|
|
291
|
+
if source is None:
|
|
292
|
+
return _fallback_type(fallback)
|
|
293
|
+
pt = py_type_for(source)
|
|
294
|
+
return pt.expr, set(pt.imports)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _resolve_aggregate_type(
|
|
298
|
+
origin: MetaOrigin, root: MetaData, fallback: MetaField
|
|
299
|
+
) -> tuple[str, set[str]]:
|
|
300
|
+
"""``origin.aggregate``: type rule —
|
|
301
|
+
- count → ``int``
|
|
302
|
+
- avg → ``float``
|
|
303
|
+
- sum / min / max → type of the ``@of`` field
|
|
304
|
+
"""
|
|
305
|
+
agg = origin.attr(ORIGIN_ATTR_AGG)
|
|
306
|
+
if agg == "count":
|
|
307
|
+
return "int", set()
|
|
308
|
+
if agg == "avg":
|
|
309
|
+
return "float", set()
|
|
310
|
+
if agg in ("sum", "min", "max"):
|
|
311
|
+
of_ref = origin.attr(ORIGIN_ATTR_OF)
|
|
312
|
+
if not isinstance(of_ref, str) or not of_ref:
|
|
313
|
+
return _fallback_type(fallback)
|
|
314
|
+
source = _resolve_dotted_field_ref(root, of_ref)
|
|
315
|
+
if source is None:
|
|
316
|
+
return _fallback_type(fallback)
|
|
317
|
+
pt = py_type_for(source)
|
|
318
|
+
return pt.expr, set(pt.imports)
|
|
319
|
+
return _fallback_type(fallback)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _resolve_collection_type(
|
|
323
|
+
origin: MetaOrigin,
|
|
324
|
+
root: MetaData,
|
|
325
|
+
fallback: MetaField,
|
|
326
|
+
nested_emit_queue: list[tuple[MetaObject, str]],
|
|
327
|
+
emitted_nested_fqns: set[str],
|
|
328
|
+
) -> tuple[str, set[str]]:
|
|
329
|
+
"""``origin.collection @via "Parent.rel"`` — walk Parent's relationship to
|
|
330
|
+
its ``@objectRef`` target, schedule a nested ``<TargetShortName>Payload``
|
|
331
|
+
for in-file emission, return ``list[<TargetShortName>Payload]``.
|
|
332
|
+
|
|
333
|
+
Dedupe is per-file via *emitted_nested_fqns* — if the same target is
|
|
334
|
+
referenced by two fields in the same payload module, only one nested
|
|
335
|
+
class is emitted. Cross-file dedupe would leave forward-references
|
|
336
|
+
dangling (see the module docstring)."""
|
|
337
|
+
via = origin.attr(ORIGIN_ATTR_VIA)
|
|
338
|
+
if not isinstance(via, str) or not via:
|
|
339
|
+
return _fallback_type(fallback)
|
|
340
|
+
parts = _split_dotted_ref(via)
|
|
341
|
+
if parts is None:
|
|
342
|
+
return _fallback_type(fallback)
|
|
343
|
+
parent_name, rel_name = parts
|
|
344
|
+
parent = _resolve_object_by_short_or_fqn(root, parent_name)
|
|
345
|
+
if parent is None:
|
|
346
|
+
return _fallback_type(fallback)
|
|
347
|
+
rel = _find_relationship_on(parent, rel_name)
|
|
348
|
+
if rel is None:
|
|
349
|
+
return _fallback_type(fallback)
|
|
350
|
+
target_ref = rel.object_ref()
|
|
351
|
+
if not target_ref:
|
|
352
|
+
return _fallback_type(fallback)
|
|
353
|
+
target = _resolve_object_by_short_or_fqn(root, target_ref)
|
|
354
|
+
if target is None:
|
|
355
|
+
return _fallback_type(fallback)
|
|
356
|
+
# MetaData.name is the short name (no `::`); see _resolve_object_by_short_or_fqn.
|
|
357
|
+
nested_class = payload_class_name(target.name)
|
|
358
|
+
target_fqn = target.fqn()
|
|
359
|
+
if target_fqn not in emitted_nested_fqns:
|
|
360
|
+
emitted_nested_fqns.add(target_fqn)
|
|
361
|
+
nested_emit_queue.append((target, nested_class))
|
|
362
|
+
return f"list[{nested_class}]", set()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _fallback_type(field: MetaField) -> tuple[str, set[str]]:
|
|
366
|
+
"""Type-map fallback used by every origin path when resolution fails."""
|
|
367
|
+
pt = py_type_for(field)
|
|
368
|
+
return pt.expr, set(pt.imports)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _resolve_field_type(
|
|
372
|
+
field: MetaField,
|
|
373
|
+
root: MetaData,
|
|
374
|
+
nested_emit_queue: list[tuple[MetaObject, str]],
|
|
375
|
+
emitted_nested_fqns: set[str],
|
|
376
|
+
enum_aliases: dict[str, str],
|
|
377
|
+
) -> tuple[str, set[str]]:
|
|
378
|
+
"""Resolve the Python annotation for one payload-VO field, honoring any
|
|
379
|
+
``origin.*`` child. Falls back to ``type_map.py_type_for`` when none.
|
|
380
|
+
|
|
381
|
+
Note: a payload-VO field declared as ``field.object`` (with ``@objectRef``)
|
|
382
|
+
but no origin child falls through to ``type_map.py_type_for``, which emits
|
|
383
|
+
the entity short-name as a forward-reference string. The entity model is
|
|
384
|
+
NOT auto-imported — payload modules and entity modules may live in
|
|
385
|
+
different output directories, and the consumer is expected to wire
|
|
386
|
+
cross-module imports explicitly. The metadata-driven path for "this VO
|
|
387
|
+
field is a foreign-object value" is ``origin.collection`` (nested payload)
|
|
388
|
+
or ``origin.passthrough`` (scalar projection)."""
|
|
389
|
+
origin = _find_origin_child(field)
|
|
390
|
+
if origin is None:
|
|
391
|
+
# A plain ``field.object`` (``@objectRef``, no origin) → nested payload class
|
|
392
|
+
# (single or list), emitted in the same file. This is the prompt-pillar
|
|
393
|
+
# nested-payload case the extract tier maps onto; without it the payload would
|
|
394
|
+
# reference an undefined bare entity name (Pydantic "not fully defined").
|
|
395
|
+
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
|
|
396
|
+
return _resolve_object_field_type(
|
|
397
|
+
field, root, nested_emit_queue, emitted_nested_fqns
|
|
398
|
+
)
|
|
399
|
+
# A ``field.enum`` → Literal[...] (inline) or a named module alias (shared).
|
|
400
|
+
enum_type = _enum_field_type(field, enum_aliases)
|
|
401
|
+
if enum_type is not None:
|
|
402
|
+
return enum_type
|
|
403
|
+
pt = py_type_for(field)
|
|
404
|
+
return pt.expr, set(pt.imports)
|
|
405
|
+
if origin.sub_type == "passthrough":
|
|
406
|
+
return _resolve_passthrough_type(origin, root, field)
|
|
407
|
+
if origin.sub_type == "aggregate":
|
|
408
|
+
return _resolve_aggregate_type(origin, root, field)
|
|
409
|
+
if origin.sub_type == "collection":
|
|
410
|
+
return _resolve_collection_type(
|
|
411
|
+
origin, root, field, nested_emit_queue, emitted_nested_fqns
|
|
412
|
+
)
|
|
413
|
+
return _fallback_type(field)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Class-block emission.
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _emit_payload_class(
|
|
422
|
+
class_name: str,
|
|
423
|
+
payload_vo: MetaObject,
|
|
424
|
+
root: MetaData,
|
|
425
|
+
nested_emit_queue: list[tuple[MetaObject, str]],
|
|
426
|
+
emitted_nested_fqns: set[str],
|
|
427
|
+
extra_imports: set[str],
|
|
428
|
+
enum_aliases: dict[str, str],
|
|
429
|
+
docstring: str,
|
|
430
|
+
) -> list[str]:
|
|
431
|
+
"""Build the source lines for one Pydantic ``BaseModel`` subclass."""
|
|
432
|
+
lines: list[str] = [f"class {class_name}(BaseModel):", f' """{docstring}"""']
|
|
433
|
+
field_lines: list[str] = []
|
|
434
|
+
for field in payload_vo.fields():
|
|
435
|
+
if not isinstance(field, MetaField):
|
|
436
|
+
continue
|
|
437
|
+
annotation, imports = _resolve_field_type(
|
|
438
|
+
field, root, nested_emit_queue, emitted_nested_fqns, enum_aliases
|
|
439
|
+
)
|
|
440
|
+
extra_imports.update(imports)
|
|
441
|
+
# Optionality mirrors the cross-port (TS) payload-codegen: a ``@required``
|
|
442
|
+
# field is non-optional ``T``; everything else is ``T | None = None`` so the
|
|
443
|
+
# strict payload can carry an absent optional value (and the extract-tier
|
|
444
|
+
# mapper, which shares ``is_field_required``, agrees on the boundary).
|
|
445
|
+
if is_field_required(field):
|
|
446
|
+
field_lines.append(f" {field.name}: {annotation}")
|
|
447
|
+
else:
|
|
448
|
+
field_lines.append(f" {field.name}: {annotation} | None = None")
|
|
449
|
+
if field_lines:
|
|
450
|
+
lines.extend(field_lines)
|
|
451
|
+
else:
|
|
452
|
+
lines.append(" pass")
|
|
453
|
+
return lines
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def render_payload_vo(
|
|
457
|
+
template: MetaTemplate,
|
|
458
|
+
root: MetaData,
|
|
459
|
+
*,
|
|
460
|
+
generator: "PayloadVoGenerator | None" = None,
|
|
461
|
+
) -> str | None:
|
|
462
|
+
"""Render one payload module for a ``template.*`` node.
|
|
463
|
+
|
|
464
|
+
When *generator* is supplied, its ``_emit_payload_class`` override is used for
|
|
465
|
+
each emitted class (the extension seam). When ``None`` (the module-level
|
|
466
|
+
back-compat call path), the module-level :func:`_emit_payload_class` is used —
|
|
467
|
+
output is byte-identical to the pre-refactor behavior.
|
|
468
|
+
|
|
469
|
+
Returns ``None`` when the ``@payloadRef`` can't be resolved to an
|
|
470
|
+
``object.value`` (defensive — the loader validation pass normally catches
|
|
471
|
+
this first).
|
|
472
|
+
|
|
473
|
+
Nested-payload dedupe is per-file: if the same collection target appears
|
|
474
|
+
twice within one payload module (e.g. two fields both `origin.collection`
|
|
475
|
+
on the same relationship), only one nested class is emitted. Across
|
|
476
|
+
different templates, each file owns its full class graph independently —
|
|
477
|
+
see the module docstring for the rationale."""
|
|
478
|
+
payload_ref = template.attr(tc.TEMPLATE_ATTR_PAYLOAD_REF)
|
|
479
|
+
if not isinstance(payload_ref, str) or not payload_ref:
|
|
480
|
+
return None
|
|
481
|
+
payload = resolve_payload_vo(root, payload_ref)
|
|
482
|
+
if payload is None:
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
# Per-file dedupe set: scoped to this single render call so each emitted
|
|
486
|
+
# module is self-contained (no cross-template forward references).
|
|
487
|
+
emitted_nested_fqns: set[str] = set()
|
|
488
|
+
class_name = payload_class_name(template.name)
|
|
489
|
+
extra_imports: set[str] = set()
|
|
490
|
+
nested_emit_queue: list[tuple[MetaObject, str]] = []
|
|
491
|
+
# Shared-enum aliases (``<Pascal(super.name)> = Literal[...]``), deduped by name and
|
|
492
|
+
# emitted once at module scope before the classes that reference them.
|
|
493
|
+
enum_aliases: dict[str, str] = {}
|
|
494
|
+
|
|
495
|
+
# The class-block emitter: the generator's overridable hook when an instance is
|
|
496
|
+
# supplied, else the module-level default (byte-identical back-compat path).
|
|
497
|
+
emit_class = generator._emit_payload_class if generator is not None else _emit_payload_class
|
|
498
|
+
|
|
499
|
+
# The PRIMARY class (the one named after the template). Its docstring
|
|
500
|
+
# mirrors Kotlin's KDoc.
|
|
501
|
+
primary_block = emit_class(
|
|
502
|
+
class_name=class_name,
|
|
503
|
+
payload_vo=payload,
|
|
504
|
+
root=root,
|
|
505
|
+
nested_emit_queue=nested_emit_queue,
|
|
506
|
+
emitted_nested_fqns=emitted_nested_fqns,
|
|
507
|
+
extra_imports=extra_imports,
|
|
508
|
+
enum_aliases=enum_aliases,
|
|
509
|
+
docstring=(
|
|
510
|
+
f"GENERATED payload for template ``{template.name}``.\n\n"
|
|
511
|
+
f" Field shape derived from the ``{payload.name}`` object.value."
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# NESTED classes scheduled by origin.collection. Drain the queue
|
|
516
|
+
# iteratively so nested-of-nested chains also get emitted (Kotlin behaves
|
|
517
|
+
# the same way — the recursive emit is queue-driven here for clarity).
|
|
518
|
+
nested_blocks: list[list[str]] = []
|
|
519
|
+
nested_class_names: list[str] = []
|
|
520
|
+
while nested_emit_queue:
|
|
521
|
+
target, nested_class = nested_emit_queue.pop(0)
|
|
522
|
+
block = emit_class(
|
|
523
|
+
class_name=nested_class,
|
|
524
|
+
payload_vo=target,
|
|
525
|
+
root=root,
|
|
526
|
+
nested_emit_queue=nested_emit_queue,
|
|
527
|
+
emitted_nested_fqns=emitted_nested_fqns,
|
|
528
|
+
extra_imports=extra_imports,
|
|
529
|
+
enum_aliases=enum_aliases,
|
|
530
|
+
docstring=(
|
|
531
|
+
f"GENERATED nested payload for collection target ``{target.name}``."
|
|
532
|
+
),
|
|
533
|
+
)
|
|
534
|
+
nested_blocks.append(block)
|
|
535
|
+
nested_class_names.append(nested_class)
|
|
536
|
+
|
|
537
|
+
# Build the file. Module FQN follows the convention used by sibling
|
|
538
|
+
# generators (entity_model._effective_fqn) — package from nearest ancestor.
|
|
539
|
+
fqn = _effective_fqn_for(template, payload)
|
|
540
|
+
lines: list[str] = [generated_header(template.name, fqn), "from __future__ import annotations\n"]
|
|
541
|
+
for imp in sorted(extra_imports):
|
|
542
|
+
lines.append(imp)
|
|
543
|
+
if extra_imports:
|
|
544
|
+
lines.append("")
|
|
545
|
+
lines.append("from pydantic import BaseModel")
|
|
546
|
+
lines.append("")
|
|
547
|
+
lines.append("")
|
|
548
|
+
# Shared-enum aliases (module scope, deduped, sorted for deterministic output).
|
|
549
|
+
# Referenced by one OR MORE payload fields that extend the same abstract field.enum.
|
|
550
|
+
if enum_aliases:
|
|
551
|
+
for alias in sorted(enum_aliases):
|
|
552
|
+
lines.append(f"{alias} = {enum_aliases[alias]}")
|
|
553
|
+
lines.append("")
|
|
554
|
+
lines.append("")
|
|
555
|
+
# Emit nested classes FIRST. Pydantic v2 with `from __future__ import
|
|
556
|
+
# annotations` evaluates field annotations lazily, but it needs every
|
|
557
|
+
# referenced class to be defined in the module namespace at model-build
|
|
558
|
+
# time — otherwise it raises PydanticUserError("not fully defined") and
|
|
559
|
+
# callers would have to run model_rebuild(). Nested-first avoids that.
|
|
560
|
+
for block in nested_blocks:
|
|
561
|
+
lines.extend(block)
|
|
562
|
+
lines.append("")
|
|
563
|
+
lines.append("")
|
|
564
|
+
lines.extend(primary_block)
|
|
565
|
+
lines.append("")
|
|
566
|
+
lines.append("")
|
|
567
|
+
all_names = [class_name, *nested_class_names]
|
|
568
|
+
quoted = ", ".join(f'"{n}"' for n in all_names)
|
|
569
|
+
lines.append(f"__all__ = [{quoted}]")
|
|
570
|
+
lines.append("")
|
|
571
|
+
return "\n".join(lines)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _effective_fqn_for(template: MetaTemplate, payload: MetaObject) -> str:
|
|
575
|
+
"""``package::name`` for the doc-header. Prefer the template's own package
|
|
576
|
+
chain; fall back to the payload's; finally the bare template name."""
|
|
577
|
+
|
|
578
|
+
def _walk(node: MetaData) -> str | None:
|
|
579
|
+
pkg = node.package
|
|
580
|
+
parent = node.parent
|
|
581
|
+
while pkg is None and parent is not None:
|
|
582
|
+
pkg = parent.package
|
|
583
|
+
parent = parent.parent
|
|
584
|
+
return pkg
|
|
585
|
+
|
|
586
|
+
pkg = _walk(template) or _walk(payload)
|
|
587
|
+
return f"{pkg}{PACKAGE_SEP}{template.name}" if pkg else template.name
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ---------------------------------------------------------------------------
|
|
591
|
+
# Generator wrapper.
|
|
592
|
+
# ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class PayloadVoGenerator:
|
|
596
|
+
"""Generator wrapping ``render_payload_vo``. Emits one file per declared
|
|
597
|
+
``template.*`` (prompt / output / toolcall) — iterates ALL template
|
|
598
|
+
subtypes uniformly to match the Kotlin reference (no subtype filter)."""
|
|
599
|
+
|
|
600
|
+
name = _GENERATOR_NAME
|
|
601
|
+
|
|
602
|
+
def __init__(self, *, filter: Callable[[MetaObject], bool] | None = None) -> None:
|
|
603
|
+
# The ``filter`` arg matches the cross-generator contract even though
|
|
604
|
+
# this generator iterates templates (not entities).
|
|
605
|
+
self.filter = filter
|
|
606
|
+
|
|
607
|
+
def _emit_payload_class(
|
|
608
|
+
self,
|
|
609
|
+
class_name: str,
|
|
610
|
+
payload_vo: MetaObject,
|
|
611
|
+
root: MetaData,
|
|
612
|
+
nested_emit_queue: list[tuple[MetaObject, str]],
|
|
613
|
+
emitted_nested_fqns: set[str],
|
|
614
|
+
extra_imports: set[str],
|
|
615
|
+
enum_aliases: dict[str, str],
|
|
616
|
+
docstring: str,
|
|
617
|
+
) -> list[str]:
|
|
618
|
+
"""EXTENSION SEAM — the source lines for one Pydantic ``BaseModel`` subclass
|
|
619
|
+
(primary OR a nested collection target). Defaults to the module-level
|
|
620
|
+
:func:`_emit_payload_class`; override to customize the emitted class body
|
|
621
|
+
(e.g. inject ``model_config``, change optionality, add validators)."""
|
|
622
|
+
return _emit_payload_class(
|
|
623
|
+
class_name,
|
|
624
|
+
payload_vo,
|
|
625
|
+
root,
|
|
626
|
+
nested_emit_queue,
|
|
627
|
+
emitted_nested_fqns,
|
|
628
|
+
extra_imports,
|
|
629
|
+
enum_aliases,
|
|
630
|
+
docstring,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
def _render_module(self, template: MetaTemplate, root: MetaData) -> str | None:
|
|
634
|
+
"""EXTENSION SEAM — render the whole payload module for one ``template.*``.
|
|
635
|
+
Defaults to :func:`render_payload_vo` (passing this instance so the
|
|
636
|
+
``_emit_payload_class`` override is honored). Override to pre/post-process
|
|
637
|
+
the emitted source, or replace the render path entirely."""
|
|
638
|
+
return render_payload_vo(template, root, generator=self)
|
|
639
|
+
|
|
640
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
641
|
+
root = ctx.loaded_root
|
|
642
|
+
if root is None:
|
|
643
|
+
return []
|
|
644
|
+
files: list[EmittedFile] = []
|
|
645
|
+
templates = sorted(
|
|
646
|
+
(c for c in root.own_children() if c.type == TYPE_TEMPLATE and isinstance(c, MetaTemplate)),
|
|
647
|
+
key=lambda c: c.name,
|
|
648
|
+
)
|
|
649
|
+
# Nested-payload dedupe is per-file (inside render_payload_vo). Each
|
|
650
|
+
# template's emitted module is self-contained — see module docstring.
|
|
651
|
+
for tmpl in templates:
|
|
652
|
+
content = self._render_module(tmpl, root)
|
|
653
|
+
if content is None:
|
|
654
|
+
ctx.warn(
|
|
655
|
+
f"{_GENERATOR_NAME}: skipping template "
|
|
656
|
+
f"'{tmpl.name}' (no resolvable @payloadRef to an object.value)."
|
|
657
|
+
)
|
|
658
|
+
continue
|
|
659
|
+
files.append(
|
|
660
|
+
EmittedFile(
|
|
661
|
+
path=f"{payload_module_name(tmpl.name)}.py",
|
|
662
|
+
content=ruff_format(content),
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
return files
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def payload_vo_generator(
|
|
669
|
+
*, filter: Callable[[MetaObject], bool] | None = None
|
|
670
|
+
) -> Generator:
|
|
671
|
+
"""Factory mirroring the TS / C# / Kotlin payload-VO generators."""
|
|
672
|
+
return PayloadVoGenerator(filter=filter)
|