use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LexiconDoc {
pub lexicon: u32,
pub id: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub defs: HashMap<String, LexiconDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum LexiconDef {
#[serde(rename = "record")]
Record {
#[serde(default)]
description: Option<String>,
#[serde(default)]
key: Option<String>,
#[serde(default)]
record: Option<LexiconObject>,
},
#[serde(rename = "object")]
Object(LexiconObject),
#[serde(rename = "query")]
Query {
#[serde(default)]
description: Option<String>,
#[serde(default)]
parameters: Option<LexiconObject>,
#[serde(default)]
output: Option<LexiconOutput>,
},
#[serde(rename = "procedure")]
Procedure {
#[serde(default)]
description: Option<String>,
#[serde(default)]
input: Option<LexiconBody>,
#[serde(default)]
output: Option<LexiconOutput>,
},
#[serde(rename = "string")]
String {
#[serde(default)]
description: Option<String>,
#[serde(default)]
known_values: Option<Vec<String>>,
},
#[serde(rename = "token")]
Token {
#[serde(default)]
description: Option<String>,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LexiconObject {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub required: Vec<String>,
#[serde(default)]
pub properties: HashMap<String, LexiconProperty>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LexiconProperty {
#[serde(rename = "type")]
pub prop_type: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub format: Option<String>,
#[serde(default)]
pub max_length: Option<u64>,
#[serde(default)]
pub minimum: Option<i64>,
#[serde(default)]
pub maximum: Option<i64>,
#[serde(default)]
pub default: Option<serde_json::Value>,
#[serde(default)]
pub items: Option<Box<LexiconProperty>>,
#[serde(rename = "ref", default)]
pub ref_: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LexiconOutput {
#[serde(default)]
pub encoding: Option<String>,
#[serde(default)]
pub schema: Option<LexiconObject>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LexiconBody {
#[serde(default)]
pub encoding: Option<String>,
#[serde(default)]
pub schema: Option<LexiconObject>,
}
pub fn parse_lexicon(json: &str) -> anyhow::Result<LexiconDoc> {
let doc: LexiconDoc = serde_json::from_str(json)?;
if doc.lexicon != 1 {
anyhow::bail!("unsupported lexicon version: {} (expected 1)", doc.lexicon);
}
if doc.id.is_empty() {
anyhow::bail!("lexicon id must not be empty");
}
Ok(doc)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal_lexicon() {
let json = r#"{
"lexicon": 1,
"id": "com.example.ping",
"defs": {
"main": {
"type": "query",
"description": "A simple ping",
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"properties": {
"pong": { "type": "boolean" }
}
}
}
}
}
}"#;
let doc = parse_lexicon(json).unwrap();
assert_eq!(doc.id, "com.example.ping");
assert!(doc.defs.contains_key("main"));
}
#[test]
fn parse_record_lexicon() {
let json = r#"{
"lexicon": 1,
"id": "com.example.post",
"defs": {
"main": {
"type": "record",
"description": "A post record",
"key": "tid",
"record": {
"type": "object",
"required": ["text", "createdAt"],
"properties": {
"text": { "type": "string", "max_length": 3000 },
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
}
}"#;
let doc = parse_lexicon(json).unwrap();
assert_eq!(doc.id, "com.example.post");
}
#[test]
fn invalid_version_rejected() {
let json = r#"{"lexicon": 2, "id": "com.example.test", "defs": {}}"#;
assert!(parse_lexicon(json).is_err());
}
#[test]
fn empty_id_rejected() {
let json = r#"{"lexicon": 1, "id": "", "defs": {}}"#;
assert!(parse_lexicon(json).is_err());
}
#[test]
fn malformed_json_gives_error() {
assert!(parse_lexicon("not json").is_err());
}
}