use std::fs; use std::path::Path; use std::sync::Arc; pub static BUILTINS: &[(&str, &str)] = &[ ("example", include_str!("../scripts/example.lua")), ("theverge", include_str!("../scripts/theverge.lua")), ( "eldenring-patch-notes", include_str!("../scripts/eldenring-patch-notes.lua"), ), ( "overwatch-patch-notes", include_str!("../scripts/overwatch-patch-notes.lua"), ), ( "toby-fox-newsletters", include_str!("../scripts/toby-fox-newsletters.lua"), ), ( "humble-bundles", include_str!("../scripts/humble-bundles.lua"), ), ( "thedailybeast", include_str!("../scripts/thedailybeast.lua"), ), ]; #[derive(Debug)] pub enum MethodError { Io(std::io::Error), Syntax(mlua::Error), } impl std::fmt::Display for MethodError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MethodError::Io(e) => write!(f, "failed to read method script: {e}"), MethodError::Syntax(e) => write!(f, "method script has a syntax error: {e}"), } } } impl std::error::Error for MethodError {} /// Resolves a `method` string from a `site{}` entry to Lua source: a builtin /// name is checked first, otherwise the string is treated as a path (relative /// to the config file's directory) to an operator-authored script. Syntax is /// checked eagerly so a broken script fails at startup, not mid-schedule. pub fn resolve_method(method: &str, base_dir: &Path) -> Result, MethodError> { if let Some((_, source)) = BUILTINS.iter().find(|(name, _)| *name == method) { return Ok(Arc::from(*source)); } let path = base_dir.join(method); let source = fs::read_to_string(&path).map_err(MethodError::Io)?; mlua::Lua::new() .load(&source) .into_function() .map_err(MethodError::Syntax)?; Ok(Arc::from(source)) } #[cfg(test)] mod tests { use super::*; fn scratch_dir() -> std::path::PathBuf { let dir = std::env::temp_dir().join(format!("luafeed-method-test-{}", std::process::id())); fs::create_dir_all(&dir).unwrap(); dir } #[test] fn resolves_builtin_by_name() { let source = resolve_method("example", Path::new("/nonexistent")).unwrap(); assert!(source.contains("find_all")); } #[test] fn resolves_external_file_path() { let dir = scratch_dir(); fs::write(dir.join("custom.lua"), "local root = ...\nreturn {}\n").unwrap(); let source = resolve_method("custom.lua", &dir).unwrap(); assert!(source.contains("return {}")); } #[test] fn rejects_nonexistent_path() { let dir = scratch_dir(); let err = resolve_method("missing.lua", &dir).unwrap_err(); assert!(matches!(err, MethodError::Io(_))); } #[test] fn rejects_syntax_errors() { let dir = scratch_dir(); fs::write(dir.join("broken.lua"), "this is not valid lua (((").unwrap(); let err = resolve_method("broken.lua", &dir).unwrap_err(); assert!(matches!(err, MethodError::Syntax(_))); } }