phlow_engine/
condition.rs

1use std::sync::Arc;
2
3use phlow_sdk::valu3;
4use rhai::Engine;
5use serde::Serialize;
6use valu3::{prelude::StringBehavior, traits::ToValueBehavior, value::Value};
7
8use crate::{
9    context::Context,
10    script::{Script, ScriptError},
11};
12
13#[derive(Debug)]
14pub enum ConditionError {
15    InvalidOperator(String),
16    RightInvalid(String),
17    LeftInvalid(String),
18    AssertInvalid(String),
19    ScriptError(ScriptError),
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize)]
23pub enum Operator {
24    Or,
25    And,
26    Equal,
27    NotEqual,
28    GreaterThan,
29    LessThan,
30    GreaterThanOrEqual,
31    LessThanOrEqual,
32    Contains,
33    NotContains,
34    StartsWith,
35    EndsWith,
36    Regex,
37    NotRegex,
38}
39
40impl ToValueBehavior for Operator {
41    fn to_value(&self) -> Value {
42        match self {
43            Operator::Or => "or".to_value(),
44            Operator::And => "and".to_value(),
45            Operator::Equal => "equal".to_value(),
46            Operator::NotEqual => "not_equal".to_value(),
47            Operator::GreaterThan => "greater_than".to_value(),
48            Operator::LessThan => "less_than".to_value(),
49            Operator::GreaterThanOrEqual => "greater_than_or_equal".to_value(),
50            Operator::LessThanOrEqual => "less_than_or_equal".to_value(),
51            Operator::Contains => "contains".to_value(),
52            Operator::NotContains => "not_contains".to_value(),
53            Operator::StartsWith => "starts_with".to_value(),
54            Operator::EndsWith => "ends_with".to_value(),
55            Operator::Regex => "regex".to_value(),
56            Operator::NotRegex => "not_regex".to_value(),
57        }
58    }
59}
60
61impl From<&Value> for Operator {
62    fn from(value: &Value) -> Self {
63        match value.as_str() {
64            "or" => Operator::Or,
65            "and" => Operator::And,
66            "equal" => Operator::Equal,
67            "not_equal" => Operator::NotEqual,
68            "greater_than" => Operator::GreaterThan,
69            "less_than" => Operator::LessThan,
70            "greater_than_or_equal" => Operator::GreaterThanOrEqual,
71            "less_than_or_equal" => Operator::LessThanOrEqual,
72            "contains" => Operator::Contains,
73            "not_contains" => Operator::NotContains,
74            "starts_with" => Operator::StartsWith,
75            "ends_with" => Operator::EndsWith,
76            "regex" => Operator::Regex,
77            "not_regex" => Operator::NotRegex,
78            _ => panic!("Invalid operator"),
79        }
80    }
81}
82
83#[derive(Debug, Clone)]
84pub struct Condition {
85    pub(crate) expression: Script,
86    pub(crate) raw: Value,
87}
88
89impl Condition {
90    pub fn try_from_value(engine: Arc<Engine>, value: &Value) -> Result<Self, ConditionError> {
91        if let Some(assert) = value.get("assert") {
92            return Ok(Self::try_build_with_assert(engine, assert.to_string())?);
93        }
94
95        let left = match value.get("left") {
96            Some(left) => left.to_string(),
97            None => return Err(ConditionError::LeftInvalid("does not exist".to_string())),
98        };
99
100        let right = match value.get("right") {
101            Some(right) => {
102                if let Value::String(right) = right {
103                    right.to_string()
104                } else {
105                    right.to_json(valu3::prelude::JsonMode::Inline)
106                }
107            }
108            None => return Err(ConditionError::RightInvalid("does not exist".to_string())),
109        };
110
111        let operator = match value.get("operator") {
112            Some(operator) => Operator::from(operator),
113            None => {
114                return Err(ConditionError::InvalidOperator(
115                    "does not exist".to_string(),
116                ))
117            }
118        };
119
120        let condition = Self::try_build_with_operator(engine, left, right, operator)?;
121
122        Ok(condition)
123    }
124
125    pub fn try_build_with_assert(
126        engine: Arc<Engine>,
127        assert: String,
128    ) -> Result<Self, ConditionError> {
129        let expression =
130            Script::try_build(engine, &assert.to_value()).map_err(ConditionError::ScriptError)?;
131
132        Ok(Self {
133            expression,
134            raw: assert.to_value(),
135        })
136    }
137
138    pub fn try_build_with_operator(
139        engine: Arc<Engine>,
140        left: String,
141        right: String,
142        operator: Operator,
143    ) -> Result<Self, ConditionError> {
144        let left = Script::to_code_string(&left);
145        let right = Script::to_code_string(&right);
146
147        let assert = {
148            match operator {
149                Operator::Or => {
150                    let query = format!("{{{{{} || {}}}}}", left, right);
151                    query
152                }
153                Operator::And => {
154                    let query = format!("{{{{{} && {}}}}}", left, right);
155                    query
156                }
157                Operator::Equal => {
158                    let query = format!("{{{{{} == {}}}}}", left, right);
159                    query
160                }
161                Operator::NotEqual => {
162                    let query = format!("{{{{{} != {}}}}}", left, right);
163                    query
164                }
165                Operator::GreaterThan => {
166                    let query = format!("{{{{{} > {}}}}}", left, right);
167                    query
168                }
169                Operator::LessThan => {
170                    let query = format!("{{{{{} < {}}}}}", left, right);
171                    query
172                }
173                Operator::GreaterThanOrEqual => {
174                    let query = format!("{{{{{} >= {}}}}}", left, right);
175                    query
176                }
177                Operator::LessThanOrEqual => {
178                    let query = format!("{{{{{} <= {}}}}}", left, right);
179                    query
180                }
181                Operator::Contains => {
182                    let query = format!("{{{{{} in {}}}}}", right, left);
183                    query
184                }
185                Operator::NotContains => {
186                    let query = format!("{{{{!({} in {})}}}}", right, left);
187                    query
188                }
189                Operator::StartsWith => {
190                    let query = format!("{{{{{} starts_with {}}}}}", left, right);
191                    query
192                }
193                Operator::EndsWith => {
194                    let query = format!("{{{{{} ends_with {}}}}}", left, right);
195                    query
196                }
197                Operator::Regex => {
198                    let query = format!("{{{{{} search {}}}}}", left, right);
199                    query
200                }
201                Operator::NotRegex => {
202                    let query = format!("{{{{!({} search {})}}}}", left, right);
203                    query
204                }
205            }
206        };
207
208        let expression =
209            Script::try_build(engine, &assert.to_value()).map_err(ConditionError::ScriptError)?;
210
211        Ok(Self {
212            expression,
213            raw: assert.to_value(),
214        })
215    }
216
217    pub fn evaluate(&self, context: &Context) -> Result<bool, ConditionError> {
218        let result = self
219            .expression
220            .evaluate(context)
221            .map_err(ConditionError::ScriptError)?;
222
223        match result {
224            Value::Boolean(result) => Ok(result),
225            _ => Err(ConditionError::ScriptError(ScriptError::InvalidType(
226                result,
227            ))),
228        }
229    }
230}
231
232#[cfg(test)]
233mod test {
234    use super::*;
235    use phs::build_engine;
236
237    #[test]
238    fn test_condition_execute_equal() {
239        let engine = build_engine(None);
240        let condition = Condition::try_build_with_operator(
241            engine,
242            "10".to_string(),
243            "20".to_string(),
244            Operator::Equal,
245        )
246        .unwrap();
247
248        let context = Context::new();
249
250        let result = condition.evaluate(&context).unwrap();
251        assert_eq!(result, false);
252    }
253
254    #[test]
255    fn test_condition_execute_not_equal() {
256        let engine = build_engine(None);
257        let condition = Condition::try_build_with_operator(
258            engine,
259            "10".to_string(),
260            "20".to_string(),
261            Operator::NotEqual,
262        )
263        .unwrap();
264
265        let context = Context::new();
266
267        let result = condition.evaluate(&context).unwrap();
268        assert_eq!(result, true);
269    }
270
271    #[test]
272    fn test_condition_execute_greater_than() {
273        let engine = build_engine(None);
274        let condition = Condition::try_build_with_operator(
275            engine,
276            "10".to_string(),
277            "20".to_string(),
278            Operator::GreaterThan,
279        )
280        .unwrap();
281
282        let context = Context::new();
283
284        let result = condition.evaluate(&context).unwrap();
285        assert_eq!(result, false);
286    }
287
288    #[test]
289    fn test_condition_execute_contains() {
290        let engine = build_engine(None);
291        let condition = Condition::try_build_with_operator(
292            engine,
293            "hello world".to_string(),
294            "hello".to_string(),
295            Operator::Contains,
296        )
297        .unwrap();
298
299        let context = Context::new();
300
301        let result = condition.evaluate(&context).unwrap();
302        assert_eq!(result, true);
303    }
304
305    #[test]
306    fn test_condition_execute_regex() {
307        let engine = build_engine(None);
308        let condition = Condition::try_build_with_operator(
309            engine,
310            "hello".to_string(),
311            "hello world".to_string(),
312            Operator::Regex,
313        )
314        .unwrap();
315
316        let context = Context::new();
317
318        let result = condition.evaluate(&context).unwrap();
319        assert_eq!(result, true);
320    }
321
322    #[test]
323    fn test_condition_execute_not_regex() {
324        let engine = build_engine(None);
325        let condition = Condition::try_build_with_operator(
326            engine,
327            "hello".to_string(),
328            "hello world".to_string(),
329            Operator::NotRegex,
330        )
331        .unwrap();
332
333        let context = Context::new();
334
335        let result = condition.evaluate(&context).unwrap();
336        assert_eq!(result, false);
337    }
338
339    #[test]
340    fn test_condition_execute_start_with() {
341        let engine = build_engine(None);
342        let condition = Condition::try_build_with_operator(
343            engine,
344            "hello world".to_string(),
345            "hello".to_string(),
346            Operator::StartsWith,
347        )
348        .unwrap();
349
350        let context = Context::new();
351
352        let result = condition.evaluate(&context).unwrap();
353        assert_eq!(result, true);
354    }
355
356    #[test]
357    fn test_condition_execute_end_with() {
358        let engine = build_engine(None);
359        let condition = Condition::try_build_with_operator(
360            engine,
361            "hello world".to_string(),
362            "world".to_string(),
363            Operator::EndsWith,
364        )
365        .unwrap();
366
367        let context = Context::new();
368
369        let result = condition.evaluate(&context).unwrap();
370        assert_eq!(result, true);
371    }
372}