1use serde_json::Value;
6
7use super::ops::{self, Field};
8
9const INDENT: &str = " ";
10
11pub fn format(expr: &Value) -> String {
12 format_at(expr, 0)
13}
14
15fn format_at(expr: &Value, depth: usize) -> String {
18 let Some(tag) = expr.get("op").and_then(Value::as_str) else {
19 return literal(expr);
20 };
21 match tag {
22 "Data" => expr
23 .get("name")
24 .and_then(Value::as_str)
25 .unwrap_or("?")
26 .to_string(),
27 "Const" => literal(expr.get("value").unwrap_or(&Value::Null)),
28 "Neg" => format!("(-{})", format_at(child(expr, "of"), depth)),
29 "Ceil" => format!("ceil({})", format_at(child(expr, "of"), depth)),
30 _ => {
31 if let Some(sym) = ops::binop_symbol_for_tag(tag) {
32 return format!(
34 "({} {} {})",
35 format_at(child(expr, "l"), depth),
36 sym,
37 format_at(child(expr, "r"), depth)
38 );
39 }
40 print_call(tag, expr, depth)
41 }
42 }
43}
44
45fn is_leaf(v: &Value) -> bool {
48 !matches!(v.get("op").and_then(Value::as_str), Some(tag) if tag != "Data" && tag != "Const")
49}
50
51fn print_call(tag: &str, expr: &Value, depth: usize) -> String {
52 let name = ops::dsl_name_for_tag(tag);
53 let Some(sig) = ops::sig_by_tag(tag) else {
54 return format!("{name}(/* unprintable op {tag} */)");
55 };
56 let child_depth = depth + 1;
57 let mut parts: Vec<String> = Vec::new();
58 let mut brk = false; let mut keyword_mode = false;
62 for field in sig.fields {
63 let fname = ops::field_name(field);
64 let Some(val) = expr.get(fname) else {
65 keyword_mode = true;
67 continue;
68 };
69 let rendered = match field {
70 Field::ExprList(_) => {
71 let items = val.as_array().cloned().unwrap_or_default();
72 if items.iter().any(|v| !is_leaf(v)) {
73 brk = true;
74 }
75 let rendered: Vec<String> =
76 items.iter().map(|v| format_at(v, child_depth)).collect();
77 format!("[{}]", rendered.join(", "))
78 }
79 Field::StrListOpt(_) => {
80 let empty = Vec::new();
81 let items: Vec<String> = val
82 .as_array()
83 .unwrap_or(&empty)
84 .iter()
85 .map(literal)
86 .collect();
87 format!("[{}]", items.join(", "))
88 }
89 Field::Expr(_) | Field::ExprOpt(_) => {
90 if !is_leaf(val) {
91 brk = true;
92 }
93 format_at(val, child_depth)
94 }
95 _ => literal(val),
96 };
97 let is_required_inline = matches!(field, Field::Expr(_) | Field::Num(_) | Field::Str(_));
101 if !keyword_mode && is_required_inline {
102 parts.push(rendered);
103 } else {
104 keyword_mode = true;
105 parts.push(format!("{fname}={rendered}"));
106 }
107 }
108 if brk {
109 let ind = INDENT.repeat(child_depth);
110 let close = INDENT.repeat(depth);
111 format!(
112 "{name}(\n{ind}{}\n{close})",
113 parts.join(&format!(",\n{ind}"))
114 )
115 } else {
116 format!("{name}({})", parts.join(", "))
117 }
118}
119
120fn child<'a>(expr: &'a Value, key: &str) -> &'a Value {
121 expr.get(key).unwrap_or(&Value::Null)
122}
123
124fn literal(v: &Value) -> String {
125 match v {
126 Value::Number(n) => n.to_string(),
127 Value::Bool(b) => b.to_string(),
128 Value::String(s) => format!("\"{s}\""),
129 Value::Null => "null".into(),
130 other => other.to_string(),
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use serde_json::json;
138
139 #[test]
140 fn prints_leaves_and_call() {
141 assert_eq!(format(&json!({"op":"Data","name":"close"})), "close");
142 assert_eq!(format(&json!({"op":"Const","value":1000000})), "1000000");
143 assert_eq!(
144 format(&json!({"op":"Average","of":{"op":"Data","name":"close"},"n":2})),
145 "sma(close, 2)"
146 );
147 }
148
149 #[test]
150 fn prints_binop_parenthesised() {
151 assert_eq!(
152 format(&json!({"op":"Gt","l":{"op":"Data","name":"close"},
153 "r":{"op":"Const","value":2}})),
154 "(close > 2)"
155 );
156 }
157
158 #[test]
159 fn prints_keyword_fields_and_list() {
160 assert_eq!(
161 format(&json!({"op":"Rebalance","of":{"op":"Data","name":"x"},"freq":"ME"})),
162 "rebalance(x, freq=\"ME\")"
163 );
164 assert_eq!(
165 format(&json!({"op":"Neutralize","of":{"op":"Data","name":"x"},
166 "by":[{"op":"Data","name":"pe"}]})),
167 "neutralize(x, by=[pe])"
168 );
169 }
170
171 #[test]
172 fn vwap_prints_all_positional_and_reparses() {
173 let v = serde_json::json!({"op":"Vwap","high":{"op":"Data","name":"high"},
174 "low":{"op":"Data","name":"low"},"close":{"op":"Data","name":"close"},
175 "volume":{"op":"Data","name":"volume"},"n":20});
176 assert_eq!(format(&v), "vwap(high, low, close, volume, 20)");
177 assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
178 }
179
180 #[test]
181 fn rank_with_gapped_optional_uses_keyword() {
182 let v = serde_json::json!({"op":"Rank","of":{"op":"Data","name":"x"},"ascending":false});
183 assert_eq!(format(&v), "rank(x, ascending=false)");
184 assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
185 }
186
187 #[test]
188 fn nested_call_breaks_with_indent_flat_stays_inline() {
189 let v = json!({"op":"Rebalance","freq":"ME",
192 "of":{"op":"IsLargest","n":10,
193 "of":{"op":"Average","of":{"op":"Data","name":"close"},"n":5}}});
194 assert_eq!(
195 format(&v),
196 "rebalance(\n is_largest(\n sma(close, 5),\n 10\n ),\n freq=\"ME\"\n)"
197 );
198 assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
199 }
200
201 #[test]
202 fn binop_arg_breaks_parent_but_stays_inline_itself() {
203 let v = json!({"op":"Mask","of":{"op":"IsLargest","of":{"op":"Data","name":"x"},"n":30},
204 "by":{"op":"Gt","l":{"op":"Data","name":"market_cap"},"r":{"op":"Const","value":5}}});
205 assert_eq!(
206 format(&v),
207 "mask(\n is_largest(x, 30),\n (market_cap > 5)\n)"
208 );
209 assert_eq!(super::super::parse(&format(&v)).unwrap(), v);
210 }
211}