switchbot_api/
conditional_expression.rs

1use std::{borrow::Cow, fmt::Display, sync::LazyLock};
2
3use regex::Regex;
4
5#[derive(Debug, Default, PartialEq)]
6pub(crate) struct ConditionalExpression<'a> {
7    pub key: &'a str,
8    operator: &'a str,
9    value: &'a str,
10}
11
12impl Display for ConditionalExpression<'_> {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        write!(f, "{}{}{}", self.key, self.operator, self.value)
15    }
16}
17
18impl<'a> TryFrom<&'a str> for ConditionalExpression<'a> {
19    type Error = anyhow::Error;
20
21    fn try_from(condition: &'a str) -> Result<Self, Self::Error> {
22        const RE_PAT: &str = r"^([a-zA-Z]+)(\s*(=|<=?|>=?)\s*([a-zA-Z0-9]+))?$";
23        static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(RE_PAT).unwrap());
24        if let Some(captures) = RE.captures(condition) {
25            return Ok(ConditionalExpression {
26                key: captures.get(1).map_or_else(|| "", |m| m.as_str()),
27                operator: captures.get(3).map_or_else(|| "", |m| m.as_str()),
28                value: captures.get(4).map_or_else(|| "", |m| m.as_str()),
29            });
30        }
31        Err(anyhow::anyhow!(r#"Not a valid expression "{condition}""#))
32    }
33}
34
35impl ConditionalExpression<'_> {
36    pub fn evaluate(&self, value: &serde_json::Value) -> anyhow::Result<bool> {
37        let value_str: Cow<'_, str> = match value {
38            serde_json::Value::Bool(b) => {
39                if self.operator.is_empty() {
40                    log::debug!("evaluate: bool {b}");
41                    return Ok(*b);
42                }
43                Cow::Owned(b.to_string())
44            }
45            serde_json::Value::Number(num) => {
46                if let Some(num_as_f64) = num.as_f64() {
47                    let value_as_f64: f64 = self.value.parse()?;
48                    return Self::eval_op(self.operator, num_as_f64, value_as_f64);
49                }
50                Cow::Owned(value.to_string())
51            }
52            serde_json::Value::String(str) => Cow::Borrowed(str),
53            _ => Cow::Owned(value.to_string()),
54        };
55        if self.operator == "=" {
56            let result = value_str == self.value;
57            log::debug!(r#"evaluate: "{value_str}"="{}" -> {result}"#, self.value);
58            return Ok(result);
59        }
60        anyhow::bail!("Unsupported condition {self} for {value}");
61    }
62
63    fn eval_op<T: Display + PartialOrd>(op: &str, left: T, right: T) -> anyhow::Result<bool> {
64        let result = match op {
65            "=" => left == right,
66            "<" => left < right,
67            "<=" => left <= right,
68            ">" => left > right,
69            ">=" => left >= right,
70            _ => anyhow::bail!("Unsupported operator: {op}"),
71        };
72        log::debug!(r#"evaluate: "{left}"{op}"{right}" -> {result}"#);
73        Ok(result)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn parse(str: &str) -> anyhow::Result<ConditionalExpression> {
82        ConditionalExpression::try_from(str)
83    }
84
85    fn from_key(key: &str) -> ConditionalExpression {
86        ConditionalExpression {
87            key,
88            ..Default::default()
89        }
90    }
91
92    fn from_strs<'a>(key: &'a str, operator: &'a str, value: &'a str) -> ConditionalExpression<'a> {
93        ConditionalExpression {
94            key,
95            operator,
96            value,
97        }
98    }
99
100    #[test]
101    fn parse_condition() -> anyhow::Result<()> {
102        assert_eq!(parse("a")?, from_key("a"));
103        assert_eq!(parse("a=b")?, from_strs("a", "=", "b"));
104        assert_eq!(parse("a = b")?, from_strs("a", "=", "b"));
105        assert!(parse("a=").is_err());
106        assert!(parse("1=a").is_err());
107        assert_eq!(parse("a=12")?, from_strs("a", "=", "12"));
108        assert_eq!(parse("aZ=xZ2")?, from_strs("aZ", "=", "xZ2"));
109
110        assert_eq!(parse("a<b")?, from_strs("a", "<", "b"));
111        assert_eq!(parse("a>b")?, from_strs("a", ">", "b"));
112        assert_eq!(parse("a<=b")?, from_strs("a", "<=", "b"));
113        assert_eq!(parse("a>=b")?, from_strs("a", ">=", "b"));
114        Ok(())
115    }
116
117    fn evaluate(expr: &str, value: impl serde::Serialize) -> anyhow::Result<bool> {
118        ConditionalExpression::try_from(expr)?.evaluate(&serde_json::json!(value))
119    }
120
121    #[test]
122    fn evaluate_bool() -> anyhow::Result<()> {
123        assert!(evaluate("a", true)?);
124        assert!(!(evaluate("a", false)?));
125        assert!(evaluate("a=true", true)?);
126        assert!(!(evaluate("a=true", false)?));
127        assert!(evaluate("a=false", false)?);
128        assert!(evaluate("a>true", false).is_err());
129        Ok(())
130    }
131
132    #[test]
133    fn evaluate_str() -> anyhow::Result<()> {
134        assert!(evaluate("a", "on").is_err());
135        assert!(evaluate("a=on", "on")?);
136        assert!(!(evaluate("a=on", "off")?));
137        assert!(evaluate("a>on", "off").is_err());
138        Ok(())
139    }
140
141    #[test]
142    fn evaluate_num() -> anyhow::Result<()> {
143        assert!(evaluate("a", 123).is_err());
144        assert!(evaluate("a=123", 123)?);
145        assert!(!(evaluate("a=123", 124)?));
146
147        assert!(evaluate("a<123", 122)?);
148        assert!(!(evaluate("a<123", 123)?));
149        assert!(evaluate("a>123", 124)?);
150        assert!(!(evaluate("a>123", 123)?));
151        assert!(evaluate("a<=123", 123)?);
152        assert!(!(evaluate("a<=123", 124)?));
153        assert!(evaluate("a>=123", 123)?);
154        assert!(!(evaluate("a>=123", 122)?));
155        Ok(())
156    }
157}