Skip to main content

sim_codec_json/
helpers.rs

1//! Shared helpers for the JSON projections.
2//!
3//! Symbol/quote-mode conversion and typed field accessors (required/optional
4//! string, bool, array, and base64 bytes) used by the canonical and tree
5//! projections.
6
7use std::fmt::Write as _;
8
9use serde_json::{Map, Value as JsonValue};
10use sim_kernel::{Error, Expr, QuoteMode, Result, Symbol};
11
12pub(crate) use sim_codec_binary_base64::{
13    decode_base64 as base64_decode, encode_base64 as base64_encode,
14};
15
16/// Escapes a string for use inside a JSON string literal.
17///
18/// The returned text does not include the surrounding quote characters.
19pub fn json_escape(input: &str) -> String {
20    let mut out = String::with_capacity(input.len());
21    for ch in input.chars() {
22        match ch {
23            '"' => out.push_str("\\\""),
24            '\\' => out.push_str("\\\\"),
25            '\n' => out.push_str("\\n"),
26            '\r' => out.push_str("\\r"),
27            '\t' => out.push_str("\\t"),
28            '\u{08}' => out.push_str("\\b"),
29            '\u{0c}' => out.push_str("\\f"),
30            '\u{00}'..='\u{1f}' => {
31                write!(&mut out, "\\u{:04x}", ch as u32).expect("string writes do not fail");
32            }
33            other => out.push(other),
34        }
35    }
36    out
37}
38
39pub(crate) fn symbol_to_json(symbol: &Symbol) -> JsonValue {
40    let mut object = Map::new();
41    object.insert("$expr".to_owned(), JsonValue::String("symbol".to_owned()));
42    if let Some(namespace) = &symbol.namespace {
43        object.insert(
44            "namespace".to_owned(),
45            JsonValue::String(namespace.to_string()),
46        );
47    }
48    object.insert(
49        "name".to_owned(),
50        JsonValue::String(symbol.name.to_string()),
51    );
52    JsonValue::Object(object)
53}
54
55pub(crate) fn json_to_symbol(codec: sim_kernel::CodecId, value: &JsonValue) -> Result<Expr> {
56    Ok(Expr::Symbol(symbol_from_json(codec, value)?))
57}
58
59/// Decodes a symbol carried in operator/tag/key/local position.
60///
61/// Accepts the structured `{ "namespace": ..., "name": ... }` form (the same
62/// shape [`symbol_to_json`] emits) so that symbols containing `/` -- the
63/// division operator, for one -- round-trip exactly. A bare JSON string is also
64/// accepted and parsed with [`parse_symbol`] for backward compatibility with
65/// the legacy `to_string()`-flattened encoding.
66pub(crate) fn symbol_from_json(codec: sim_kernel::CodecId, value: &JsonValue) -> Result<Symbol> {
67    if let Some(text) = value.as_str() {
68        return Ok(parse_symbol(text));
69    }
70    let name = required_str(codec, value, "name")?;
71    let namespace = optional_str(codec, value, "namespace")?;
72    Ok(match namespace {
73        Some(namespace) => Symbol::qualified(namespace.to_owned(), name.to_owned()),
74        None => Symbol::new(name.to_owned()),
75    })
76}
77
78/// Encodes an [`Expr::Local`] symbol as a `$expr: "local"` object that carries
79/// the namespace and name as structured fields, so locals named with reserved
80/// characters round-trip exactly. A namespace-less local encodes identically to
81/// the legacy `{ "$expr": "local", "name": ... }` form.
82pub(crate) fn local_to_json(symbol: &Symbol) -> JsonValue {
83    let mut object = Map::new();
84    object.insert("$expr".to_owned(), JsonValue::String("local".to_owned()));
85    if let Some(namespace) = &symbol.namespace {
86        object.insert(
87            "namespace".to_owned(),
88            JsonValue::String(namespace.to_string()),
89        );
90    }
91    object.insert(
92        "name".to_owned(),
93        JsonValue::String(symbol.name.to_string()),
94    );
95    JsonValue::Object(object)
96}
97
98pub(crate) fn quote_mode_name(mode: QuoteMode) -> &'static str {
99    match mode {
100        QuoteMode::Quote => "quote",
101        QuoteMode::QuasiQuote => "quasiquote",
102        QuoteMode::Unquote => "unquote",
103        QuoteMode::Splice => "splice",
104        QuoteMode::Syntax => "syntax",
105    }
106}
107
108pub(crate) fn parse_quote_mode(codec: sim_kernel::CodecId, raw: &str) -> Result<QuoteMode> {
109    match raw {
110        "quote" => Ok(QuoteMode::Quote),
111        "quasiquote" => Ok(QuoteMode::QuasiQuote),
112        "unquote" => Ok(QuoteMode::Unquote),
113        "splice" => Ok(QuoteMode::Splice),
114        "syntax" => Ok(QuoteMode::Syntax),
115        other => Err(json_error(codec, format!("unknown quote mode {other}"))),
116    }
117}
118
119pub(crate) fn required_value<'a>(
120    codec: sim_kernel::CodecId,
121    value: &'a JsonValue,
122    field: &str,
123) -> Result<&'a JsonValue> {
124    value
125        .get(field)
126        .ok_or_else(|| json_error(codec, format!("missing field {field}")))
127}
128
129pub(crate) fn required_str<'a>(
130    codec: sim_kernel::CodecId,
131    value: &'a JsonValue,
132    field: &str,
133) -> Result<&'a str> {
134    required_value(codec, value, field)?
135        .as_str()
136        .ok_or_else(|| json_error(codec, format!("field {field} must be a string")))
137}
138
139pub(crate) fn optional_str<'a>(
140    codec: sim_kernel::CodecId,
141    value: &'a JsonValue,
142    field: &str,
143) -> Result<Option<&'a str>> {
144    match value.get(field) {
145        None | Some(JsonValue::Null) => Ok(None),
146        Some(other) => other
147            .as_str()
148            .map(Some)
149            .ok_or_else(|| json_error(codec, format!("field {field} must be a string"))),
150    }
151}
152
153pub(crate) fn required_bool(
154    codec: sim_kernel::CodecId,
155    value: &JsonValue,
156    field: &str,
157) -> Result<bool> {
158    required_value(codec, value, field)?
159        .as_bool()
160        .ok_or_else(|| json_error(codec, format!("field {field} must be a bool")))
161}
162
163pub(crate) fn required_u64(
164    codec: sim_kernel::CodecId,
165    value: &JsonValue,
166    field: &str,
167) -> Result<u64> {
168    required_value(codec, value, field)?
169        .as_u64()
170        .ok_or_else(|| json_error(codec, format!("field {field} must be a u64")))
171}
172
173pub(crate) fn required_array<'a>(
174    codec: sim_kernel::CodecId,
175    value: &'a JsonValue,
176    field: &str,
177) -> Result<&'a Vec<JsonValue>> {
178    required_value(codec, value, field)?
179        .as_array()
180        .ok_or_else(|| json_error(codec, format!("field {field} must be an array")))
181}
182
183pub(crate) fn optional_bytes(
184    codec: sim_kernel::CodecId,
185    value: &JsonValue,
186    field: &str,
187) -> Result<Option<Vec<u8>>> {
188    match value.get(field) {
189        None | Some(JsonValue::Null) => Ok(None),
190        Some(other) => Ok(Some(
191            serde_json::from_value::<Vec<u8>>(other.clone())
192                .map_err(|err| json_error(codec, err.to_string()))?,
193        )),
194    }
195}
196
197pub(crate) fn parse_symbol(raw: &str) -> Symbol {
198    match raw.split_once('/') {
199        Some((namespace, name)) => Symbol::qualified(namespace.to_owned(), name.to_owned()),
200        None => Symbol::new(raw.to_owned()),
201    }
202}
203
204pub(crate) fn json_error(codec: sim_kernel::CodecId, message: impl Into<String>) -> Error {
205    Error::CodecError {
206        codec,
207        message: message.into(),
208    }
209}