1use nom::{
2 branch::alt,
3 bytes::complete::{tag, take_while1},
4 character::complete::{alpha1, alphanumeric1, char, digit1, multispace0},
5 combinator::{map, opt, recognize, value},
6 multi::{many0, separated_list0},
7 sequence::{delimited, pair, preceded},
8 IResult,
9};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub enum Condition {
14 Default,
15 Comparison(Comparison),
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct Comparison {
20 pub left: Operand,
21 pub op: Comparator,
22 pub right: Operand,
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub enum Operand {
27 Path(PathExpr),
28 Literal(Literal),
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct PathExpr {
33 pub segments: Vec<PathSegment>,
34}
35
36impl PathExpr {
37 pub fn new(segments: Vec<PathSegment>) -> Self {
38 PathExpr { segments }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub enum PathSegment {
44 Field(String),
45 Wildcard,
46 Index(usize),
47 QuotedKey(String),
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub enum Comparator {
52 Eq,
53 Neq,
54 Gte,
55 Lte,
56 Gt,
57 Lt,
58 In,
59 Matches,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub enum Literal {
64 Number(f64),
65 String(String),
66 Bool(bool),
67 Null,
68 Array(Vec<Literal>),
69}
70
71pub fn parse_condition(input: &str) -> Result<Condition, String> {
72 if input.trim() == "default" {
73 return Ok(Condition::Default);
74 }
75
76 match parse_comparison(input) {
77 Ok((remaining, comparison)) => {
78 if remaining.trim().is_empty() {
79 Ok(Condition::Comparison(comparison))
80 } else {
81 Err(format!(
82 "trailing content in condition expression: '{}'",
83 remaining
84 ))
85 }
86 }
87 Err(e) => Err(format!("failed to parse condition expression: {}", e)),
88 }
89}
90
91fn parse_comparison(input: &str) -> IResult<&str, Comparison> {
92 let (input, left) = parse_operand(input)?;
93 let (input, _) = multispace0(input)?;
94 let (input, op) = parse_comparator(input)?;
95 let (input, _) = multispace0(input)?;
96 let (input, right) = parse_operand(input)?;
97 Ok((input, Comparison { left, op, right }))
98}
99
100fn parse_operand(input: &str) -> IResult<&str, Operand> {
101 alt((
102 map(parse_path_expr, Operand::Path),
103 map(parse_literal, Operand::Literal),
104 ))(input)
105}
106
107fn parse_path_expr(input: &str) -> IResult<&str, PathExpr> {
108 let (input, root) = parse_root_var(input)?;
109 let (input, segments) = many0(parse_member_access)(input)?;
110 let mut all_segments = vec![PathSegment::Field(root)];
111 all_segments.extend(segments);
112 Ok((input, PathExpr::new(all_segments)))
113}
114
115fn parse_root_var(input: &str) -> IResult<&str, String> {
116 let (input, _) = tag("message")(input)?;
117 Ok((input, "message".to_string()))
118}
119
120fn parse_member_access(input: &str) -> IResult<&str, PathSegment> {
121 alt((
122 parse_dot_access,
123 parse_bracket_access,
124 ))(input)
125}
126
127fn parse_dot_access(input: &str) -> IResult<&str, PathSegment> {
128 let (input, _) = char('.')(input)?;
129 let (input, ident) = parse_identifier(input)?;
130 Ok((input, PathSegment::Field(ident)))
131}
132
133fn parse_bracket_access(input: &str) -> IResult<&str, PathSegment> {
134 delimited(
135 char('['),
136 alt((
137 map(tag("*"), |_| PathSegment::Wildcard),
138 map(parse_index, PathSegment::Index),
139 map(parse_quoted_key, PathSegment::QuotedKey),
140 )),
141 char(']'),
142 )(input)
143}
144
145fn parse_identifier(input: &str) -> IResult<&str, String> {
146 map(
147 recognize(pair(
148 alpha1,
149 many0(alt((alphanumeric1, tag("_")))),
150 )),
151 |s: &str| s.to_string(),
152 )(input)
153}
154
155fn parse_index(input: &str) -> IResult<&str, usize> {
156 map(digit1, |s: &str| s.parse::<usize>().unwrap())(input)
157}
158
159fn parse_quoted_key(input: &str) -> IResult<&str, String> {
160 delimited(
161 char('"'),
162 map(
163 take_while1(|c: char| c != '"' && c >= '\x20' && c <= '\x7e'),
164 |s: &str| s.to_string(),
165 ),
166 char('"'),
167 )(input)
168}
169
170fn parse_comparator(input: &str) -> IResult<&str, Comparator> {
171 alt((
172 value(Comparator::Eq, tag("==")),
173 value(Comparator::Neq, tag("!=")),
174 value(Comparator::Gte, tag(">=")),
175 value(Comparator::Lte, tag("<=")),
176 value(Comparator::Gt, tag(">")),
177 value(Comparator::Lt, tag("<")),
178 value(Comparator::In, tag("in")),
179 value(Comparator::Matches, tag("matches")),
180 ))(input)
181}
182
183fn parse_literal(input: &str) -> IResult<&str, Literal> {
184 alt((
185 value(Literal::Bool(true), tag("true")),
186 value(Literal::Bool(false), tag("false")),
187 value(Literal::Null, tag("null")),
188 map(parse_number, Literal::Number),
189 map(parse_string_literal, Literal::String),
190 map(parse_array_literal, Literal::Array),
191 ))(input)
192}
193
194fn parse_array_literal(input: &str) -> IResult<&str, Vec<Literal>> {
195 delimited(
196 char('['),
197 preceded(
198 multispace0,
199 separated_list0(
200 preceded(multispace0, char(',')),
201 preceded(multispace0, parse_literal),
202 ),
203 ),
204 preceded(multispace0, char(']')),
205 )(input)
206}
207
208fn parse_number(input: &str) -> IResult<&str, f64> {
209 let (input, sign) = opt(char('-'))(input)?;
210 let (input, int_part) = digit1(input)?;
211 let (input, frac_part) = opt(preceded(char('.'), digit1))(input)?;
212
213 let num_str = format!(
214 "{}{}{}",
215 sign.map(|_| "-").unwrap_or(""),
216 int_part,
217 frac_part.map(|f| format!(".{}", f)).unwrap_or_default()
218 );
219 let value = num_str.parse::<f64>().map_err(|_| {
220 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
221 })?;
222 Ok((input, value))
223}
224
225fn parse_string_literal(input: &str) -> IResult<&str, String> {
226 delimited(
227 char('"'),
228 map(
229 take_while1(|c: char| c != '"'),
230 |s: &str| s.to_string(),
231 ),
232 char('"'),
233 )(input)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_default_condition() {
242 assert_eq!(parse_condition("default").unwrap(), Condition::Default);
243 }
244
245 #[test]
246 fn test_simple_comparison() {
247 let cond = parse_condition("message.payload.status == \"ok\"").unwrap();
248 match cond {
249 Condition::Comparison(c) => {
250 assert_eq!(c.op, Comparator::Eq);
251 match &c.left {
252 Operand::Path(p) => assert_eq!(p.segments.len(), 3),
253 _ => panic!("expected path"),
254 }
255 match &c.right {
256 Operand::Literal(Literal::String(s)) => assert_eq!(s, "ok"),
257 _ => panic!("expected string literal"),
258 }
259 }
260 _ => panic!("expected comparison"),
261 }
262 }
263
264 #[test]
265 fn test_wildcard_path() {
266 let cond = parse_condition("message.payload.items[*].qty > 0").unwrap();
267 match cond {
268 Condition::Comparison(c) => {
269 match &c.left {
270 Operand::Path(p) => {
271 assert_eq!(p.segments.len(), 5); assert_eq!(p.segments[3], PathSegment::Wildcard);
273 }
274 _ => panic!("expected path"),
275 }
276 }
277 _ => panic!("expected comparison"),
278 }
279 }
280
281 #[test]
282 fn test_in_operator() {
283 let cond = parse_condition("message.payload.type in [\"A\", \"B\"]").unwrap();
284 match cond {
285 Condition::Comparison(c) => {
286 assert_eq!(c.op, Comparator::In);
287 }
288 _ => panic!("expected comparison"),
289 }
290 }
291
292 #[test]
293 fn test_matches_operator() {
294 let cond = parse_condition("message.payload.email matches \"@\"").unwrap();
295 match cond {
296 Condition::Comparison(c) => {
297 assert_eq!(c.op, Comparator::Matches);
298 }
299 _ => panic!("expected comparison"),
300 }
301 }
302
303 #[test]
304 fn test_bracket_index() {
305 let cond = parse_condition("message.payload.items[0].name == \"test\"").unwrap();
306 match cond {
307 Condition::Comparison(c) => {
308 match &c.left {
309 Operand::Path(p) => {
310 assert_eq!(p.segments.len(), 5);
311 assert_eq!(p.segments[3], PathSegment::Index(0));
312 }
313 _ => panic!("expected path"),
314 }
315 }
316 _ => panic!("expected comparison"),
317 }
318 }
319}