Skip to main content

atrg_codegen/
lexicon.rs

1//! AT Protocol lexicon JSON parsing.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// A parsed AT Protocol lexicon document.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LexiconDoc {
9    /// The lexicon version (should be 1).
10    pub lexicon: u32,
11    /// The NSID (e.g. "com.example.getPosts").
12    pub id: String,
13    /// Optional description.
14    #[serde(default)]
15    pub description: Option<String>,
16    /// The definitions in this lexicon.
17    #[serde(default)]
18    pub defs: HashMap<String, LexiconDef>,
19}
20
21/// A single definition within a lexicon.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type")]
24pub enum LexiconDef {
25    /// A record type.
26    #[serde(rename = "record")]
27    Record {
28        /// Description.
29        #[serde(default)]
30        description: Option<String>,
31        /// Record key type.
32        #[serde(default)]
33        key: Option<String>,
34        /// The record schema.
35        #[serde(default)]
36        record: Option<LexiconObject>,
37    },
38    /// An object type.
39    #[serde(rename = "object")]
40    Object(LexiconObject),
41    /// A query (read) procedure.
42    #[serde(rename = "query")]
43    Query {
44        /// Description.
45        #[serde(default)]
46        description: Option<String>,
47        /// Query parameters.
48        #[serde(default)]
49        parameters: Option<LexiconObject>,
50        /// Output schema.
51        #[serde(default)]
52        output: Option<LexiconOutput>,
53    },
54    /// A mutation procedure.
55    #[serde(rename = "procedure")]
56    Procedure {
57        /// Description.
58        #[serde(default)]
59        description: Option<String>,
60        /// Input body schema.
61        #[serde(default)]
62        input: Option<LexiconBody>,
63        /// Output schema.
64        #[serde(default)]
65        output: Option<LexiconOutput>,
66    },
67    /// A string type.
68    #[serde(rename = "string")]
69    String {
70        /// Description.
71        #[serde(default)]
72        description: Option<String>,
73        /// Known values.
74        #[serde(default)]
75        known_values: Option<Vec<String>>,
76    },
77    /// A token type.
78    #[serde(rename = "token")]
79    Token {
80        /// Description.
81        #[serde(default)]
82        description: Option<String>,
83    },
84    /// Catch-all for types we don't generate code for yet.
85    #[serde(other)]
86    Unknown,
87}
88
89/// An object schema (used in records, params, etc.).
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LexiconObject {
92    /// Description.
93    #[serde(default)]
94    pub description: Option<String>,
95    /// Required field names.
96    #[serde(default)]
97    pub required: Vec<String>,
98    /// Property definitions.
99    #[serde(default)]
100    pub properties: HashMap<String, LexiconProperty>,
101}
102
103/// A single property in an object schema.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LexiconProperty {
106    /// The type of this property.
107    #[serde(rename = "type")]
108    pub prop_type: String,
109    /// Description.
110    #[serde(default)]
111    pub description: Option<String>,
112    /// Format hint (e.g. "datetime", "uri").
113    #[serde(default)]
114    pub format: Option<String>,
115    /// Maximum length.
116    #[serde(default)]
117    pub max_length: Option<u64>,
118    /// Minimum value.
119    #[serde(default)]
120    pub minimum: Option<i64>,
121    /// Maximum value.
122    #[serde(default)]
123    pub maximum: Option<i64>,
124    /// Default value.
125    #[serde(default)]
126    pub default: Option<serde_json::Value>,
127    /// Items type (for arrays).
128    #[serde(default)]
129    pub items: Option<Box<LexiconProperty>>,
130    /// Reference to another type.
131    #[serde(rename = "ref", default)]
132    pub ref_: Option<String>,
133}
134
135/// Output schema for queries/procedures.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct LexiconOutput {
138    /// Encoding (usually "application/json").
139    #[serde(default)]
140    pub encoding: Option<String>,
141    /// The schema.
142    #[serde(default)]
143    pub schema: Option<LexiconObject>,
144}
145
146/// Input body for procedures.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct LexiconBody {
149    /// Encoding.
150    #[serde(default)]
151    pub encoding: Option<String>,
152    /// The schema.
153    #[serde(default)]
154    pub schema: Option<LexiconObject>,
155}
156
157/// Load a lexicon document from a JSON string.
158pub fn parse_lexicon(json: &str) -> anyhow::Result<LexiconDoc> {
159    let doc: LexiconDoc = serde_json::from_str(json)?;
160    if doc.lexicon != 1 {
161        anyhow::bail!("unsupported lexicon version: {} (expected 1)", doc.lexicon);
162    }
163    if doc.id.is_empty() {
164        anyhow::bail!("lexicon id must not be empty");
165    }
166    Ok(doc)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn parse_minimal_lexicon() {
175        let json = r#"{
176            "lexicon": 1,
177            "id": "com.example.ping",
178            "defs": {
179                "main": {
180                    "type": "query",
181                    "description": "A simple ping",
182                    "output": {
183                        "encoding": "application/json",
184                        "schema": {
185                            "type": "object",
186                            "properties": {
187                                "pong": { "type": "boolean" }
188                            }
189                        }
190                    }
191                }
192            }
193        }"#;
194        let doc = parse_lexicon(json).unwrap();
195        assert_eq!(doc.id, "com.example.ping");
196        assert!(doc.defs.contains_key("main"));
197    }
198
199    #[test]
200    fn parse_record_lexicon() {
201        let json = r#"{
202            "lexicon": 1,
203            "id": "com.example.post",
204            "defs": {
205                "main": {
206                    "type": "record",
207                    "description": "A post record",
208                    "key": "tid",
209                    "record": {
210                        "type": "object",
211                        "required": ["text", "createdAt"],
212                        "properties": {
213                            "text": { "type": "string", "max_length": 3000 },
214                            "createdAt": { "type": "string", "format": "datetime" }
215                        }
216                    }
217                }
218            }
219        }"#;
220        let doc = parse_lexicon(json).unwrap();
221        assert_eq!(doc.id, "com.example.post");
222    }
223
224    #[test]
225    fn invalid_version_rejected() {
226        let json = r#"{"lexicon": 2, "id": "com.example.test", "defs": {}}"#;
227        assert!(parse_lexicon(json).is_err());
228    }
229
230    #[test]
231    fn empty_id_rejected() {
232        let json = r#"{"lexicon": 1, "id": "", "defs": {}}"#;
233        assert!(parse_lexicon(json).is_err());
234    }
235
236    #[test]
237    fn malformed_json_gives_error() {
238        assert!(parse_lexicon("not json").is_err());
239    }
240}