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}