Skip to main content

lex_extension/wire/
format_out.rs

1//! Wire types for the `on_format` reverse hook (#570 Phase 4).
2//!
3//! See the spec at
4//! `comms/specs/proposals/lex-extension-wire.lex` ยง4.8 (`on_format`)
5//! for the full contract. The hook is the inverse of `on_resolve`:
6//! given a typed AST subtree previously produced by `on_resolve`, the
7//! handler returns the Lex-source representation as a [`LexAnnotationOut`].
8//!
9//! Two types live here:
10//!
11//! - [`FormatCtx`] โ€” the request payload. Mirrors [`LabelCtx`] but
12//!   carries the *typed* `WireNode` the handler must serialize back,
13//!   along with the originating label/params so a namespace with
14//!   several labels driving the same node kind can route on the label.
15//! - [`LexAnnotationOut`] โ€” the structured response. Describes the
16//!   label, parameters, body, and verbatim-flag the host needs to
17//!   emit Lex source. Returning `None` (i.e. result `{ "annotation":
18//!   null }`) lets the host fall back to its built-in formatter.
19//!
20//! [`LabelCtx`]: super::LabelCtx
21
22use serde::{Deserialize, Serialize};
23
24use super::ast::WireNode;
25
26/// Request payload for [`LexHandler::on_format`](crate::handler::LexHandler::on_format).
27///
28/// The handler receives the originating `label` and `params` (lifted
29/// from the AST node that the prior `on_resolve` pass produced this
30/// typed `node` from), the typed `WireNode` to serialize, and an
31/// optional `format_options` object whose shape is namespace-defined.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct FormatCtx {
34    /// Fully-qualified label of the schema that owns this format pass,
35    /// e.g. `"lex.tabular.table"`.
36    pub label: String,
37    /// Originating parameters, in the (key, value) order the host
38    /// deserialized them. Quoting and escaping decisions are left to
39    /// the host on emission.
40    pub params: Vec<(String, String)>,
41    /// The typed wire subtree to serialize back as Lex source.
42    pub node: WireNode,
43    /// Optional, namespace-defined options object. Hosts pass `None`
44    /// when no options are configured; the wire form is
45    /// `"format_options": null` (omitted from the JSON when absent).
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub format_options: Option<serde_json::Value>,
48}
49
50/// Response payload from [`LexHandler::on_format`](crate::handler::LexHandler::on_format).
51///
52/// Returned wrapped in `Option`: `Some(LexAnnotationOut)` carries the
53/// structured serialization; `None` lets the host fall back to its
54/// built-in formatter for the underlying node kind.
55///
56/// `verbatim_label: true` selects the verbatim closing form
57/// (subject-line content + `:: label ::` closer); `false` selects the
58/// inline annotation form (`:: label :: text` or `:: label ::` plus
59/// indented content).
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct LexAnnotationOut {
62    /// Canonical fully-qualified label, e.g. `"lex.tabular.table"`.
63    pub label: String,
64    /// `(key, value)` pairs emitted in `key=value` order.
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub params: Vec<(String, String)>,
67    /// Verbatim or inline text body. Empty for marker-form annotations.
68    #[serde(default, skip_serializing_if = "String::is_empty")]
69    pub body: String,
70    /// `true` for verbatim closing form, `false` for inline annotation
71    /// form. Defaults to `false` on the wire โ€” omitted entirely from
72    /// the serialized JSON when `false` so marker-form annotations get
73    /// the compact `{ "label": "..." }` shape.
74    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
75    pub verbatim_label: bool,
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::wire::range::{Position, Range};
82
83    fn r(s_l: u32, s_c: u32, e_l: u32, e_c: u32) -> Range {
84        Range::new(Position::new(s_l, s_c), Position::new(e_l, e_c))
85    }
86
87    fn sample_node() -> WireNode {
88        WireNode::Paragraph {
89            range: r(0, 0, 0, 5),
90            origin: None,
91            inlines: vec![],
92        }
93    }
94
95    #[test]
96    fn format_ctx_round_trips_through_json() {
97        let c = FormatCtx {
98            label: "lex.tabular.table".into(),
99            params: vec![("align".into(), "lcr".into())],
100            node: sample_node(),
101            format_options: Some(serde_json::json!({ "max_width": 80 })),
102        };
103        let s = serde_json::to_string(&c).unwrap();
104        let back: FormatCtx = serde_json::from_str(&s).unwrap();
105        assert_eq!(back, c);
106    }
107
108    #[test]
109    fn format_ctx_omits_options_when_none() {
110        let c = FormatCtx {
111            label: "lex.media.image".into(),
112            params: vec![("src".into(), "x.png".into())],
113            node: sample_node(),
114            format_options: None,
115        };
116        let s = serde_json::to_string(&c).unwrap();
117        assert!(
118            !s.contains("format_options"),
119            "format_options must be omitted when None, got: {s}"
120        );
121        let back: FormatCtx = serde_json::from_str(&s).unwrap();
122        assert_eq!(back, c);
123    }
124
125    #[test]
126    fn lex_annotation_out_round_trips_through_json() {
127        let a = LexAnnotationOut {
128            label: "lex.tabular.table".into(),
129            params: vec![("header".into(), "1".into())],
130            body: "| a | b |\n|---|---|\n| 1 | 2 |".into(),
131            verbatim_label: true,
132        };
133        let s = serde_json::to_string(&a).unwrap();
134        let back: LexAnnotationOut = serde_json::from_str(&s).unwrap();
135        assert_eq!(back, a);
136    }
137
138    #[test]
139    fn lex_annotation_out_minimal_form_omits_defaults() {
140        // Marker-form annotation: empty params, empty body, not verbatim.
141        // The serialized form must skip every default field so the
142        // wire shape collapses to `{ "label": "..." }`.
143        let a = LexAnnotationOut {
144            label: "lex.metadata.author".into(),
145            params: vec![],
146            body: String::new(),
147            verbatim_label: false,
148        };
149        let s = serde_json::to_string(&a).unwrap();
150        assert!(!s.contains("params"), "params must be omitted: {s}");
151        assert!(!s.contains("body"), "body must be omitted: {s}");
152        assert!(
153            !s.contains("verbatim_label"),
154            "verbatim_label must be omitted when false: {s}"
155        );
156        let back: LexAnnotationOut = serde_json::from_str(&s).unwrap();
157        assert_eq!(back, a);
158    }
159
160    #[test]
161    fn lex_annotation_out_deserializes_with_omitted_defaults() {
162        // A handler that returns only `{ "label": "..." }` must be
163        // accepted on the wire as a marker-form annotation.
164        let json = r#"{"label":"lex.metadata.date"}"#;
165        let a: LexAnnotationOut = serde_json::from_str(json).unwrap();
166        assert_eq!(a.label, "lex.metadata.date");
167        assert!(a.params.is_empty());
168        assert!(a.body.is_empty());
169        assert!(!a.verbatim_label);
170    }
171}