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((parse_dot_access, parse_bracket_access))(input)
122}
123
124fn parse_dot_access(input: &str) -> IResult<&str, PathSegment> {
125 let (input, _) = char('.')(input)?;
126 let (input, ident) = parse_identifier(input)?;
127 Ok((input, PathSegment::Field(ident)))
128}
129
130fn parse_bracket_access(input: &str) -> IResult<&str, PathSegment> {
131 delimited(
132 char('['),
133 alt((
134 map(tag("*"), |_| PathSegment::Wildcard),
135 map(parse_index, PathSegment::Index),
136 map(parse_quoted_key, PathSegment::QuotedKey),
137 )),
138 char(']'),
139 )(input)
140}
141
142fn parse_identifier(input: &str) -> IResult<&str, String> {
143 map(
144 recognize(pair(alpha1, many0(alt((alphanumeric1, tag("_")))))),
145 |s: &str| s.to_string(),
146 )(input)
147}
148
149fn parse_index(input: &str) -> IResult<&str, usize> {
150 map(digit1, |s: &str| {
153 s.bytes().fold(0usize, |acc, b| {
154 acc.saturating_mul(10).saturating_add((b - b'0') as usize)
155 })
156 })(input)
157}
158
159fn parse_quoted_key(input: &str) -> IResult<&str, String> {
160 delimited(
161 char('"'),
162 map(
163 take_while1(|c: char| c != '"' && ('\x20'..='\x7e').contains(&c)),
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(take_while1(|c: char| c != '"'), |s: &str| s.to_string()),
229 char('"'),
230 )(input)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_default_condition() {
239 assert_eq!(parse_condition("default").unwrap(), Condition::Default);
240 }
241
242 #[test]
243 fn test_simple_comparison() {
244 let cond = parse_condition("message.payload.status == \"ok\"").unwrap();
245 match cond {
246 Condition::Comparison(c) => {
247 assert_eq!(c.op, Comparator::Eq);
248 match &c.left {
249 Operand::Path(p) => assert_eq!(p.segments.len(), 3),
250 _ => panic!("expected path"),
251 }
252 match &c.right {
253 Operand::Literal(Literal::String(s)) => assert_eq!(s, "ok"),
254 _ => panic!("expected string literal"),
255 }
256 }
257 _ => panic!("expected comparison"),
258 }
259 }
260
261 #[test]
262 fn test_wildcard_path() {
263 let cond = parse_condition("message.payload.items[*].qty > 0").unwrap();
264 match cond {
265 Condition::Comparison(c) => {
266 match &c.left {
267 Operand::Path(p) => {
268 assert_eq!(p.segments.len(), 5); assert_eq!(p.segments[3], PathSegment::Wildcard);
270 }
271 _ => panic!("expected path"),
272 }
273 }
274 _ => panic!("expected comparison"),
275 }
276 }
277
278 #[test]
279 fn test_in_operator() {
280 let cond = parse_condition("message.payload.type in [\"A\", \"B\"]").unwrap();
281 match cond {
282 Condition::Comparison(c) => {
283 assert_eq!(c.op, Comparator::In);
284 }
285 _ => panic!("expected comparison"),
286 }
287 }
288
289 #[test]
290 fn test_matches_operator() {
291 let cond = parse_condition("message.payload.email matches \"@\"").unwrap();
292 match cond {
293 Condition::Comparison(c) => {
294 assert_eq!(c.op, Comparator::Matches);
295 }
296 _ => panic!("expected comparison"),
297 }
298 }
299
300 #[test]
301 fn test_bracket_index() {
302 let cond = parse_condition("message.payload.items[0].name == \"test\"").unwrap();
303 match cond {
304 Condition::Comparison(c) => match &c.left {
305 Operand::Path(p) => {
306 assert_eq!(p.segments.len(), 5);
307 assert_eq!(p.segments[3], PathSegment::Index(0));
308 }
309 _ => panic!("expected path"),
310 },
311 _ => panic!("expected comparison"),
312 }
313 }
314
315 #[test]
316 fn oversized_index_does_not_panic() {
317 let idx = "9999999999999999999999999999999999999999";
319 let cond =
320 parse_condition(&format!("message.payload.items[{}].name == \"test\"", idx)).unwrap();
321 match cond {
322 Condition::Comparison(c) => match &c.left {
323 Operand::Path(p) => {
324 assert_eq!(p.segments[3], PathSegment::Index(usize::MAX));
325 }
326 _ => panic!("expected path"),
327 },
328 _ => panic!("expected comparison"),
329 }
330 }
331
332 #[test]
333 fn trailing_content_is_error() {
334 assert!(parse_condition("message.payload.ok == true && message.payload.x").is_err());
335 }
336
337 #[test]
338 fn default_is_parsed() {
339 assert!(matches!(parse_condition("default"), Ok(Condition::Default)));
340 }
341
342 #[test]
343 fn negated_numbers_parse() {
344 let cond = parse_condition("message.payload.temp < -5").unwrap();
345 match cond {
346 Condition::Comparison(c) => match &c.right {
347 Operand::Literal(Literal::Number(n)) => assert_eq!(*n, -5.0),
348 _ => panic!("expected negative number"),
349 },
350 _ => panic!("expected comparison"),
351 }
352 }
353}