use serde_json::Value;
use super::ops::{self, Field};
const INDENT: &str = " ";
pub fn format(expr: &Value) -> String {
format_at(expr, 0)
}
fn format_at(expr: &Value, depth: usize) -> String {
let Some(tag) = expr.get("op").and_then(Value::as_str) else {
return literal(expr);
};
match tag {
"Data" => expr
.get("name")
.and_then(Value::as_str)
.unwrap_or("?")
.to_string(),
"Const" => literal(expr.get("value").unwrap_or(&Value::Null)),
"Neg" => format!("(-{})", format_at(child(expr, "of"), depth)),
"Not" => format!("(not {})", format_at(child(expr, "of"), depth)),
"Ceil" => format!("ceil({})", format_at(child(expr, "of"), depth)),
_ => {
if let Some(sym) = ops::binop_symbol_for_tag(tag) {
return format!(
"({} {} {})",
format_at(child(expr, "l"), depth),
sym,
format_at(child(expr, "r"), depth)
);
}
print_call(tag, expr, depth)
}
}
}
fn is_leaf(v: &Value) -> bool {
!matches!(v.get("op").and_then(Value::as_str), Some(tag) if tag != "Data" && tag != "Const")
}
fn print_call(tag: &str, expr: &Value, depth: usize) -> String {
let name = ops::dsl_name_for_tag(tag);
let Some(sig) = ops::sig_by_tag(tag) else {
return format!("{name}(/* unprintable op {tag} */)");
};
let child_depth = depth + 1;
let mut parts: Vec<String> = Vec::new();
let mut brk = false; let mut keyword_mode = false;
for field in sig.fields {
let fname = ops::field_name(field);
let Some(val) = expr.get(fname) else {
keyword_mode = true;
continue;
};
let rendered = match field {
Field::ExprList(_) => {
let items = val.as_array().cloned().unwrap_or_default();
if items.iter().any(|v| !is_leaf(v)) {
brk = true;
}
let rendered: Vec<String> =
items.iter().map(|v| format_at(v, child_depth)).collect();
format!("[{}]", rendered.join(", "))
}
Field::StrListOpt(_) => {
let empty = Vec::new();
let items: Vec<String> = val
.as_array()
.unwrap_or(&empty)
.iter()
.map(literal)
.collect();
format!("[{}]", items.join(", "))
}
Field::Expr(_) | Field::ExprOpt(_) => {
if !is_leaf(val) {
brk = true;
}
format_at(val, child_depth)
}
_ => literal(val),
};
let is_required_inline = matches!(field, Field::Expr(_) | Field::Num(_) | Field::Str(_));
if !keyword_mode && is_required_inline {
parts.push(rendered);
} else {
keyword_mode = true;
parts.push(format!("{fname}={rendered}"));
}
}
if brk {
let ind = INDENT.repeat(child_depth);
let close = INDENT.repeat(depth);
format!(
"{name}(\n{ind}{}\n{close})",
parts.join(&format!(",\n{ind}"))
)
} else {
format!("{name}({})", parts.join(", "))
}
}
fn child<'a>(expr: &'a Value, key: &str) -> &'a Value {
expr.get(key).unwrap_or(&Value::Null)
}
fn literal(v: &Value) -> String {
match v {
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::String(s) => format!("\"{s}\""),
Value::Null => "null".into(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn prints_leaves_and_call() {
assert_eq!(format(&json!({"op":"Data","name":"close"})), "close");
assert_eq!(format(&json!({"op":"Const","value":1000000})), "1000000");
assert_eq!(
format(&json!({"op":"Average","of":{"op":"Data","name":"close"},"n":2})),
"sma(close, 2)"
);
}
#[test]
fn prints_not_and_round_trips() {
let v = json!({"op":"Not","of":{"op":"Gt",
"l":{"op":"Data","name":"a"},"r":{"op":"Const","value":1}}});
assert_eq!(format(&v), "(not (a > 1))");
assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
}
#[test]
fn prints_binop_parenthesised() {
assert_eq!(
format(&json!({"op":"Gt","l":{"op":"Data","name":"close"},
"r":{"op":"Const","value":2}})),
"(close > 2)"
);
}
#[test]
fn prints_keyword_fields_and_list() {
assert_eq!(
format(&json!({"op":"Rebalance","of":{"op":"Data","name":"x"},"freq":"ME"})),
"rebalance(x, freq=\"ME\")"
);
assert_eq!(
format(&json!({"op":"Neutralize","of":{"op":"Data","name":"x"},
"by":[{"op":"Data","name":"pe"}]})),
"neutralize(x, by=[pe])"
);
}
#[test]
fn vwap_prints_all_positional_and_reparses() {
let v = serde_json::json!({"op":"Vwap","high":{"op":"Data","name":"high"},
"low":{"op":"Data","name":"low"},"close":{"op":"Data","name":"close"},
"volume":{"op":"Data","name":"volume"},"n":20});
assert_eq!(format(&v), "vwap(high, low, close, volume, 20)");
assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
}
#[test]
fn rank_with_gapped_optional_uses_keyword() {
let v = serde_json::json!({"op":"Rank","of":{"op":"Data","name":"x"},"ascending":false});
assert_eq!(format(&v), "rank(x, ascending=false)");
assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
}
#[test]
fn nested_call_breaks_with_indent_flat_stays_inline() {
let v = json!({"op":"Rebalance","freq":"ME",
"of":{"op":"IsLargest","n":10,
"of":{"op":"Average","of":{"op":"Data","name":"close"},"n":5}}});
assert_eq!(
format(&v),
"rebalance(\n is_largest(\n sma(close, 5),\n 10\n ),\n freq=\"ME\"\n)"
);
assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
}
#[test]
fn binop_arg_breaks_parent_but_stays_inline_itself() {
let v = json!({"op":"Mask","of":{"op":"IsLargest","of":{"op":"Data","name":"x"},"n":30},
"by":{"op":"Gt","l":{"op":"Data","name":"market_cap"},"r":{"op":"Const","value":5}}});
assert_eq!(
format(&v),
"mask(\n is_largest(x, 30),\n (market_cap > 5)\n)"
);
assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
}
}