ucp/pred/
field_cmp.rs

1//! CLI helper for key comparison predicate representation and syntax parsing.
2//!
3//! This is very similar to [`crate::pred::cmp`], but with support for key parsing.
4//!
5//! ## Use
6//!
7//! It can be used to parse CLI options such as:
8//!
9//! ```sh
10//! list-files -f size>1024              # keep files with size greater than 1024 bytes (1 KiB)
11//! list-files -f size=1024              # keep files with size equals to 1024 bytes (1 KiB)
12//! list-files -f count>1 -f size>=1024  # keep duplicated files with size greater or equals to 1024 bytes (1 KiB)
13//! ```
14//!
15//! ## Syntax options
16//!
17//! Unlike [`crate::pred::cmp`], this module only supports [`Symbol`] syntax.
18
19use crate::pred::cmp::parser::{OpParser, OpParserError};
20use crate::pred::cmp::{Comparison, Operator, Symbol};
21use std::fmt::Display;
22use std::str::FromStr;
23
24#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
25pub struct FieldComparison<K, V, S = Symbol> {
26    key: K,
27    inner: Comparison<V, S>,
28}
29
30impl<K, V, S> FieldComparison<K, V, S>
31where
32    V: PartialOrd,
33{
34    pub fn test(&self, value: V) -> bool {
35        self.inner.test(value)
36    }
37}
38
39impl<K, V, S> FieldComparison<K, V, S> {
40    pub fn new(key: K, operator: Operator, value: V) -> Self {
41        Self {
42            key,
43            inner: Comparison::new(operator, value),
44        }
45    }
46
47    fn new_with_inner(key: K, inner: Comparison<V, S>) -> Self {
48        Self { key, inner }
49    }
50
51    pub fn key(&self) -> &K {
52        &self.key
53    }
54
55    pub fn operator(&self) -> Operator {
56        self.inner.operator()
57    }
58
59    pub fn value(&self) -> &V {
60        self.inner.value()
61    }
62
63    pub fn into_inner(self) -> (K, Comparison<V, S>) {
64        (self.key, self.inner)
65    }
66}
67
68impl<K, V> FromStr for FieldComparison<K, V, Symbol>
69where
70    K: FromStr,
71    V: FromStr,
72    <K as FromStr>::Err: Display,
73    <V as FromStr>::Err: Display,
74    Comparison<V, Symbol>: FromStr<Err = String>,
75{
76    type Err = String;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        // [ref:000]
80        // Note: `simple_op_parser` parser translates empty operators to `Eq`.
81        // Example: `1024` is the same as `=1024`.
82        // However, in our case, if the user provides `1024`, we will error out with
83        // the correct suggestion of `key=1024`. If `key1024` is provided, the same
84        // error will be printed out because `key1024` parses correctly, but this
85        // parser needs the `simple_op_parser` to fail.
86        match OpParser::simple_op_parser(s) {
87            Ok((op, text)) => {
88                let op = match op {
89                    Operator::Ne => "≠",
90                    Operator::Eq => "=",
91                    Operator::Gt => ">",
92                    Operator::Lt => "<",
93                    Operator::Gte => ">=",
94                    Operator::Lte => "<=",
95                };
96
97                Err(format!(
98                    "missing key in expression `{s}`, should be in the form `key{op}{text}`"
99                ))
100            }
101            Err(OpParserError::InvalidCharsBeforeOp {
102                text_before_op,
103                op_index: before_op_index,
104                ..
105            }) => {
106                let key = text_before_op
107                    .trim_end_matches(' ')
108                    .parse::<K>()
109                    .map_err(|e| e.to_string())?;
110
111                let text_after = &s[before_op_index..];
112                let inner = text_after.parse::<Comparison<V, Symbol>>()?;
113                Ok(Self::new_with_inner(key, inner))
114            }
115            Err(OpParserError::Other(s)) => Err(s),
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::pred::field_cmp::FieldComparison;
123    use crate::pred::Operator;
124
125    macro_rules! test_cases {
126        (
127            key_type = $key_type:ty;
128            value_type = $value_type:ty;
129            $($operator:expr => [$($input:literal),+] = ($expected_key:literal, $expected_value:literal),)+
130        ) => {$($(
131            {
132                let input = $input;
133                let pred = input.parse::<FieldComparison<$key_type, $value_type>>().unwrap();
134                assert_eq!(pred.key(), &$expected_key, "input: {input}");
135                assert_eq!(pred.operator(), $operator, "input: {input}");
136                assert_eq!(pred.value(), &$expected_value, "input: {input}");
137            }
138        )+)+};
139    }
140
141    #[test]
142    fn test_field_comparison() {
143        test_cases!(
144            key_type = String;
145            value_type = usize;
146            Operator::Eq => ["size=1024", "size =1024", "size = 1024"] = ("size", 1024),
147            Operator::Ne => ["size≠1024", "size ≠ 1024", "size!1024", "size ! 1024"] = ("size", 1024),
148            Operator::Gt => ["size>1024", "size >1024", "size  >1024"] = ("size", 1024),
149            Operator::Gte => ["size>=1024", "size >=1024", "size >= 1024"] = ("size", 1024),
150            Operator::Lt => ["size<1024", "size <1024", "size  <1024"] = ("size", 1024),
151            Operator::Lte => ["size<=1024", "size <=1024", "size <= 1024"] = ("size", 1024),
152        );
153    }
154
155    /// Tests the case in comment `ref:000`.
156    #[test]
157    fn test_case_in_comment() {
158        let field_comparison = "size1024".parse::<FieldComparison<String, usize>>();
159        assert_eq!(
160            field_comparison,
161            Err(String::from(
162                "missing key in expression `size1024`, should be in the form `key=size1024`"
163            ))
164        );
165    }
166}