1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
7#[serde(tag = "op", rename_all = "snake_case")]
8pub enum Expr {
9 Literal {
10 value: Value,
11 },
12 Var {
13 path: String,
14 },
15 Answer {
16 path: String,
17 },
18 IsSet {
19 path: String,
20 },
21 Concat {
26 parts: Vec<Expr>,
27 },
28 And {
29 expressions: Vec<Expr>,
30 },
31 Or {
32 expressions: Vec<Expr>,
33 },
34 Not {
35 expression: Box<Expr>,
36 },
37 Eq {
38 left: Box<Expr>,
39 right: Box<Expr>,
40 },
41 Ne {
42 left: Box<Expr>,
43 right: Box<Expr>,
44 },
45 Lt {
46 left: Box<Expr>,
47 right: Box<Expr>,
48 },
49 Lte {
50 left: Box<Expr>,
51 right: Box<Expr>,
52 },
53 Gt {
54 left: Box<Expr>,
55 right: Box<Expr>,
56 },
57 Gte {
58 left: Box<Expr>,
59 right: Box<Expr>,
60 },
61}
62
63impl Expr {
64 pub fn evaluate_value(&self, ctx: &Value) -> Option<Value> {
66 match self {
67 Expr::Literal { value } => Some(value.clone()),
68 Expr::Var { path } => Self::lookup(ctx, path).cloned(),
69 Expr::Answer { path } => Self::lookup_answer(ctx, path).cloned(),
70 Expr::IsSet { path } => {
71 let present = Self::lookup_answer(ctx, path).is_some();
72 Some(Value::Bool(present))
73 }
74 Expr::Concat { parts } => {
75 let mut out = String::new();
76 for part in parts {
77 match part.evaluate_value(ctx)? {
78 Value::String(text) => out.push_str(&text),
79 Value::Null | Value::Array(_) | Value::Object(_) => return None,
80 scalar => out.push_str(&scalar.to_string()),
81 }
82 }
83 Some(Value::String(out))
84 }
85 Expr::And { expressions } => Self::evaluate_and(expressions, ctx),
86 Expr::Or { expressions } => Self::evaluate_or(expressions, ctx),
87 Expr::Not { expression } => expression
88 .evaluate_bool(ctx)
89 .map(|value| Value::Bool(!value)),
90 Expr::Eq { left, right } => {
91 let left_value = left.evaluate_value(ctx)?;
92 let right_value = right.evaluate_value(ctx)?;
93 Some(Value::Bool(left_value == right_value))
94 }
95 Expr::Ne { left, right } => {
96 let left_value = left.evaluate_value(ctx)?;
97 let right_value = right.evaluate_value(ctx)?;
98 Some(Value::Bool(left_value != right_value))
99 }
100 Expr::Lt { left, right } => {
101 Self::evaluate_compare(left, right, ctx, |o| matches!(o, std::cmp::Ordering::Less))
102 }
103 Expr::Lte { left, right } => Self::evaluate_compare(left, right, ctx, |o| {
104 matches!(o, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
105 }),
106 Expr::Gt { left, right } => Self::evaluate_compare(left, right, ctx, |o| {
107 matches!(o, std::cmp::Ordering::Greater)
108 }),
109 Expr::Gte { left, right } => Self::evaluate_compare(left, right, ctx, |o| {
110 matches!(o, std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
111 }),
112 }
113 }
114
115 pub fn evaluate_bool(&self, ctx: &Value) -> Option<bool> {
117 let value = self.evaluate_value(ctx)?;
118 match value {
119 Value::Bool(value) => Some(value),
120 Value::Number(number) => number.as_f64().map(|value| value != 0.0),
121 Value::String(text) => match text.to_lowercase().as_str() {
122 "true" | "t" | "yes" | "y" | "1" => Some(true),
123 "false" | "f" | "no" | "n" | "0" => Some(false),
124 _ => None,
125 },
126 Value::Null => Some(false),
127 _ => None,
128 }
129 }
130
131 fn evaluate_and(expressions: &[Expr], ctx: &Value) -> Option<Value> {
132 let mut seen_none = false;
133 for expression in expressions {
134 match expression.evaluate_bool(ctx) {
135 Some(false) => return Some(Value::Bool(false)),
136 Some(true) => continue,
137 None => seen_none = true,
138 }
139 }
140 if seen_none {
141 None
142 } else {
143 Some(Value::Bool(true))
144 }
145 }
146
147 fn evaluate_or(expressions: &[Expr], ctx: &Value) -> Option<Value> {
148 let mut seen_none = false;
149 for expression in expressions {
150 match expression.evaluate_bool(ctx) {
151 Some(true) => return Some(Value::Bool(true)),
152 Some(false) => continue,
153 None => seen_none = true,
154 }
155 }
156 if seen_none {
157 None
158 } else {
159 Some(Value::Bool(false))
160 }
161 }
162
163 fn evaluate_compare<F>(left: &Expr, right: &Expr, ctx: &Value, predicate: F) -> Option<Value>
164 where
165 F: Fn(std::cmp::Ordering) -> bool,
166 {
167 let left_value = left.evaluate_value(ctx)?;
168 let right_value = right.evaluate_value(ctx)?;
169 let ordering = Self::compare_values(&left_value, &right_value)?;
170 if predicate(ordering) {
171 Some(Value::Bool(true))
172 } else {
173 Some(Value::Bool(false))
174 }
175 }
176
177 fn compare_values(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
178 match (left, right) {
179 (Value::Number(left), Value::Number(right)) => {
180 let left_num = left.as_f64()?;
181 let right_num = right.as_f64()?;
182 left_num.partial_cmp(&right_num)
183 }
184 (Value::String(left_text), Value::String(right_text)) => {
185 Some(left_text.cmp(right_text))
186 }
187 _ => {
188 if left == right {
189 Some(std::cmp::Ordering::Equal)
190 } else {
191 None
192 }
193 }
194 }
195 }
196
197 fn lookup<'a>(ctx: &'a Value, path: &str) -> Option<&'a Value> {
198 let pointer = Self::normalize_pointer(path);
199 ctx.pointer(&pointer)
200 }
201
202 fn lookup_answer<'a>(ctx: &'a Value, path: &str) -> Option<&'a Value> {
203 if let Some(value) = ctx.get("answers") {
204 Self::fetch_nested(value, path)
205 } else {
206 Self::fetch_nested(ctx, path)
207 }
208 }
209
210 fn fetch_nested<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
211 if path.starts_with('/') {
212 return value.pointer(path);
213 }
214 let mut current = value;
215 for segment in path.split('.') {
216 if segment.is_empty() {
217 continue;
218 }
219 current = if let Ok(index) = segment.parse::<usize>() {
220 current.get(index)?
221 } else {
222 current.get(segment)?
223 };
224 }
225 Some(current)
226 }
227
228 fn normalize_pointer(path: &str) -> String {
229 let trimmed = path.trim();
230 if trimmed.is_empty() {
231 return "/".to_string();
232 }
233 if trimmed.starts_with('/') {
234 return trimmed.to_string();
235 }
236 let cleaned = trimmed
237 .trim_start_matches('/')
238 .split('.')
239 .filter(|segment| !segment.is_empty())
240 .collect::<Vec<_>>();
241 format!("/{}", cleaned.join("/"))
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use serde_json::json;
249
250 fn concat(parts: Vec<Expr>) -> Expr {
251 Expr::Concat { parts }
252 }
253
254 #[test]
255 fn concat_joins_literal_and_var() {
256 let ctx = json!({ "bundle_id": "legal" });
259 let expr = concat(vec![
260 Expr::Literal { value: json!("/") },
261 Expr::Var {
262 path: "bundle_id".into(),
263 },
264 ]);
265 assert_eq!(expr.evaluate_value(&ctx), Some(json!("/legal")));
266 }
267
268 #[test]
269 fn concat_coerces_scalars_but_rejects_missing_and_structured() {
270 let ctx = json!({ "n": 7, "flag": true, "list": [1], "nested": {"a": 1} });
271 assert_eq!(
273 concat(vec![
274 Expr::Var { path: "n".into() },
275 Expr::Literal { value: json!("-") },
276 Expr::Var {
277 path: "flag".into()
278 },
279 ])
280 .evaluate_value(&ctx),
281 Some(json!("7-true"))
282 );
283 assert_eq!(
285 concat(vec![Expr::Var {
286 path: "absent".into()
287 }])
288 .evaluate_value(&ctx),
289 None
290 );
291 assert_eq!(
293 concat(vec![Expr::Var {
294 path: "list".into()
295 }])
296 .evaluate_value(&ctx),
297 None
298 );
299 }
300}