use crate::CompiledNode;
use crate::OpCode;
use crate::node::PathSegment;
pub(crate) fn push_json_str(out: &mut String, s: &str) {
const HEX: &[u8; 16] = b"0123456789abcdef";
out.reserve(s.len() + 2);
out.push('"');
let mut run = 0;
for (i, b) in s.bytes().enumerate() {
let escape = match b {
b'"' => "\\\"",
b'\\' => "\\\\",
b'\n' => "\\n",
b'\r' => "\\r",
b'\t' => "\\t",
0x08 => "\\b",
0x0C => "\\f",
0x00..=0x1F => "",
_ => continue,
};
out.push_str(&s[run..i]);
if escape.is_empty() {
out.push_str("\\u00");
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0F) as usize] as char);
} else {
out.push_str(escape);
}
run = i + 1;
}
out.push_str(&s[run..]);
out.push('"');
}
pub(crate) fn json_str(s: &str) -> String {
let mut out = String::new();
push_json_str(&mut out, s);
out
}
#[inline]
fn segment_str(seg: &PathSegment) -> &str {
match seg {
PathSegment::Field(s) | PathSegment::FieldOrIndex(s, _) => s,
}
}
fn segments_array(segments: &[PathSegment]) -> String {
let items: Vec<String> = segments.iter().map(|s| json_str(segment_str(s))).collect();
format!("[{}]", items.join(", "))
}
fn call(name: &str, args: &str) -> String {
let mut out = String::with_capacity(name.len() + args.len() + 6);
out.push('{');
push_json_str(&mut out, name);
out.push_str(": ");
out.push_str(args);
out.push('}');
out
}
pub(crate) fn node_to_json_string(node: &CompiledNode) -> String {
match node {
CompiledNode::Value { value, .. } => value.to_json_string(),
CompiledNode::Array { nodes, .. } => {
let items: Vec<String> = nodes.iter().map(node_to_json_string).collect();
format!("[{}]", items.join(", "))
}
CompiledNode::BuiltinOperator { opcode, args, .. } => builtin_to_json_string(opcode, args),
CompiledNode::CustomOperator(data) => custom_to_json_string(&data.name, &data.args),
CompiledNode::Cse(data) => node_to_json_string(&data.inner),
#[cfg(feature = "templating")]
CompiledNode::StructuredObject(data) => structured_to_json_string(&data.fields),
CompiledNode::Var {
scope_level,
segments,
default_value,
..
} => compiled_var_to_json_string(*scope_level, segments, default_value.as_deref()),
#[cfg(feature = "ext-control")]
CompiledNode::Exists(data) => compiled_exists_to_json_string(&data.segments),
#[cfg(feature = "error-handling")]
CompiledNode::Throw(data) => {
if let datavalue::OwnedDataValue::Object(pairs) = &data.error
&& let Some((_, datavalue::OwnedDataValue::String(s))) =
pairs.iter().find(|(k, _)| k == "type")
{
return call("throw", &json_str(s));
}
call("throw", &data.error.to_json_string())
}
CompiledNode::Missing(data) => {
let parts: Vec<String> = data
.args
.iter()
.map(|a| match a {
crate::node::CompiledMissingArg::Now((path, _)) => json_str(path),
crate::node::CompiledMissingArg::Later(n) => node_to_json_string(n),
})
.collect();
format!("{{\"missing\": [{}]}}", parts.join(", "))
}
CompiledNode::MissingSome(data) => {
let min_str = match &data.min_present {
crate::node::CompiledMissingMin::Now(n) => n.to_string(),
crate::node::CompiledMissingMin::Later(n) => node_to_json_string(n),
};
let paths_str = match &data.paths {
crate::node::CompiledMissingPaths::Now(paths) => {
let items: Vec<String> = paths.iter().map(|(p, _)| json_str(p)).collect();
format!("[{}]", items.join(", "))
}
crate::node::CompiledMissingPaths::Later(n) => node_to_json_string(n),
};
format!("{{\"missing_some\": [{}, {}]}}", min_str, paths_str)
}
CompiledNode::InvalidArgs { op_name, args, .. } => call(op_name, &args.to_json_string()),
}
}
fn args_to_json_string(args: &[CompiledNode], inline: bool) -> String {
if let [arg] = args {
let rendered = node_to_json_string(arg);
if inline && !rendered.starts_with('[') {
return rendered;
}
return format!("[{rendered}]");
}
let items: Vec<String> = args.iter().map(node_to_json_string).collect();
format!("[{}]", items.join(", "))
}
pub(crate) fn builtin_to_json_string(opcode: &OpCode, args: &[CompiledNode]) -> String {
let inline = opcode.meta().args_form != crate::operators::meta::ArgsForm::ArrayOnly;
call(opcode.as_str(), &args_to_json_string(args, inline))
}
pub(crate) fn custom_to_json_string(name: &str, args: &[CompiledNode]) -> String {
call(name, &args_to_json_string(args, true))
}
#[cfg(feature = "templating")]
pub(crate) fn structured_to_json_string(fields: &[(String, CompiledNode)]) -> String {
let items: Vec<String> = fields
.iter()
.map(|(key, node)| format!("{}: {}", json_str(key), node_to_json_string(node)))
.collect();
format!("{{{}}}", items.join(", "))
}
fn compiled_var_to_json_string(
scope_level: u32,
segments: &[PathSegment],
default_value: Option<&CompiledNode>,
) -> String {
if scope_level > 0 {
let mut list = format!("[[{}]", scope_level);
for seg in segments {
list.push_str(", ");
push_json_str(&mut list, segment_str(seg));
}
list.push(']');
return call("val", &list);
}
let needs_val = match segments {
[seg] => segment_str(seg).is_empty() || segment_str(seg).contains('.'),
_ => segments.iter().any(|s| segment_str(s).contains('.')),
};
if needs_val && default_value.is_none() {
return match segments {
[seg] => call("val", &json_str(segment_str(seg))),
_ => call("val", &segments_array(segments)),
};
}
let path = segments
.iter()
.map(segment_str)
.collect::<Vec<_>>()
.join(".");
match default_value {
Some(def) => call(
"var",
&format!("[{}, {}]", json_str(&path), node_to_json_string(def)),
),
None => call("var", &json_str(&path)),
}
}
#[cfg(feature = "ext-control")]
pub(crate) fn compiled_exists_to_json_string(segments: &[PathSegment]) -> String {
match segments {
[seg] => call("exists", &json_str(segment_str(seg))),
_ => call("exists", &segments_array(segments)),
}
}