use mlua::Lua;
const LUA_ALC_SHAPES_T: &str = include_str!("alc_shapes/t.lua");
const LUA_ALC_SHAPES_REFLECT: &str = include_str!("alc_shapes/reflect.lua");
const LUA_ALC_SHAPES_CHECK: &str = include_str!("alc_shapes/check.lua");
const LUA_ALC_SHAPES_LUACATS: &str = include_str!("alc_shapes/luacats.lua");
const LUA_ALC_SHAPES_SPEC_RESOLVER: &str = include_str!("alc_shapes/spec_resolver.lua");
const LUA_ALC_SHAPES_INSTRUMENT: &str = include_str!("alc_shapes/instrument.lua");
const LUA_ALC_SHAPES_INIT: &str = include_str!("alc_shapes/init.lua");
const ALC_SHAPES_PRELOADS: &[(&str, &str)] = &[
("alc_shapes.t", LUA_ALC_SHAPES_T),
("alc_shapes.reflect", LUA_ALC_SHAPES_REFLECT),
("alc_shapes.check", LUA_ALC_SHAPES_CHECK),
("alc_shapes.luacats", LUA_ALC_SHAPES_LUACATS),
("alc_shapes.spec_resolver", LUA_ALC_SHAPES_SPEC_RESOLVER),
("alc_shapes.instrument", LUA_ALC_SHAPES_INSTRUMENT),
("alc_shapes", LUA_ALC_SHAPES_INIT),
];
pub fn gen_alc_shapes_dlua_contents() -> anyhow::Result<String> {
let lua = Lua::new();
{
let package: mlua::Table = lua.globals().get("package")?;
let preload: mlua::Table = package.get("preload")?;
for (mod_name, src) in ALC_SHAPES_PRELOADS.iter().copied() {
let chunk_name = format!("@embedded:gendoc/{mod_name}.lua");
let loader = lua.create_function(move |lua, ()| {
lua.load(src)
.set_name(chunk_name.clone())
.eval::<mlua::Value>()
})?;
preload.set(mod_name, loader)?;
}
}
let shapes: mlua::Table = lua.load(r#"return require("alc_shapes")"#).eval()?;
let luacats: mlua::Table = shapes.get("LuaCats")?;
let gen: mlua::Function = luacats.get("gen")?;
let contents: String = gen.call(shapes.clone())?;
Ok(contents)
}
#[cfg(test)]
pub(crate) fn gen_alc_pkgs_dlua_for_test(specs_lua: &str) -> anyhow::Result<String> {
let lua = Lua::new();
{
let package: mlua::Table = lua.globals().get("package")?;
let preload: mlua::Table = package.get("preload")?;
for (mod_name, src) in ALC_SHAPES_PRELOADS.iter().copied() {
let chunk_name = format!("@embedded:gendoc/{mod_name}.lua");
let loader = lua.create_function(move |lua, ()| {
lua.load(src)
.set_name(chunk_name.clone())
.eval::<mlua::Value>()
})?;
preload.set(mod_name, loader)?;
}
}
let script = format!(
r#"local S = require("alc_shapes"); local T = S.T; return S.LuaCats.gen_pkgs({specs_lua})"#
);
let contents: String = lua.load(script).eval()?;
Ok(contents)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gen_returns_nonempty_with_trailing_newline() {
let contents = gen_alc_shapes_dlua_contents().expect("generation failed");
assert!(!contents.is_empty(), "generated output should not be empty");
assert!(
contents.ends_with('\n'),
"generated output should end with newline"
);
}
#[test]
fn gen_is_deterministic() {
let first = gen_alc_shapes_dlua_contents().expect("first call failed");
let second = gen_alc_shapes_dlua_contents().expect("second call failed");
assert_eq!(first, second, "generation must be deterministic");
}
#[test]
fn gen_pkgs_empty_returns_meta_header_only() {
let contents = gen_alc_pkgs_dlua_for_test("{}").expect("gen_pkgs(empty) failed");
assert_eq!(contents, "---@meta\n");
}
#[test]
fn gen_pkgs_inline_shape_emits_class_block() {
let specs = r#"{ { name = "cot", input = T.shape({ task = T.string }) } }"#;
let contents = gen_alc_pkgs_dlua_for_test(specs).expect("gen_pkgs(inline) failed");
assert!(
contents.contains("---@class AlcPkgInput_cot"),
"expected class block for inline shape, got:\n{contents}"
);
assert!(
contents.contains("---@field task"),
"expected task field rendered, got:\n{contents}"
);
}
#[test]
fn gen_pkgs_ref_shape_emits_alias() {
let specs = r#"{ { name = "sc", result = T.ref("voted") } }"#;
let contents = gen_alc_pkgs_dlua_for_test(specs).expect("gen_pkgs(ref) failed");
assert!(
contents.contains("---@alias AlcPkgResult_sc AlcResultVoted"),
"expected alias for T.ref('voted'), got:\n{contents}"
);
}
#[test]
fn gen_pkgs_is_deterministic_and_sorted() {
let specs = r#"{
{ name = "z_pkg", input = T.shape({ a = T.string }) },
{ name = "a_pkg", input = T.shape({ b = T.number }) }
}"#;
let first = gen_alc_pkgs_dlua_for_test(specs).expect("first call failed");
let second = gen_alc_pkgs_dlua_for_test(specs).expect("second call failed");
assert_eq!(first, second, "gen_pkgs must be deterministic");
let a_idx = first
.find("AlcPkgInput_a_pkg")
.expect("a_pkg class missing");
let z_idx = first
.find("AlcPkgInput_z_pkg")
.expect("z_pkg class missing");
assert!(
a_idx < z_idx,
"a_pkg must appear before z_pkg (sorted by pkg name), got:\n{first}"
);
}
#[test]
fn regenerate_if_env_set() {
if std::env::var("ALC_REGENERATE").as_deref() != Ok("1") {
return;
}
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = std::path::Path::new(manifest_dir).join("../..");
let out_path = workspace_root.join("types/alc_shapes.d.lua");
let contents = gen_alc_shapes_dlua_contents().expect("generation failed");
std::fs::write(&out_path, &contents).expect("failed to write types/alc_shapes.d.lua");
println!("Written {} bytes to {}", contents.len(), out_path.display());
}
}