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((
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); // message, payload, items, [*], qty
272                        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}