Expand description
§mlua-pkg
Composable Lua module loader built in Rust.
§Design philosophy
Lua’s require("name") is a name -> value transformation.
This crate defines that transformation as a composable abstraction,
allowing multiple sources (memory, filesystem, Rust functions, assets)
to be handled uniformly.
§Resolution model
§Abstractions
| Concept | Type | Role |
|---|---|---|
| Resolution unit | Resolver | name -> Option<Result<Value>> |
| Composition (Chain) | Registry | Resolvers in priority order, first match wins |
| Composition (Prefix) | resolvers::PrefixResolver | Strip prefix and delegate to inner Resolver |
Resolvers come in two kinds: leaf (directly produce values) and
combinator (compose other Resolvers). Both implement the same
Resolver trait, enabling infinite composition.
§Leaf Resolvers
| Resolver | Source | Match condition |
|---|---|---|
resolvers::MemoryResolver | HashMap<String, String> | Name is registered |
resolvers::NativeResolver | Fn(&Lua) -> Result<Value> | Name is registered |
resolvers::FsResolver | Filesystem | File exists |
resolvers::AssetResolver | Filesystem | Known extension + file exists |
§Combinators
| Combinator | Behavior |
|---|---|
Registry (Chain) | Try [R1, R2, ..., Rn] in order, adopt first Some |
resolvers::PrefixResolver | "prefix.rest" -> strip prefix -> delegate "rest" to inner Resolver |
§Resolution flow
require("name")
|
v
package.searchers[1] <- Registry inserts its hook here
|
+- Resolver A: resolve(lua, "name") -> None (not responsible)
+- Resolver B: resolve(lua, "name") -> Some(Ok(Value)) (first match wins)
|
v
package.loaded["name"] = Value <- Lua standard require auto-caches§Return value protocol
| Return value | Meaning | Next Resolver |
|---|---|---|
None | Not this Resolver’s responsibility | Tried |
Some(Ok(value)) | Resolution succeeded | Skipped |
Some(Err(e)) | Responsible but load failed | Skipped |
Some(Err) intentionally does not fall through to the next Resolver.
If a module was “found but broken”, having another Resolver return
something different would be a source of bugs.
§Naming conventions
| Name pattern | Example | Responsible Resolver |
|---|---|---|
@scope/name | @std/http | resolvers::NativeResolver – exact name match |
prefix.name | game.engine | resolvers::PrefixResolver -> delegates to inner Resolver |
dot.separated | lib.helper | resolvers::FsResolver – lib/helper.lua |
name.ext | config.json | resolvers::AssetResolver – auto-convert by extension |
resolvers::FsResolver converts dot separators to path separators
(lib.helper -> lib/helper.lua).
resolvers::AssetResolver treats filenames literally
(config.json -> config.json).
The two naturally partition by the presence of a file extension.
§Composition example
Registry (Chain)
+- NativeResolver @std/http -> factory(lua)
+- Prefix("sm", FsResolver) sm.helper -> strip -> helper.lua
+- FsResolver(root/) sm -> sm/init.lua
| lib.utils -> lib/utils.lua
+- AssetResolver config.json -> parse -> Tableresolvers::PrefixResolver acts as a namespace mount point.
require("sm") (init.lua) is handled by the outer resolvers::FsResolver,
while require("sm.helper") is handled by resolvers::PrefixResolver.
Responsibilities are clearly separated.
§Usage
use mlua_pkg::{Registry, resolvers::*};
use mlua::Lua;
let lua = Lua::new();
let mut reg = Registry::new();
// 1st: Rust native modules (highest priority)
reg.add(NativeResolver::new().add("@std/http", |lua| {
let t = lua.create_table()?;
t.set("version", 1)?;
Ok(mlua::Value::Table(t))
}));
// 2nd: Embedded Lua sources
reg.add(MemoryResolver::new().add("utils", "return { pi = 3.14 }"));
// 3rd: Filesystem (sandboxed)
reg.add(FsResolver::new(&plugins)?);
// 4th: Assets (register parsers explicitly)
reg.add(AssetResolver::new(&assets)?
.parser("json", json_parser())
.parser("sql", text_parser()));
reg.install(&lua)?;
// Lua side: require("@std/http"), require("utils"), etc.§Lua integration
Registry::install() inserts a hook at the front of Lua’s
package.searchers table. It takes priority over the standard
package.preload, so registered Resolvers are tried first.
Caching is delegated to Lua’s standard package.loaded.
On the second and subsequent require calls for the same module,
Lua’s cache hits and the Resolver is not invoked.
§Error design
| Error type | When raised | Defined in |
|---|---|---|
ResolveError | During resolve() execution | This module |
sandbox::InitError | During FsSandbox::new() construction | sandbox |
sandbox::ReadError | During SandboxedFs::read() | sandbox |
By separating construction-time and runtime errors at the type level, callers can choose the appropriate recovery strategy.
Modules§
- resolvers
- Resolver implementations: leaves and combinators.
- sandbox
- Sandboxed file I/O abstraction and implementation.
Structs§
- LuaConvention
- Configuration bundle for Lua dialect naming conventions.
- Registry
- Chain combinator for
Resolver. Registration order = priority order. First match wins.
Enums§
- Resolve
Error - Error type for module resolution.
Traits§
- Resolver
- Minimal abstraction for module resolution.