Skip to main content

lex_extension/wire/
ast.rs

1//! Wire AST: the cross-version representation of Lex content.
2//!
3//! [`WireNode`] is a tagged enum (kind discriminator) covering all block
4//! kinds. Inline content uses [`WireInline`](crate::wire::WireInline).
5//!
6//! # Forward compatibility
7//!
8//! Adding a new `kind` to the wire format is a *breaking* change: the
9//! `wire_version` integer bumps, and old hosts/handlers reject the
10//! mismatched protocol at the `initialize` handshake. Within a single
11//! `wire_version`, the set of node kinds is closed.
12//!
13//! This is deliberately stricter than the per-string-enum forward-compat
14//! policy used for [`DiagnosticSeverity`](crate::wire::DiagnosticSeverity)
15//! and similar (which fall back to a documented default on unknown
16//! values). Block AST is structural; treating an unknown `kind` as
17//! "ignore me" silently drops document content, which is worse than
18//! refusing the document with a clear protocol-version diagnostic.
19//!
20//! On the Rust side, [`WireNode`] is `#[non_exhaustive]` so that adding a
21//! new variant in a future major-version release of this crate is not a
22//! breaking source-level change for downstream `match` consumers.
23
24use serde::{Deserialize, Serialize};
25
26use super::inline::WireInline;
27use super::range::Range;
28
29/// A block-level wire AST node. Wire form is a tagged object with `"kind"`
30/// selecting the variant, plus shared `range` and optional `origin` fields.
31///
32/// See the module-level docs for the forward-compatibility contract.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(tag = "kind", rename_all = "snake_case")]
35#[non_exhaustive]
36pub enum WireNode {
37    Document {
38        range: Range,
39        #[serde(skip_serializing_if = "Option::is_none")]
40        origin: Option<String>,
41        children: Vec<WireNode>,
42    },
43    Session {
44        range: Range,
45        #[serde(skip_serializing_if = "Option::is_none")]
46        origin: Option<String>,
47        title: String,
48        #[serde(skip_serializing_if = "Option::is_none")]
49        marker: Option<String>,
50        children: Vec<WireNode>,
51    },
52    Definition {
53        range: Range,
54        #[serde(skip_serializing_if = "Option::is_none")]
55        origin: Option<String>,
56        subject: String,
57        children: Vec<WireNode>,
58    },
59    Paragraph {
60        range: Range,
61        #[serde(skip_serializing_if = "Option::is_none")]
62        origin: Option<String>,
63        inlines: Vec<WireInline>,
64    },
65    List {
66        range: Range,
67        #[serde(skip_serializing_if = "Option::is_none")]
68        origin: Option<String>,
69        marker_style: String,
70        items: Vec<WireListItem>,
71    },
72    Verbatim {
73        range: Range,
74        #[serde(skip_serializing_if = "Option::is_none")]
75        origin: Option<String>,
76        label: String,
77        params: serde_json::Value,
78        body_text: String,
79        /// The verbatim block's subject (the lead-in line, e.g.
80        /// `Code:`). Empty string for verbatim blocks that have no
81        /// subject (or for the placeholder shape used to flag
82        /// unsupported variants).
83        #[serde(default, skip_serializing_if = "String::is_empty")]
84        subject: String,
85        /// Rendering mode: `"inflow"` (content indented relative to
86        /// subject) or `"fullwidth"` (content at column 2). Defaults
87        /// to `"inflow"` on deserialise — matching the parser's
88        /// default mode — when the field is absent from the wire
89        /// payload.
90        #[serde(default = "default_verbatim_mode")]
91        mode: String,
92    },
93    Table {
94        range: Range,
95        #[serde(skip_serializing_if = "Option::is_none")]
96        origin: Option<String>,
97        caption: String,
98        header_rows: u32,
99        align: String,
100        rows: Vec<WireRow>,
101        #[serde(default, skip_serializing_if = "Vec::is_empty")]
102        footnotes: Vec<WireFootnote>,
103    },
104    Annotation {
105        range: Range,
106        #[serde(skip_serializing_if = "Option::is_none")]
107        origin: Option<String>,
108        label: String,
109        params: serde_json::Value,
110        /// `null` for marker-form annotations, a string for opaque-text
111        /// bodies, an object `{ "kind": "block", "children": [...] }` for
112        /// parsed-Lex bodies. See [`AnnotationBody`](super::ctx::AnnotationBody)
113        /// for the corresponding [`LabelCtx`](super::ctx::LabelCtx) shape.
114        body: serde_json::Value,
115    },
116    Blank {
117        range: Range,
118        #[serde(skip_serializing_if = "Option::is_none")]
119        origin: Option<String>,
120    },
121}
122
123/// One item inside a [`WireNode::List`].
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct WireListItem {
126    pub range: Range,
127    pub inlines: Vec<WireInline>,
128    #[serde(default, skip_serializing_if = "Vec::is_empty")]
129    pub children: Vec<WireNode>,
130}
131
132/// One row in a [`WireNode::Table`].
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct WireRow {
135    pub cells: Vec<WireTableCell>,
136}
137
138/// One cell in a [`WireRow`]. `inlines` holds the cell's content; merge
139/// markers (`>>`, `^^`) are surfaced as `colspan` / `rowspan` for downstream
140/// renderers.
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142pub struct WireTableCell {
143    pub inlines: Vec<WireInline>,
144    #[serde(default = "one")]
145    pub colspan: u32,
146    #[serde(default = "one")]
147    pub rowspan: u32,
148}
149
150fn one() -> u32 {
151    1
152}
153
154fn default_verbatim_mode() -> String {
155    "inflow".to_string()
156}
157
158/// One footnote attached to a table.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct WireFootnote {
161    pub marker: String,
162    pub inlines: Vec<WireInline>,
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::wire::range::Position;
169
170    fn r(s_l: u32, s_c: u32, e_l: u32, e_c: u32) -> Range {
171        Range::new(Position::new(s_l, s_c), Position::new(e_l, e_c))
172    }
173
174    #[test]
175    fn paragraph_round_trips() {
176        let p = WireNode::Paragraph {
177            range: r(0, 0, 0, 5),
178            origin: None,
179            inlines: vec![WireInline::Text {
180                text: "hello".into(),
181            }],
182        };
183        let s = serde_json::to_string(&p).unwrap();
184        let back: WireNode = serde_json::from_str(&s).unwrap();
185        assert_eq!(back, p);
186    }
187
188    #[test]
189    fn paragraph_kind_in_serialized_form() {
190        let p = WireNode::Paragraph {
191            range: r(0, 0, 0, 5),
192            origin: None,
193            inlines: vec![],
194        };
195        let s = serde_json::to_string(&p).unwrap();
196        assert!(s.contains(r#""kind":"paragraph""#));
197    }
198
199    #[test]
200    fn document_with_children() {
201        let d = WireNode::Document {
202            range: r(0, 0, 10, 0),
203            origin: Some("doc.lex".into()),
204            children: vec![WireNode::Paragraph {
205                range: r(0, 0, 0, 3),
206                origin: None,
207                inlines: vec![WireInline::Text { text: "x".into() }],
208            }],
209        };
210        let s = serde_json::to_string(&d).unwrap();
211        assert!(s.contains(r#""origin":"doc.lex""#));
212        let back: WireNode = serde_json::from_str(&s).unwrap();
213        assert_eq!(back, d);
214    }
215
216    #[test]
217    fn annotation_with_lex_body() {
218        let a = WireNode::Annotation {
219            range: r(3, 0, 6, 0),
220            origin: None,
221            label: "acme.commenting".into(),
222            params: serde_json::json!({"role": "editor"}),
223            body: serde_json::json!({
224                "kind": "block",
225                "children": []
226            }),
227        };
228        let s = serde_json::to_string(&a).unwrap();
229        let back: WireNode = serde_json::from_str(&s).unwrap();
230        assert_eq!(back, a);
231    }
232
233    #[test]
234    fn verbatim_carries_label_and_body_text() {
235        let v = WireNode::Verbatim {
236            range: r(0, 0, 4, 0),
237            origin: None,
238            label: "rust".into(),
239            params: serde_json::json!({}),
240            body_text: "fn main() {}".into(),
241            subject: "Code:".into(),
242            mode: "inflow".into(),
243        };
244        let s = serde_json::to_string(&v).unwrap();
245        let back: WireNode = serde_json::from_str(&s).unwrap();
246        assert_eq!(back, v);
247    }
248
249    #[test]
250    fn verbatim_mode_field_defaults_to_inflow_on_deserialise() {
251        // A wire payload missing `mode` should round-trip as
252        // "inflow" — the documented default for older producers
253        // that don't emit the field.
254        let payload = r#"{
255            "kind":"verbatim",
256            "range":{"start":[0,0],"end":[4,0]},
257            "label":"rust",
258            "params":{},
259            "body_text":"x"
260        }"#;
261        let v: WireNode = serde_json::from_str(payload).unwrap();
262        match v {
263            WireNode::Verbatim {
264                ref mode,
265                ref subject,
266                ..
267            } => {
268                assert_eq!(mode, "inflow");
269                assert_eq!(subject, "");
270            }
271            _ => panic!("expected Verbatim"),
272        }
273    }
274
275    #[test]
276    fn list_with_items() {
277        let l = WireNode::List {
278            range: r(0, 0, 2, 0),
279            origin: None,
280            marker_style: "dash".into(),
281            items: vec![
282                WireListItem {
283                    range: r(0, 0, 0, 5),
284                    inlines: vec![WireInline::Text { text: "a".into() }],
285                    children: vec![],
286                },
287                WireListItem {
288                    range: r(1, 0, 1, 5),
289                    inlines: vec![WireInline::Text { text: "b".into() }],
290                    children: vec![],
291                },
292            ],
293        };
294        let s = serde_json::to_string(&l).unwrap();
295        let back: WireNode = serde_json::from_str(&s).unwrap();
296        assert_eq!(back, l);
297    }
298}