1use serde_json::Value;
21
22#[derive(Debug, Clone)]
24pub struct Assertion {
25 source: String,
26 path: Vec<Step>,
27 length: bool,
28 op: Op,
29 literal: Value,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33enum Step {
34 Key(String),
35 Elements,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
39enum Op {
40 Eq,
41 Ne,
42 Gt,
43 Ge,
44 Lt,
45 Le,
46 Exists,
47}
48
49impl Assertion {
50 pub fn parse(line: &str) -> Result<Assertion, String> {
51 let mut parts = line.split_whitespace();
52 let path_text = parts.next().ok_or("empty assertion")?;
53 let op_text = parts
54 .next()
55 .ok_or_else(|| format!("`{line}`: missing operator"))?;
56 let op = match op_text {
57 "==" => Op::Eq,
58 "!=" => Op::Ne,
59 ">" => Op::Gt,
60 ">=" => Op::Ge,
61 "<" => Op::Lt,
62 "<=" => Op::Le,
63 "exists" => Op::Exists,
64 other => return Err(format!("`{line}`: unknown operator `{other}`")),
65 };
66 let literal = if op == Op::Exists {
67 if parts.next().is_some() {
68 return Err(format!("`{line}`: `exists` takes no operand"));
69 }
70 Value::Null
71 } else {
72 let rest = parts.collect::<Vec<_>>().join(" ");
73 serde_json::from_str(&rest)
74 .map_err(|_| format!("`{line}`: operand `{rest}` is not a JSON literal"))?
75 };
76 let (path_text, length) = match path_text.strip_suffix("#len") {
77 Some(stripped) => (stripped, true),
78 None => (path_text, false),
79 };
80 let mut path = Vec::new();
81 for segment in path_text.split('.').filter(|s| !s.is_empty()) {
82 match segment.strip_suffix("[]") {
83 Some("") => path.push(Step::Elements),
84 Some(key) => {
85 path.push(Step::Key(key.to_string()));
86 path.push(Step::Elements);
87 }
88 None => path.push(Step::Key(segment.to_string())),
89 }
90 }
91 Ok(Assertion {
92 source: line.to_string(),
93 path,
94 length,
95 op,
96 literal,
97 })
98 }
99
100 pub fn source(&self) -> &str {
102 &self.source
103 }
104
105 pub fn check(&self, value: &Value) -> Result<(), String> {
107 let mut targets = vec![value];
108 for step in &self.path {
109 let mut next = Vec::new();
110 for target in targets {
111 match step {
112 Step::Key(key) => next.push(target.get(key).unwrap_or(&Value::Null)),
113 Step::Elements => match target.as_array() {
114 Some(items) => next.extend(items.iter()),
115 None => next.push(&Value::Null),
116 },
117 }
118 }
119 targets = next;
120 }
121 if targets.is_empty() {
122 return Ok(());
124 }
125 for target in targets {
126 self.check_one(target)?;
127 }
128 Ok(())
129 }
130
131 fn check_one(&self, target: &Value) -> Result<(), String> {
132 let length_value;
133 let target = if self.length {
134 let len = match target {
135 Value::Array(items) => items.len(),
136 Value::String(s) => s.len(),
137 other => return Err(format!("{}: {} has no length", self.source, kind(other))),
138 };
139 length_value = Value::from(len);
140 &length_value
141 } else {
142 target
143 };
144 let holds = match self.op {
145 Op::Exists => !target.is_null(),
146 Op::Eq => target == &self.literal,
147 Op::Ne => target != &self.literal,
148 Op::Gt | Op::Ge | Op::Lt | Op::Le => {
149 let (Some(actual), Some(expected)) = (target.as_f64(), self.literal.as_f64())
150 else {
151 return Err(format!("{}: got {}", self.source, short(target)));
152 };
153 match self.op {
154 Op::Gt => actual > expected,
155 Op::Ge => actual >= expected,
156 Op::Lt => actual < expected,
157 _ => actual <= expected,
158 }
159 }
160 };
161 if holds {
162 Ok(())
163 } else {
164 Err(format!("{}: got {}", self.source, short(target)))
165 }
166 }
167}
168
169fn kind(value: &Value) -> &'static str {
170 match value {
171 Value::Null => "null",
172 Value::Bool(_) => "boolean",
173 Value::Number(_) => "number",
174 Value::String(_) => "string",
175 Value::Array(_) => "array",
176 Value::Object(_) => "object",
177 }
178}
179
180fn short(value: &Value) -> String {
181 let text = value.to_string();
182 if text.len() > 60 {
183 format!("{}…", &text[..60])
184 } else {
185 text
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use serde_json::json;
193
194 fn assert_holds(line: &str, value: &Value) {
195 Assertion::parse(line).unwrap().check(value).unwrap();
196 }
197
198 fn assert_fails(line: &str, value: &Value) {
199 assert!(Assertion::parse(line).unwrap().check(value).is_err());
200 }
201
202 #[test]
203 fn scalar_comparisons() {
204 let value = json!({"meta": {"price": 12.5, "currency": "USD"}});
205 assert_holds("meta.price > 0", &value);
206 assert_holds("meta.currency == \"USD\"", &value);
207 assert_fails("meta.price < 1", &value);
208 assert_fails("meta.currency == \"EUR\"", &value);
209 }
210
211 #[test]
212 fn missing_and_null_fail_comparisons_but_exist_checks_say_why() {
213 let value = json!({"meta": {"price": null}});
214 assert_fails("meta.price > 0", &value);
215 assert_fails("meta.gone > 0", &value);
216 assert_fails("meta.price exists", &value);
217 assert_holds("meta.price != 3", &value);
218 }
219
220 #[test]
221 fn length_of_arrays_and_strings() {
222 let value = json!({"candles": [1, 2, 3], "symbol": "AAPL"});
223 assert_holds("candles#len >= 1", &value);
224 assert_holds("symbol#len == 4", &value);
225 assert_fails("candles#len > 3", &value);
226 assert_fails("candles.0#len > 0", &value);
227 }
228
229 #[test]
230 fn element_descent_must_hold_for_every_element() {
231 let value = json!({"candles": [{"close": 1.0}, {"close": 0.0}]});
232 assert_fails("candles[].close > 0", &value);
233 assert_holds("candles[].close >= 0", &value);
234 assert_holds("empty[].close > 0", &json!({"empty": []}));
235 }
236
237 #[test]
238 fn parse_rejects_malformed_lines() {
239 assert!(Assertion::parse("price >").is_err());
240 assert!(Assertion::parse("price ~ 3").is_err());
241 assert!(Assertion::parse("price exists 3").is_err());
242 assert!(Assertion::parse("price == not-json").is_err());
243 }
244}