Skip to main content

lex_extension/wire/
inline.rs

1//! Inline-content wire types.
2//!
3//! Inlines appear inside paragraphs and list items. The serde derives produce
4//! the JSON shapes documented in wire spec §2.3.
5//!
6//! Forward compatibility: as with [`WireNode`](super::ast::WireNode), adding
7//! a new inline `kind` is a breaking wire-format change (bumps
8//! `wire_version`); within a `wire_version` the set is closed. The Rust
9//! enum is `#[non_exhaustive]` so a future major release can add a variant
10//! without breaking downstream `match` arms.
11
12use serde::{Deserialize, Serialize};
13
14/// One inline element. Wire form is a tagged object with `"kind"` selecting
15/// the variant.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(tag = "kind", rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum WireInline {
20    /// Plain text.
21    Text { text: String },
22    /// `*bold*` content.
23    Bold { children: Vec<WireInline> },
24    /// `_italic_` content.
25    Italic { children: Vec<WireInline> },
26    /// `` `code` `` content. Literal — no nested inlines.
27    Code { text: String },
28    /// `#math#` content. Literal — no nested inlines.
29    Math { text: String },
30    /// `[reference]` content. The kind sub-discriminator (`url`, `citation`,
31    /// `footnote`, …) selects the semantic meaning of `target`.
32    Reference {
33        ref_kind: RefKind,
34        target: String,
35        #[serde(skip_serializing_if = "Option::is_none")]
36        label: Option<String>,
37    },
38}
39
40/// Sub-kind of an inline `reference`.
41///
42/// Forward compatibility is implemented in the [`Deserialize`] impl: any
43/// string that doesn't match a known variant deserialises as
44/// [`RefKind::General`], matching the wire spec's "handlers must treat
45/// unknown `ref_kind` values as `general`" rule. The `#[non_exhaustive]`
46/// attribute makes adding new variants a non-breaking Rust change.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
48#[serde(rename_all = "snake_case")]
49#[non_exhaustive]
50pub enum RefKind {
51    Url,
52    Citation,
53    Footnote,
54    Session,
55    File,
56    Placeholder,
57    Unsure,
58    General,
59}
60
61impl<'de> Deserialize<'de> for RefKind {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        let s = String::deserialize(deserializer)?;
67        Ok(match s.as_str() {
68            "url" => Self::Url,
69            "citation" => Self::Citation,
70            "footnote" => Self::Footnote,
71            "session" => Self::Session,
72            "file" => Self::File,
73            "placeholder" => Self::Placeholder,
74            "unsure" => Self::Unsure,
75            _ => Self::General,
76        })
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn text_inline_round_trips() {
86        let i = WireInline::Text {
87            text: "hello".into(),
88        };
89        let s = serde_json::to_string(&i).unwrap();
90        assert_eq!(s, r#"{"kind":"text","text":"hello"}"#);
91        let back: WireInline = serde_json::from_str(&s).unwrap();
92        assert_eq!(back, i);
93    }
94
95    #[test]
96    fn bold_with_nested_text() {
97        let i = WireInline::Bold {
98            children: vec![WireInline::Text {
99                text: "loud".into(),
100            }],
101        };
102        let s = serde_json::to_string(&i).unwrap();
103        assert_eq!(
104            s,
105            r#"{"kind":"bold","children":[{"kind":"text","text":"loud"}]}"#
106        );
107    }
108
109    #[test]
110    fn reference_url_round_trips() {
111        let i = WireInline::Reference {
112            ref_kind: RefKind::Url,
113            target: "https://example.com".into(),
114            label: None,
115        };
116        let s = serde_json::to_string(&i).unwrap();
117        assert_eq!(
118            s,
119            r#"{"kind":"reference","ref_kind":"url","target":"https://example.com"}"#
120        );
121        let back: WireInline = serde_json::from_str(&s).unwrap();
122        assert_eq!(back, i);
123    }
124
125    #[test]
126    fn reference_with_label() {
127        let i = WireInline::Reference {
128            ref_kind: RefKind::Footnote,
129            target: "1".into(),
130            label: Some("note one".into()),
131        };
132        let s = serde_json::to_string(&i).unwrap();
133        let back: WireInline = serde_json::from_str(&s).unwrap();
134        assert_eq!(back, i);
135    }
136
137    #[test]
138    fn unknown_ref_kind_falls_back_to_general() {
139        let kind: RefKind = serde_json::from_str(r#""acronym""#).unwrap();
140        assert_eq!(kind, RefKind::General);
141    }
142}