diff --git a/CHANGELOG.md b/CHANGELOG.md index 9799178..3f6da6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `NetRequest` query parameters value has been changed to be a table of key-value pairs similar to `process.env`. If any query parameter is specified more than once in the request url, the value chosen will be the last one that was specified. - The internal http client for `net.request` now reuses headers and connections for more efficient requests. +- Refactored the Lune rust crate to be much more user-friendly and documented all of the public functions. ### Fixed diff --git a/packages/cli/src/cli.rs b/packages/cli/src/cli.rs index 6b8ad6b..246c36a 100644 --- a/packages/cli/src/cli.rs +++ b/packages/cli/src/cli.rs @@ -15,9 +15,9 @@ use crate::{ }, }; -const LUNE_SELENE_FILE_NAME: &str = "lune.yml"; -const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau"; -const LUNE_DOCS_FILE_NAME: &str = "luneDocs.json"; +pub(crate) const LUNE_SELENE_FILE_NAME: &str = "lune.yml"; +pub(crate) const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau"; +pub(crate) const LUNE_DOCS_FILE_NAME: &str = "luneDocs.json"; /// Lune CLI #[derive(Parser, Debug, Default)] @@ -160,7 +160,7 @@ impl Cli { // Display the file path relative to cwd with no extensions in stack traces let file_display_name = file_path.with_extension("").display().to_string(); // Create a new lune object with all globals & run the script - let lune = Lune::new().with_args(self.script_args).with_all_globals(); + let lune = Lune::new().with_all_globals_and_args(self.script_args); let result = lune.run(&file_display_name, &file_contents).await; Ok(match result { Err(e) => { @@ -171,64 +171,3 @@ impl Cli { }) } } - -#[cfg(test)] -mod tests { - use std::env::{current_dir, set_current_dir}; - - use anyhow::{bail, Context, Result}; - use serde_json::Value; - use tokio::fs::{create_dir_all, read_to_string, remove_file}; - - use super::{Cli, LUNE_LUAU_FILE_NAME, LUNE_SELENE_FILE_NAME}; - - async fn run_cli(cli: Cli) -> Result<()> { - let path = current_dir() - .context("Failed to get current dir")? - .join("bin"); - create_dir_all(&path) - .await - .context("Failed to create bin dir")?; - set_current_dir(&path).context("Failed to set current dir")?; - cli.run().await?; - Ok(()) - } - - async fn ensure_file_exists_and_is_not_json(file_name: &str) -> Result<()> { - match read_to_string(file_name) - .await - .context("Failed to read definitions file") - { - Ok(file_contents) => match serde_json::from_str::(&file_contents) { - Err(_) => { - remove_file(file_name) - .await - .context("Failed to remove definitions file")?; - Ok(()) - } - Ok(_) => bail!("Downloading selene definitions returned json, expected luau"), - }, - Err(e) => bail!("Failed to download selene definitions!\n{e}"), - } - } - - #[tokio::test] - async fn list() -> Result<()> { - Cli::list().run().await?; - Ok(()) - } - - #[tokio::test] - async fn download_selene_types() -> Result<()> { - run_cli(Cli::download_selene_types()).await?; - ensure_file_exists_and_is_not_json(LUNE_SELENE_FILE_NAME).await?; - Ok(()) - } - - #[tokio::test] - async fn download_luau_types() -> Result<()> { - run_cli(Cli::download_luau_types()).await?; - ensure_file_exists_and_is_not_json(LUNE_LUAU_FILE_NAME).await?; - Ok(()) - } -} diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 1abb499..833496b 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -13,9 +13,12 @@ use std::process::ExitCode; use anyhow::Result; use clap::Parser; -mod cli; -mod gen; -mod utils; +pub(crate) mod cli; +pub(crate) mod gen; +pub(crate) mod utils; + +#[cfg(test)] +mod tests; use cli::Cli; diff --git a/packages/cli/src/tests.rs b/packages/cli/src/tests.rs new file mode 100644 index 0000000..2d470f5 --- /dev/null +++ b/packages/cli/src/tests.rs @@ -0,0 +1,57 @@ +use std::env::{current_dir, set_current_dir}; + +use anyhow::{bail, Context, Result}; +use serde_json::Value; +use tokio::fs::{create_dir_all, read_to_string, remove_file}; + +use crate::cli::{Cli, LUNE_LUAU_FILE_NAME, LUNE_SELENE_FILE_NAME}; + +async fn run_cli(cli: Cli) -> Result<()> { + let path = current_dir() + .context("Failed to get current dir")? + .join("bin"); + create_dir_all(&path) + .await + .context("Failed to create bin dir")?; + set_current_dir(&path).context("Failed to set current dir")?; + cli.run().await?; + Ok(()) +} + +async fn ensure_file_exists_and_is_not_json(file_name: &str) -> Result<()> { + match read_to_string(file_name) + .await + .context("Failed to read definitions file") + { + Ok(file_contents) => match serde_json::from_str::(&file_contents) { + Err(_) => { + remove_file(file_name) + .await + .context("Failed to remove definitions file")?; + Ok(()) + } + Ok(_) => bail!("Downloading selene definitions returned json, expected luau"), + }, + Err(e) => bail!("Failed to download selene definitions!\n{e}"), + } +} + +#[tokio::test] +async fn list() -> Result<()> { + Cli::list().run().await?; + Ok(()) +} + +#[tokio::test] +async fn download_selene_types() -> Result<()> { + run_cli(Cli::download_selene_types()).await?; + ensure_file_exists_and_is_not_json(LUNE_SELENE_FILE_NAME).await?; + Ok(()) +} + +#[tokio::test] +async fn download_luau_types() -> Result<()> { + run_cli(Cli::download_luau_types()).await?; + ensure_file_exists_and_is_not_json(LUNE_LUAU_FILE_NAME).await?; + Ok(()) +} diff --git a/packages/lib/src/globals/fs.rs b/packages/lib/src/globals/fs.rs index 1b3c978..1846fd1 100644 --- a/packages/lib/src/globals/fs.rs +++ b/packages/lib/src/globals/fs.rs @@ -5,20 +5,17 @@ use tokio::fs; use crate::utils::table::TableBuilder; -pub fn create(lua: &Lua) -> LuaResult<()> { - lua.globals().raw_set( - "fs", - TableBuilder::new(lua)? - .with_async_function("readFile", fs_read_file)? - .with_async_function("readDir", fs_read_dir)? - .with_async_function("writeFile", fs_write_file)? - .with_async_function("writeDir", fs_write_dir)? - .with_async_function("removeFile", fs_remove_file)? - .with_async_function("removeDir", fs_remove_dir)? - .with_async_function("isFile", fs_is_file)? - .with_async_function("isDir", fs_is_dir)? - .build_readonly()?, - ) +pub fn create(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_async_function("readFile", fs_read_file)? + .with_async_function("readDir", fs_read_dir)? + .with_async_function("writeFile", fs_write_file)? + .with_async_function("writeDir", fs_write_dir)? + .with_async_function("removeFile", fs_remove_file)? + .with_async_function("removeDir", fs_remove_dir)? + .with_async_function("isFile", fs_is_file)? + .with_async_function("isDir", fs_is_dir)? + .build_readonly() } async fn fs_read_file(_: &Lua, path: String) -> LuaResult { diff --git a/packages/lib/src/globals/mod.rs b/packages/lib/src/globals/mod.rs index 7040a81..c728006 100644 --- a/packages/lib/src/globals/mod.rs +++ b/packages/lib/src/globals/mod.rs @@ -1,83 +1,117 @@ +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use mlua::prelude::*; + mod fs; mod net; mod process; mod require; mod stdio; mod task; +mod top_level; -// Global tables - -pub use fs::create as create_fs; -pub use net::create as create_net; -pub use process::create as create_process; -pub use require::create as create_require; -pub use stdio::create as create_stdio; -pub use task::create as create_task; - -// Individual top-level global values - -use mlua::prelude::*; - -use crate::utils::formatting::{format_label, pretty_format_multi_value}; - -pub fn create_top_level(lua: &Lua) -> LuaResult<()> { - let globals = lua.globals(); - // HACK: We need to preserve the default behavior of the - // print and error functions, for pcall and such, which - // is really tricky to do from scratch so we will just - // proxy the default print and error functions here - let print_fn: LuaFunction = globals.raw_get("print")?; - let error_fn: LuaFunction = globals.raw_get("error")?; - lua.set_named_registry_value("print", print_fn)?; - lua.set_named_registry_value("error", error_fn)?; - globals.raw_set( - "print", - lua.create_function(|lua, args: LuaMultiValue| { - let formatted = pretty_format_multi_value(&args)?; - let print: LuaFunction = lua.named_registry_value("print")?; - print.call(formatted)?; - Ok(()) - })?, - )?; - globals.raw_set( - "info", - lua.create_function(|lua, args: LuaMultiValue| { - let print: LuaFunction = lua.named_registry_value("print")?; - print.call(format!( - "{}\n{}", - format_label("info"), - pretty_format_multi_value(&args)? - ))?; - Ok(()) - })?, - )?; - globals.raw_set( - "warn", - lua.create_function(|lua, args: LuaMultiValue| { - let print: LuaFunction = lua.named_registry_value("print")?; - print.call(format!( - "{}\n{}", - format_label("warn"), - pretty_format_multi_value(&args)? - ))?; - Ok(()) - })?, - )?; - globals.raw_set( - "error", - lua.create_function(|lua, (arg, level): (LuaValue, Option)| { - let error: LuaFunction = lua.named_registry_value("error")?; - let multi = arg.to_lua_multi(lua)?; - error.call(( - format!( - "{}\n{}", - format_label("error"), - pretty_format_multi_value(&multi)? - ), - level, - ))?; - Ok(()) - })?, - )?; - Ok(()) +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum LuneGlobal { + Fs, + Net, + Process { args: Vec }, + Require, + Stdio, + Task, + TopLevel, +} + +impl Display for LuneGlobal { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{}", + match self { + Self::Fs => "fs", + Self::Net => "net", + Self::Process { .. } => "process", + Self::Require => "require", + Self::Stdio => "stdio", + Self::Task => "task", + Self::TopLevel => "toplevel", + } + ) + } +} + +impl LuneGlobal { + /** + Create a vector that contains all available Lune globals, with + the [`LuneGlobal::Process`] global containing the given args. + */ + pub fn all>(args: &[S]) -> Vec { + vec![ + Self::Fs, + Self::Net, + Self::Process { + args: args.iter().map(|s| s.as_ref().to_string()).collect(), + }, + Self::Require, + Self::Stdio, + Self::Task, + Self::TopLevel, + ] + } + + /** + Checks if this Lune global is a proxy global. + + A proxy global is a global that re-implements or proxies functionality of one or + more existing lua globals, and may store internal references to the original global(s). + + This means that proxy globals should only be injected into a lua global + environment once, since injecting twice or more will potentially break the + functionality of the proxy global and / or cause undefined behavior. + */ + pub fn is_proxy(&self) -> bool { + matches!(self, Self::Require | Self::TopLevel) + } + + /** + Creates the [`mlua::Table`] value for this Lune global. + + Note that proxy globals should be handled with special care and that [`LuneGlobal::inject()`] + should be preferred over manually creating and manipulating the value(s) of any Lune global. + */ + pub fn value<'a>(&'a self, lua: &'a Lua) -> LuaResult { + match self { + LuneGlobal::Fs => fs::create(lua), + LuneGlobal::Net => net::create(lua), + LuneGlobal::Process { args } => process::create(lua, args.clone()), + LuneGlobal::Require => require::create(lua), + LuneGlobal::Stdio => stdio::create(lua), + LuneGlobal::Task => task::create(lua), + LuneGlobal::TopLevel => top_level::create(lua), + } + } + + /** + Injects the Lune global into a lua global environment. + + This takes ownership since proxy Lune globals should + only ever be injected into a lua global environment once. + + Refer to [`LuneGlobal::is_top_level()`] for more info on proxy globals. + */ + pub fn inject(self, lua: &Lua) -> LuaResult<()> { + let globals = lua.globals(); + let table = self.value(lua)?; + // NOTE: Top level globals are special, the values + // *in* the table they return should be set directly, + // instead of setting the table itself as the global + if self.is_proxy() { + for pair in table.pairs::() { + let (key, value) = pair?; + globals.raw_set(key, value)?; + } + Ok(()) + } else { + globals.raw_set(self.to_string(), table) + } + } } diff --git a/packages/lib/src/globals/net.rs b/packages/lib/src/globals/net.rs index d7d1a77..d3944c0 100644 --- a/packages/lib/src/globals/net.rs +++ b/packages/lib/src/globals/net.rs @@ -19,7 +19,7 @@ use crate::utils::{ table::TableBuilder, }; -pub fn create(lua: &Lua) -> LuaResult<()> { +pub fn create(lua: &Lua) -> LuaResult { // Create a reusable client for performing our // web requests and store it in the lua registry let mut default_headers = HeaderMap::new(); @@ -35,15 +35,12 @@ pub fn create(lua: &Lua) -> LuaResult<()> { ); lua.set_named_registry_value("NetClient", client)?; // Create the global table for net - lua.globals().raw_set( - "net", - TableBuilder::new(lua)? - .with_function("jsonEncode", net_json_encode)? - .with_function("jsonDecode", net_json_decode)? - .with_async_function("request", net_request)? - .with_async_function("serve", net_serve)? - .build_readonly()?, - ) + TableBuilder::new(lua)? + .with_function("jsonEncode", net_json_encode)? + .with_function("jsonDecode", net_json_decode)? + .with_async_function("request", net_request)? + .with_async_function("serve", net_serve)? + .build_readonly() } fn net_json_encode(_: &Lua, (val, pretty): (LuaValue, Option)) -> LuaResult { diff --git a/packages/lib/src/globals/process.rs b/packages/lib/src/globals/process.rs index 8952425..0ea009f 100644 --- a/packages/lib/src/globals/process.rs +++ b/packages/lib/src/globals/process.rs @@ -10,7 +10,7 @@ use crate::utils::{ table::TableBuilder, }; -pub fn create(lua: &Lua, args_vec: Vec) -> LuaResult<()> { +pub fn create(lua: &Lua, args_vec: Vec) -> LuaResult { let cwd = env::current_dir()?.canonicalize()?; let mut cwd_str = cwd.to_string_lossy().to_string(); if !cwd_str.ends_with('/') { @@ -31,16 +31,13 @@ pub fn create(lua: &Lua, args_vec: Vec) -> LuaResult<()> { )? .build_readonly()?; // Create the full process table - lua.globals().raw_set( - "process", - TableBuilder::new(lua)? - .with_value("args", args_tab)? - .with_value("cwd", cwd_str)? - .with_value("env", env_tab)? - .with_async_function("exit", process_exit)? - .with_async_function("spawn", process_spawn)? - .build_readonly()?, - ) + TableBuilder::new(lua)? + .with_value("args", args_tab)? + .with_value("cwd", cwd_str)? + .with_value("env", env_tab)? + .with_async_function("exit", process_exit)? + .with_async_function("spawn", process_spawn)? + .build_readonly() } fn process_env_get<'lua>( diff --git a/packages/lib/src/globals/require.rs b/packages/lib/src/globals/require.rs index de1991a..f8cf684 100644 --- a/packages/lib/src/globals/require.rs +++ b/packages/lib/src/globals/require.rs @@ -7,10 +7,15 @@ use std::{ use mlua::prelude::*; use os_str_bytes::{OsStrBytes, RawOsStr}; -pub fn create(lua: &Lua) -> LuaResult<()> { +use crate::utils::table::TableBuilder; + +pub fn create(lua: &Lua) -> LuaResult { + let require: LuaFunction = lua.globals().raw_get("require")?; // Preserve original require behavior if we have a special env var set if env::var_os("LUAU_PWD_REQUIRE").is_some() { - return Ok(()); + return TableBuilder::new(lua)? + .with_value("require", require)? + .build_readonly(); } /* Store the current working directory so that we can use it later @@ -28,8 +33,7 @@ pub fn create(lua: &Lua) -> LuaResult<()> { let debug: LuaTable = lua.globals().raw_get("debug")?; let info: LuaFunction = debug.raw_get("info")?; lua.set_named_registry_value("require_getinfo", info)?; - // Fetch the original require function and store it in the registry - let require: LuaFunction = lua.globals().raw_get("require")?; + // Store the original require function in the registry lua.set_named_registry_value("require_original", require)?; /* Create a new function that fetches the file name from the current thread, @@ -90,6 +94,7 @@ pub fn create(lua: &Lua) -> LuaResult<()> { } })?; // Override the original require global with our monkey-patched one - lua.globals().raw_set("require", new_require)?; - Ok(()) + TableBuilder::new(lua)? + .with_value("require", new_require)? + .build_readonly() } diff --git a/packages/lib/src/globals/stdio.rs b/packages/lib/src/globals/stdio.rs index 002831e..b39d929 100644 --- a/packages/lib/src/globals/stdio.rs +++ b/packages/lib/src/globals/stdio.rs @@ -8,32 +8,29 @@ use crate::utils::{ table::TableBuilder, }; -pub fn create(lua: &Lua) -> LuaResult<()> { - lua.globals().raw_set( - "stdio", - TableBuilder::new(lua)? - .with_function("color", |_, color: String| { - let ansi_string = format_style(style_from_color_str(&color)?); - Ok(ansi_string) - })? - .with_function("style", |_, style: String| { - let ansi_string = format_style(style_from_style_str(&style)?); - Ok(ansi_string) - })? - .with_function("format", |_, args: LuaMultiValue| { - pretty_format_multi_value(&args) - })? - .with_function("write", |_, s: String| { - print!("{s}"); - Ok(()) - })? - .with_function("ewrite", |_, s: String| { - eprint!("{s}"); - Ok(()) - })? - .with_function("prompt", prompt)? - .build_readonly()?, - ) +pub fn create(lua: &Lua) -> LuaResult { + TableBuilder::new(lua)? + .with_function("color", |_, color: String| { + let ansi_string = format_style(style_from_color_str(&color)?); + Ok(ansi_string) + })? + .with_function("style", |_, style: String| { + let ansi_string = format_style(style_from_style_str(&style)?); + Ok(ansi_string) + })? + .with_function("format", |_, args: LuaMultiValue| { + pretty_format_multi_value(&args) + })? + .with_function("write", |_, s: String| { + print!("{s}"); + Ok(()) + })? + .with_function("ewrite", |_, s: String| { + eprint!("{s}"); + Ok(()) + })? + .with_function("prompt", prompt)? + .build_readonly() } fn prompt_theme() -> ColorfulTheme { diff --git a/packages/lib/src/globals/task.rs b/packages/lib/src/globals/task.rs index d934555..f429423 100644 --- a/packages/lib/src/globals/task.rs +++ b/packages/lib/src/globals/task.rs @@ -13,7 +13,7 @@ use crate::utils::{ const MINIMUM_WAIT_OR_DELAY_DURATION: f32 = 10.0 / 1_000.0; // 10ms -pub fn create(lua: &Lua) -> LuaResult<()> { +pub fn create(lua: &Lua) -> LuaResult { // HACK: There is no way to call coroutine.close directly from the mlua // crate, so we need to fetch the function and store it in the registry let coroutine: LuaTable = lua.globals().raw_get("coroutine")?; @@ -24,16 +24,13 @@ pub fn create(lua: &Lua) -> LuaResult<()> { // overwrite the original coroutine.resume function with it to fix that coroutine.raw_set("resume", lua.create_async_function(task_spawn)?)?; // Rest of the task library is normal, just async functions, no metatable - lua.globals().raw_set( - "task", - TableBuilder::new(lua)? - .with_async_function("cancel", task_cancel)? - .with_async_function("delay", task_delay)? - .with_async_function("defer", task_defer)? - .with_async_function("spawn", task_spawn)? - .with_async_function("wait", task_wait)? - .build_readonly()?, - ) + TableBuilder::new(lua)? + .with_async_function("cancel", task_cancel)? + .with_async_function("delay", task_delay)? + .with_async_function("defer", task_defer)? + .with_async_function("spawn", task_spawn)? + .with_async_function("wait", task_wait)? + .build_readonly() } fn tof_to_thread<'a>(lua: &'a Lua, tof: LuaValue<'a>) -> LuaResult> { diff --git a/packages/lib/src/globals/top_level.rs b/packages/lib/src/globals/top_level.rs new file mode 100644 index 0000000..caea783 --- /dev/null +++ b/packages/lib/src/globals/top_level.rs @@ -0,0 +1,57 @@ +use mlua::prelude::*; + +use crate::utils::{ + formatting::{format_label, pretty_format_multi_value}, + table::TableBuilder, +}; + +pub fn create(lua: &Lua) -> LuaResult { + let globals = lua.globals(); + // HACK: We need to preserve the default behavior of the + // print and error functions, for pcall and such, which + // is really tricky to do from scratch so we will just + // proxy the default print and error functions here + let print_fn: LuaFunction = globals.raw_get("print")?; + let error_fn: LuaFunction = globals.raw_get("error")?; + lua.set_named_registry_value("print", print_fn)?; + lua.set_named_registry_value("error", error_fn)?; + TableBuilder::new(lua)? + .with_function("print", |lua, args: LuaMultiValue| { + let formatted = pretty_format_multi_value(&args)?; + let print: LuaFunction = lua.named_registry_value("print")?; + print.call(formatted)?; + Ok(()) + })? + .with_function("info", |lua, args: LuaMultiValue| { + let print: LuaFunction = lua.named_registry_value("print")?; + print.call(format!( + "{}\n{}", + format_label("info"), + pretty_format_multi_value(&args)? + ))?; + Ok(()) + })? + .with_function("warn", |lua, args: LuaMultiValue| { + let print: LuaFunction = lua.named_registry_value("print")?; + print.call(format!( + "{}\n{}", + format_label("warn"), + pretty_format_multi_value(&args)? + ))?; + Ok(()) + })? + .with_function("error", |lua, (arg, level): (LuaValue, Option)| { + let error: LuaFunction = lua.named_registry_value("error")?; + let multi = arg.to_lua_multi(lua)?; + error.call(( + format!( + "{}\n{}", + format_label("error"), + pretty_format_multi_value(&multi)? + ), + level, + ))?; + Ok(()) + })? + .build_readonly() +} diff --git a/packages/lib/src/lib.rs b/packages/lib/src/lib.rs index 1252620..aa1e42a 100644 --- a/packages/lib/src/lib.rs +++ b/packages/lib/src/lib.rs @@ -6,68 +6,82 @@ use tokio::{sync::mpsc, task}; pub(crate) mod globals; pub(crate) mod utils; -use crate::{ - globals::{ - create_fs, create_net, create_process, create_require, create_stdio, create_task, - create_top_level, - }, - utils::{formatting::pretty_format_luau_error, message::LuneMessage}, -}; +#[cfg(test)] +mod tests; -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum LuneGlobal { - Fs, - Net, - Process, - Require, - Stdio, - Task, - TopLevel, -} +use crate::utils::{formatting::pretty_format_luau_error, message::LuneMessage}; -impl LuneGlobal { - pub fn get_all() -> Vec { - vec![ - Self::Fs, - Self::Net, - Self::Process, - Self::Require, - Self::Stdio, - Self::Task, - Self::TopLevel, - ] - } -} +pub use globals::LuneGlobal; #[derive(Clone, Debug, Default)] pub struct Lune { - globals: HashSet, - args: Vec, + includes: HashSet, + excludes: HashSet, } impl Lune { + /** + Creates a new Lune script runner. + */ pub fn new() -> Self { Self::default() } - pub fn with_args(mut self, args: Vec) -> Self { - self.args = args; - self - } - + /** + Include a global in the lua environment created for running a Lune script. + */ pub fn with_global(mut self, global: LuneGlobal) -> Self { - self.globals.insert(global); + self.includes.insert(global); self } + /** + Include all globals in the lua environment created for running a Lune script. + */ pub fn with_all_globals(mut self) -> Self { - for global in LuneGlobal::get_all() { - self.globals.insert(global); + for global in LuneGlobal::all::(&[]) { + self.includes.insert(global); } self } - pub async fn run(&self, name: &str, chunk: &str) -> Result { + /** + Include all globals in the lua environment created for running a + Lune script, as well as supplying args for [`LuneGlobal::Process`]. + */ + pub fn with_all_globals_and_args(mut self, args: Vec) -> Self { + for global in LuneGlobal::all(&args) { + self.includes.insert(global); + } + self + } + + /** + Exclude a global from the lua environment created for running a Lune script. + + This should be preferred over manually iterating and filtering + which Lune globals to add to the global environment. + */ + pub fn without_global(mut self, global: LuneGlobal) -> Self { + self.excludes.insert(global); + self + } + + /** + Runs a Lune script. + + This will create a new sandboxed Luau environment with the configured + globals and arguments, running inside of a [`tokio::task::LocalSet`]. + + Some Lune globals such as [`LuneGlobal::Process`] may spawn + separate tokio tasks on other threads, but the Luau environment + itself is guaranteed to run on a single thread in the local set. + */ + pub async fn run( + &self, + script_name: &str, + script_contents: &str, + ) -> Result { let task_set = task::LocalSet::new(); let (sender, mut receiver) = mpsc::channel::(64); let lua = Arc::new(mlua::Lua::new()); @@ -75,21 +89,15 @@ impl Lune { lua.set_app_data(Arc::downgrade(&lua)); lua.set_app_data(Arc::downgrade(&snd)); // Add in wanted lune globals - for global in &self.globals { - match &global { - LuneGlobal::Fs => create_fs(&lua)?, - LuneGlobal::Net => create_net(&lua)?, - LuneGlobal::Process => create_process(&lua, self.args.clone())?, - LuneGlobal::Require => create_require(&lua)?, - LuneGlobal::Stdio => create_stdio(&lua)?, - LuneGlobal::Task => create_task(&lua)?, - LuneGlobal::TopLevel => create_top_level(&lua)?, + for global in self.includes.clone() { + if !self.excludes.contains(&global) { + global.inject(&lua)?; } } // Spawn the main thread from our entrypoint script let script_lua = lua.clone(); - let script_name = name.to_string(); - let script_chunk = chunk.to_string(); + let script_name = script_name.to_string(); + let script_chunk = script_contents.to_string(); let script_sender = snd.clone(); script_sender .send(LuneMessage::Spawned) @@ -165,83 +173,3 @@ impl Lune { } } } - -#[cfg(test)] -mod tests { - use std::{env::set_current_dir, path::PathBuf, process::ExitCode}; - - use anyhow::Result; - use console::set_colors_enabled; - use console::set_colors_enabled_stderr; - use tokio::fs::read_to_string; - - use crate::Lune; - - const ARGS: &[&str] = &["Foo", "Bar"]; - - macro_rules! run_tests { - ($($name:ident: $value:expr,)*) => { - $( - #[tokio::test] - async fn $name() -> Result { - // Disable styling for stdout and stderr since - // some tests rely on output not being styled - set_colors_enabled(false); - set_colors_enabled_stderr(false); - // NOTE: This path is relative to the lib - // package, not the cwd or workspace root, - // so we need to cd to the repo root first - let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let root_dir = crate_dir.join("../../").canonicalize()?; - set_current_dir(root_dir)?; - // The rest of the test logic can continue as normal - let full_name = format!("tests/{}.luau", $value); - let script = read_to_string(&full_name).await?; - let lune = Lune::new() - .with_args( - ARGS - .clone() - .iter() - .map(ToString::to_string) - .collect() - ) - .with_all_globals(); - let script_name = full_name.strip_suffix(".luau").unwrap(); - let exit_code = lune.run(&script_name, &script).await?; - Ok(exit_code) - } - )* - } - } - - run_tests! { - fs_files: "fs/files", - fs_dirs: "fs/dirs", - net_request_codes: "net/request/codes", - net_request_methods: "net/request/methods", - net_request_redirect: "net/request/redirect", - net_json_decode: "net/json/decode", - net_json_encode: "net/json/encode", - net_serve: "net/serve", - process_args: "process/args", - process_cwd: "process/cwd", - process_env: "process/env", - process_exit: "process/exit", - process_spawn: "process/spawn", - require_children: "require/tests/children", - require_invalid: "require/tests/invalid", - require_nested: "require/tests/nested", - require_parents: "require/tests/parents", - require_siblings: "require/tests/siblings", - stdio_format: "stdio/format", - stdio_color: "stdio/color", - stdio_style: "stdio/style", - stdio_write: "stdio/write", - stdio_ewrite: "stdio/ewrite", - task_cancel: "task/cancel", - task_defer: "task/defer", - task_delay: "task/delay", - task_spawn: "task/spawn", - task_wait: "task/wait", - } -} diff --git a/packages/lib/src/tests.rs b/packages/lib/src/tests.rs new file mode 100644 index 0000000..3fef645 --- /dev/null +++ b/packages/lib/src/tests.rs @@ -0,0 +1,72 @@ +use std::{env::set_current_dir, path::PathBuf, process::ExitCode}; + +use anyhow::Result; +use console::set_colors_enabled; +use console::set_colors_enabled_stderr; +use tokio::fs::read_to_string; + +use crate::Lune; + +const ARGS: &[&str] = &["Foo", "Bar"]; + +macro_rules! create_tests { + ($($name:ident: $value:expr,)*) => { $( + #[tokio::test] + async fn $name() -> Result { + // Disable styling for stdout and stderr since + // some tests rely on output not being styled + set_colors_enabled(false); + set_colors_enabled_stderr(false); + // NOTE: This path is relative to the lib + // package, not the cwd or workspace root, + // so we need to cd to the repo root first + let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = crate_dir.join("../../").canonicalize()?; + set_current_dir(root_dir)?; + // The rest of the test logic can continue as normal + let full_name = format!("tests/{}.luau", $value); + let script = read_to_string(&full_name).await?; + let lune = Lune::new().with_all_globals_and_args( + ARGS + .clone() + .iter() + .map(ToString::to_string) + .collect() + ); + let script_name = full_name.strip_suffix(".luau").unwrap(); + let exit_code = lune.run(&script_name, &script).await?; + Ok(exit_code) + } + )* } +} + +create_tests! { + fs_files: "fs/files", + fs_dirs: "fs/dirs", + net_request_codes: "net/request/codes", + net_request_methods: "net/request/methods", + net_request_redirect: "net/request/redirect", + net_json_decode: "net/json/decode", + net_json_encode: "net/json/encode", + net_serve: "net/serve", + process_args: "process/args", + process_cwd: "process/cwd", + process_env: "process/env", + process_exit: "process/exit", + process_spawn: "process/spawn", + require_children: "require/tests/children", + require_invalid: "require/tests/invalid", + require_nested: "require/tests/nested", + require_parents: "require/tests/parents", + require_siblings: "require/tests/siblings", + stdio_format: "stdio/format", + stdio_color: "stdio/color", + stdio_style: "stdio/style", + stdio_write: "stdio/write", + stdio_ewrite: "stdio/ewrite", + task_cancel: "task/cancel", + task_defer: "task/defer", + task_delay: "task/delay", + task_spawn: "task/spawn", + task_wait: "task/wait", +}