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}