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}