1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct LexiconDoc {
9 pub lexicon: u32,
11 pub id: String,
13 #[serde(default)]
15 pub description: Option<String>,
16 #[serde(default)]
18 pub defs: HashMap<String, LexiconDef>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type")]
24pub enum LexiconDef {
25 #[serde(rename = "record")]
27 Record {
28 #[serde(default)]
30 description: Option<String>,
31 #[serde(default)]
33 key: Option<String>,
34 #[serde(default)]
36 record: Option<LexiconObject>,
37 },
38 #[serde(rename = "object")]
40 Object(LexiconObject),
41 #[serde(rename = "query")]
43 Query {
44 #[serde(default)]
46 description: Option<String>,
47 #[serde(default)]
49 parameters: Option<LexiconObject>,
50 #[serde(default)]
52 output: Option<LexiconOutput>,
53 },
54 #[serde(rename = "procedure")]
56 Procedure {
57 #[serde(default)]
59 description: Option<String>,
60 #[serde(default)]
62 input: Option<LexiconBody>,
63 #[serde(default)]
65 output: Option<LexiconOutput>,
66 },
67 #[serde(rename = "string")]
69 String {
70 #[serde(default)]
72 description: Option<String>,
73 #[serde(default)]
75 known_values: Option<Vec<String>>,
76 },
77 #[serde(rename = "token")]
79 Token {
80 #[serde(default)]
82 description: Option<String>,
83 },
84 #[serde(other)]
86 Unknown,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LexiconObject {
92 #[serde(default)]
94 pub description: Option<String>,
95 #[serde(default)]
97 pub required: Vec<String>,
98 #[serde(default)]
100 pub properties: HashMap<String, LexiconProperty>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LexiconProperty {
106 #[serde(rename = "type")]
108 pub prop_type: String,
109 #[serde(default)]
111 pub description: Option<String>,
112 #[serde(default)]
114 pub format: Option<String>,
115 #[serde(default)]
117 pub max_length: Option<u64>,
118 #[serde(default)]
120 pub minimum: Option<i64>,
121 #[serde(default)]
123 pub maximum: Option<i64>,
124 #[serde(default)]
126 pub default: Option<serde_json::Value>,
127 #[serde(default)]
129 pub items: Option<Box<LexiconProperty>>,
130 #[serde(rename = "ref", default)]
132 pub ref_: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct LexiconOutput {
138 #[serde(default)]
140 pub encoding: Option<String>,
141 #[serde(default)]
143 pub schema: Option<LexiconObject>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct LexiconBody {
149 #[serde(default)]
151 pub encoding: Option<String>,
152 #[serde(default)]
154 pub schema: Option<LexiconObject>,
155}
156
157pub 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}