phlow_engine/
script.rs

1use crate::context::Context;
2use crate::variable::Variable;
3use phlow_sdk::prelude::*;
4use rhai::{
5    plugin::*,
6    serde::{from_dynamic, to_dynamic},
7    Engine, EvalAltResult, ParseError, Scope, AST,
8};
9use std::{collections::HashMap, sync::Arc};
10
11#[derive(Debug)]
12pub enum ScriptError {
13    EvalError(Box<EvalAltResult>),
14    InvalidType(Value),
15    CompileError(String, ParseError),
16}
17
18#[derive(Debug, Clone)]
19pub struct Script {
20    map_extracted: Value,
21    map_index_ast: HashMap<usize, AST>,
22    engine: Arc<Engine>,
23}
24
25impl Script {
26    pub fn try_build(engine: Arc<Engine>, script: &Value) -> Result<Self, ScriptError> {
27        let mut map_index_ast = HashMap::new();
28        let mut counter = 0;
29        let map_extracted =
30            Self::extract_primitives(&engine, &script, &mut map_index_ast, &mut counter)?;
31
32        Ok(Self {
33            map_extracted,
34            map_index_ast,
35            engine,
36        })
37    }
38
39    pub fn to_code_string(code: &str) -> String {
40        let code = code.trim();
41        if code.starts_with("{{") && code.ends_with("}}") {
42            code[2..code.len() - 2].to_string()
43        } else if code.parse::<i128>().is_ok()
44            || code.parse::<f64>().is_ok()
45            || code == "true".to_string()
46            || code == "false".to_string()
47            || code == "null".to_string()
48            || code == "undefined".to_string()
49        {
50            code.to_string()
51        } else {
52            format!("`{}`", code)
53        }
54    }
55
56    pub fn evaluate(&self, context: &Context) -> Result<Value, ScriptError> {
57        let mut scope = Scope::new();
58
59        let steps: Dynamic = to_dynamic(context.steps.clone()).map_err(ScriptError::EvalError)?;
60        let main: Dynamic = to_dynamic(context.main.clone()).map_err(ScriptError::EvalError)?;
61        let payload: Dynamic =
62            to_dynamic(context.payload.clone()).map_err(ScriptError::EvalError)?;
63        let input: Dynamic = to_dynamic(context.input.clone()).map_err(ScriptError::EvalError)?;
64
65        scope.push_constant("steps", steps);
66        scope.push_constant("main", main);
67        scope.push_constant("payload", payload);
68        scope.push_constant("input", input);
69
70        let mut result_map: HashMap<usize, Value> = HashMap::new();
71
72        for (key, value) in self.map_index_ast.iter() {
73            let value = self
74                .engine
75                .eval_ast_with_scope(&mut scope, &value)
76                .map_err(ScriptError::EvalError)?;
77
78            result_map.insert(*key, from_dynamic(&value).map_err(ScriptError::EvalError)?);
79        }
80
81        let result = Self::replace_primitives(&self.map_extracted, &result_map);
82
83        Ok(result)
84    }
85
86    pub fn evaluate_variable(&self, context: &Context) -> Result<Variable, ScriptError> {
87        let value = self.evaluate(context)?;
88        Ok(Variable::new(value))
89    }
90
91    fn extract_primitives(
92        engine: &Engine,
93        value: &Value,
94        map_index_ast: &mut HashMap<usize, AST>,
95        counter: &mut usize,
96    ) -> Result<Value, ScriptError> {
97        match value {
98            Value::Object(map) => {
99                let mut new_map = HashMap::new();
100
101                for (key, value) in map.iter() {
102                    let item = Self::extract_primitives(engine, value, map_index_ast, counter)?;
103                    new_map.insert(key.to_string(), item);
104                }
105
106                Ok(Value::from(new_map))
107            }
108            Value::Array(array) => {
109                let mut new_array = Vec::new();
110                for value in array.into_iter() {
111                    let item = Self::extract_primitives(engine, value, map_index_ast, counter)?;
112
113                    new_array.push(item);
114                }
115
116                Ok(Value::from(new_array))
117            }
118            _ => {
119                let code = Self::to_code_string(&value.to_string());
120
121                let ast = match engine.compile(&code) {
122                    Ok(ast) => ast,
123                    Err(err) => return Err(ScriptError::CompileError(code.clone(), err)),
124                };
125                map_index_ast.insert(*counter, ast);
126
127                let result = Value::from(*counter);
128                *counter += 1;
129
130                Ok(result)
131            }
132        }
133    }
134
135    fn replace_primitives(map_extracted: &Value, result: &HashMap<usize, Value>) -> Value {
136        match map_extracted {
137            Value::Object(map) => {
138                let mut new_map = HashMap::new();
139                for (key, value) in map.iter() {
140                    new_map.insert(key.to_string(), Self::replace_primitives(value, result));
141                }
142                Value::from(new_map)
143            }
144            Value::Array(array) => {
145                let mut new_array = Vec::new();
146                for value in array.into_iter() {
147                    new_array.push(Self::replace_primitives(value, result));
148                }
149                Value::from(new_array)
150            }
151            _ => {
152                let index = match map_extracted.to_i64() {
153                    Some(index) => index as usize,
154                    None => panic!("Index not found"),
155                };
156                let value = match result.get(&index) {
157                    Some(value) => value.clone(),
158                    None => panic!("Index not found"),
159                };
160
161                value
162            }
163        }
164    }
165}
166
167#[cfg(test)]
168mod test {
169    use crate::{id::ID, step_worker::StepWorker};
170
171    use super::*;
172    use phs::build_engine;
173    use std::collections::HashMap;
174
175    #[test]
176    fn test_payload_execute() {
177        let script: &str = r#"{{
178            let a = 10;
179            let b = 20;
180            a + b
181        }}"#;
182
183        let context = Context::new();
184        let engine = build_engine(None);
185        let payload = Script::try_build(engine, &script.to_value()).unwrap();
186
187        let result = payload.evaluate(&context).unwrap();
188        assert_eq!(result, Value::from(30i64));
189    }
190
191    #[test]
192    fn test_payload_json() {
193        let script = r#"{{
194            let a = 10;
195            let b = 20;
196            let c = "hello";
197            
198            #{
199                a: a,
200                b: b,
201                sum: a + b
202            }
203        }}"#;
204
205        let context = Context::new();
206        let engine = build_engine(None);
207        let payload = Script::try_build(engine, &script.to_value()).unwrap();
208
209        let result = payload.evaluate(&context).unwrap();
210        let expected = Value::from({
211            let mut map = HashMap::new();
212            map.insert("a".to_string(), Value::from(10i64));
213            map.insert("b".to_string(), Value::from(20i64));
214            map.insert("sum".to_string(), Value::from(30i64));
215            map
216        });
217
218        assert_eq!(result, expected);
219    }
220
221    #[test]
222    fn test_payload_execute_variable() {
223        let script = "hello world";
224
225        let context = Context::new();
226        let engine = build_engine(None);
227        let payload = Script::try_build(engine, &script.to_value()).unwrap();
228
229        let variable = payload.evaluate_variable(&context).unwrap();
230        assert_eq!(variable, Variable::new(Value::from("hello world")));
231    }
232
233    #[test]
234    fn test_payload_execute_variable_context() {
235        let script = r#"{{
236            let a = payload.a;
237            let b = payload.b;
238            a + b
239        }}"#;
240
241        let context = Context::from_payload(Value::from({
242            let mut map = HashMap::new();
243            map.insert("a".to_string(), Value::from(10i64));
244            map.insert("b".to_string(), Value::from(20i64));
245            map
246        }));
247
248        let engine = build_engine(None);
249        let payload = Script::try_build(engine, &script.to_value()).unwrap();
250
251        let variable = payload.evaluate_variable(&context).unwrap();
252        assert_eq!(variable, Variable::new(Value::from(30i64)));
253    }
254
255    #[test]
256    fn test_payload_execute_variable_context_params() {
257        let script = r#"{{payload.a}}"#;
258
259        let context = Context::from_payload(Value::from({
260            let mut map = HashMap::new();
261            map.insert("a".to_string(), Value::from(10i64));
262            map.insert("b".to_string(), Value::from(20i64));
263            map
264        }));
265
266        let engine = build_engine(None);
267        let payload = Script::try_build(engine, &script.to_value()).unwrap();
268
269        let variable = payload.evaluate_variable(&context).unwrap();
270        assert_eq!(variable, Variable::new(Value::from(10i64)));
271    }
272
273    #[test]
274    fn test_payload_execute_variable_step() {
275        let script = r#"{{
276            let a = steps.me.a;
277            let b = steps.me.b;
278   
279            a + b
280        }}"#;
281        let step = StepWorker {
282            id: ID::from("me"),
283            ..Default::default()
284        };
285
286        let mut context = Context::new();
287        context.add_step_id_output(step.get_id().clone(), {
288            let mut map = HashMap::new();
289            map.insert("a".to_string(), Value::from(10i64));
290            map.insert("b".to_string(), Value::from(20i64));
291            map.to_value()
292        });
293
294        let engine = build_engine(None);
295        let payload = Script::try_build(engine, &script.to_value()).unwrap();
296
297        let variable = payload.evaluate_variable(&context).unwrap();
298
299        assert_eq!(variable, Variable::new(Value::from(30i64)));
300    }
301}