#[cfg(feature = "http-import")]
pub(crate) mod http_resolver;
pub(crate) mod local_fs_resolver;
pub(crate) mod std_resolver;
use crate::module::error::ModuleError;
use std::path::PathBuf;
pub trait ModuleResolver: Clone + Default {
fn resolve(&self, module_name: &str) -> Result<String, ModuleError>;
fn get_path(&self, module_name: &str) -> Result<String, ModuleError>;
fn search_paths(&self) -> Vec<PathBuf>;
fn set_search_paths(&mut self, paths: Vec<PathBuf>);
fn canonical_name<'a>(&self, module_path: &'a str) -> &'a str {
module_path
}
}
#[derive(Debug, Clone, Default)]
pub struct DefaultModuleResolver {
local_fs_resolver: local_fs_resolver::LocalFsModuleResolver,
std_resolver: std_resolver::StdModuleResolver,
#[cfg(feature = "http-import")]
http_resolver: http_resolver::HttpModuleResolver,
}
impl ModuleResolver for DefaultModuleResolver {
fn resolve(&self, module_name: &str) -> Result<String, ModuleError> {
match self.std_resolver.resolve(module_name) {
Ok(content) => return Ok(content),
Err(ModuleError::NotFound(_)) => {}
Err(e) => return Err(e),
}
match self.local_fs_resolver.resolve(module_name) {
Ok(content) => return Ok(content),
Err(ModuleError::NotFound(_)) => {}
Err(e) => return Err(e),
}
#[cfg(feature = "http-import")]
match self.http_resolver.resolve(module_name) {
Ok(content) => return Ok(content),
Err(ModuleError::NotFound(_)) => {}
Err(e) => return Err(e),
}
Err(ModuleError::NotFound(format!("{}.mq", module_name).into()))
}
fn get_path(&self, module_name: &str) -> Result<String, ModuleError> {
match self.std_resolver.get_path(module_name) {
Ok(path) => return Ok(path),
Err(ModuleError::NotFound(_)) => {}
Err(e) => return Err(e),
}
match self.local_fs_resolver.get_path(module_name) {
Ok(path) => return Ok(path),
Err(ModuleError::NotFound(_)) => {}
Err(e) => return Err(e),
}
#[cfg(feature = "http-import")]
match self.http_resolver.get_path(module_name) {
Ok(path) => return Ok(path),
Err(ModuleError::NotFound(_)) => {}
Err(e) => return Err(e),
}
Err(ModuleError::NotFound(format!("{}.mq", module_name).into()))
}
fn search_paths(&self) -> Vec<PathBuf> {
self.local_fs_resolver.search_paths()
}
fn set_search_paths(&mut self, paths: Vec<PathBuf>) {
self.local_fs_resolver.set_search_paths(paths)
}
fn canonical_name<'a>(&self, module_path: &'a str) -> &'a str {
#[cfg(feature = "http-import")]
{
use http_resolver::HttpModuleResolver;
if HttpModuleResolver::is_github_url(module_path) || HttpModuleResolver::is_remote_url(module_path) {
return self.http_resolver.canonical_name(module_path);
}
}
module_path
}
}
impl DefaultModuleResolver {
pub fn new(paths: Vec<PathBuf>) -> Self {
Self {
local_fs_resolver: local_fs_resolver::LocalFsModuleResolver::new(if paths.is_empty() {
None
} else {
Some(paths)
}),
std_resolver: std_resolver::StdModuleResolver,
#[cfg(feature = "http-import")]
http_resolver: http_resolver::HttpModuleResolver::default(),
}
}
#[cfg(feature = "http-import")]
pub fn with_http(mut self, allowed_domains: Vec<String>, timeout: Option<std::time::Duration>) -> Self {
self.http_resolver = http_resolver::HttpModuleResolver::new(
allowed_domains,
timeout.unwrap_or(std::time::Duration::from_secs(10)),
);
self
}
#[cfg(feature = "http-import")]
pub fn set_allowed_domains(&mut self, domains: Vec<String>) {
self.http_resolver.allowed_remote_domains = domains
.into_iter()
.map(|d| http_resolver::HttpModuleResolver::normalize_allowed_domain(&d))
.collect();
}
#[cfg(feature = "http-import")]
pub fn clear_http_cache(&self) -> Result<(), crate::module::error::ModuleError> {
self.http_resolver.clear_cache()
}
#[cfg(feature = "http-import")]
pub fn clear_http_cache_all(&self) -> Result<(), crate::module::error::ModuleError> {
self.http_resolver.clear_all_cache()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use tempfile::TempDir;
use super::*;
fn write_module(dir: &TempDir, name: &str, content: &str) {
std::fs::write(dir.path().join(format!("{}.mq", name)), content).unwrap();
}
#[rstest]
#[case("csv")]
#[case("json")]
#[case("yaml")]
#[case("toml")]
fn test_resolve_standard_module(#[case] name: &str) {
let resolver = DefaultModuleResolver::default();
assert!(resolver.resolve(name).is_ok());
}
#[rstest]
#[case("csv")]
#[case("json")]
fn test_get_path_standard_module(#[case] name: &str) {
let resolver = DefaultModuleResolver::default();
assert!(resolver.get_path(name).is_ok());
}
#[rstest]
#[case("nonexistent_xyz")]
fn test_resolve_unknown_module_returns_error(#[case] name: &str) {
let resolver = DefaultModuleResolver::new(vec![]);
assert!(resolver.resolve(name).is_err());
}
#[test]
fn test_resolve_local_module() {
let dir = TempDir::new().unwrap();
write_module(&dir, "mymod", "def foo(): 1;");
let resolver = DefaultModuleResolver::new(vec![dir.path().to_path_buf()]);
assert!(resolver.resolve("mymod").is_ok());
}
#[test]
fn test_std_takes_priority_over_local() {
let dir = TempDir::new().unwrap();
write_module(&dir, "csv", "def foo(): 1;");
let resolver = DefaultModuleResolver::new(vec![dir.path().to_path_buf()]);
let content = resolver.resolve("csv").unwrap();
assert!(!content.contains("def foo(): 1;"));
}
#[test]
fn test_search_paths_empty_uses_defaults() {
let resolver = DefaultModuleResolver::new(vec![]);
assert!(!resolver.search_paths().is_empty());
}
#[test]
fn test_search_paths_custom() {
let paths = vec![PathBuf::from("/custom")];
let resolver = DefaultModuleResolver::new(paths.clone());
assert_eq!(resolver.search_paths(), paths);
}
#[test]
fn test_set_search_paths() {
let mut resolver = DefaultModuleResolver::new(vec![]);
let paths = vec![PathBuf::from("/new")];
resolver.set_search_paths(paths.clone());
assert_eq!(resolver.search_paths(), paths);
}
#[cfg(feature = "http-import")]
#[rstest]
#[case("https://nonexistent.invalid/foo.mq")]
fn test_http_url_not_in_local(#[case] url: &str) {
let resolver = DefaultModuleResolver::new(vec![]);
assert!(resolver.resolve(url).is_err());
}
#[cfg(feature = "http-import")]
#[test]
fn test_with_http_normalizes_github_domains() {
let resolver = DefaultModuleResolver::new(vec![]).with_http(vec!["github.com/alice/myrepo".to_string()], None);
assert!(
resolver
.http_resolver
.is_allowed_domain("https://raw.githubusercontent.com/alice/myrepo/HEAD/mod.mq")
);
assert!(
!resolver
.http_resolver
.is_allowed_domain("https://raw.githubusercontent.com/alice/other/HEAD/mod.mq")
);
}
#[cfg(feature = "http-import")]
#[test]
fn test_set_allowed_domains_normalizes_github_domains() {
let mut resolver = DefaultModuleResolver::new(vec![]);
resolver.set_allowed_domains(vec!["github.com/bob/myrepo".to_string()]);
assert!(
resolver
.http_resolver
.is_allowed_domain("https://raw.githubusercontent.com/bob/myrepo/HEAD/mod.mq")
);
assert!(
!resolver
.http_resolver
.is_allowed_domain("https://raw.githubusercontent.com/bob/other/HEAD/mod.mq")
);
}
}