#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::{
format,
string::{String, ToString},
};
use minijinja::{Error, ErrorKind, Value};
pub fn sorted_tojson(value: Value) -> Result<String, Error> {
let tree: serde_json::Value = serde_json::to_value(&value).map_err(|error| {
Error::new(
ErrorKind::InvalidOperation,
format!("tojson: value is not JSON-serializable: {error}"),
)
})?;
let mut out = String::new();
write_value(&tree, &mut out);
Ok(out)
}
fn write_value(value: &serde_json::Value, out: &mut String) {
match value {
serde_json::Value::Null => out.push_str("null"),
serde_json::Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
serde_json::Value::Number(n) => out.push_str(&n.to_string()),
serde_json::Value::String(s) => write_json_string(s, out),
serde_json::Value::Array(items) => write_array(items, out),
serde_json::Value::Object(map) => write_object(map, out),
}
}
fn write_array(items: &[serde_json::Value], out: &mut String) {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i != 0 {
out.push_str(", ");
}
write_value(item, out);
}
out.push(']');
}
fn write_object(map: &serde_json::Map<String, serde_json::Value>, out: &mut String) {
out.push('{');
for (i, (key, val)) in map.iter().enumerate() {
if i != 0 {
out.push_str(", ");
}
write_json_string(key, out);
out.push_str(": ");
write_value(val, out);
}
out.push('}');
}
fn write_json_string(s: &str, out: &mut String) {
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
#[test]
fn sorted_tojson_key_order() {
let env = crate::env::build();
let ctx = serde_json::json!({
"value": {
"b": 1,
"a": [1, 2, {"z": 1, "y": 2}],
"amp": "R&D <tag> 'quote' \"dq\"",
"uni": "café ❤",
}
});
let rendered = env
.template_from_str("{{ value | tojson }}")
.expect("template compiles")
.render(&ctx)
.expect("template renders");
assert_eq!(
rendered,
r#"{"a": [1, 2, {"y": 2, "z": 1}], "amp": "R&D <tag> 'quote' \"dq\"", "b": 1, "uni": "café ❤"}"#,
);
}
#[test]
fn negative_control_builtin_tojson_diverges() {
let ctx = serde_json::json!({"value": {"b": 1, "a": "R&D <tag> 'q'"}});
let naked = minijinja::Environment::new();
let builtin = naked
.template_from_str("{{ value | tojson }}")
.expect("template compiles")
.render(&ctx)
.expect("template renders");
let ours = crate::env::build()
.template_from_str("{{ value | tojson }}")
.expect("template compiles")
.render(&ctx)
.expect("template renders");
assert_ne!(
builtin, ours,
"builtin tojson (HTML-escaped, unsorted) must diverge from the registered \
sorted_tojson override -- identical output here means the override never took effect"
);
}
}