use crate::Result;
use crate::hub::{HubEvent, get_hub};
use crate::model::{LogKind, RuntimeCtx};
use crate::run::Literals;
use crate::runtime::Runtime;
use crate::script::aip_modules::aip_lua;
use crate::script::serde_value_to_lua_value;
use crate::script::support::process_lua_eval_result;
use mlua::{IntoLua, Lua, Table, Value};
pub struct LuaEngine {
#[allow(unused)]
name: String,
lua: Lua,
#[allow(unused)]
runtime: Runtime,
}
impl Drop for LuaEngine {
fn drop(&mut self) {
}
}
impl LuaEngine {
pub fn new(runtime: Runtime, name: impl Into<String>) -> Result<Self> {
let name = name.into();
let lua = Lua::new();
init_aip(&lua, &runtime)?;
init_null(&lua)?;
init_print(&runtime, &lua)?;
let engine = LuaEngine { name, lua, runtime };
Ok(engine)
}
pub fn new_with_ctx(runtime: Runtime, ctx: &Literals, rt_ctx: RuntimeCtx) -> Result<Self> {
let mut name_buf: Vec<&str> = Vec::new();
if let Some(_run_uid) = rt_ctx.run_uid() {
name_buf.push("run")
}
if let Some(_task) = rt_ctx.task_uid() {
name_buf.push("task")
}
if let Some(stage) = rt_ctx.stage() {
let stage: &'static str = stage.into();
name_buf.push(stage);
}
let name = name_buf.join(" - ");
let engine = LuaEngine::new(runtime, name)?;
let lua = &engine.lua;
let ctx = ctx.to_lua(&engine)?;
let ctx = if let Value::Table(ctx) = ctx {
if let Some(run_uid) = rt_ctx.run_uid() {
ctx.set("RUN_UID", run_uid.to_string())?;
}
if let Some(run_num) = rt_ctx.run_num() {
ctx.set("RUN_NUM", run_num)?;
}
if let Some(parent_run_uid) = rt_ctx.parent_run_uid() {
ctx.set("PARENT_RUN_UID", parent_run_uid.to_string())?;
}
if let Some(task_uid) = rt_ctx.task_uid() {
ctx.set("TASK_UID", task_uid.to_string())?;
}
if let Some(task_num) = rt_ctx.task_num() {
ctx.set("TASK_NUM", task_num)?;
}
if let Some(stage) = rt_ctx.stage() {
ctx.set("STAGE", stage.to_string())?;
}
if let Some(flow_redo_run_count) = rt_ctx.flow_redo_run_count() {
ctx.set("RUN_FLOW_REDO_COUNT", flow_redo_run_count)?;
}
Value::Table(ctx)
} else {
ctx
};
let globals = lua.globals();
globals.set("CTX", ctx)?;
Ok(engine)
}
}
impl LuaEngine {
pub async fn eval(&self, script: &str, scope: Option<Table>) -> Result<Value> {
self.eval_with_paths(script, scope, std::iter::empty::<&str>()).await
}
pub async fn eval_with_paths<I, S>(&self, script: &str, scope: Option<Table>, addl_lua_paths: I) -> Result<Value>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let lua = &self.lua;
let chunck = lua.load(script);
let chunck = if let Some(scope) = scope {
let env = self.upgrade_scope(scope, addl_lua_paths)?;
chunck.set_environment(env)
} else {
chunck
};
let res = chunck.eval_async::<Value>().await;
let res = process_lua_eval_result(&self.lua, res, script)?;
Ok(res)
}
pub fn create_table(&self) -> Result<Table> {
let res = self.lua.create_table()?;
Ok(res)
}
pub fn serde_to_lua_value(&self, val: serde_json::Value) -> Result<Value> {
serde_value_to_lua_value(&self.lua, val)
}
#[allow(unused)]
pub fn to_lua(&self, val: impl IntoLua) -> Result<Value> {
let res = val.into_lua(&self.lua)?;
Ok(res)
}
}
impl LuaEngine {
fn upgrade_scope<I, S>(&self, scope: Table, addl_base_lua_paths: I) -> Result<Table>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let globals = self.lua.globals();
for pair in globals.pairs::<Value, Value>() {
let (key, value) = pair?;
scope.set(key, value)?; }
let lua_paths = addl_base_lua_paths.into_iter().collect::<Vec<_>>();
if !lua_paths.is_empty() {
let mut paths: Vec<String> = Vec::new();
for path in lua_paths {
let path = path.as_ref();
paths.push(format!("{path}/lua/?.lua;{path}/lua/?/init.lua"));
}
if let Ok(lua_package) = globals.get::<Table>("package") {
let path: String = lua_package.get("path")?;
let joined_paths = paths.join(";");
let new_path = format!("{joined_paths};{path}");
lua_package.set("path", new_path)?;
}
}
Ok(scope)
}
}
fn init_null(lua: &Lua) -> Result<()> {
let globals = lua.globals();
globals.set("null", Value::NULL)?;
globals.set("Null", Value::NULL)?;
globals.set("NULL", Value::NULL)?;
globals.set(
"is_null",
lua.create_function(|_, v: Value| Ok(matches!(v, Value::Nil) || v == Value::NULL))?,
)?;
globals.set(
"nil_if_null",
lua.create_function(|_, v: Value| {
if matches!(v, Value::Nil) || v == Value::NULL {
Ok(Value::Nil)
} else {
Ok(v)
}
})?,
)?;
globals.set(
"value_or",
lua.create_function(|_, (v, alt): (Value, Value)| {
if matches!(v, Value::Nil) || v == Value::NULL {
Ok(alt)
} else {
Ok(v)
}
})?,
)?;
Ok(())
}
fn init_print(runtime: &Runtime, lua: &Lua) -> Result<()> {
let globals = lua.globals();
let rt = runtime.clone();
globals.set(
"print",
lua.create_function(move |lua, args: mlua::Variadic<Value>| lua_print(lua, &rt, args))?,
)?;
Ok(())
}
fn lua_print(lua: &Lua, runtime: &Runtime, args: mlua::Variadic<Value>) -> mlua::Result<()> {
let output: Vec<String> = args
.into_iter()
.map(|arg| match arg {
Value::String(s) => s.to_str().map(|s| s.to_string()).unwrap_or_default(),
Value::Number(n) => n.to_string(),
Value::Integer(n) => n.to_string(),
Value::Boolean(b) => b.to_string(),
_ => {
let res = aip_lua::dump(lua, arg);
res.unwrap_or_else(|err| format!("Cannot print content.\nCause: {err}"))
}
})
.collect();
let text = output.join("\n");
let ctx = RuntimeCtx::extract_from_global(lua)?;
runtime.rec_log_with_rt_ctx(&ctx, LogKind::AgentPrint, &text)?;
get_hub().publish_sync(HubEvent::LuaPrint(text.into()));
Ok(())
}
macro_rules! init_and_set {
($table:expr, $lua:expr, $runtime:expr, $($name:ident),*) => {
paste::paste! {
$(
let $name = super::aip_modules::[<aip_ $name>]::init_module($lua, $runtime)?;
$table.set(stringify!($name), $name)?;
)*
}
};
}
fn init_aip(lua_vm: &Lua, runtime: &Runtime) -> Result<()> {
let table = lua_vm.create_table()?;
init_and_set!(
table, lua_vm, runtime, flow, file, git, web, text, rust, path, md, tag, json, toml, csv, yaml, html, cmd, lua, code, hbs, semver, agent, uuid, hash, time, shape, pdf, editor, zip, udiffx
);
init_and_set!(table, lua_vm, runtime, run, task);
let globals = lua_vm.globals();
globals.set("aip", &table)?;
globals.set("utils", table)?;
Ok(())
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
use crate::runtime::Runtime;
#[tokio::test]
async fn test_lua_engine_eval_simple_ok() -> Result<()> {
let runtime = Runtime::new_test_runtime_sandbox_01().await?;
let engine = LuaEngine::new(runtime.clone(), "test_lua_engine_eval_simple_ok")?;
let fx_script = r#"
local square_root = math.sqrt(25)
return "Hello " .. my_name .. " - " .. square_root
"#;
let scope = engine.create_table()?;
scope.set("my_name", "Lua World")?;
let res = engine.eval(fx_script, Some(scope)).await?;
let res = serde_json::to_value(res)?;
let res = res.as_str().ok_or("Should be string")?;
assert_eq!(res, "Hello Lua World - 5.0");
Ok(())
}
}