phlow_engine/
script.rs

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