Skip to main content

sim_codec_json/
grammar.rs

1//! JSON codec grammar rendering over the neutral Shape grammar graph.
2
3use 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/// Renders neutral Shape grammars for `codec:json`.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct JsonGrammarRenderer {
14    dialect: GrammarDialect,
15}
16
17impl JsonGrammarRenderer {
18    /// Builds a renderer for `dialect`.
19    pub fn new(dialect: GrammarDialect) -> Self {
20        Self { dialect }
21    }
22
23    /// Builds a JSON Schema renderer.
24    pub fn json_schema() -> Self {
25        Self::new(GrammarDialect::JsonSchema)
26    }
27
28    /// Builds a JSON-shaped GBNF renderer.
29    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}