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,451 @@
|
|
|
1
|
+
"""Render-helper codegen — one ``<template_name_snake>_render_helper.py`` per
|
|
2
|
+
``template.output`` declaration (render-helper phase 2).
|
|
3
|
+
|
|
4
|
+
For each ``template.output`` this emits a typed ``render_<name>(payload, provider)``
|
|
5
|
+
function that WRAPS the existing :func:`metaobjects.render.renderer.render` engine,
|
|
6
|
+
and enforces the mustache↔payload-VO drift check (the existing
|
|
7
|
+
:func:`metaobjects.render.verify.verify`) at BUILD time.
|
|
8
|
+
|
|
9
|
+
Two shapes keyed off ``@kind``:
|
|
10
|
+
|
|
11
|
+
* ``document`` (default) → renders ``@textRef`` in ``@format`` → one ``str``.
|
|
12
|
+
* ``email`` → renders ``@subjectRef`` (text) + ``@htmlBodyRef`` (html)
|
|
13
|
+
(+ optional ``@textBodyRef``, text) → an
|
|
14
|
+
:class:`metaobjects.render.email_document.EmailDocument`.
|
|
15
|
+
|
|
16
|
+
THE HEADLINE is the BUILD-TIME drift gate. BEFORE emitting, every referenced
|
|
17
|
+
mustache (document: ``@textRef``; email: subject + html (+ optional text)) is
|
|
18
|
+
resolved through a :class:`~metaobjects.render.filesystem_provider.FilesystemProvider`
|
|
19
|
+
rooted at the ``template_root`` ctor arg and run through ``verify``. If a referenced
|
|
20
|
+
text is unresolvable OR carries any NON-warning error (anything other than
|
|
21
|
+
``ERR_REQUIRED_SLOT_UNUSED`` — a ``{{field}}`` not on the payload VO produces
|
|
22
|
+
``ERR_VAR_NOT_ON_PAYLOAD``), the generator RAISES (fails codegen), naming the
|
|
23
|
+
template, the ref, the error code, and the offending field. This is what makes the
|
|
24
|
+
build fail when a mustache references a field the payload VO doesn't declare.
|
|
25
|
+
|
|
26
|
+
Reuse, not reimplementation: ``render`` (the emitted runtime call), ``verify`` (the
|
|
27
|
+
build-time gate), ``FilesystemProvider`` (build-time ref resolution), and
|
|
28
|
+
``EmailDocument`` (email return type). The payload field tree is walked from the VO
|
|
29
|
+
the same way the other generators walk it — but a nested ``field.object``'s
|
|
30
|
+
``@objectRef`` is resolved by BARE short-name (cross-port render-helper consensus:
|
|
31
|
+
TS ``findObject`` / Java ``resolveNestedObjectRef`` / C# ``ResolveNestedObjectRef``),
|
|
32
|
+
only recursing into ``object.value`` targets, cycle-guarded.
|
|
33
|
+
|
|
34
|
+
Python divergence vs TS / Java / C#: Python's ``RenderRequest`` has NO ``verify``
|
|
35
|
+
field — the Python render engine does not run a runtime drift pass — so the emitted
|
|
36
|
+
helper does NOT pass a runtime ``verify`` field-tree. The BUILD-TIME gate that runs
|
|
37
|
+
here is the only, and a complete, drift guarantee: codegen cannot succeed unless
|
|
38
|
+
every referenced mustache verifies clean against the payload VO. The field-tree walk
|
|
39
|
+
is still performed (it is what ``verify`` checks against); it is simply not baked into
|
|
40
|
+
the emitted call. The emitted helper shape is otherwise identical to the other ports.
|
|
41
|
+
|
|
42
|
+
Drift message style matches the other ports exactly:
|
|
43
|
+
``render-helper drift: template "<N>" ref "<r>" — <CODE>: {{<f>}} not on payload VO``
|
|
44
|
+
"""
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
|
|
49
|
+
from metaobjects.codegen.constants import generated_header
|
|
50
|
+
from metaobjects.codegen.format import ruff_format
|
|
51
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
|
|
52
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
53
|
+
from metaobjects.meta.core.field.meta_field import MetaField
|
|
54
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
55
|
+
from metaobjects.meta.core.object.object_constants import OBJECT_SUBTYPE_VALUE
|
|
56
|
+
from metaobjects.meta.meta_data import MetaData
|
|
57
|
+
from metaobjects.meta.template import template_constants as tc
|
|
58
|
+
from metaobjects.render.filesystem_provider import FilesystemProvider
|
|
59
|
+
from metaobjects.render.verify import (
|
|
60
|
+
ERR_REQUIRED_SLOT_UNUSED,
|
|
61
|
+
PayloadField,
|
|
62
|
+
verify,
|
|
63
|
+
)
|
|
64
|
+
from metaobjects.shared.base_types import TYPE_FIELD, TYPE_OBJECT, TYPE_TEMPLATE
|
|
65
|
+
from metaobjects.shared.separators import PACKAGE_SEP
|
|
66
|
+
|
|
67
|
+
_GENERATOR_NAME = "render-helper-generator"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _snake_case(name: str) -> str:
|
|
71
|
+
"""``WelcomePage`` → ``welcome_page``. Trivial PascalCase → snake_case (no acronym
|
|
72
|
+
handling; matches the convention used by the sibling generators)."""
|
|
73
|
+
out: list[str] = []
|
|
74
|
+
for i, ch in enumerate(name):
|
|
75
|
+
if ch.isupper() and i > 0:
|
|
76
|
+
out.append("_")
|
|
77
|
+
out.append(ch.lower())
|
|
78
|
+
return "".join(out)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _py_str(s: str) -> str:
|
|
82
|
+
"""A double-quoted Python string literal with the minimal escaping the refs/formats
|
|
83
|
+
need (refs are ``group/source`` slugs; formats are closed-enum words)."""
|
|
84
|
+
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Payload field-tree walk — nested @objectRef resolved by BARE short-name
|
|
89
|
+
# (cross-port render-helper consensus). Object-ref fields recurse into their
|
|
90
|
+
# target object.value (SUBTYPE_VALUE only); a `seen` set guards reference cycles.
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _resolve_nested_object_ref(root: MetaData, reference: str) -> MetaObject | None:
|
|
95
|
+
"""Resolve a ``field.object``'s ``@objectRef`` to its target ``object.value`` by BARE
|
|
96
|
+
short-name — mirroring the C# ``ResolveNestedObjectRef`` / TS ``findObject`` / Java
|
|
97
|
+
``resolveNestedObjectRef``. If the ref carries a package, only the segment after the
|
|
98
|
+
last ``::`` is compared, against each ``object.value``'s own short name."""
|
|
99
|
+
if not reference:
|
|
100
|
+
return None
|
|
101
|
+
ref_short = reference.rsplit(PACKAGE_SEP, 1)[-1]
|
|
102
|
+
for child in root.own_children():
|
|
103
|
+
if child.type != TYPE_OBJECT or not isinstance(child, MetaObject):
|
|
104
|
+
continue
|
|
105
|
+
if child.sub_type != OBJECT_SUBTYPE_VALUE:
|
|
106
|
+
continue
|
|
107
|
+
if child.name.rsplit(PACKAGE_SEP, 1)[-1] == ref_short:
|
|
108
|
+
return child
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _derive_payload_field_tree(
|
|
113
|
+
root: MetaData, vo: MetaObject, seen: frozenset[str]
|
|
114
|
+
) -> list[PayloadField]:
|
|
115
|
+
"""Walk *vo*'s fields into a :class:`PayloadField` tree. A ``field.object`` with a
|
|
116
|
+
resolvable ``@objectRef`` to an ``object.value`` becomes a context-pushing node whose
|
|
117
|
+
children are the target VO's tree (recursed, cycle-guarded). Every other field is a
|
|
118
|
+
leaf."""
|
|
119
|
+
if vo is None or vo.name in seen:
|
|
120
|
+
return []
|
|
121
|
+
next_seen = seen | {vo.name}
|
|
122
|
+
fields: list[PayloadField] = []
|
|
123
|
+
for f in vo.children():
|
|
124
|
+
if f.type != TYPE_FIELD or not isinstance(f, MetaField):
|
|
125
|
+
continue
|
|
126
|
+
if f.sub_type == fc.FIELD_SUBTYPE_OBJECT:
|
|
127
|
+
ref = f.attr(fc.FIELD_ATTR_OBJECT_REF)
|
|
128
|
+
if isinstance(ref, str) and ref:
|
|
129
|
+
target = _resolve_nested_object_ref(root, ref)
|
|
130
|
+
if target is not None and target.sub_type == OBJECT_SUBTYPE_VALUE:
|
|
131
|
+
children = _derive_payload_field_tree(root, target, next_seen)
|
|
132
|
+
fields.append(PayloadField(f.name, children))
|
|
133
|
+
continue
|
|
134
|
+
fields.append(PayloadField(f.name))
|
|
135
|
+
return fields
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _field_tree_literal(fields: list[PayloadField]) -> str:
|
|
139
|
+
"""Emit a ``list[PayloadField]`` as a deterministic Python literal:
|
|
140
|
+
``[PayloadField("name"), PayloadField("nested", [PayloadField("x")])]``."""
|
|
141
|
+
parts: list[str] = []
|
|
142
|
+
for f in fields:
|
|
143
|
+
if f.fields is not None:
|
|
144
|
+
parts.append(f"PayloadField({_py_str(f.name)}, {_field_tree_literal(f.fields)})")
|
|
145
|
+
else:
|
|
146
|
+
parts.append(f"PayloadField({_py_str(f.name)})")
|
|
147
|
+
return "[" + ", ".join(parts) + "]"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Resolution + emission.
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _resolve_payload_vo(root: MetaData, payload_ref: str) -> MetaObject | None:
|
|
156
|
+
"""``@payloadRef`` must resolve to an ``object.value`` (same contract as the
|
|
157
|
+
parser/prompt generators). Bare short-name match on a value-object child."""
|
|
158
|
+
for child in root.own_children():
|
|
159
|
+
if child.type != TYPE_OBJECT or not isinstance(child, MetaObject):
|
|
160
|
+
continue
|
|
161
|
+
if child.sub_type == OBJECT_SUBTYPE_VALUE and child.name == payload_ref:
|
|
162
|
+
return child
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _max_chars_of(tmpl: MetaData) -> int | None:
|
|
167
|
+
"""Resolve ``@maxChars`` (own attr) as an int, or ``None`` when absent/non-numeric."""
|
|
168
|
+
v = tmpl.attr(tc.TEMPLATE_ATTR_MAX_CHARS)
|
|
169
|
+
if isinstance(v, bool):
|
|
170
|
+
return None
|
|
171
|
+
if isinstance(v, int):
|
|
172
|
+
return v
|
|
173
|
+
if isinstance(v, str):
|
|
174
|
+
try:
|
|
175
|
+
return int(v)
|
|
176
|
+
except ValueError:
|
|
177
|
+
return None
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class RenderHelperGenerator:
|
|
182
|
+
"""Generator wrapping the per-``template.output`` render-helper emit. Construct with
|
|
183
|
+
the on-disk template root the build-time drift gate resolves each referenced
|
|
184
|
+
mustache against — required, without it the gate cannot run.
|
|
185
|
+
|
|
186
|
+
EXTENSION SEAM (open-for-extension). Adopters subclass this and override one of
|
|
187
|
+
the protected emit hooks to customize the emitted helper without forking:
|
|
188
|
+
|
|
189
|
+
* ``_emit_document(header, template_name, snake, payload_ref, tmpl, fields)`` —
|
|
190
|
+
the ``document``-kind helper (single ``str`` return).
|
|
191
|
+
* ``_emit_email(header, template_name, snake, payload_ref, tmpl, fields)`` — the
|
|
192
|
+
``email``-kind helper (``EmailDocument`` return).
|
|
193
|
+
* ``_emit_helper(tmpl, vo, payload_ref, root)`` — the per-template dispatch
|
|
194
|
+
(runs the build-time drift gate, then routes to document/email).
|
|
195
|
+
|
|
196
|
+
The build-time drift gate (``_gate_ref``) is also overridable, but tightening it
|
|
197
|
+
is the safer direction than loosening it. The factory ``render_helper_generator()``
|
|
198
|
+
returns a default instance, so the default suite stays byte-identical."""
|
|
199
|
+
|
|
200
|
+
name = _GENERATOR_NAME
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
template_root: str,
|
|
205
|
+
*,
|
|
206
|
+
filter: Callable[[MetaObject], bool] | None = None,
|
|
207
|
+
) -> None:
|
|
208
|
+
if not template_root:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"RenderHelperGenerator requires a template_root (the on-disk template "
|
|
211
|
+
"dir) for the build-time drift gate"
|
|
212
|
+
)
|
|
213
|
+
# The ``filter`` arg matches the cross-generator contract even though this
|
|
214
|
+
# generator iterates templates (not entities).
|
|
215
|
+
self.filter = filter
|
|
216
|
+
self._provider = FilesystemProvider(template_root)
|
|
217
|
+
|
|
218
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
219
|
+
root = ctx.loaded_root
|
|
220
|
+
if root is None:
|
|
221
|
+
return []
|
|
222
|
+
outputs = sorted(
|
|
223
|
+
(
|
|
224
|
+
c
|
|
225
|
+
for c in root.own_children()
|
|
226
|
+
if c.type == TYPE_TEMPLATE
|
|
227
|
+
and c.sub_type == tc.TEMPLATE_SUBTYPE_OUTPUT
|
|
228
|
+
),
|
|
229
|
+
key=lambda c: c.name,
|
|
230
|
+
)
|
|
231
|
+
files: list[EmittedFile] = []
|
|
232
|
+
for tmpl in outputs:
|
|
233
|
+
payload_ref = tmpl.attr(tc.TEMPLATE_ATTR_PAYLOAD_REF)
|
|
234
|
+
if not isinstance(payload_ref, str) or not payload_ref:
|
|
235
|
+
ctx.warn(
|
|
236
|
+
f"{_GENERATOR_NAME}: template.output '{tmpl.name}' missing "
|
|
237
|
+
"@payloadRef — skipped."
|
|
238
|
+
)
|
|
239
|
+
continue
|
|
240
|
+
vo = _resolve_payload_vo(root, payload_ref)
|
|
241
|
+
if vo is None:
|
|
242
|
+
ctx.warn(
|
|
243
|
+
f"{_GENERATOR_NAME}: template.output '{tmpl.name}' @payloadRef "
|
|
244
|
+
f"'{payload_ref}' does not resolve to an object.value — skipped."
|
|
245
|
+
)
|
|
246
|
+
continue
|
|
247
|
+
# _emit_helper runs the build-time drift gate first and RAISES (fails
|
|
248
|
+
# codegen) on a mustache↔VO drift — intentionally NOT caught.
|
|
249
|
+
content = self._emit_helper(tmpl, vo, payload_ref, root)
|
|
250
|
+
files.append(
|
|
251
|
+
EmittedFile(
|
|
252
|
+
path=f"{_snake_case(tmpl.name)}_render_helper.py",
|
|
253
|
+
content=ruff_format(content),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
return files
|
|
257
|
+
|
|
258
|
+
# -- build-time drift gate -------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def _gate_ref(
|
|
261
|
+
self, template_name: str, reference: str, fields: list[PayloadField]
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Run the build-time drift gate for one referenced mustache. RAISES (fails
|
|
264
|
+
codegen) when the ref is unresolvable OR ``verify`` reports a non-warning error.
|
|
265
|
+
Warnings (``ERR_REQUIRED_SLOT_UNUSED``) are tolerated. Matches the cross-port
|
|
266
|
+
message style exactly."""
|
|
267
|
+
text = self._provider.resolve(reference)
|
|
268
|
+
if text is None:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f'render-helper drift: template "{template_name}" ref "{reference}" '
|
|
271
|
+
"— unresolved (provider returned no text)"
|
|
272
|
+
)
|
|
273
|
+
for e in verify(text, fields, provider=self._provider):
|
|
274
|
+
if e.code == ERR_REQUIRED_SLOT_UNUSED:
|
|
275
|
+
continue # warning
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f'render-helper drift: template "{template_name}" ref "{reference}" '
|
|
278
|
+
f"— {e.code}: {{{{{e.path}}}}} not on payload VO"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# -- emission --------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def _emit_helper(
|
|
284
|
+
self,
|
|
285
|
+
tmpl: MetaData,
|
|
286
|
+
vo: MetaObject,
|
|
287
|
+
payload_ref: str,
|
|
288
|
+
root: MetaData,
|
|
289
|
+
) -> str:
|
|
290
|
+
template_name = tmpl.name
|
|
291
|
+
snake = _snake_case(template_name)
|
|
292
|
+
fields = _derive_payload_field_tree(root, vo, frozenset())
|
|
293
|
+
kind = tmpl.attr(tc.TEMPLATE_ATTR_KIND)
|
|
294
|
+
kind = (kind if isinstance(kind, str) else tc.TEMPLATE_KIND_DEFAULT).lower()
|
|
295
|
+
|
|
296
|
+
fqn = (
|
|
297
|
+
f"{vo.package}{PACKAGE_SEP}{template_name}"
|
|
298
|
+
if getattr(vo, "package", None)
|
|
299
|
+
else template_name
|
|
300
|
+
)
|
|
301
|
+
header = generated_header(template_name, fqn)
|
|
302
|
+
|
|
303
|
+
if kind == tc.TEMPLATE_KIND_EMAIL:
|
|
304
|
+
return self._emit_email(header, template_name, snake, payload_ref, tmpl, fields)
|
|
305
|
+
return self._emit_document(header, template_name, snake, payload_ref, tmpl, fields)
|
|
306
|
+
|
|
307
|
+
def _emit_document(
|
|
308
|
+
self,
|
|
309
|
+
header: str,
|
|
310
|
+
template_name: str,
|
|
311
|
+
snake: str,
|
|
312
|
+
payload_ref: str,
|
|
313
|
+
tmpl: MetaData,
|
|
314
|
+
fields: list[PayloadField],
|
|
315
|
+
) -> str:
|
|
316
|
+
text_ref = tmpl.attr(tc.TEMPLATE_ATTR_TEXT_REF)
|
|
317
|
+
if not isinstance(text_ref, str) or not text_ref:
|
|
318
|
+
raise ValueError(
|
|
319
|
+
f'template.output "{template_name}" (document) missing @textRef'
|
|
320
|
+
)
|
|
321
|
+
fmt = tmpl.attr(tc.TEMPLATE_ATTR_FORMAT)
|
|
322
|
+
fmt = fmt if isinstance(fmt, str) and fmt else tc.TEMPLATE_FORMAT_DEFAULT
|
|
323
|
+
max_chars = _max_chars_of(tmpl)
|
|
324
|
+
|
|
325
|
+
# BUILD-TIME drift gate (THE headline) — raises on drift.
|
|
326
|
+
self._gate_ref(template_name, text_ref, fields)
|
|
327
|
+
|
|
328
|
+
request_lines = [
|
|
329
|
+
" RenderRequest(",
|
|
330
|
+
" payload=payload,",
|
|
331
|
+
" provider=provider,",
|
|
332
|
+
f" ref={_py_str(text_ref)},",
|
|
333
|
+
f" format={_py_str(fmt)},",
|
|
334
|
+
]
|
|
335
|
+
if max_chars is not None:
|
|
336
|
+
request_lines.append(f" max_chars={max_chars},")
|
|
337
|
+
request_lines.append(" )")
|
|
338
|
+
|
|
339
|
+
lines = [
|
|
340
|
+
header,
|
|
341
|
+
"from __future__ import annotations",
|
|
342
|
+
"",
|
|
343
|
+
"from metaobjects.render.renderer import render, RenderRequest",
|
|
344
|
+
"",
|
|
345
|
+
"",
|
|
346
|
+
f"def render_{snake}(payload, provider) -> str:",
|
|
347
|
+
f' """Render the ``{template_name}`` document from a typed '
|
|
348
|
+
f"``{payload_ref}`` payload.",
|
|
349
|
+
"",
|
|
350
|
+
" Wraps the render engine; the mustache↔payload-VO drift check ran at",
|
|
351
|
+
' BUILD time (codegen fails on drift)."""',
|
|
352
|
+
" return render(",
|
|
353
|
+
*request_lines,
|
|
354
|
+
" )",
|
|
355
|
+
"",
|
|
356
|
+
"",
|
|
357
|
+
f'__all__ = ["render_{snake}"]',
|
|
358
|
+
"",
|
|
359
|
+
]
|
|
360
|
+
return "\n".join(lines)
|
|
361
|
+
|
|
362
|
+
def _emit_email(
|
|
363
|
+
self,
|
|
364
|
+
header: str,
|
|
365
|
+
template_name: str,
|
|
366
|
+
snake: str,
|
|
367
|
+
payload_ref: str,
|
|
368
|
+
tmpl: MetaData,
|
|
369
|
+
fields: list[PayloadField],
|
|
370
|
+
) -> str:
|
|
371
|
+
subject_ref = tmpl.attr(tc.TEMPLATE_ATTR_SUBJECT_REF)
|
|
372
|
+
html_body_ref = tmpl.attr(tc.TEMPLATE_ATTR_HTML_BODY_REF)
|
|
373
|
+
text_body_ref = tmpl.attr(tc.TEMPLATE_ATTR_TEXT_BODY_REF)
|
|
374
|
+
if not isinstance(subject_ref, str) or not subject_ref:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f'template.output "{template_name}" (email) missing @subjectRef'
|
|
377
|
+
)
|
|
378
|
+
if not isinstance(html_body_ref, str) or not html_body_ref:
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f'template.output "{template_name}" (email) missing @htmlBodyRef'
|
|
381
|
+
)
|
|
382
|
+
has_text = isinstance(text_body_ref, str) and bool(text_body_ref)
|
|
383
|
+
|
|
384
|
+
# BUILD-TIME drift gate — every email part-ref is resolved + verified.
|
|
385
|
+
self._gate_ref(template_name, subject_ref, fields)
|
|
386
|
+
self._gate_ref(template_name, html_body_ref, fields)
|
|
387
|
+
if has_text:
|
|
388
|
+
self._gate_ref(template_name, text_body_ref, fields)
|
|
389
|
+
|
|
390
|
+
text_body_expr = (
|
|
391
|
+
"render(\n"
|
|
392
|
+
" RenderRequest(\n"
|
|
393
|
+
" payload=payload,\n"
|
|
394
|
+
" provider=provider,\n"
|
|
395
|
+
f" ref={_py_str(text_body_ref)},\n"
|
|
396
|
+
' format="text",\n'
|
|
397
|
+
" )\n"
|
|
398
|
+
" )"
|
|
399
|
+
if has_text
|
|
400
|
+
else "None"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
lines = [
|
|
404
|
+
header,
|
|
405
|
+
"from __future__ import annotations",
|
|
406
|
+
"",
|
|
407
|
+
"from metaobjects.render.email_document import EmailDocument",
|
|
408
|
+
"from metaobjects.render.renderer import render, RenderRequest",
|
|
409
|
+
"",
|
|
410
|
+
"",
|
|
411
|
+
f"def render_{snake}(payload, provider) -> EmailDocument:",
|
|
412
|
+
f' """Render the ``{template_name}`` email (subject + html body'
|
|
413
|
+
f'{" + text body" if has_text else ""}) from a typed ``{payload_ref}`` payload.',
|
|
414
|
+
"",
|
|
415
|
+
" Wraps the render engine; the mustache↔payload-VO drift check ran at",
|
|
416
|
+
' BUILD time (codegen fails on drift)."""',
|
|
417
|
+
" return EmailDocument(",
|
|
418
|
+
" subject=render(",
|
|
419
|
+
" RenderRequest(",
|
|
420
|
+
" payload=payload,",
|
|
421
|
+
" provider=provider,",
|
|
422
|
+
f" ref={_py_str(subject_ref)},",
|
|
423
|
+
' format="text",',
|
|
424
|
+
" )",
|
|
425
|
+
" ),",
|
|
426
|
+
" html_body=render(",
|
|
427
|
+
" RenderRequest(",
|
|
428
|
+
" payload=payload,",
|
|
429
|
+
" provider=provider,",
|
|
430
|
+
f" ref={_py_str(html_body_ref)},",
|
|
431
|
+
' format="html",',
|
|
432
|
+
" )",
|
|
433
|
+
" ),",
|
|
434
|
+
f" text_body={text_body_expr},",
|
|
435
|
+
" )",
|
|
436
|
+
"",
|
|
437
|
+
"",
|
|
438
|
+
f'__all__ = ["render_{snake}"]',
|
|
439
|
+
"",
|
|
440
|
+
]
|
|
441
|
+
return "\n".join(lines)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def render_helper_generator(
|
|
445
|
+
*,
|
|
446
|
+
template_root: str,
|
|
447
|
+
filter: Callable[[MetaObject], bool] | None = None,
|
|
448
|
+
) -> Generator:
|
|
449
|
+
"""Factory mirroring the TS ``renderHelper()`` / C# ``RenderHelperGenerator`` /
|
|
450
|
+
Java ``SpringRenderHelperGenerator`` constructors."""
|
|
451
|
+
return RenderHelperGenerator(template_root, filter=filter)
|