use mlua::{AnyUserData, Lua, Table};
use crate::Value;
pub const MAX_DEPTH: usize = 64;
pub type FromUserData<'a> = &'a dyn Fn(&AnyUserData) -> Option<mlua::Result<Value>>;
pub type ToLuaMap<'a> = &'a dyn Fn(&Lua, &Value) -> Option<mlua::Result<mlua::Value>>;
const TRACKED_PAST: usize = 8;
struct Path {
depth: usize,
seen: Vec<*const std::ffi::c_void>,
}
fn bad(msg: impl std::fmt::Display) -> mlua::Error {
mlua::Error::runtime(msg.to_string())
}
pub fn from_lua(v: &mlua::Value) -> mlua::Result<Value> {
from_lua_with(v, &|_| None)
}
pub fn from_lua_with(v: &mlua::Value, userdata: FromUserData<'_>) -> mlua::Result<Value> {
convert(
v,
&mut Path {
depth: 0,
seen: Vec::new(),
},
userdata,
)
}
fn convert(v: &mlua::Value, path: &mut Path, userdata: FromUserData<'_>) -> mlua::Result<Value> {
Ok(match v {
mlua::Value::Nil => Value::Null,
mlua::Value::Boolean(b) => Value::Bool(*b),
mlua::Value::Integer(i) => Value::Int(*i),
mlua::Value::Number(n) => Value::Float(*n),
mlua::Value::String(s) => Value::Str(s.to_str()?.to_string()),
mlua::Value::Table(t) => {
let tracked = path.depth >= TRACKED_PAST;
if tracked {
let at = t.to_pointer();
if path.seen.contains(&at) {
return Err(bad("a table that holds itself cannot be a value"));
}
path.seen.push(at);
}
if path.depth >= MAX_DEPTH {
return Err(bad(format!("a value nested past {MAX_DEPTH} tables")));
}
path.depth += 1;
let len = t.raw_len();
let value = if len > 0 {
let mut list = Vec::with_capacity(len);
for item in t.sequence_values::<mlua::Value>() {
list.push(convert(&item?, path, userdata)?);
}
Value::List(list)
} else {
let mut map = Vec::new();
for pair in t.pairs::<String, mlua::Value>() {
let (k, v) = pair?;
map.push((k, convert(&v, path, userdata)?));
}
if map.is_empty() && is_marked_array(t) {
Value::List(Vec::new())
} else {
Value::Map(map)
}
};
path.depth -= 1;
if tracked {
path.seen.pop();
}
value
}
mlua::Value::UserData(ud) => match userdata(ud) {
Some(v) => v?,
None => return Err(refused(v)),
},
other => return Err(refused(other)),
})
}
fn refused(v: &mlua::Value) -> mlua::Error {
mlua::Error::runtime(format!("cannot convert {} to event payload", v.type_name()))
}
pub fn to_lua(lua: &Lua, v: &Value) -> mlua::Result<mlua::Value> {
to_lua_with(lua, v, &|_, _| None)
}
pub fn to_lua_with(lua: &Lua, v: &Value, map: ToLuaMap<'_>) -> mlua::Result<mlua::Value> {
Ok(match v {
Value::Null => mlua::Value::Nil,
Value::Bool(b) => mlua::Value::Boolean(*b),
Value::Int(i) => mlua::Value::Integer(*i),
Value::Float(f) => mlua::Value::Number(*f),
Value::Str(s) => mlua::Value::String(lua.create_string(s)?),
Value::List(items) => {
let t = lua.create_table_with_capacity(items.len(), 0)?;
for (i, item) in items.iter().enumerate() {
t.set(i + 1, to_lua_with(lua, item, map)?)?;
}
if items.is_empty() {
mark_array(lua, &t)?;
}
mlua::Value::Table(t)
}
Value::Map(entries) => {
if let Some(other) = map(lua, v) {
return other;
}
let t = lua.create_table_with_capacity(0, entries.len())?;
for (k, v) in entries {
t.set(k.as_str(), to_lua_with(lua, v, map)?)?;
}
mlua::Value::Table(t)
}
})
}
const ARRAY_MARK: &str = "kui.array";
pub fn mark_array(lua: &Lua, t: &Table) -> mlua::Result<()> {
let mt = match lua.named_registry_value::<Option<Table>>(ARRAY_MARK)? {
Some(mt) => mt,
None => {
let mt = lua.create_table()?;
mt.raw_set("__kui_array", true)?;
lua.set_named_registry_value(ARRAY_MARK, &mt)?;
mt
}
};
if t.metatable().is_none() {
t.set_metatable(Some(mt))?;
}
Ok(())
}
pub fn is_marked_array(t: &Table) -> bool {
t.metatable()
.is_some_and(|mt| mt.raw_get::<bool>("__kui_array").unwrap_or(false))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_value_round_trips_and_an_empty_list_stays_one() {
let lua = Lua::new();
let v = Value::map([
("kind", "inc".into()),
("by", Value::Int(2)),
("w", Value::List(vec![Value::Float(0.5)])),
("none", Value::List(Vec::new())),
("on", true.into()),
]);
let back = from_lua(&to_lua(&lua, &v).unwrap()).unwrap();
assert_eq!(back.get("none"), Some(&Value::List(Vec::new())));
assert_eq!(back.get_int("by"), Some(2));
assert_eq!(back.get_bool("on"), Some(true));
}
#[test]
fn a_table_that_holds_itself_and_a_function_are_refused() {
let lua = Lua::new();
let r: mlua::Value = lua
.load("local r = { x = 1 } r.r = r return r")
.eval()
.unwrap();
let e = from_lua(&r).unwrap_err().to_string();
assert!(e.contains("holds itself"), "{e}");
let f: mlua::Value = lua.load("return function() end").eval().unwrap();
assert!(from_lua(&f).is_err());
}
}