phlow_engine/
script.rs

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