Skip to main content

etdl_parser/
ecel.rs

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    // `s.parse::<usize>()` panics on overflow (>= 20 digits); use a saturating
151    // fold so untrusted input can never crash the parser.
152    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); // message, payload, items, [*], qty
269                        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        // 40 digits overflows usize; must parse to usize::MAX, never panic.
318        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}