pub mod functions;
pub mod preprocessor;
mod repositories;
pub mod script;
pub mod variable;
use functions::build_functions;
pub use repositories::{Repositories, RepositoryFunction};
use rhai::serde::from_dynamic;
pub use rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext};
pub use script::{Script, ScriptError};
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use valu3::prelude::*;
pub fn build_engine(repositories: Option<Repositories>) -> Arc<Engine> {
let mut engine = build_functions();
if let Some(repositories) = repositories {
for (key, repo) in repositories.repositories {
let call: Arc<
dyn Fn(Value) -> std::pin::Pin<Box<dyn Future<Output = Value> + Send>>
+ Send
+ Sync,
> = repo.function.clone();
let arg_types: Vec<std::any::TypeId> =
vec![std::any::TypeId::of::<Dynamic>(); repo.args.len()];
let repo_args = repo.args.clone();
let call_clone = call.clone();
let handler_arc: Arc<
dyn Fn(&NativeCallContext, &mut [&mut Dynamic]) -> Result<Value, Box<EvalAltResult>>
+ Send
+ Sync,
> = Arc::new(move |_context, args| {
let mut args_map = HashMap::new();
for dynamic in args.iter() {
let value: Value = from_dynamic(&*dynamic).unwrap_or(Value::Null);
if let Some(key) = repo_args.get(args_map.len()) {
args_map.insert(key.clone(), value);
}
}
if repo_args.len() > 1 && args_map.len() == 1 {
if let Some((_only_key, only_value_ref)) = args_map.iter().next() {
let only_value = only_value_ref.clone();
if let Value::Object(obj) = only_value {
args_map = obj
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
}
}
}
let call = call_clone.clone();
let args_value = args_map.to_value();
let result = if let Ok(handle) = tokio::runtime::Handle::try_current() {
let call = call.clone();
let args_value = args_value.clone();
tokio::task::block_in_place(move || {
let future = (call)(args_value);
handle.block_on(future)
})
} else {
tokio::runtime::Runtime::new()
.expect("failed to create runtime")
.block_on((call)(args_value))
};
Ok(result)
});
{
let handler_clone = handler_arc.clone();
engine.register_raw_fn(&key, arg_types, move |c, a| (handler_clone)(&c, a));
}
{
let handler_clone = handler_arc.clone();
engine.register_raw_fn(&key, &[std::any::TypeId::of::<Dynamic>()], move |c, a| {
(handler_clone)(&c, a)
});
}
}
}
Arc::new(engine)
}
fn args_to_abstration(name: String, args: &Vec<String>) -> String {
let args = args.join(", ");
let target = format!("__module_{}", &name);
format!("{}({}){{{}([{}])}}", name, args, target, args) }
pub fn wrap_async_fn<F, Fut>(
name: String,
func: F,
args: Vec<String>,
) -> repositories::RepositoryFunction
where
F: Fn(Value) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Value> + Send + 'static,
{
RepositoryFunction {
function: Arc::new(move |value| Box::pin(func(value))),
abstration: args_to_abstration(name, &args),
args,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use valu3::value::Value;
#[tokio::test(flavor = "multi_thread")]
async fn test_repository_function() {
let mut repositories = HashMap::new();
let mock_function = wrap_async_fn(
"process".to_string(),
|value: Value| async move {
if let Value::Object(obj) = value {
if let Some(Value::String(s)) = obj.get("input") {
Value::from(format!("{}-processed", s))
} else {
Value::Null
}
} else {
Value::Null
}
},
vec!["input".into()],
);
repositories.insert("process".to_string(), mock_function);
let repos = Repositories { repositories };
let engine = build_engine(Some(repos));
let result: Value = engine.eval(r#"process("data")"#).unwrap();
assert_eq!(result, Value::from("data-processed"));
}
#[test]
fn test_respository_log() {
let mut repositories = HashMap::new();
let mock_function = wrap_async_fn(
"log".into(),
|value: Value| async move {
if let Value::Object(obj) = value {
let level = obj
.get("level")
.and_then(|v| Some(v.as_str()))
.unwrap_or("info");
let message = obj
.get("message")
.and_then(|v| Some(v.as_str()))
.unwrap_or("no message");
Value::from(format!("Logged [{}]: {}", level, message))
} else if let Value::String(s) = value {
Value::from(format!("Logged: {}", s))
} else {
Value::from("Logged: unknown")
}
},
vec!["level".into(), "message".into()],
);
repositories.insert("log".to_string(), mock_function);
let repos = Repositories { repositories };
let engine = build_engine(Some(repos));
let result: Value = engine.eval(r#"log("info", "message")"#).unwrap();
assert_eq!(result, Value::from("Logged [info]: message"));
let result: Value = engine
.eval(r#"log(#{"level": "warn", "message": "message"})"#)
.unwrap();
assert_eq!(result, Value::from("Logged [warn]: message"));
}
}