use crate::{Htl, lib_dir, write_if_changed};
use anyhow::{Context, Result};
use std::path::PathBuf;
pub const PREFIX: &str = "std";
pub const CRATE: &str = "mlua-batteries";
pub(crate) fn declarations() -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = mlua_batteries::dts::entries()
.into_iter()
.map(|e| (format!("{PREFIX}/{}.d.tl", e.name), e.source.to_string()))
.collect();
out.push((
format!("{PREFIX}/init.d.tl"),
mlua_batteries::dts::init_source(PREFIX),
));
out
}
fn write_declarations() -> Result<PathBuf> {
let dir = lib_dir();
for (path, source) in declarations() {
write_if_changed(&dir.join(path), &source)
.with_context(|| format!("writing bundled declarations under {}", dir.display()))?;
}
Ok(dir)
}
const WRAP_LOADERS: &str = r#"
local prefix, names = ...
local preload = package.preload
local function plain(err)
local msg = tostring(err)
local at = msg:find("\nstack traceback:", 1, true)
if at then msg = msg:sub(1, at - 1) end
return msg
end
local function wrap(f)
return function(...)
local r = table.pack(pcall(f, ...))
if r[1] then return table.unpack(r, 2, r.n) end
error(plain(r[2]), 0)
end
end
for _, name in ipairs(names) do
local key = prefix .. "." .. name
local loader = preload[key]
preload[key] = function(...)
local m = loader(...)
for k, v in pairs(m) do
if type(v) == "function" then m[k] = wrap(v) end
end
return m
end
end
"#;
impl Htl {
pub fn install_std(&self) -> Result<()> {
mlua_batteries::preload_all(self.lua(), PREFIX)
.context("registering std.* (mlua-batteries) in package.preload")?;
let names: Vec<&str> = mlua_batteries::dts::entries()
.into_iter()
.map(|e| e.name)
.collect();
self.lua()
.load(WRAP_LOADERS)
.set_name("=std")
.call::<()>((PREFIX, names))
.context("wrapping std.* loaders")?;
self.add_path(&write_declarations()?)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_module_the_build_carries_is_required_as_std() -> Result<()> {
let h = Htl::new()?;
h.install_std()?;
for e in mlua_batteries::dts::entries() {
let t: mlua::Table = h
.lua()
.load(format!("return require('std.{}')", e.name))
.eval()
.with_context(|| format!("std.{}", e.name))?;
assert!(t.len()? > 0 || t.pairs::<String, mlua::Value>().count() > 0);
}
let ns: mlua::Table = h.lua().load("return require('std')").eval()?;
assert!(ns.contains_key("json")?);
Ok(())
}
#[test]
fn the_declarations_are_on_the_path_and_the_namespace_is_typed() -> Result<()> {
let h = Htl::new()?;
h.install_std()?;
let dir = lib_dir();
for (path, _) in declarations() {
assert!(path.starts_with(&format!("{PREFIX}/")), "{path}");
assert!(dir.join(&path).is_file(), "{}", dir.join(&path).display());
}
let init = std::fs::read_to_string(dir.join(PREFIX).join("init.d.tl"))?;
assert!(init.contains("local record std\n"), "{init}");
assert!(init.contains("require(\"std.json\")"), "{init}");
Ok(())
}
#[test]
fn a_failing_std_call_raises_a_plain_string_without_a_traceback() -> Result<()> {
let h = Htl::new()?;
h.install_std()?;
let (kind, msg): (String, String) = h
.lua()
.load(
"local json = require('std.json')\n\
local ok, err = pcall(json.decode, '{')\n\
assert(not ok)\n\
return type(err), err",
)
.eval()?;
assert_eq!(kind, "string");
assert_eq!(
msg,
"json.decode: EOF while parsing an object at line 1 column 1"
);
let via_ns: String = h
.lua()
.load(
"local std = require('std')\n\
local ok, err = pcall(std.json.decode, '[1,')\n\
assert(not ok)\n\
return err",
)
.eval()?;
assert!(!via_ns.contains("traceback"), "{via_ns}");
assert!(via_ns.starts_with("json.decode: "), "{via_ns}");
assert_eq!(via_ns.lines().count(), 1, "{via_ns}");
Ok(())
}
#[test]
fn a_succeeding_std_call_returns_what_it_returned() -> Result<()> {
let h = Htl::new()?;
h.install_std()?;
let (n, first, rest): (i64, mlua::Value, String) = h
.lua()
.load(
"local json = require('std.json')\n\
local r = table.pack(json.decode('null'))\n\
local s = require('std.string')\n\
return r.n, r[1], table.concat(s.split('a,b', ','), '+')",
)
.eval()?;
assert_eq!(n, 1);
assert!(matches!(first, mlua::Value::Nil));
assert_eq!(rest, "a+b");
Ok(())
}
#[test]
fn a_script_using_std_checks_and_runs() -> Result<()> {
let dir = std::env::temp_dir().join(format!("htl-std-{}", std::process::id()));
std::fs::create_dir_all(&dir)?;
let file = dir.join("main.tl");
std::fs::write(
&file,
"local json = require(\"std.json\")\n\
local s = require(\"std.string\")\n\
local t: {string:integer} = { a = 1 }\n\
local out = json.encode(t)\n\
assert(out == '{\"a\":1}', out)\n\
assert(s.trim(\" x \") == \"x\")\n",
)?;
let h = Htl::new()?;
h.install_std()?;
let (code, info) = h.gen_lua(&file)?;
assert!(info.ok(), "{info:?}");
h.exec(&code.expect("generated"), "@main.tl", &[])?;
std::fs::remove_dir_all(&dir)?;
Ok(())
}
}