use crate::error::{Error, Result};
use crate::sandbox::{GrantSet, Policy};
use crate::types::{ModuleName, RootTable};
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct InstallContext<'a> {
policy: &'a Policy,
root_table: &'a RootTable,
}
impl<'a> InstallContext<'a> {
pub(crate) const fn new(policy: &'a Policy, root_table: &'a RootTable) -> Self {
Self { policy, root_table }
}
#[must_use]
pub const fn policy(&self) -> &Policy {
self.policy
}
#[must_use]
pub const fn grants(&self) -> &GrantSet {
self.policy.grants()
}
#[must_use]
pub const fn root_table(&self) -> &RootTable {
self.root_table
}
}
pub trait HostModule: Send + Sync {
fn name(&self) -> &ModuleName;
fn install(
&self,
lua: &mlua::Lua,
table: &mlua::Table,
context: &InstallContext<'_>,
) -> Result<()>;
}
#[derive(Default)]
pub struct ModuleSet {
modules: Vec<Box<dyn HostModule>>,
}
impl ModuleSet {
#[must_use]
pub fn new() -> Self {
Self {
modules: Vec::new(),
}
}
pub fn insert(&mut self, module: Box<dyn HostModule>) -> Result<()> {
if self.contains(module.name()) {
return Err(Error::DuplicateModule {
module: module.name().to_string(),
});
}
self.modules.push(module);
Ok(())
}
#[must_use]
pub fn contains(&self, name: &ModuleName) -> bool {
self.modules.iter().any(|m| m.name() == name)
}
#[must_use]
pub fn names(&self) -> Vec<&ModuleName> {
self.modules.iter().map(|m| m.name()).collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.modules.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.modules.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &dyn HostModule> {
self.modules.iter().map(AsRef::as_ref)
}
}
impl core::fmt::Debug for ModuleSet {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ModuleSet")
.field("modules", &self.names())
.finish()
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests unwrap known-valid fixtures; a panic is the intended failure signal"
)]
use super::{HostModule, InstallContext, ModuleSet};
use crate::error::Result;
use crate::types::ModuleName;
struct Stub(ModuleName);
impl Stub {
fn boxed(name: &str) -> Box<dyn HostModule> {
Box::new(Self(ModuleName::new(name).unwrap()))
}
}
impl HostModule for Stub {
fn name(&self) -> &ModuleName {
&self.0
}
fn install(
&self,
_lua: &mlua::Lua,
_table: &mlua::Table,
_context: &InstallContext<'_>,
) -> Result<()> {
Ok(())
}
}
#[test]
fn a_new_set_is_empty() {
let set = ModuleSet::new();
assert!(set.is_empty());
assert_eq!(set.len(), 0);
}
#[test]
fn insertion_order_is_preserved() {
let mut set = ModuleSet::new();
for name in ["json", "fs", "path"] {
set.insert(Stub::boxed(name)).unwrap();
}
let names: Vec<_> = set.names().iter().map(ToString::to_string).collect();
assert_eq!(names, ["json", "fs", "path"]);
}
#[test]
fn a_duplicate_name_is_rejected_and_leaves_the_set_unchanged() {
let mut set = ModuleSet::new();
set.insert(Stub::boxed("fs")).unwrap();
let err = set.insert(Stub::boxed("fs")).unwrap_err();
assert!(err.to_string().contains("`fs`"), "{err}");
assert_eq!(set.len(), 1);
}
#[test]
fn contains_reports_registered_names_only() {
let mut set = ModuleSet::new();
set.insert(Stub::boxed("fs")).unwrap();
assert!(set.contains(&ModuleName::new("fs").unwrap()));
assert!(!set.contains(&ModuleName::new("json").unwrap()));
}
#[test]
fn iter_yields_every_module_in_order() {
let mut set = ModuleSet::new();
set.insert(Stub::boxed("a")).unwrap();
set.insert(Stub::boxed("b")).unwrap();
let seen: Vec<_> = set.iter().map(|m| m.name().to_string()).collect();
assert_eq!(seen, ["a", "b"]);
}
}