|
| 1 | +"""The ADR-0052 direction rule, in ONE place (Python port). |
| 2 | +
|
| 3 | +A template subtype's axis is DIRECTION: ``template.output`` renders outbound (a document or |
| 4 | +an email) and generates no parser; the inbound half — the response shape, the FR-010 |
| 5 | +response-format fragment, and the parser-on-receipt — belongs to a ``template.prompt`` that |
| 6 | +declares ``@responseRef``. |
| 7 | +
|
| 8 | +Every inbound generator calls through here rather than re-deriving "which templates have a |
| 9 | +response". Call sites each deciding for themselves is exactly how the pre-ADR-0052 tier |
| 10 | +drifted: the parser applied NO format filter to the parser FILE while gating its tolerant |
| 11 | +extract on ``@format``, and the fragment emitter applied a different ``@format`` gate — the |
| 12 | +format of the OUTBOUND body, which is not the format of the reply. |
| 13 | +
|
| 14 | +Mirrors ``codegen-ts/src/templates/find-inbound.ts``, C#'s ``FindInbound.cs``, Java's |
| 15 | +``FindInbound.java`` and Kotlin's ``FindInbound.kt``. |
| 16 | +""" |
| 17 | + |
| 18 | +from __future__ import annotations |
| 19 | + |
| 20 | +from dataclasses import dataclass |
| 21 | + |
| 22 | +from metaobjects.codegen.generators.payload_vo_generator import resolve_payload_vo |
| 23 | +from metaobjects.meta.meta_data import MetaData |
| 24 | +from metaobjects.meta.template import template_constants as tc |
| 25 | +from metaobjects.shared.base_types import TYPE_TEMPLATE |
| 26 | + |
| 27 | + |
| 28 | +@dataclass(frozen=True) |
| 29 | +class InboundShape: |
| 30 | + """What an inbound generator needs about one responding prompt. |
| 31 | +
|
| 32 | + ``vo`` — the resolved response value-object; the shape a reply is parsed INTO. |
| 33 | + ``ref`` — the ``@responseRef`` string as authored (bare or fully-qualified). |
| 34 | + ``format`` — the syntax of the REPLY (ADR-0053), never the template's ``@format``, |
| 35 | + which is the syntax of the rendered prompt BODY. The two genuinely differ. |
| 36 | + """ |
| 37 | + |
| 38 | + vo: MetaData |
| 39 | + ref: str |
| 40 | + format: str |
| 41 | + |
| 42 | + |
| 43 | +def response_ref_of(template: MetaData) -> str | None: |
| 44 | + """The authored ``@responseRef`` of a responding prompt, or ``None``. |
| 45 | +
|
| 46 | + ADR-0039: read RESOLVING — a template may inherit ``@responseRef`` through ``extends``, |
| 47 | + and shipped fixtures rely on exactly that. |
| 48 | + """ |
| 49 | + if template.type != TYPE_TEMPLATE or template.sub_type != tc.TEMPLATE_SUBTYPE_PROMPT: |
| 50 | + return None |
| 51 | + ref = template.get_meta_attr(tc.TEMPLATE_ATTR_RESPONSE_REF) |
| 52 | + if not isinstance(ref, str) or not ref: |
| 53 | + return None |
| 54 | + return ref |
| 55 | + |
| 56 | + |
| 57 | +def inbound_templates(root: MetaData) -> list[MetaData]: |
| 58 | + """Every ``template.prompt`` that declares a response shape, ordered by name. |
| 59 | +
|
| 60 | + The gate is ``@responseRef`` PRESENCE, not a format value: declaring a response shape IS |
| 61 | + the request for a parser. Gating on ``@format`` was what let a ``text`` template get a |
| 62 | + strict parser but no tolerant extract, and — because ``@format`` defaults to ``text`` — |
| 63 | + would silently emit nothing at all after the re-homing. |
| 64 | + """ |
| 65 | + return sorted( |
| 66 | + (c for c in root.children() if response_ref_of(c) is not None), |
| 67 | + key=lambda t: t.name, |
| 68 | + ) |
| 69 | + |
| 70 | + |
| 71 | +def response_shape(root: MetaData, template: MetaData, referrer_pkg: str) -> InboundShape | None: |
| 72 | + """Resolve a prompt's response value-object and reply syntax, or ``None``. |
| 73 | +
|
| 74 | + ``None`` when the template declares no ``@responseRef`` or the ref does not resolve — |
| 75 | + callers skip rather than raise, matching the pre-ADR-0052 contract for an unresolvable |
| 76 | + payload ref. |
| 77 | +
|
| 78 | + Resolution goes through ``resolve_payload_vo``, the SAME target rule ``@payloadRef`` |
| 79 | + obeys, so a parser can never bind a record the payload tier refused to emit. (C# used the |
| 80 | + any-object resolver here and shipped exactly that defect: a ``@responseRef`` naming an |
| 81 | + ``object.entity`` produced a parser returning a type nobody declared.) |
| 82 | + """ |
| 83 | + ref = response_ref_of(template) |
| 84 | + if ref is None: |
| 85 | + return None |
| 86 | + vo = resolve_payload_vo(root, ref, referrer_pkg) |
| 87 | + if vo is None: |
| 88 | + return None |
| 89 | + return InboundShape(vo=vo, ref=ref, format=response_format_of(template)) |
| 90 | + |
| 91 | + |
| 92 | +def response_format_of(template: MetaData) -> str: |
| 93 | + """The declared reply syntax, defaulted per ADR-0053. |
| 94 | +
|
| 95 | + The default is ``json`` because that reproduces the trace helper's pre-ADR-0053 fallback |
| 96 | + exactly (anything that was not ``"xml"`` was treated as JSON), which is what makes the |
| 97 | + attribute's introduction behaviour-preserving rather than a new policy. |
| 98 | + """ |
| 99 | + raw = template.get_meta_attr(tc.TEMPLATE_ATTR_RESPONSE_FORMAT) |
| 100 | + if isinstance(raw, str) and raw.lower() == tc.RESPONSE_FORMAT_XML: |
| 101 | + return tc.RESPONSE_FORMAT_XML |
| 102 | + return tc.RESPONSE_FORMAT_DEFAULT |
| 103 | + |
| 104 | + |
| 105 | +def is_xml(response_format: str) -> bool: |
| 106 | + """True iff the reply is XML. |
| 107 | +
|
| 108 | + The strict tier is JSON-ONLY by construction — not because no XML reader exists (the |
| 109 | + render package ships a forgiving one) but because strict all-or-nothing semantics layered |
| 110 | + over a REPAIRING parser is incoherent: it would raise or accept based on how much repair |
| 111 | + happened. So an XML reply gets the tolerant extract and nothing strict. |
| 112 | + """ |
| 113 | + return response_format.lower() == tc.RESPONSE_FORMAT_XML |
0 commit comments