use crate::error::{Error, Result};
use crate::types::ModuleName;
const DEFAULT: &str = "airsstack";
const RESERVED_WORDS: [&str; 22] = [
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
];
const SHADOWED_GLOBALS: [&str; 29] = [
"arg",
"assert",
"collectgarbage",
"coroutine",
"debug",
"dofile",
"error",
"getmetatable",
"io",
"ipairs",
"load",
"loadfile",
"loadstring",
"math",
"next",
"os",
"package",
"pairs",
"pcall",
"print",
"rawequal",
"rawget",
"rawlen",
"rawset",
"require",
"select",
"setmetatable",
"string",
"table",
];
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RootTable(ModuleName);
impl RootTable {
pub fn new(raw: impl Into<String>) -> Result<Self> {
let raw = raw.into();
let name = ModuleName::new(raw).map_err(|error| match error {
Error::InvalidName { value, reason, .. } => Error::InvalidName {
kind: "root table name",
value,
reason,
},
other => other,
})?;
let invalid = |reason: &'static str| Error::InvalidName {
kind: "root table name",
value: name.as_str().to_owned(),
reason,
};
if RESERVED_WORDS.contains(&name.as_str()) {
return Err(invalid("must not be a Lua reserved word"));
}
if SHADOWED_GLOBALS.contains(&name.as_str()) {
return Err(invalid("must not shadow a Lua standard global"));
}
Ok(Self(name))
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Default for RootTable {
fn default() -> Self {
Self::new(DEFAULT)
.unwrap_or_else(|_| unreachable!("`airsstack` is a valid root table name"))
}
}
impl core::fmt::Display for RootTable {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests unwrap known-valid fixtures; a panic is the intended failure signal"
)]
use super::{DEFAULT, RootTable};
#[test]
fn the_default_root_table_is_the_workspace_name() {
assert_eq!(RootTable::default().as_str(), DEFAULT);
}
#[test]
fn an_ordinary_name_is_accepted() {
for name in ["myapp", "redis_tools", "x1"] {
assert!(RootTable::new(name).is_ok(), "{name}");
}
}
#[test]
fn a_reserved_word_is_rejected_because_it_would_not_parse() {
for name in ["end", "local", "function", "return", "do", "then"] {
let err = RootTable::new(name).unwrap_err();
assert!(err.to_string().contains("reserved word"), "{name}: {err}");
}
}
#[test]
fn a_standard_library_table_is_rejected_because_it_would_shadow() {
for name in [
"os",
"string",
"table",
"math",
"io",
"coroutine",
"package",
] {
let err = RootTable::new(name).unwrap_err();
assert!(err.to_string().contains("shadow"), "{name}: {err}");
}
}
#[test]
fn a_base_global_a_script_relies_on_is_rejected() {
for name in ["print", "pcall", "require", "arg", "setmetatable"] {
let err = RootTable::new(name).unwrap_err();
assert!(err.to_string().contains("shadow"), "{name}: {err}");
}
}
#[test]
fn a_malformed_name_is_rejected_and_reported_as_a_root_table_name() {
let err = RootTable::new("Not-A-Name").unwrap_err();
assert!(err.to_string().contains("root table name"), "{err}");
}
#[test]
fn an_empty_name_is_rejected() {
assert!(RootTable::new("").is_err());
}
#[test]
fn a_root_table_renders_as_its_name() {
assert_eq!(RootTable::new("myapp").unwrap().to_string(), "myapp");
}
}