use mlua::prelude::*;
const REGISTRY_NAME: &str = "__mlua_batteries_schema_registry";
fn get_or_create_registry(lua: &Lua) -> LuaResult<LuaTable> {
let val: LuaValue = lua.named_registry_value(REGISTRY_NAME)?;
if let LuaValue::Table(t) = val {
Ok(t)
} else {
let t = lua.create_table()?;
lua.set_named_registry_value(REGISTRY_NAME, t.clone())?;
Ok(t)
}
}
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
let t = lua.create_table()?;
get_or_create_registry(lua)?;
t.set(
"get",
lua.create_function(|lua, name: String| {
let registry = get_or_create_registry(lua)?;
let value: LuaValue = registry.get(name.as_str())?;
Ok(value)
})?,
)?;
t.set(
"list",
lua.create_function(|lua, ()| {
let registry = get_or_create_registry(lua)?;
let result = lua.create_table()?;
let mut i = 1;
for pair in registry.pairs::<String, LuaValue>() {
let (k, _) = pair?;
result.set(i, k)?;
i += 1;
}
Ok(result)
})?,
)?;
t.set(
"check",
lua.create_function(|lua, (name, data): (String, LuaTable)| {
let registry = get_or_create_registry(lua)?;
let schema_table: LuaValue = registry.get(name.as_str())?;
let schema_table = match schema_table {
LuaValue::Table(t) => t,
_ => {
return Err(LuaError::external(format!(
"schema.check: unknown schema \"{name}\""
)));
}
};
let validate_mod = crate::validate::module(lua)?;
let check_fn: LuaFunction = validate_mod.get("check")?;
check_fn.call::<LuaMultiValue>((data, schema_table))
})?,
)?;
Ok(t)
}
pub fn register<T: schema_bridge::SchemaBridge>(
lua: &Lua,
namespace: &str,
name: &str,
) -> LuaResult<()> {
let schema = T::to_schema();
let lua_table = schema.to_lua_table(lua)?;
let registry = get_or_create_registry(lua)?;
registry.set(name, lua_table)?;
let ns: LuaTable = lua.globals().get(namespace)?;
if ns.get::<LuaValue>("schema")?.is_nil() {
let schema_mod = module(lua)?;
ns.set("schema", schema_mod)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use mlua::prelude::*;
#[derive(schema_bridge::SchemaBridge)]
struct TestUser {
name: String,
age: i32,
email: Option<String>,
}
fn setup_lua() -> Lua {
let lua = Lua::new();
crate::register_all(&lua, "std").unwrap();
super::register::<TestUser>(&lua, "std", "TestUser").unwrap();
lua
}
#[test]
fn register_and_get() {
let lua = setup_lua();
let result: LuaValue = lua
.load(r#"return std.schema.get("TestUser")"#)
.eval()
.unwrap();
assert!(result.is_table());
}
#[test]
fn get_unknown_returns_nil() {
let lua = setup_lua();
let result: LuaValue = lua
.load(r#"return std.schema.get("Unknown")"#)
.eval()
.unwrap();
assert!(result.is_nil());
}
#[test]
fn list_registered_schemas() {
let lua = setup_lua();
let result: LuaTable = lua.load(r#"return std.schema.list()"#).eval().unwrap();
let first: String = result.get(1).unwrap();
assert_eq!(first, "TestUser");
}
#[test]
fn check_valid_data() {
let lua = setup_lua();
let (ok, errs): (bool, LuaValue) = lua
.load(
r#"return std.schema.check("TestUser", {name = "Alice", age = 30, email = "a@b.com"})"#,
)
.eval()
.unwrap();
assert!(ok, "valid data should pass, errors: {errs:?}");
}
#[test]
fn check_invalid_data() {
let lua = setup_lua();
let (ok, errs): (bool, LuaValue) = lua
.load(r#"return std.schema.check("TestUser", {name = 123, age = "not a number"})"#)
.eval()
.unwrap();
assert!(!ok);
assert!(errs.is_table());
}
#[test]
fn check_unknown_schema_returns_error() {
let lua = setup_lua();
let result: mlua::Result<LuaValue> = lua
.load(r#"return std.schema.check("Nope", {x = 1})"#)
.eval();
assert!(result.is_err());
}
}