use std::path::Path;
use mlua::{Function, Lua, LuaSerdeExt, Table, Value as LuaValue};
use serde_json::Value;
use super::context::{DependencyInfo, PackageInfo, TransformContext};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub struct TransformEngine {
lua: Lua,
trace_mode: bool,
}
impl TransformEngine {
pub fn new(workspace_root: &Path) -> Result<Self, BoxError> {
Self::with_trace(workspace_root, false)
}
pub fn with_trace(workspace_root: &Path, trace_mode: bool) -> Result<Self, BoxError> {
let lua = Lua::new();
let context = TransformContext::new(workspace_root)?;
register_helpers(&lua)?;
register_context_api(&lua, &context, trace_mode)?;
Ok(Self { lua, trace_mode })
}
pub fn apply_transform(
&self,
matrix: &mut Vec<serde_json::Map<String, Value>>,
script: &str,
) -> Result<(), BoxError> {
let original_len = matrix.len();
let original_matrix = if self.trace_mode {
Some(matrix.clone())
} else {
None
};
if self.trace_mode {
log::info!("[Transform] Input matrix: {original_len} entries");
}
let lua_matrix = self.lua.to_value(matrix).map_err(|e| {
format!(
"Failed to convert matrix to Lua value: {e}\nMatrix: {}",
serde_json::to_string_pretty(matrix).unwrap_or_default()
)
})?;
self.lua
.load(script)
.exec()
.map_err(|e| format!("Failed to load transform script: {e}"))?;
let transform_fn: Function = self
.lua
.globals()
.get("transform")
.map_err(|e| format!("Transform script must define a 'transform' function: {e}"))?;
let context_table: Table = self
.lua
.globals()
.get("context")
.map_err(|e| format!("Failed to get context table: {e}"))?;
let result: LuaValue = transform_fn
.call((context_table, lua_matrix))
.map_err(|e| {
let mut err_msg = format!("Transform function failed: {e}");
if let Some(orig) = &original_matrix {
use std::fmt::Write;
write!(
&mut err_msg,
"\n\nMatrix before transform:\n{}",
serde_json::to_string_pretty(orig).unwrap_or_default()
)
.unwrap();
}
err_msg
})?;
*matrix = self.lua.from_value(result).map_err(|e| {
format!(
"Transform function must return a valid matrix array: {e}\n\
Make sure your transform function returns the matrix (or modified copy)"
)
})?;
if self.trace_mode {
let new_len = matrix.len();
let delta_str = if new_len >= original_len {
format!("+{}", new_len - original_len)
} else {
format!("-{}", original_len - new_len)
};
log::info!("[Transform] Output matrix: {new_len} entries ({delta_str} change)");
if new_len != original_len
&& let Some(orig) = original_matrix
{
log::debug!("[Transform] Matrix diff:");
log::debug!(" Removed: {}", original_len.saturating_sub(new_len));
log::debug!(
" Before: {}",
serde_json::to_string(&orig).unwrap_or_default()
);
log::debug!(
" After: {}",
serde_json::to_string(matrix).unwrap_or_default()
);
}
}
Ok(())
}
}
fn register_helpers(lua: &Lua) -> Result<(), BoxError> {
lua.load(
r"
-- table.filter: filter elements based on predicate
function table.filter(t, predicate)
local result = {}
for _, v in ipairs(t) do
if predicate(v) then
table.insert(result, v)
end
end
return result
end
-- table.map: transform elements
function table.map(t, fn)
local result = {}
for _, v in ipairs(t) do
table.insert(result, fn(v))
end
return result
end
-- table.contains: check if value exists
function table.contains(t, value)
for _, v in ipairs(t) do
if v == value then
return true
end
end
return false
end
-- table.find: find first element matching predicate
function table.find(t, predicate)
for _, v in ipairs(t) do
if predicate(v) then
return v
end
end
return nil
end
",
)
.exec()
.map_err(|e| format!("Failed to register helpers: {e}"))?;
Ok(())
}
fn lua_err(e: &mlua::Error) -> BoxError {
format!("{e}").into()
}
#[allow(clippy::too_many_lines)]
fn register_context_api(
lua: &Lua,
context: &TransformContext,
trace_mode: bool,
) -> Result<(), BoxError> {
let globals = lua.globals();
let context_table = lua.create_table().map_err(|e| lua_err(&e))?;
let ctx = context.clone();
context_table
.set(
"get_package",
lua.create_function(move |lua, (_self, name): (mlua::Value, String)| {
let Some(pkg) = ctx.get_package(&name) else {
return Err(mlua::Error::RuntimeError(format!(
"Package not found: {name}"
)));
};
create_package_table(lua, pkg)
})
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
let ctx = context.clone();
context_table
.set(
"is_workspace_member",
lua.create_function(move |_lua, (_self, name): (mlua::Value, String)| {
Ok(ctx.is_workspace_member(&name))
})
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
let ctx = context.clone();
context_table
.set(
"get_all_packages",
lua.create_function(move |_lua, _self: mlua::Value| Ok(ctx.get_all_packages()))
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
let ctx = context.clone();
context_table
.set(
"package_depends_on",
lua.create_function(
move |_lua, (_self, pkg, dep): (mlua::Value, String, String)| {
Ok(ctx.package_depends_on(&pkg, &dep))
},
)
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
let ctx = context.clone();
context_table
.set(
"feature_exists",
lua.create_function(
move |_lua, (_self, pkg, feat): (mlua::Value, String, String)| {
Ok(ctx.feature_exists(&pkg, &feat))
},
)
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
context_table
.set(
"log",
lua.create_function(|_lua, (_self, message): (mlua::Value, String)| {
log::info!("[Transform] {message}");
Ok(())
})
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
context_table
.set(
"warn",
lua.create_function(|_lua, (_self, message): (mlua::Value, String)| {
log::warn!("[Transform] {message}");
Ok(())
})
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
context_table
.set(
"error",
lua.create_function(|_lua, (_self, message): (mlua::Value, String)| {
log::error!("[Transform] {message}");
Ok(())
})
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
context_table
.set(
"debug",
lua.create_function(move |lua, (_self, data): (mlua::Value, mlua::Value)| {
let debug_str = lua
.from_value::<serde_json::Value>(data.clone())
.map_or_else(
|_| format!("{data:?}"),
|json_val| {
serde_json::to_string_pretty(&json_val)
.unwrap_or_else(|_| format!("{data:?}"))
},
);
if trace_mode {
log::debug!("[Transform Debug]\n{debug_str}");
} else {
log::trace!("[Transform Debug]\n{debug_str}");
}
Ok(())
})
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
let ctx = context.clone();
context_table
.set(
"inspect",
lua.create_function(
move |_lua, (_self, pkg_name, feature): (mlua::Value, String, String)| {
let Some(pkg) = ctx.get_package(&pkg_name) else {
return Ok(format!("Package '{pkg_name}' not found"));
};
let mut output = format!("Inspecting {pkg_name}:{feature}\n");
if !pkg.has_feature(&feature) {
use std::fmt::Write;
writeln!(&mut output, " ⚠ Feature '{feature}' does not exist").unwrap();
return Ok(output);
}
let deps = pkg.feature_activates_dependencies(&feature);
if deps.is_empty() {
output.push_str(" No dependencies activated\n");
} else {
output.push_str(" Activates dependencies:\n");
for dep in deps {
use std::fmt::Write;
writeln!(
&mut output,
" → {}{}",
dep.name,
if dep.features.is_empty() {
String::new()
} else {
format!("/{}", dep.features.join(","))
}
)
.unwrap();
}
}
Ok(output)
},
)
.map_err(|e| lua_err(&e))?,
)
.map_err(|e| lua_err(&e))?;
globals
.set("context", context_table)
.map_err(|e| lua_err(&e))?;
Ok(())
}
fn create_package_table(lua: &Lua, pkg: &PackageInfo) -> mlua::Result<Table> {
let pkg_table = lua.create_table()?;
pkg_table.set("name", pkg.name.clone())?;
pkg_table.set("path", pkg.path.to_string_lossy().to_string())?;
let pkg_clone = pkg.clone();
pkg_table.set(
"depends_on",
lua.create_function(move |_lua, (_self, dep_name): (mlua::Value, String)| {
Ok(pkg_clone.depends_on(&dep_name))
})?,
)?;
let pkg_clone = pkg.clone();
pkg_table.set(
"has_feature",
lua.create_function(move |_lua, (_self, feature): (mlua::Value, String)| {
Ok(pkg_clone.has_feature(&feature))
})?,
)?;
let pkg_clone = pkg.clone();
pkg_table.set(
"feature_definition",
lua.create_function(move |_lua, (_self, feature): (mlua::Value, String)| {
Ok(pkg_clone.feature_definition(&feature).cloned())
})?,
)?;
let pkg_clone = pkg.clone();
pkg_table.set(
"feature_activates_dependencies",
lua.create_function(move |lua, (_self, feature): (mlua::Value, String)| {
let deps = pkg_clone.feature_activates_dependencies(&feature);
create_dependencies_table(lua, &deps)
})?,
)?;
let pkg_clone = pkg.clone();
pkg_table.set(
"skips_feature_on_os",
lua.create_function(
move |_lua, (_self, feature, os): (mlua::Value, String, String)| {
Ok(pkg_clone.skips_feature_on_os(&feature, &os))
},
)?,
)?;
let pkg_clone = pkg.clone();
pkg_table.set(
"get_all_features",
lua.create_function(move |_lua, _self: mlua::Value| Ok(pkg_clone.get_all_features()))?,
)?;
Ok(pkg_table)
}
fn create_dependencies_table(lua: &Lua, deps: &[DependencyInfo]) -> mlua::Result<Table> {
let deps_table = lua.create_table()?;
for (i, dep) in deps.iter().enumerate() {
let dep_entry = lua.create_table()?;
dep_entry.set("name", dep.name.clone())?;
dep_entry.set("optional", dep.optional)?;
dep_entry.set("workspace_member", dep.workspace_member)?;
dep_entry.set("features", dep.features.clone())?;
deps_table.set(i + 1, dep_entry)?;
}
Ok(deps_table)
}