ucp 0.1.0

Collection of Useful CLI Parsers
Documentation
use crate::pred::cmp::Operator;
use std::error::Error;
use std::fmt::Display;

pub(crate) struct OpParser;

#[derive(Debug)]
pub(crate) enum OpParserError<'a> {
    InvalidCharsBeforeOp {
        text_before_op: &'a str,
        op_index: usize,
        propagate_message: String,
    },
    #[allow(dead_code)]
    Other(String),
}

impl<'a> Display for OpParserError<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidCharsBeforeOp {
                propagate_message, ..
            } => write!(f, "{}", propagate_message),
            Self::Other(message) => write!(f, "{message}"),
        }
    }
}

impl<'a> Error for OpParserError<'a> {}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
struct ParsedOp {
    character: char,
    index: usize,
}

impl ParsedOp {
    fn new(character: char, index: usize) -> Self {
        Self { character, index }
    }

    fn relative_starting_from(self, other: &Self) -> Self {
        Self::new(self.character, self.index + other.index + 1)
    }
}

impl OpParser {
    fn parse_op<'a, 'b>(
        s: &'a str,
        allowed_chars: &'b [char],
    ) -> Result<Option<ParsedOp>, OpParserError<'a>> {
        let op_part = s.find(|c| allowed_chars.contains(&c));
        match op_part {
            None => Ok(None),
            Some(idx) => {
                let op_char_idx = {
                    let mut boundary = idx;
                    loop {
                        if boundary >= s.len() || s.is_char_boundary(boundary + 1) {
                            break boundary;
                        }
                        boundary += 1;
                    }
                };

                let subs = &s[..=op_char_idx].trim();
                if subs.chars().count() > 1 {
                    return Err(OpParserError::InvalidCharsBeforeOp {
                        text_before_op: &subs[..idx],
                        op_index: idx,
                        propagate_message: format!("invalid operator: {subs}"),
                    });
                }

                let Some(char) = subs.chars().next() else {
                    unreachable!("prior check guarantees this should never fail");
                };

                Ok(Some(ParsedOp::new(char, op_char_idx)))
            }
        }
    }

    /// # DevNote
    ///
    /// This parser implementation apparently can support variables with slight modifications,
    /// example: a>b, c>d
    pub(crate) fn simple_op_parser(str: &str) -> Result<(Operator, &str), OpParserError<'_>> {
        // For the sake of simplicity and making the parser as flexible as possible, we
        // test char by char.

        // Operations that can appear as single characters.
        const SINGLE_CHAR_OP: &[char] = &['!', '', '=', '>', '<'];
        // Operations that may appear with two characters.
        const DOUBLE_CHAR_OP: &[char] = &['>', '<'];
        const DOUBLE_CHAR_OP_NEXT: &[char] = &['='];
        let op_part = Self::parse_op(str, SINGLE_CHAR_OP)?;
        let operator = match op_part {
            // `Equals` is the only allowed to appear alone.
            None => (Operator::Eq, str),
            Some(parsed_op) => {
                let new_substr = &str[parsed_op.index + 1..];
                let second_op = if DOUBLE_CHAR_OP.contains(&parsed_op.character) {
                    // We ignore errors in this case because we still can interpret the input
                    Self::parse_op(new_substr, DOUBLE_CHAR_OP_NEXT)
                        .ok()
                        .and_then(|op| op.map(|op| op.relative_starting_from(&parsed_op)))
                } else {
                    None
                };

                let new_str = second_op
                    .as_ref()
                    .map(|op| &str[op.index + 1..])
                    .unwrap_or(new_substr);

                let operator = match (parsed_op.character, second_op.map(|op| op.character)) {
                    ('!' | '', None) => Operator::Ne,
                    ('=', None) => Operator::Eq,
                    ('>', None) => Operator::Gt,
                    ('<', None) => Operator::Lt,
                    ('>', Some('=')) => Operator::Gte,
                    ('<', Some('=')) => Operator::Lte,
                    ('>', Some(c)) => unreachable!("{c} found but not included in the original array, `parse_op` guarantees this never happens."),
                    _ => unreachable!("prior checks guarantee this should never happen"),
                };
                (operator, new_str)
            }
        };

        Ok(operator)
    }
}

#[cfg(test)]
mod tests {
    macro_rules! test_cases {
        ($($operator:expr => [$($input:literal),+] = $expected:literal,)+) => {$($(
            {
                let input = $input;
                let (operator, rest) = OpParser::simple_op_parser(input).unwrap();
                assert_eq!(operator, $operator, "input: {input}");
                assert_eq!(rest, $expected, "input: {input}");
            }
        )+)+};
    }

    #[test]
    fn test_simple_op_parser_good_cases() {
        use super::OpParser;
        use crate::pred::cmp::Operator;

        test_cases!(
            Operator::Eq => ["=10", "10", " =10"] = "10",
            Operator::Eq => ["= 10", " 10", " = 10"] = " 10",
            Operator::Eq => ["==10"] = "=10",
            Operator::Ne => ["≠10", "!10", "  ≠10"] = "10",
            Operator::Ne => ["≠ 10", "! 10", "  ≠ 10"] = " 10",
            Operator::Ne => ["≠=10", "!=10"] = "=10",
            Operator::Gt => [">10", " >10", "  >10"] = "10",
            Operator::Gte => [">=10", " >=10", "  >=10"] = "10",
            Operator::Gte => [">= 10", " >= 10", "  >= 10"] = " 10",
            Operator::Lt => ["<10", " <10", "  <10"] = "10",
            Operator::Lte => ["<=10", " <=10", "  <=10"] = "10",
            Operator::Lte => ["<= 10", " <= 10", "  <= 10"] = " 10",
            Operator::Lt => ["<<10", " <<10", "  <<10"] = "<10",
            Operator::Lt => ["<a=10", " <a=10", "  <a=10"] = "a=10",
            Operator::Gt => [">>10", " >>10", "  >>10"] = ">10",
            Operator::Gt => [">a=10", " >a=10", "  >a=10"] = "a=10",
        );
    }

    #[test]
    fn test_simple_op_parser_bad_cases() {
        use super::OpParser;

        let bad_cases = &["a>10", "a>=10", "c<10", "c<=10", "c=10", "c!10"];

        for input in bad_cases {
            let result = OpParser::simple_op_parser(input);
            assert!(result.is_err(), "input: {input}");
        }
    }
}