1pub mod functions;
2pub mod preprocessor;
3mod repositories;
4pub mod script;
5pub mod variable;
6use functions::build_functions;
7pub use repositories::{Repositories, RepositoryFunction};
8use rhai::serde::from_dynamic;
9pub use rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext};
10pub use script::{Script, ScriptError};
11use std::collections::HashMap;
12use std::future::Future;
13use std::sync::Arc;
14use valu3::prelude::*;
15
16pub fn build_engine(repositories: Option<Repositories>) -> Arc<Engine> {
17 let mut engine = build_functions();
18
19 if let Some(repositories) = repositories {
20 for (key, repo) in repositories.repositories {
21 let call: Arc<
22 dyn Fn(Value) -> std::pin::Pin<Box<dyn Future<Output = Value> + Send>>
23 + Send
24 + Sync,
25 > = repo.function.clone();
26
27 let arg_types: Vec<std::any::TypeId> =
28 vec![std::any::TypeId::of::<Dynamic>(); repo.args.len()];
29 let repo_args = repo.args.clone();
30 let call_clone = call.clone();
31
32 let handler_arc: Arc<
33 dyn Fn(&NativeCallContext, &mut [&mut Dynamic]) -> Result<Value, Box<EvalAltResult>>
34 + Send
35 + Sync,
36 > = Arc::new(move |_context, args| {
37 let mut args_map = HashMap::new();
38
39 for dynamic in args.iter() {
40 let value: Value = from_dynamic(&*dynamic).unwrap_or(Value::Null);
42
43 if let Some(key) = repo_args.get(args_map.len()) {
44 args_map.insert(key.clone(), value);
45 }
46 }
47
48 if repo_args.len() > 1 && args_map.len() == 1 {
53 if let Some((_only_key, only_value_ref)) = args_map.iter().next() {
54 let only_value = only_value_ref.clone();
56 if let Value::Object(obj) = only_value {
57 args_map = obj
59 .iter()
60 .map(|(k, v)| (k.to_string(), v.clone()))
61 .collect();
62 }
63 }
64 }
65
66 let call = call_clone.clone();
67 let args_value = args_map.to_value();
68
69 let result = if let Ok(handle) = tokio::runtime::Handle::try_current() {
74 let call = call.clone();
75 let args_value = args_value.clone();
76 tokio::task::block_in_place(move || {
77 let future = (call)(args_value);
78 handle.block_on(future)
79 })
80 } else {
81 tokio::runtime::Runtime::new()
83 .expect("failed to create runtime")
84 .block_on((call)(args_value))
85 };
86
87 Ok(result)
88 });
89
90 {
92 let handler_clone = handler_arc.clone();
93 engine.register_raw_fn(&key, arg_types, move |c, a| (handler_clone)(&c, a));
94 }
95
96 {
98 let handler_clone = handler_arc.clone();
99 engine.register_raw_fn(&key, &[std::any::TypeId::of::<Dynamic>()], move |c, a| {
100 (handler_clone)(&c, a)
101 });
102 }
103 }
104 }
105
106 Arc::new(engine)
107}
108
109fn args_to_abstration(name: String, args: &Vec<String>) -> String {
110 let args = args.join(", ");
111 let target = format!("__module_{}", &name);
112
113 format!("{}({}){{{}([{}])}}", name, args, target, args) }
115
116pub fn wrap_async_fn<F, Fut>(
117 name: String,
118 func: F,
119 args: Vec<String>,
120) -> repositories::RepositoryFunction
121where
122 F: Fn(Value) -> Fut + Send + Sync + 'static,
123 Fut: Future<Output = Value> + Send + 'static,
124{
125 RepositoryFunction {
126 function: Arc::new(move |value| Box::pin(func(value))),
127 abstration: args_to_abstration(name, &args),
128 args,
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::collections::HashMap;
136 use valu3::value::Value;
137
138 #[tokio::test(flavor = "multi_thread")]
139 async fn test_repository_function() {
140 let mut repositories = HashMap::new();
141
142 let mock_function = wrap_async_fn(
143 "process".to_string(),
144 |value: Value| async move {
145 if let Value::Object(obj) = value {
147 if let Some(Value::String(s)) = obj.get("input") {
148 Value::from(format!("{}-processed", s))
149 } else {
150 Value::Null
151 }
152 } else {
153 Value::Null
154 }
155 },
156 vec!["input".into()],
157 );
158
159 repositories.insert("process".to_string(), mock_function);
160
161 let repos = Repositories { repositories };
162 let engine = build_engine(Some(repos));
163
164 let result: Value = engine.eval(r#"process("data")"#).unwrap();
165
166 assert_eq!(result, Value::from("data-processed"));
167 }
168
169 #[test]
170 fn test_respository_log() {
171 let mut repositories = HashMap::new();
172
173 let mock_function = wrap_async_fn(
174 "log".into(),
175 |value: Value| async move {
176 if let Value::Object(obj) = value {
178 let level = obj
179 .get("level")
180 .and_then(|v| Some(v.as_str()))
181 .unwrap_or("info");
182 let message = obj
183 .get("message")
184 .and_then(|v| Some(v.as_str()))
185 .unwrap_or("no message");
186
187 Value::from(format!("Logged [{}]: {}", level, message))
188 } else if let Value::String(s) = value {
189 Value::from(format!("Logged: {}", s))
191 } else {
192 Value::from("Logged: unknown")
193 }
194 },
195 vec!["level".into(), "message".into()],
196 );
197
198 repositories.insert("log".to_string(), mock_function);
199
200 let repos = Repositories { repositories };
201 let engine = build_engine(Some(repos));
202
203 let result: Value = engine.eval(r#"log("info", "message")"#).unwrap();
204 assert_eq!(result, Value::from("Logged [info]: message"));
205
206 let result: Value = engine
207 .eval(r#"log(#{"level": "warn", "message": "message"})"#)
208 .unwrap();
209 assert_eq!(result, Value::from("Logged [warn]: message"));
210 }
211}