1use serde_json::{Map, Value as JsonValue};
4use sim_kernel::{Error, Expr, Result, Symbol};
5use sim_shape::{
6 GrammarDialect, GrammarGraph, GrammarPosition, GrammarRenderer, Production, TerminalAtom,
7};
8
9use crate::{expr_to_json, json_escape};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct JsonGrammarRenderer {
14 dialect: GrammarDialect,
15}
16
17impl JsonGrammarRenderer {
18 pub fn new(dialect: GrammarDialect) -> Self {
20 Self { dialect }
21 }
22
23 pub fn json_schema() -> Self {
25 Self::new(GrammarDialect::JsonSchema)
26 }
27
28 pub fn gbnf() -> Self {
30 Self::new(GrammarDialect::Gbnf)
31 }
32}
33
34impl GrammarRenderer for JsonGrammarRenderer {
35 fn codec_symbol(&self) -> Symbol {
36 Symbol::qualified("codec", "json")
37 }
38
39 fn dialect(&self) -> GrammarDialect {
40 self.dialect
41 }
42
43 fn render(&self, graph: &GrammarGraph, position: GrammarPosition) -> Result<String> {
44 match self.dialect {
45 GrammarDialect::JsonSchema => render_json_schema_graph(graph, position),
46 GrammarDialect::Gbnf => render_json_gbnf_graph(graph, position),
47 unsupported => Err(grammar_error(format!(
48 "codec/json does not support {unsupported:?} grammar dialect"
49 ))),
50 }
51 }
52}
53
54fn render_json_schema_graph(graph: &GrammarGraph, position: GrammarPosition) -> Result<String> {
55 let mut root = match render_json_schema(&graph.root)? {
56 JsonValue::Object(object) => object,
57 scalar => {
58 let mut object = Map::new();
59 object.insert("allOf".to_owned(), JsonValue::Array(vec![scalar]));
60 object
61 }
62 };
63 root.insert(
64 "$comment".to_owned(),
65 JsonValue::String(format!(
66 "codec/json position={} target={}",
67 position_name(position),
68 json_decode_target(position)
69 )),
70 );
71 if !graph.defs.is_empty() {
72 let mut defs = Map::new();
73 for (name, production) in &graph.defs {
74 defs.insert(name.to_string(), render_json_schema(production)?);
75 }
76 root.insert("$defs".to_owned(), JsonValue::Object(defs));
77 }
78 serde_json::to_string(&JsonValue::Object(root)).map_err(|err| grammar_error(err.to_string()))
79}
80
81fn render_json_schema(production: &Production) -> Result<JsonValue> {
82 match production {
83 Production::Terminal(atom) => render_json_terminal(atom),
84 Production::Seq(items) => render_json_seq(items),
85 Production::Alt(choices) => {
86 if choices.len() == 1
87 && matches!(
88 choices.first(),
89 Some(Production::Terminal(TerminalAtom::Any))
90 )
91 {
92 return Ok(JsonValue::Bool(true));
93 }
94 object_with_array("anyOf", choices.iter().map(render_json_schema))
95 }
96 Production::Repeat { inner, at_least } => {
97 let mut object = typed_schema("array");
98 object.insert("items".to_owned(), render_json_schema(inner)?);
99 if *at_least > 0 {
100 object.insert(
101 "minItems".to_owned(),
102 JsonValue::Number(serde_json::Number::from(*at_least)),
103 );
104 }
105 Ok(JsonValue::Object(object))
106 }
107 Production::Call { head, args } => render_json_call(head, args),
108 Production::Ref(name) => {
109 let mut object = Map::new();
110 object.insert(
111 "$ref".to_owned(),
112 JsonValue::String(format!(
113 "#/$defs/{}",
114 json_pointer_escape(&name.to_string())
115 )),
116 );
117 Ok(JsonValue::Object(object))
118 }
119 }
120}
121
122fn render_json_terminal(atom: &TerminalAtom) -> Result<JsonValue> {
123 Ok(match atom {
124 TerminalAtom::Any => JsonValue::Bool(true),
125 TerminalAtom::Nil => JsonValue::Object(typed_schema("null")),
126 TerminalAtom::Bool => JsonValue::Object(typed_schema("boolean")),
127 TerminalAtom::Number => JsonValue::Object(typed_schema("number")),
128 TerminalAtom::String => JsonValue::Object(typed_schema("string")),
129 TerminalAtom::List => JsonValue::Object(typed_schema("array")),
130 TerminalAtom::Map => JsonValue::Object(typed_schema("object")),
131 TerminalAtom::Symbol => {
132 let mut object = typed_schema("string");
133 object.insert(
134 "description".to_owned(),
135 JsonValue::String("symbol".to_owned()),
136 );
137 JsonValue::Object(object)
138 }
139 TerminalAtom::Exact(expr) => {
140 let mut object = Map::new();
141 object.insert("const".to_owned(), expr_to_json(expr));
142 JsonValue::Object(object)
143 }
144 })
145}
146
147fn render_json_seq(items: &[Production]) -> Result<JsonValue> {
148 let (prefix, rest) = match items.split_last() {
149 Some((Production::Repeat { inner, at_least: 0 }, prefix)) => (prefix, Some(inner)),
150 _ => (items, None),
151 };
152 let mut object = typed_schema("array");
153 object.insert(
154 "prefixItems".to_owned(),
155 JsonValue::Array(
156 prefix
157 .iter()
158 .map(render_json_schema)
159 .collect::<Result<Vec<_>>>()?,
160 ),
161 );
162 object.insert(
163 "items".to_owned(),
164 match rest {
165 Some(rest) => render_json_schema(rest)?,
166 None => JsonValue::Bool(false),
167 },
168 );
169 if rest.is_none() {
170 object.insert(
171 "minItems".to_owned(),
172 JsonValue::Number(serde_json::Number::from(prefix.len())),
173 );
174 object.insert(
175 "maxItems".to_owned(),
176 JsonValue::Number(serde_json::Number::from(prefix.len())),
177 );
178 }
179 Ok(JsonValue::Object(object))
180}
181
182fn render_json_call(head: &Production, args: &[Production]) -> Result<JsonValue> {
183 let mut properties = Map::new();
184 let mut required = Vec::new();
185 for (index, arg) in args.iter().enumerate() {
186 let (name, value) = call_arg_schema(index, arg)?;
187 required.push(JsonValue::String(name.clone()));
188 properties.insert(name, value);
189 }
190 let mut object = typed_schema("object");
191 object.insert("properties".to_owned(), JsonValue::Object(properties));
192 object.insert("required".to_owned(), JsonValue::Array(required));
193 object.insert("additionalProperties".to_owned(), JsonValue::Bool(false));
194 if let Some(head) = exact_symbol(head) {
195 object.insert(
196 "$comment".to_owned(),
197 JsonValue::String(format!("call {}", head)),
198 );
199 }
200 Ok(JsonValue::Object(object))
201}
202
203fn call_arg_schema(index: usize, arg: &Production) -> Result<(String, JsonValue)> {
204 if let Production::Seq(parts) = arg
205 && let [
206 Production::Terminal(TerminalAtom::Exact(Expr::Symbol(name))),
207 value,
208 ] = parts.as_slice()
209 {
210 return Ok((name.name.to_string(), render_json_schema(value)?));
211 }
212 Ok((format!("arg{index}"), render_json_schema(arg)?))
213}
214
215fn render_json_gbnf_graph(graph: &GrammarGraph, position: GrammarPosition) -> Result<String> {
216 let mut lines = vec![
217 format!(
218 "# codec/json position={} target={}",
219 position_name(position),
220 json_decode_target(position)
221 ),
222 format!("root ::= {}", render_json_gbnf(&graph.root)?),
223 ];
224 for (name, production) in &graph.defs {
225 lines.push(format!(
226 "{} ::= {}",
227 rule_name(name),
228 render_json_gbnf(production)?
229 ));
230 }
231 Ok(lines.join("\n"))
232}
233
234fn render_json_gbnf(production: &Production) -> Result<String> {
235 match production {
236 Production::Terminal(atom) => render_json_gbnf_terminal(atom),
237 Production::Seq(items) => {
238 let rendered = items
239 .iter()
240 .map(render_json_gbnf)
241 .collect::<Result<Vec<_>>>()?;
242 Ok(format!("[ {} ]", rendered.join(" \",\" ")))
243 }
244 Production::Alt(choices) => {
245 let rendered = choices
246 .iter()
247 .map(render_json_gbnf)
248 .collect::<Result<Vec<_>>>()?;
249 Ok(format!("({})", rendered.join(" | ")))
250 }
251 Production::Repeat { inner, .. } => Ok(format!("({})*", render_json_gbnf(inner)?)),
252 Production::Call { head: _, args } => {
253 let fields = args
254 .iter()
255 .enumerate()
256 .map(|(index, arg)| {
257 let (name, value) = call_arg_gbnf(index, arg)?;
258 Ok(format!("{} \":\" {}", gbnf_literal(&name), value))
259 })
260 .collect::<Result<Vec<_>>>()?;
261 Ok(format!("{{ {} }}", fields.join(" \",\" ")))
262 }
263 Production::Ref(name) => Ok(rule_name(name)),
264 }
265}
266
267fn call_arg_gbnf(index: usize, arg: &Production) -> Result<(String, String)> {
268 if let Production::Seq(parts) = arg
269 && let [
270 Production::Terminal(TerminalAtom::Exact(Expr::Symbol(name))),
271 value,
272 ] = parts.as_slice()
273 {
274 return Ok((name.name.to_string(), render_json_gbnf(value)?));
275 }
276 Ok((format!("arg{index}"), render_json_gbnf(arg)?))
277}
278
279fn render_json_gbnf_terminal(atom: &TerminalAtom) -> Result<String> {
280 Ok(match atom {
281 TerminalAtom::Any => "json-value".to_owned(),
282 TerminalAtom::Nil => "\"null\"".to_owned(),
283 TerminalAtom::Bool => "(\"true\" | \"false\")".to_owned(),
284 TerminalAtom::Number => "json-number".to_owned(),
285 TerminalAtom::String | TerminalAtom::Symbol => "json-string".to_owned(),
286 TerminalAtom::List => "json-array".to_owned(),
287 TerminalAtom::Map => "json-object".to_owned(),
288 TerminalAtom::Exact(expr) => {
289 let text = serde_json::to_string(&expr_to_json(expr))
290 .map_err(|err| grammar_error(err.to_string()))?;
291 gbnf_literal(&text)
292 }
293 })
294}
295
296fn exact_symbol(production: &Production) -> Option<&Symbol> {
297 let Production::Terminal(TerminalAtom::Exact(Expr::Symbol(symbol))) = production else {
298 return None;
299 };
300 Some(symbol)
301}
302
303fn object_with_array(
304 key: &str,
305 values: impl Iterator<Item = Result<JsonValue>>,
306) -> Result<JsonValue> {
307 let mut object = Map::new();
308 object.insert(
309 key.to_owned(),
310 JsonValue::Array(values.collect::<Result<Vec<_>>>()?),
311 );
312 Ok(JsonValue::Object(object))
313}
314
315fn typed_schema(kind: &str) -> Map<String, JsonValue> {
316 let mut object = Map::new();
317 object.insert("type".to_owned(), JsonValue::String(kind.to_owned()));
318 object
319}
320
321fn json_pointer_escape(text: &str) -> String {
322 text.replace('~', "~0").replace('/', "~1")
323}
324
325fn gbnf_literal(text: &str) -> String {
326 format!("\"{}\"", json_escape(text))
327}
328
329fn rule_name(symbol: &Symbol) -> String {
330 let mut out = String::new();
331 for ch in symbol.to_string().chars() {
332 if ch.is_ascii_alphanumeric() || ch == '-' {
333 out.push(ch);
334 } else {
335 out.push('-');
336 }
337 }
338 if out
339 .chars()
340 .next()
341 .is_none_or(|ch| !ch.is_ascii_alphabetic())
342 {
343 out.insert_str(0, "r-");
344 }
345 out
346}
347
348fn position_name(position: GrammarPosition) -> &'static str {
349 match position {
350 GrammarPosition::Eval => "eval",
351 GrammarPosition::Quote => "quote",
352 GrammarPosition::Data => "data",
353 GrammarPosition::Pattern => "pattern",
354 GrammarPosition::Surface => "surface",
355 }
356}
357
358fn json_decode_target(_position: GrammarPosition) -> &'static str {
359 "datum"
360}
361
362fn grammar_error(message: impl Into<String>) -> Error {
363 Error::Eval(format!("codec/json grammar renderer: {}", message.into()))
364}
365
366#[cfg(test)]
367mod tests {
368 use std::sync::Arc;
369
370 use sim_codec_lisp::LispGrammarRenderer;
371 use sim_kernel::Symbol;
372 use sim_shape::{
373 ExprKind, ExprKindShape, FieldShape, FieldSpec, GrammarDialect, GrammarPosition,
374 GrammarTarget, OneOfShape, Shape, ShapeDefRef, ShapeDefs, shape_grammar,
375 };
376
377 use super::JsonGrammarRenderer;
378
379 #[test]
380 fn json_and_lisp_renderers_share_fields_and_refs_but_not_text() {
381 let shape = recursive_node_shape();
382 let json = shape_grammar(
383 shape.as_ref(),
384 GrammarTarget {
385 codec: Symbol::qualified("codec", "json"),
386 dialect: GrammarDialect::JsonSchema,
387 position: GrammarPosition::Data,
388 },
389 &JsonGrammarRenderer::json_schema(),
390 )
391 .unwrap();
392 let lisp = shape_grammar(
393 shape.as_ref(),
394 GrammarTarget {
395 codec: Symbol::qualified("codec", "lisp"),
396 dialect: GrammarDialect::SExpr,
397 position: GrammarPosition::Data,
398 },
399 &LispGrammarRenderer::sexpr(),
400 )
401 .unwrap();
402
403 assert_ne!(json.text, lisp.text);
404 for token in ["name", "next", "Node"] {
405 assert!(json.text.contains(token), "missing {token} in JSON grammar");
406 assert!(lisp.text.contains(token), "missing {token} in Lisp grammar");
407 }
408 assert!(json.text.contains(r##""$ref":"#/$defs/Node""##));
409 assert!(lisp.text.contains("(ref Node)"));
410 }
411
412 #[test]
413 fn json_gbnf_uses_named_rules_for_refs() {
414 let shape = recursive_node_shape();
415 let grammar = shape_grammar(
416 shape.as_ref(),
417 GrammarTarget {
418 codec: Symbol::qualified("codec", "json"),
419 dialect: GrammarDialect::Gbnf,
420 position: GrammarPosition::Eval,
421 },
422 &JsonGrammarRenderer::gbnf(),
423 )
424 .unwrap();
425
426 assert!(grammar.text.contains("target=datum"));
427 assert!(grammar.text.contains("Node ::="));
428 assert!(grammar.text.contains("Node"));
429 assert!(grammar.text.contains("\"name\""));
430 }
431
432 #[test]
433 fn json_renderer_rejects_unsupported_dialect() {
434 let err = shape_grammar(
435 recursive_node_shape().as_ref(),
436 GrammarTarget {
437 codec: Symbol::qualified("codec", "json"),
438 dialect: GrammarDialect::SExpr,
439 position: GrammarPosition::Data,
440 },
441 &JsonGrammarRenderer::new(GrammarDialect::SExpr),
442 )
443 .unwrap_err();
444
445 assert!(err.to_string().contains("does not support SExpr"));
446 }
447
448 fn recursive_node_shape() -> Arc<dyn Shape> {
449 let node = Symbol::new("Node");
450 Arc::new(ShapeDefs::new(
451 Arc::new(ShapeDefRef::new(node.clone())),
452 vec![(
453 node.clone(),
454 Arc::new(FieldShape::anonymous(vec![
455 FieldSpec::required(
456 Symbol::new("name"),
457 Arc::new(ExprKindShape::new(ExprKind::String)),
458 ),
459 FieldSpec::required(
460 Symbol::new("next"),
461 Arc::new(OneOfShape::new(vec![
462 Arc::new(ExprKindShape::new(ExprKind::Nil)),
463 Arc::new(ShapeDefRef::new(node)),
464 ])),
465 ),
466 ])),
467 )],
468 ))
469 }
470}