phs/
lib.rs

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                    // dynamic: &mut Dynamic
41                    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                // Se o repositório espera múltiplos argumentos, mas recebemos
49                // um único argumento que é um objeto, desembrulhe esse objeto
50                // e use-o como `args_map`. Isso permite chamadas como
51                // fn(a, b) onde o usuário passou um único objeto {a:.., b:..}.
52                if repo_args.len() > 1 && args_map.len() == 1 {
53                    if let Some((_only_key, only_value_ref)) = args_map.iter().next() {
54                        // clone para obter um `Value` owned e poder mover o Object
55                        let only_value = only_value_ref.clone();
56                        if let Value::Object(obj) = only_value {
57                            // substituir args_map pelo conteúdo do objeto
58                            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                // Try to use the current Tokio runtime if present. If there is no
70                // runtime (e.g. running in a synchronous unit test), create a
71                // temporary runtime to execute the future. When inside a runtime
72                // we use `block_in_place` + `Handle::block_on` to block safely.
73                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                    // No runtime available — create a new temporary runtime.
82                    tokio::runtime::Runtime::new()
83                        .expect("failed to create runtime")
84                        .block_on((call)(args_value))
85                };
86
87                Ok(result)
88            });
89
90            // Register using the full arg list
91            {
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            // Register using a single Dynamic argument (fallback)
97            {
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) // name(arg1, arg2){__module_name([arg1, arg2])}
114}
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                // O valor recebido é um objeto com os argumentos mapeados
146                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                // `build_engine` maps named args into an object, e.g. {"message": "..."}
177                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                    // fallback: accept a plain string too
190                    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}