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 { value: Value },
10 Var { path: String },
11 Answer { path: String },
12 IsSet { path: String },
13 And { expressions: Vec<Expr> },
14 Or { expressions: Vec<Expr> },
15 Not { expression: Box<Expr> },
16 Eq { left: Box<Expr>, right: Box<Expr> },
17 Ne { left: Box<Expr>, right: Box<Expr> },
18 Lt { left: Box<Expr>, right: Box<Expr> },
19 Lte { left: Box<Expr>, right: Box<Expr> },
20 Gt { left: Box<Expr>, right: Box<Expr> },
21 Gte { left: Box<Expr>, right: Box<Expr> },
22}
23
24impl Expr {
25 pub fn evaluate_value(&self, ctx: &Value) -> Option<Value> {
27 match self {
28 Expr::Literal { value } => Some(value.clone()),
29 Expr::Var { path } => Self::lookup(ctx, path).cloned(),
30 Expr::Answer { path } => Self::lookup_answer(ctx, path).cloned(),
31 Expr::IsSet { path } => {
32 let present = Self::lookup_answer(ctx, path).is_some();
33 Some(Value::Bool(present))
34 }
35 Expr::And { expressions } => Self::evaluate_and(expressions, ctx),
36 Expr::Or { expressions } => Self::evaluate_or(expressions, ctx),
37 Expr::Not { expression } => expression
38 .evaluate_bool(ctx)
39 .map(|value| Value::Bool(!value)),
40 Expr::Eq { left, right } => {
41 let left_value = left.evaluate_value(ctx)?;
42 let right_value = right.evaluate_value(ctx)?;
43 Some(Value::Bool(left_value == right_value))
44 }
45 Expr::Ne { left, right } => {
46 let left_value = left.evaluate_value(ctx)?;
47 let right_value = right.evaluate_value(ctx)?;
48 Some(Value::Bool(left_value != right_value))
49 }
50 Expr::Lt { left, right } => {
51 Self::evaluate_compare(left, right, ctx, |o| matches!(o, std::cmp::Ordering::Less))
52 }
53 Expr::Lte { left, right } => Self::evaluate_compare(left, right, ctx, |o| {
54 matches!(o, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
55 }),
56 Expr::Gt { left, right } => Self::evaluate_compare(left, right, ctx, |o| {
57 matches!(o, std::cmp::Ordering::Greater)
58 }),
59 Expr::Gte { left, right } => Self::evaluate_compare(left, right, ctx, |o| {
60 matches!(o, std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
61 }),
62 }
63 }
64
65 pub fn evaluate_bool(&self, ctx: &Value) -> Option<bool> {
67 let value = self.evaluate_value(ctx)?;
68 match value {
69 Value::Bool(value) => Some(value),
70 Value::Number(number) => number.as_f64().map(|value| value != 0.0),
71 Value::String(text) => match text.to_lowercase().as_str() {
72 "true" | "t" | "yes" | "y" | "1" => Some(true),
73 "false" | "f" | "no" | "n" | "0" => Some(false),
74 _ => None,
75 },
76 Value::Null => Some(false),
77 _ => None,
78 }
79 }
80
81 fn evaluate_and(expressions: &[Expr], ctx: &Value) -> Option<Value> {
82 let mut seen_none = false;
83 for expression in expressions {
84 match expression.evaluate_bool(ctx) {
85 Some(false) => return Some(Value::Bool(false)),
86 Some(true) => continue,
87 None => seen_none = true,
88 }
89 }
90 if seen_none {
91 None
92 } else {
93 Some(Value::Bool(true))
94 }
95 }
96
97 fn evaluate_or(expressions: &[Expr], ctx: &Value) -> Option<Value> {
98 let mut seen_none = false;
99 for expression in expressions {
100 match expression.evaluate_bool(ctx) {
101 Some(true) => return Some(Value::Bool(true)),
102 Some(false) => continue,
103 None => seen_none = true,
104 }
105 }
106 if seen_none {
107 None
108 } else {
109 Some(Value::Bool(false))
110 }
111 }
112
113 fn evaluate_compare<F>(left: &Expr, right: &Expr, ctx: &Value, predicate: F) -> Option<Value>
114 where
115 F: Fn(std::cmp::Ordering) -> bool,
116 {
117 let left_value = left.evaluate_value(ctx)?;
118 let right_value = right.evaluate_value(ctx)?;
119 let ordering = Self::compare_values(&left_value, &right_value)?;
120 if predicate(ordering) {
121 Some(Value::Bool(true))
122 } else {
123 Some(Value::Bool(false))
124 }
125 }
126
127 fn compare_values(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
128 match (left, right) {
129 (Value::Number(left), Value::Number(right)) => {
130 let left_num = left.as_f64()?;
131 let right_num = right.as_f64()?;
132 left_num.partial_cmp(&right_num)
133 }
134 (Value::String(left_text), Value::String(right_text)) => {
135 Some(left_text.cmp(right_text))
136 }
137 _ => {
138 if left == right {
139 Some(std::cmp::Ordering::Equal)
140 } else {
141 None
142 }
143 }
144 }
145 }
146
147 fn lookup<'a>(ctx: &'a Value, path: &str) -> Option<&'a Value> {
148 let pointer = Self::normalize_pointer(path);
149 ctx.pointer(&pointer)
150 }
151
152 fn lookup_answer<'a>(ctx: &'a Value, path: &str) -> Option<&'a Value> {
153 if let Some(value) = ctx.get("answers") {
154 Self::fetch_nested(value, path)
155 } else {
156 Self::fetch_nested(ctx, path)
157 }
158 }
159
160 fn fetch_nested<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
161 if path.starts_with('/') {
162 return value.pointer(path);
163 }
164 let mut current = value;
165 for segment in path.split('.') {
166 if segment.is_empty() {
167 continue;
168 }
169 current = if let Ok(index) = segment.parse::<usize>() {
170 current.get(index)?
171 } else {
172 current.get(segment)?
173 };
174 }
175 Some(current)
176 }
177
178 fn normalize_pointer(path: &str) -> String {
179 let trimmed = path.trim();
180 if trimmed.is_empty() {
181 return "/".to_string();
182 }
183 if trimmed.starts_with('/') {
184 return trimmed.to_string();
185 }
186 let cleaned = trimmed
187 .trim_start_matches('/')
188 .split('.')
189 .filter(|segment| !segment.is_empty())
190 .collect::<Vec<_>>();
191 format!("/{}", cleaned.join("/"))
192 }
193}