diff --git a/src/lib.rs b/src/lib.rs index bee63c2..39144dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ mod error; mod searcher; mod types; +mod utils; pub use crate::error::Error; pub use crate::searcher::AddSearcher; diff --git a/src/searcher.rs b/src/searcher.rs index 19b2d9c..e19b9e8 100644 --- a/src/searcher.rs +++ b/src/searcher.rs @@ -1,7 +1,11 @@ +use rlua::prelude::LuaError; use rlua::{Context, MetaMethod, RegistryKey, Table, UserData, UserDataMethods, Value}; use std::collections::HashMap; +use std::io::Read; +use std::path::Path; use crate::types::Result; +use crate::utils; /// Stores Lua modules indexed by module name, and provides an `rlua::MetaMethod` /// to enable `require`ing the stored modules by name in an `rlua::Context`. @@ -53,7 +57,7 @@ impl StaticSearcher { impl UserData for StaticSearcher { fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_meta_method(MetaMethod::Call, |lua_ctx, this, name: String| { - match this.modules.get(&name.as_str()) { + match this.modules.get(name.as_str()) { Some(content) => Ok(Value::Function( lua_ctx .load(content) @@ -67,6 +71,62 @@ impl UserData for StaticSearcher { } } +/// Like `Searcher`, but with `modules` values given as paths to files containing Lua +/// source code to facilitate module reloading. +struct PathSearcher

+where + P: 'static + AsRef + Send, +{ + modules: HashMap, + globals: RegistryKey, +} + +impl

PathSearcher

+where + P: 'static + AsRef + Send, +{ + fn new(modules: HashMap, globals: RegistryKey) -> Self { + Self { modules, globals } + } +} + +impl

UserData for PathSearcher

+where + P: 'static + AsRef + Send, +{ + fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(MetaMethod::Call, |lua_ctx, this, name: String| { + match this.modules.get(&name) { + Some(ref path) => { + let path = path.as_ref(); + + // Ensure `module_path` is relative to `$CARGO_MANIFEST_DIR`. + let path = if path.is_relative() { + utils::runtime_root().join(path) + } else { + path.to_path_buf() + }; + + let mut content = String::new(); + let mut file = std::fs::File::open(path) + .map_err(|e| LuaError::RuntimeError(format!("io error: {:#?}", e)))?; + file.read_to_string(&mut content) + .map_err(|e| LuaError::RuntimeError(format!("io error: {:#?}", e)))?; + + Ok(Value::Function( + lua_ctx + .load(&content) + .set_name(&name)? + .set_environment(lua_ctx.registry_value::(&this.globals)?)? + .into_function()?, + )) + } + None => Ok(Value::Nil), + } + }); + } +} + /// Extend `rlua::Context` to support `require`ing Lua modules by name. pub trait AddSearcher { /// Add a `HashMap` of Lua modules indexed by module name to Lua’s @@ -77,6 +137,12 @@ pub trait AddSearcher { /// Like `add_searcher`, but with Fennel source code encoded as `&'static str` /// to facilitate compile-time includes. fn add_static_searcher(&self, modules: HashMap<&'static str, &'static str>) -> Result<()>; + + /// Like `add_searcher`, but with `modules` values given as paths to files containing + /// Lua source code to facilitate module reloading. + fn add_path_searcher

(&self, modules: HashMap) -> Result<()> + where + P: 'static + AsRef + Send; } impl<'a> AddSearcher for Context<'a> { @@ -99,4 +165,17 @@ impl<'a> AddSearcher for Context<'a> { .set(searchers.len()? + 1, searcher) .map_err(|e| e.into()) } + + fn add_path_searcher

(&self, modules: HashMap) -> Result<()> + where + P: 'static + AsRef + Send, + { + let globals = self.globals(); + let searchers: Table = globals.get::<_, Table>("package")?.get("searchers")?; + let registry_key = self.create_registry_value(globals)?; + let searcher = PathSearcher::new(modules, registry_key); + searchers + .set(searchers.len()? + 1, searcher) + .map_err(|e| e.into()) + } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4969f97 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,6 @@ +use std::path::PathBuf; + +/// Return the value of `$CARGO_MANIFEST_DIR` at runtime. +pub(crate) fn runtime_root() -> PathBuf { + PathBuf::new().join(std::env::var("CARGO_MANIFEST_DIR").unwrap()) +} diff --git a/tests/data/lume.lua b/tests/data/lume.lua new file mode 100644 index 0000000..8371e26 --- /dev/null +++ b/tests/data/lume.lua @@ -0,0 +1 @@ +return "hello lume" diff --git a/tests/tests.rs b/tests/tests.rs index af2a9d2..5ad705b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,6 +1,9 @@ -use rlua::Lua; +use rlua::{Lua, Table, Value}; use rlua_searcher::{AddSearcher, Result}; use std::collections::HashMap; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; #[test] fn add_searcher_works() { @@ -40,6 +43,123 @@ fn add_static_searcher_works() { assert_eq!("hello lume", hello); } +#[test] +fn add_path_searcher_works() { + let name = "lume".to_string(); + let path = PathBuf::new() + .join(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("tests") + .join("data") + .join("lume.lua"); + let mut map = HashMap::new(); + map.insert(name, path); + + let lua = Lua::new(); + + let hello = lua + .context::<_, Result>(|lua_ctx| { + lua_ctx.add_path_searcher(map)?; + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello lume", hello); +} + +#[test] +fn module_reloading_works() { + let name = "lume".to_string(); + let path = PathBuf::new() + .join(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("tests") + .join("data") + .join("lume.lua"); + let mut map = HashMap::new(); + map.insert(name.clone(), path.clone()); + + let lua = Lua::new(); + + // Add searcher for lume module on disk, and read from it. + let hello = lua + .context::<_, Result>(|lua_ctx| { + lua_ctx.add_path_searcher(map)?; + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello lume", hello); + + // Twice. + let hello = lua + .context::<_, Result>(|lua_ctx| { + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello lume", hello); + + // Modify lume module on disk. + let mut out = File::create(path.clone()).expect("Could not create Lume module on disk"); + write!(out, "{}\n", r#"return "hello again lume""#) + .expect("Could not modify Lume module on disk"); + + // Thrice. Should still be unchanged due to caching. + let hello = lua + .context::<_, Result>(|lua_ctx| { + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello lume", hello); + + // Remove lume module from Lua’s `package.loaded` cache to facilitate reload. + lua.context::<_, rlua::Result<()>>(|lua_ctx| { + let globals = lua_ctx.globals(); + let loaded: Table = globals.get::<_, Table>("package")?.get("loaded")?; + loaded.set(name.clone(), Value::Nil) + }) + .unwrap(); + + // Re-read from lume module on disk. + let hello = lua + .context::<_, Result>(|lua_ctx| { + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello again lume", hello); + + // Twice. + let hello = lua + .context::<_, Result>(|lua_ctx| { + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello again lume", hello); + + // Revert changes to lume module on disk. + let mut out = File::create(path).expect("Could not create Lume module on disk"); + write!(out, "{}\n", r#"return "hello lume""#).expect("Could not modify Lume module on disk"); + + // Clear cache again. + lua.context::<_, rlua::Result<()>>(|lua_ctx| { + let globals = lua_ctx.globals(); + let loaded: Table = globals.get::<_, Table>("package")?.get("loaded")?; + loaded.set(name, Value::Nil) + }) + .unwrap(); + + // Ensure changes have been successfully reverted. + let hello = lua + .context::<_, Result>(|lua_ctx| { + Ok(lua_ctx.load(r#"return require("lume")"#).eval()?) + }) + .unwrap(); + + assert_eq!("hello lume", hello); +} + fn read_lume_to_string() -> String { r#"return "hello lume""#.to_string() }