etdl-parser 0.1.1

ETDL parser: event tree (IEC 62502) and fault tree (IEC 61025) documents, ECEL condition expressions, AsyncAPI 3.0 JSON Pointer resolution
Documentation
use nom::{
    branch::alt,
    bytes::complete::{tag, take_while1},
    character::complete::{alpha1, alphanumeric1, char, digit1, multispace0},
    combinator::{map, opt, recognize, value},
    multi::{many0, separated_list0},
    sequence::{delimited, pair, preceded},
    IResult,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Condition {
    Default,
    Comparison(Comparison),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Comparison {
    pub left: Operand,
    pub op: Comparator,
    pub right: Operand,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Operand {
    Path(PathExpr),
    Literal(Literal),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PathExpr {
    pub segments: Vec<PathSegment>,
}

impl PathExpr {
    pub fn new(segments: Vec<PathSegment>) -> Self {
        PathExpr { segments }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PathSegment {
    Field(String),
    Wildcard,
    Index(usize),
    QuotedKey(String),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Comparator {
    Eq,
    Neq,
    Gte,
    Lte,
    Gt,
    Lt,
    In,
    Matches,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Literal {
    Number(f64),
    String(String),
    Bool(bool),
    Null,
    Array(Vec<Literal>),
}

pub fn parse_condition(input: &str) -> Result<Condition, String> {
    if input.trim() == "default" {
        return Ok(Condition::Default);
    }

    match parse_comparison(input) {
        Ok((remaining, comparison)) => {
            if remaining.trim().is_empty() {
                Ok(Condition::Comparison(comparison))
            } else {
                Err(format!(
                    "trailing content in condition expression: '{}'",
                    remaining
                ))
            }
        }
        Err(e) => Err(format!("failed to parse condition expression: {}", e)),
    }
}

fn parse_comparison(input: &str) -> IResult<&str, Comparison> {
    let (input, left) = parse_operand(input)?;
    let (input, _) = multispace0(input)?;
    let (input, op) = parse_comparator(input)?;
    let (input, _) = multispace0(input)?;
    let (input, right) = parse_operand(input)?;
    Ok((input, Comparison { left, op, right }))
}

fn parse_operand(input: &str) -> IResult<&str, Operand> {
    alt((
        map(parse_path_expr, Operand::Path),
        map(parse_literal, Operand::Literal),
    ))(input)
}

fn parse_path_expr(input: &str) -> IResult<&str, PathExpr> {
    let (input, root) = parse_root_var(input)?;
    let (input, segments) = many0(parse_member_access)(input)?;
    let mut all_segments = vec![PathSegment::Field(root)];
    all_segments.extend(segments);
    Ok((input, PathExpr::new(all_segments)))
}

fn parse_root_var(input: &str) -> IResult<&str, String> {
    let (input, _) = tag("message")(input)?;
    Ok((input, "message".to_string()))
}

fn parse_member_access(input: &str) -> IResult<&str, PathSegment> {
    alt((
        parse_dot_access,
        parse_bracket_access,
    ))(input)
}

fn parse_dot_access(input: &str) -> IResult<&str, PathSegment> {
    let (input, _) = char('.')(input)?;
    let (input, ident) = parse_identifier(input)?;
    Ok((input, PathSegment::Field(ident)))
}

fn parse_bracket_access(input: &str) -> IResult<&str, PathSegment> {
    delimited(
        char('['),
        alt((
            map(tag("*"), |_| PathSegment::Wildcard),
            map(parse_index, PathSegment::Index),
            map(parse_quoted_key, PathSegment::QuotedKey),
        )),
        char(']'),
    )(input)
}

fn parse_identifier(input: &str) -> IResult<&str, String> {
    map(
        recognize(pair(
            alpha1,
            many0(alt((alphanumeric1, tag("_")))),
        )),
        |s: &str| s.to_string(),
    )(input)
}

fn parse_index(input: &str) -> IResult<&str, usize> {
    map(digit1, |s: &str| s.parse::<usize>().unwrap())(input)
}

fn parse_quoted_key(input: &str) -> IResult<&str, String> {
    delimited(
        char('"'),
        map(
            take_while1(|c: char| c != '"' && c >= '\x20' && c <= '\x7e'),
            |s: &str| s.to_string(),
        ),
        char('"'),
    )(input)
}

fn parse_comparator(input: &str) -> IResult<&str, Comparator> {
    alt((
        value(Comparator::Eq, tag("==")),
        value(Comparator::Neq, tag("!=")),
        value(Comparator::Gte, tag(">=")),
        value(Comparator::Lte, tag("<=")),
        value(Comparator::Gt, tag(">")),
        value(Comparator::Lt, tag("<")),
        value(Comparator::In, tag("in")),
        value(Comparator::Matches, tag("matches")),
    ))(input)
}

fn parse_literal(input: &str) -> IResult<&str, Literal> {
    alt((
        value(Literal::Bool(true), tag("true")),
        value(Literal::Bool(false), tag("false")),
        value(Literal::Null, tag("null")),
        map(parse_number, Literal::Number),
        map(parse_string_literal, Literal::String),
        map(parse_array_literal, Literal::Array),
    ))(input)
}

fn parse_array_literal(input: &str) -> IResult<&str, Vec<Literal>> {
    delimited(
        char('['),
        preceded(
            multispace0,
            separated_list0(
                preceded(multispace0, char(',')),
                preceded(multispace0, parse_literal),
            ),
        ),
        preceded(multispace0, char(']')),
    )(input)
}

fn parse_number(input: &str) -> IResult<&str, f64> {
    let (input, sign) = opt(char('-'))(input)?;
    let (input, int_part) = digit1(input)?;
    let (input, frac_part) = opt(preceded(char('.'), digit1))(input)?;

    let num_str = format!(
        "{}{}{}",
        sign.map(|_| "-").unwrap_or(""),
        int_part,
        frac_part.map(|f| format!(".{}", f)).unwrap_or_default()
    );
    let value = num_str.parse::<f64>().map_err(|_| {
        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
    })?;
    Ok((input, value))
}

fn parse_string_literal(input: &str) -> IResult<&str, String> {
    delimited(
        char('"'),
        map(
            take_while1(|c: char| c != '"'),
            |s: &str| s.to_string(),
        ),
        char('"'),
    )(input)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_condition() {
        assert_eq!(parse_condition("default").unwrap(), Condition::Default);
    }

    #[test]
    fn test_simple_comparison() {
        let cond = parse_condition("message.payload.status == \"ok\"").unwrap();
        match cond {
            Condition::Comparison(c) => {
                assert_eq!(c.op, Comparator::Eq);
                match &c.left {
                    Operand::Path(p) => assert_eq!(p.segments.len(), 3),
                    _ => panic!("expected path"),
                }
                match &c.right {
                    Operand::Literal(Literal::String(s)) => assert_eq!(s, "ok"),
                    _ => panic!("expected string literal"),
                }
            }
            _ => panic!("expected comparison"),
        }
    }

    #[test]
    fn test_wildcard_path() {
        let cond = parse_condition("message.payload.items[*].qty > 0").unwrap();
        match cond {
            Condition::Comparison(c) => {
                match &c.left {
                    Operand::Path(p) => {
                        assert_eq!(p.segments.len(), 5); // message, payload, items, [*], qty
                        assert_eq!(p.segments[3], PathSegment::Wildcard);
                    }
                    _ => panic!("expected path"),
                }
            }
            _ => panic!("expected comparison"),
        }
    }

    #[test]
    fn test_in_operator() {
        let cond = parse_condition("message.payload.type in [\"A\", \"B\"]").unwrap();
        match cond {
            Condition::Comparison(c) => {
                assert_eq!(c.op, Comparator::In);
            }
            _ => panic!("expected comparison"),
        }
    }

    #[test]
    fn test_matches_operator() {
        let cond = parse_condition("message.payload.email matches \"@\"").unwrap();
        match cond {
            Condition::Comparison(c) => {
                assert_eq!(c.op, Comparator::Matches);
            }
            _ => panic!("expected comparison"),
        }
    }

    #[test]
    fn test_bracket_index() {
        let cond = parse_condition("message.payload.items[0].name == \"test\"").unwrap();
        match cond {
            Condition::Comparison(c) => {
                match &c.left {
                    Operand::Path(p) => {
                        assert_eq!(p.segments.len(), 5);
                        assert_eq!(p.segments[3], PathSegment::Index(0));
                    }
                    _ => panic!("expected path"),
                }
            }
            _ => panic!("expected comparison"),
        }
    }
}