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 crate::engine::build_engine_async;
235
236    use super::*;
237
238    #[test]
239    fn test_condition_execute_equal() {
240        let engine = build_engine_async(None);
241        let condition = Condition::try_build_with_operator(
242            engine,
243            "10".to_string(),
244            "20".to_string(),
245            Operator::Equal,
246        )
247        .unwrap();
248
249        let context = Context::new();
250
251        let result = condition.evaluate(&context).unwrap();
252        assert_eq!(result, false);
253    }
254
255    #[test]
256    fn test_condition_execute_not_equal() {
257        let engine = build_engine_async(None);
258        let condition = Condition::try_build_with_operator(
259            engine,
260            "10".to_string(),
261            "20".to_string(),
262            Operator::NotEqual,
263        )
264        .unwrap();
265
266        let context = Context::new();
267
268        let result = condition.evaluate(&context).unwrap();
269        assert_eq!(result, true);
270    }
271
272    #[test]
273    fn test_condition_execute_greater_than() {
274        let engine = build_engine_async(None);
275        let condition = Condition::try_build_with_operator(
276            engine,
277            "10".to_string(),
278            "20".to_string(),
279            Operator::GreaterThan,
280        )
281        .unwrap();
282
283        let context = Context::new();
284
285        let result = condition.evaluate(&context).unwrap();
286        assert_eq!(result, false);
287    }
288
289    #[test]
290    fn test_condition_execute_contains() {
291        let engine = build_engine_async(None);
292        let condition = Condition::try_build_with_operator(
293            engine,
294            "hello world".to_string(),
295            "hello".to_string(),
296            Operator::Contains,
297        )
298        .unwrap();
299
300        let context = Context::new();
301
302        let result = condition.evaluate(&context).unwrap();
303        assert_eq!(result, true);
304    }
305
306    #[test]
307    fn test_condition_execute_regex() {
308        let engine = build_engine_async(None);
309        let condition = Condition::try_build_with_operator(
310            engine,
311            "hello".to_string(),
312            "hello world".to_string(),
313            Operator::Regex,
314        )
315        .unwrap();
316
317        let context = Context::new();
318
319        let result = condition.evaluate(&context).unwrap();
320        assert_eq!(result, true);
321    }
322
323    #[test]
324    fn test_condition_execute_not_regex() {
325        let engine = build_engine_async(None);
326        let condition = Condition::try_build_with_operator(
327            engine,
328            "hello".to_string(),
329            "hello world".to_string(),
330            Operator::NotRegex,
331        )
332        .unwrap();
333
334        let context = Context::new();
335
336        let result = condition.evaluate(&context).unwrap();
337        assert_eq!(result, false);
338    }
339
340    #[test]
341    fn test_condition_execute_start_with() {
342        let engine = build_engine_async(None);
343        let condition = Condition::try_build_with_operator(
344            engine,
345            "hello world".to_string(),
346            "hello".to_string(),
347            Operator::StartsWith,
348        )
349        .unwrap();
350
351        let context = Context::new();
352
353        let result = condition.evaluate(&context).unwrap();
354        assert_eq!(result, true);
355    }
356
357    #[test]
358    fn test_condition_execute_end_with() {
359        let engine = build_engine_async(None);
360        let condition = Condition::try_build_with_operator(
361            engine,
362            "hello world".to_string(),
363            "world".to_string(),
364            Operator::EndsWith,
365        )
366        .unwrap();
367
368        let context = Context::new();
369
370        let result = condition.evaluate(&context).unwrap();
371        assert_eq!(result, true);
372    }
373}