1use serde_json::{Map, Value, json};
7
8use crate::params::{json_type, unknown_keys_error};
9
10const MAX_DOC: usize = 8192;
12
13#[derive(Default)]
15struct Scan {
16 depth: i32,
17 in_str: bool,
18 escaped: bool,
19}
20
21impl Scan {
22 fn feed(&mut self, c: char) -> bool {
24 if self.in_str {
25 if self.escaped {
26 self.escaped = false;
27 } else if c == '\\' {
28 self.escaped = true;
29 } else if c == '"' {
30 self.in_str = false;
31 }
32 return false;
33 }
34 if c == '"' {
35 self.in_str = true;
36 return false;
37 }
38 true
39 }
40}
41
42fn pieces(doc: &str) -> Option<Vec<&str>> {
45 if doc.len() > MAX_DOC {
46 return None;
47 }
48 let body = doc.trim().strip_prefix('{')?;
49 let mut out = Vec::new();
50 let mut scan = Scan::default();
51 let mut start = 0;
52 fn push<'a>(s: Option<&'a str>, out: &mut Vec<&'a str>) {
53 if let Some(s) = s.map(str::trim).filter(|s| !s.is_empty()) {
54 out.push(s);
55 }
56 }
57 for (i, c) in body.char_indices() {
58 if !scan.feed(c) {
59 continue;
60 }
61 match c {
62 '{' | '[' | '(' => scan.depth += 1,
63 ']' | ')' => scan.depth -= 1,
64 '}' if scan.depth == 0 => {
65 push(body.get(start..i), &mut out);
66 return Some(out);
67 }
68 '}' => scan.depth -= 1,
69 ',' if scan.depth == 0 => {
70 push(body.get(start..i), &mut out);
71 start = i + c.len_utf8();
72 }
73 _ => {}
74 }
75 }
76 None
77}
78
79fn key_and_value(piece: &str) -> (&str, Option<&str>) {
81 let mut scan = Scan::default();
82 for (i, c) in piece.char_indices() {
83 if !scan.feed(c) {
84 continue;
85 }
86 match c {
87 '{' | '[' | '(' => scan.depth += 1,
88 '}' | ']' | ')' => scan.depth -= 1,
89 ':' if scan.depth == 0 => return (piece.get(..i).unwrap_or_default(), piece.get(i + 1..)),
90 _ => {}
91 }
92 }
93 (piece, None)
94}
95
96fn piece_keys(key_part: &str) -> Vec<String> {
98 let mut keys = Vec::new();
99 let mut depth = 0i32;
100 let mut start = 0;
101 let take = |s: &str, keys: &mut Vec<String>| {
102 let k: String = s.trim().trim_start_matches('"').chars().take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '-').collect();
103 if !k.is_empty() && !keys.contains(&k) {
104 keys.push(k);
105 }
106 };
107 for (i, c) in key_part.char_indices() {
108 match c {
109 '{' | '[' | '(' => depth += 1,
110 '}' | ']' | ')' => depth -= 1,
111 '|' if depth == 0 => {
112 take(key_part.get(start..i).unwrap_or_default(), &mut keys);
113 start = i + 1;
114 }
115 _ => {}
116 }
117 }
118 take(key_part.get(start..).unwrap_or_default(), &mut keys);
119 keys
120}
121
122pub fn accepted_params(doc: &str) -> Option<Vec<String>> {
125 let mut keys: Vec<String> = Vec::new();
126 for piece in pieces(doc)? {
127 for k in piece_keys(key_and_value(piece).0) {
128 if !keys.contains(&k) {
129 keys.push(k);
130 }
131 }
132 }
133 Some(keys)
134}
135
136fn outside_strings(piece: &str) -> String {
138 let mut scan = Scan::default();
139 piece.chars().filter(|c| scan.feed(*c) && *c != '"').collect()
140}
141
142fn value_schema(value: &str) -> Map<String, Value> {
145 let mut m = Map::new();
146 let v = value.trim().trim_end_matches('?').trim();
147 let v = v.split('=').next().unwrap_or_default().trim();
148 if v.starts_with('[') {
149 m.insert("type".into(), "array".into());
150 } else if v.starts_with('{') {
151 m.insert("type".into(), "object".into());
152 } else if let Some(inner) = v.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
153 m.insert("type".into(), "string".into());
154 if inner.contains('|') && !inner.contains('…') && !inner.contains("...") {
155 let values: Vec<&str> = inner.split('|').map(str::trim).filter(|s| !s.is_empty()).collect();
156 m.insert("enum".into(), values.into());
157 }
158 } else {
159 let t = match v.to_ascii_lowercase().as_str() {
160 "str" | "string" | "text" | "name" | "path" => Some("string"),
161 "bool" | "boolean" => Some("boolean"),
162 "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" | "int" | "integer" | "n" => Some("integer"),
163 "f32" | "f64" | "float" | "number" | "num" => Some("number"),
164 _ => None,
165 };
166 if let Some(t) = t {
167 m.insert("type".into(), t.into());
168 }
169 }
170 m
171}
172
173pub(crate) fn schema(doc: &str) -> Value {
176 let Some(pieces) = pieces(doc) else {
177 return json!({"type": "object", "description": doc});
178 };
179 let mut props = Map::new();
180 let mut required: Vec<String> = Vec::new();
181 for piece in pieces {
182 let (key_part, value) = key_and_value(piece);
183 let keys = piece_keys(key_part);
184 let markers = outside_strings(piece);
185 let optional = keys.len() > 1 || markers.contains('?') || markers.contains('=');
187 for k in keys {
188 if !optional && !required.contains(&k) {
189 required.push(k.clone());
190 }
191 let mut p = value.map(value_schema).unwrap_or_default();
192 p.insert("description".into(), piece.into());
193 props.insert(k, Value::Object(p));
194 }
195 }
196 json!({"type": "object", "description": doc, "properties": props, "required": required, "additionalProperties": false})
197}
198
199pub(crate) fn check(doc: &str, value: &Value, common: &[&str], aliases: &[(&str, &str)]) -> Result<(), String> {
201 let Some(accepted) = accepted_params(doc) else { return Ok(()) };
202 let obj = match value {
203 Value::Null => return Ok(()),
204 Value::Object(m) => m,
205 other => return Err(format!("expected an object of parameters, got {}", json_type(other))),
206 };
207 let ok = |k: &str| common.contains(&k) || accepted.iter().any(|a| a == k || aliases.iter().any(|(doc, alias)| doc == a && *alias == k));
208 let unknown: Vec<&str> = obj.keys().map(String::as_str).filter(|k| !ok(k)).collect();
209 if unknown.is_empty() {
210 return Ok(());
211 }
212 let accepted: Vec<&str> = accepted.iter().map(String::as_str).collect();
213 Err(format!("{} (doc: {doc})", unknown_keys_error(&unknown, &accepted)))
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn keys_of_both_doc_styles() {
222 let film = r#"{"name":str,"width":u32=1920,"fps":f64=23.976,"mix":"Stereo|Mono|5.1|Adaptive"?,"fromItem":itemId?}"#;
223 assert_eq!(accepted_params(film).unwrap(), ["name", "width", "fps", "mix", "fromItem"]);
224 assert_eq!(accepted_params("{layers: [id|name|#n], add?, toggle?}").unwrap(), ["layers", "add", "toggle"]);
225 assert_eq!(accepted_params("{time? (s) | frame? | timecode?}").unwrap(), ["time", "frame", "timecode"]);
226 assert_eq!(accepted_params("{}").unwrap(), Vec::<String>::new());
227 assert_eq!(accepted_params(r#"{"label":"Violet|Iris|…"}"#).unwrap(), ["label"]);
228 assert_eq!(accepted_params(r#"{"sep":", or }","x":n}"#).unwrap(), ["sep", "x"]);
230 assert_eq!(accepted_params("free text, no braces"), None);
231 assert_eq!(accepted_params("{unterminated"), None);
232 }
233
234 #[test]
235 fn schema_types_and_required() {
236 let s = schema(r#"{"name":str,"width":u32=1920,"mix":"Stereo|Mono"?,"clips":[id]?}"#);
237 assert_eq!(s["required"], json!(["name"]));
238 assert_eq!(s["properties"]["name"]["type"], "string");
239 assert_eq!(s["properties"]["width"]["type"], "integer");
240 assert_eq!(s["properties"]["mix"]["enum"], json!(["Stereo", "Mono"]));
241 assert_eq!(s["properties"]["clips"]["type"], "array");
242 assert_eq!(s["additionalProperties"], false);
243 }
244}