ucp 0.1.0

Collection of Useful CLI Parsers
Documentation
//! CLI helper for key comparison predicate representation and syntax parsing.
//!
//! This is very similar to [`crate::pred::cmp`], but with support for key parsing.
//!
//! ## Use
//!
//! It can be used to parse CLI options such as:
//!
//! ```sh
//! list-files -f size>1024              # keep files with size greater than 1024 bytes (1 KiB)
//! list-files -f size=1024              # keep files with size equals to 1024 bytes (1 KiB)
//! list-files -f count>1 -f size>=1024  # keep duplicated files with size greater or equals to 1024 bytes (1 KiB)
//! ```
//!
//! ## Syntax options
//!
//! Unlike [`crate::pred::cmp`], this module only supports [`Symbol`] syntax.

use crate::pred::cmp::parser::{OpParser, OpParserError};
use crate::pred::cmp::{Comparison, Operator, Symbol};
use std::fmt::Display;
use std::str::FromStr;

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct FieldComparison<K, V, S = Symbol> {
    key: K,
    inner: Comparison<V, S>,
}

impl<K, V, S> FieldComparison<K, V, S>
where
    V: PartialOrd,
{
    pub fn test(&self, value: V) -> bool {
        self.inner.test(value)
    }
}

impl<K, V, S> FieldComparison<K, V, S> {
    pub fn new(key: K, operator: Operator, value: V) -> Self {
        Self {
            key,
            inner: Comparison::new(operator, value),
        }
    }

    fn new_with_inner(key: K, inner: Comparison<V, S>) -> Self {
        Self { key, inner }
    }

    pub fn key(&self) -> &K {
        &self.key
    }

    pub fn operator(&self) -> Operator {
        self.inner.operator()
    }

    pub fn value(&self) -> &V {
        self.inner.value()
    }

    pub fn into_inner(self) -> (K, Comparison<V, S>) {
        (self.key, self.inner)
    }
}

impl<K, V> FromStr for FieldComparison<K, V, Symbol>
where
    K: FromStr,
    V: FromStr,
    <K as FromStr>::Err: Display,
    <V as FromStr>::Err: Display,
    Comparison<V, Symbol>: FromStr<Err = String>,
{
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // [ref:000]
        // Note: `simple_op_parser` parser translates empty operators to `Eq`.
        // Example: `1024` is the same as `=1024`.
        // However, in our case, if the user provides `1024`, we will error out with
        // the correct suggestion of `key=1024`. If `key1024` is provided, the same
        // error will be printed out because `key1024` parses correctly, but this
        // parser needs the `simple_op_parser` to fail.
        match OpParser::simple_op_parser(s) {
            Ok((op, text)) => {
                let op = match op {
                    Operator::Ne => "",
                    Operator::Eq => "=",
                    Operator::Gt => ">",
                    Operator::Lt => "<",
                    Operator::Gte => ">=",
                    Operator::Lte => "<=",
                };

                Err(format!(
                    "missing key in expression `{s}`, should be in the form `key{op}{text}`"
                ))
            }
            Err(OpParserError::InvalidCharsBeforeOp {
                text_before_op,
                op_index: before_op_index,
                ..
            }) => {
                let key = text_before_op
                    .trim_end_matches(' ')
                    .parse::<K>()
                    .map_err(|e| e.to_string())?;

                let text_after = &s[before_op_index..];
                let inner = text_after.parse::<Comparison<V, Symbol>>()?;
                Ok(Self::new_with_inner(key, inner))
            }
            Err(OpParserError::Other(s)) => Err(s),
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::pred::field_cmp::FieldComparison;
    use crate::pred::Operator;

    macro_rules! test_cases {
        (
            key_type = $key_type:ty;
            value_type = $value_type:ty;
            $($operator:expr => [$($input:literal),+] = ($expected_key:literal, $expected_value:literal),)+
        ) => {$($(
            {
                let input = $input;
                let pred = input.parse::<FieldComparison<$key_type, $value_type>>().unwrap();
                assert_eq!(pred.key(), &$expected_key, "input: {input}");
                assert_eq!(pred.operator(), $operator, "input: {input}");
                assert_eq!(pred.value(), &$expected_value, "input: {input}");
            }
        )+)+};
    }

    #[test]
    fn test_field_comparison() {
        test_cases!(
            key_type = String;
            value_type = usize;
            Operator::Eq => ["size=1024", "size =1024", "size = 1024"] = ("size", 1024),
            Operator::Ne => ["size≠1024", "size ≠ 1024", "size!1024", "size ! 1024"] = ("size", 1024),
            Operator::Gt => ["size>1024", "size >1024", "size  >1024"] = ("size", 1024),
            Operator::Gte => ["size>=1024", "size >=1024", "size >= 1024"] = ("size", 1024),
            Operator::Lt => ["size<1024", "size <1024", "size  <1024"] = ("size", 1024),
            Operator::Lte => ["size<=1024", "size <=1024", "size <= 1024"] = ("size", 1024),
        );
    }

    /// Tests the case in comment `ref:000`.
    #[test]
    fn test_case_in_comment() {
        let field_comparison = "size1024".parse::<FieldComparison<String, usize>>();
        assert_eq!(
            field_comparison,
            Err(String::from(
                "missing key in expression `size1024`, should be in the form `key=size1024`"
            ))
        );
    }
}