Skip to main content

lex_extension/wire/
ctx.rs

1//! [`LabelCtx`] and supporting types — the payload every hook event carries.
2
3use serde::{Deserialize, Serialize};
4
5use super::ast::WireNode;
6use super::range::Range;
7
8/// The payload every hook receives. Describes a single label invocation:
9/// its parameters, body, and position in the source AST.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct LabelCtx {
12    /// Fully-qualified label name, e.g. `"acme.commenting"`.
13    pub label: String,
14    /// Param values, validated against the schema before dispatch. Always an
15    /// object; defaults are filled in. (Stored as `serde_json::Value` rather
16    /// than a typed map to keep the wire format generic.)
17    pub params: serde_json::Value,
18    /// Body content. Shape depends on the schema's `body.kind`.
19    pub body: AnnotationBody,
20    /// The host AST node the label is attached to.
21    pub node: NodeRef,
22}
23
24/// Annotation body shape carried by [`LabelCtx::body`]. Wire form is
25/// untagged: `null`, a JSON string, or `{ "kind": "block", "children": [...] }`.
26#[derive(Debug, Clone, PartialEq)]
27pub enum AnnotationBody {
28    /// `body.kind: none` — marker-form annotation, no body.
29    None,
30    /// `body.kind: text` — opaque body. Verbatim usage always lands here.
31    Text(String),
32    /// `body.kind: lex` — parsed body. Children are fully-formed wire AST
33    /// nodes the handler can walk directly.
34    Lex { children: Vec<WireNode> },
35}
36
37impl AnnotationBody {
38    /// Returns `true` for [`AnnotationBody::None`].
39    pub fn is_none(&self) -> bool {
40        matches!(self, Self::None)
41    }
42}
43
44impl Serialize for AnnotationBody {
45    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46    where
47        S: serde::Serializer,
48    {
49        use serde::ser::SerializeMap;
50        match self {
51            Self::None => serializer.serialize_none(),
52            Self::Text(s) => serializer.serialize_str(s),
53            Self::Lex { children } => {
54                let mut map = serializer.serialize_map(Some(2))?;
55                map.serialize_entry("kind", "block")?;
56                map.serialize_entry("children", children)?;
57                map.end()
58            }
59        }
60    }
61}
62
63impl<'de> Deserialize<'de> for AnnotationBody {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: serde::Deserializer<'de>,
67    {
68        let value = serde_json::Value::deserialize(deserializer)?;
69        match value {
70            serde_json::Value::Null => Ok(Self::None),
71            serde_json::Value::String(s) => Ok(Self::Text(s)),
72            serde_json::Value::Object(map) => {
73                let kind = map.get("kind").and_then(|v| v.as_str());
74                if kind != Some("block") {
75                    return Err(serde::de::Error::custom(
76                        "annotation body object must have kind: \"block\"",
77                    ));
78                }
79                let children_value = map
80                    .get("children")
81                    .cloned()
82                    .unwrap_or_else(|| serde_json::Value::Array(Vec::new()));
83                let children: Vec<WireNode> =
84                    serde_json::from_value(children_value).map_err(serde::de::Error::custom)?;
85                Ok(Self::Lex { children })
86            }
87            _ => Err(serde::de::Error::custom(
88                "annotation body must be null, a string, or an object",
89            )),
90        }
91    }
92}
93
94/// A reference to the AST node a label is attached to. Carries position
95/// metadata so handlers know where in the document the invocation sits;
96/// the body of the host node is *not* shipped (handlers receive only the
97/// label invocation's own body via [`LabelCtx::body`]).
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub struct NodeRef {
100    /// The host node's wire kind, e.g. `"annotation"`, `"verbatim"`.
101    pub kind: String,
102    pub range: Range,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub origin: Option<String>,
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::wire::range::Position;
111
112    fn r(s_l: u32, s_c: u32, e_l: u32, e_c: u32) -> Range {
113        Range::new(Position::new(s_l, s_c), Position::new(e_l, e_c))
114    }
115
116    fn nref() -> NodeRef {
117        NodeRef {
118            kind: "annotation".into(),
119            range: r(0, 0, 0, 12),
120            origin: None,
121        }
122    }
123
124    #[test]
125    fn none_body_serialises_as_null() {
126        let c = LabelCtx {
127            label: "foo".into(),
128            params: serde_json::json!({}),
129            body: AnnotationBody::None,
130            node: nref(),
131        };
132        let s = serde_json::to_string(&c).unwrap();
133        assert!(s.contains(r#""body":null"#));
134        let back: LabelCtx = serde_json::from_str(&s).unwrap();
135        assert_eq!(back, c);
136    }
137
138    #[test]
139    fn text_body_serialises_as_string() {
140        let c = LabelCtx {
141            label: "foo".into(),
142            params: serde_json::json!({}),
143            body: AnnotationBody::Text("raw".into()),
144            node: nref(),
145        };
146        let s = serde_json::to_string(&c).unwrap();
147        assert!(s.contains(r#""body":"raw""#));
148        let back: LabelCtx = serde_json::from_str(&s).unwrap();
149        assert_eq!(back, c);
150    }
151
152    #[test]
153    fn lex_body_serialises_as_block() {
154        let c = LabelCtx {
155            label: "foo".into(),
156            params: serde_json::json!({"k": 1}),
157            body: AnnotationBody::Lex { children: vec![] },
158            node: nref(),
159        };
160        let s = serde_json::to_string(&c).unwrap();
161        assert!(s.contains(r#""body":{"kind":"block","children":[]}"#));
162        let back: LabelCtx = serde_json::from_str(&s).unwrap();
163        assert_eq!(back, c);
164    }
165}