phlow_engine/
condition.rs

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