mq_lang/module/
resolver.rs1#[cfg(feature = "http-import")]
2pub(crate) mod http_resolver;
3pub(crate) mod local_fs_resolver;
4pub(crate) mod std_resolver;
5
6use crate::module::error::ModuleError;
7use std::path::PathBuf;
8
9pub trait ModuleResolver: Clone + Default {
11 fn resolve(&self, module_name: &str) -> Result<String, ModuleError>;
13 fn get_path(&self, module_name: &str) -> Result<String, ModuleError>;
15 fn search_paths(&self) -> Vec<PathBuf>;
17 fn set_search_paths(&mut self, paths: Vec<PathBuf>);
19 fn canonical_name<'a>(&self, module_path: &'a str) -> &'a str {
25 module_path
26 }
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct DefaultModuleResolver {
32 local_fs_resolver: local_fs_resolver::LocalFsModuleResolver,
33 std_resolver: std_resolver::StdModuleResolver,
34 #[cfg(feature = "http-import")]
35 http_resolver: http_resolver::HttpModuleResolver,
36}
37
38impl ModuleResolver for DefaultModuleResolver {
39 fn resolve(&self, module_name: &str) -> Result<String, ModuleError> {
40 match self.std_resolver.resolve(module_name) {
41 Ok(content) => return Ok(content),
42 Err(ModuleError::NotFound(_)) => {}
43 Err(e) => return Err(e),
44 }
45
46 match self.local_fs_resolver.resolve(module_name) {
47 Ok(content) => return Ok(content),
48 Err(ModuleError::NotFound(_)) => {}
49 Err(e) => return Err(e),
50 }
51
52 #[cfg(feature = "http-import")]
53 match self.http_resolver.resolve(module_name) {
54 Ok(content) => return Ok(content),
55 Err(ModuleError::NotFound(_)) => {}
56 Err(e) => return Err(e),
57 }
58
59 Err(ModuleError::NotFound(format!("{}.mq", module_name).into()))
60 }
61
62 fn get_path(&self, module_name: &str) -> Result<String, ModuleError> {
63 match self.std_resolver.get_path(module_name) {
64 Ok(path) => return Ok(path),
65 Err(ModuleError::NotFound(_)) => {}
66 Err(e) => return Err(e),
67 }
68
69 match self.local_fs_resolver.get_path(module_name) {
70 Ok(path) => return Ok(path),
71 Err(ModuleError::NotFound(_)) => {}
72 Err(e) => return Err(e),
73 }
74
75 #[cfg(feature = "http-import")]
76 match self.http_resolver.get_path(module_name) {
77 Ok(path) => return Ok(path),
78 Err(ModuleError::NotFound(_)) => {}
79 Err(e) => return Err(e),
80 }
81
82 Err(ModuleError::NotFound(format!("{}.mq", module_name).into()))
83 }
84
85 fn search_paths(&self) -> Vec<PathBuf> {
86 self.local_fs_resolver.search_paths()
87 }
88
89 fn set_search_paths(&mut self, paths: Vec<PathBuf>) {
90 self.local_fs_resolver.set_search_paths(paths)
91 }
92
93 fn canonical_name<'a>(&self, module_path: &'a str) -> &'a str {
94 #[cfg(feature = "http-import")]
95 {
96 use http_resolver::HttpModuleResolver;
97 if HttpModuleResolver::is_github_url(module_path) || HttpModuleResolver::is_remote_url(module_path) {
98 return self.http_resolver.canonical_name(module_path);
99 }
100 }
101 module_path
102 }
103}
104
105impl DefaultModuleResolver {
106 pub fn new(paths: Vec<PathBuf>) -> Self {
110 Self {
111 local_fs_resolver: local_fs_resolver::LocalFsModuleResolver::new(if paths.is_empty() {
112 None
113 } else {
114 Some(paths)
115 }),
116 std_resolver: std_resolver::StdModuleResolver,
117 #[cfg(feature = "http-import")]
118 http_resolver: http_resolver::HttpModuleResolver::default(),
119 }
120 }
121
122 #[cfg(feature = "http-import")]
128 pub fn with_http(mut self, allowed_domains: Vec<String>, timeout: Option<std::time::Duration>) -> Self {
129 self.http_resolver = http_resolver::HttpModuleResolver::new(
130 allowed_domains,
131 timeout.unwrap_or(std::time::Duration::from_secs(10)),
132 );
133 self
134 }
135
136 #[cfg(feature = "http-import")]
144 pub fn set_allowed_domains(&mut self, domains: Vec<String>) {
145 self.http_resolver.allowed_remote_domains = domains
146 .into_iter()
147 .map(|d| http_resolver::HttpModuleResolver::normalize_allowed_domain(&d))
148 .collect();
149 }
150
151 #[cfg(feature = "http-import")]
156 pub fn clear_http_cache(&self) -> Result<(), crate::module::error::ModuleError> {
157 self.http_resolver.clear_cache()
158 }
159
160 #[cfg(feature = "http-import")]
162 pub fn clear_http_cache_all(&self) -> Result<(), crate::module::error::ModuleError> {
163 self.http_resolver.clear_all_cache()
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use rstest::rstest;
170 use tempfile::TempDir;
171
172 use super::*;
173
174 fn write_module(dir: &TempDir, name: &str, content: &str) {
175 std::fs::write(dir.path().join(format!("{}.mq", name)), content).unwrap();
176 }
177
178 #[rstest]
179 #[case("csv")]
180 #[case("json")]
181 #[case("yaml")]
182 #[case("toml")]
183 fn test_resolve_standard_module(#[case] name: &str) {
184 let resolver = DefaultModuleResolver::default();
185 assert!(resolver.resolve(name).is_ok());
186 }
187
188 #[rstest]
189 #[case("csv")]
190 #[case("json")]
191 fn test_get_path_standard_module(#[case] name: &str) {
192 let resolver = DefaultModuleResolver::default();
193 assert!(resolver.get_path(name).is_ok());
194 }
195
196 #[rstest]
197 #[case("nonexistent_xyz")]
198 fn test_resolve_unknown_module_returns_error(#[case] name: &str) {
199 let resolver = DefaultModuleResolver::new(vec![]);
200 assert!(resolver.resolve(name).is_err());
201 }
202
203 #[test]
204 fn test_resolve_local_module() {
205 let dir = TempDir::new().unwrap();
206 write_module(&dir, "mymod", "def foo(): 1;");
207
208 let resolver = DefaultModuleResolver::new(vec![dir.path().to_path_buf()]);
209 assert!(resolver.resolve("mymod").is_ok());
210 }
211
212 #[test]
213 fn test_std_takes_priority_over_local() {
214 let dir = TempDir::new().unwrap();
215 write_module(&dir, "csv", "def foo(): 1;");
216
217 let resolver = DefaultModuleResolver::new(vec![dir.path().to_path_buf()]);
218 let content = resolver.resolve("csv").unwrap();
220 assert!(!content.contains("def foo(): 1;"));
221 }
222
223 #[test]
224 fn test_search_paths_empty_uses_defaults() {
225 let resolver = DefaultModuleResolver::new(vec![]);
226 assert!(!resolver.search_paths().is_empty());
227 }
228
229 #[test]
230 fn test_search_paths_custom() {
231 let paths = vec![PathBuf::from("/custom")];
232 let resolver = DefaultModuleResolver::new(paths.clone());
233 assert_eq!(resolver.search_paths(), paths);
234 }
235
236 #[test]
237 fn test_set_search_paths() {
238 let mut resolver = DefaultModuleResolver::new(vec![]);
239 let paths = vec![PathBuf::from("/new")];
240 resolver.set_search_paths(paths.clone());
241 assert_eq!(resolver.search_paths(), paths);
242 }
243
244 #[cfg(feature = "http-import")]
245 #[rstest]
246 #[case("https://nonexistent.invalid/foo.mq")]
247 fn test_http_url_not_in_local(#[case] url: &str) {
248 let resolver = DefaultModuleResolver::new(vec![]);
250 assert!(resolver.resolve(url).is_err());
252 }
253
254 #[cfg(feature = "http-import")]
255 #[test]
256 fn test_with_http_normalizes_github_domains() {
257 let resolver = DefaultModuleResolver::new(vec![]).with_http(vec!["github.com/alice/myrepo".to_string()], None);
259 assert!(
260 resolver
261 .http_resolver
262 .is_allowed_domain("https://raw.githubusercontent.com/alice/myrepo/HEAD/mod.mq")
263 );
264 assert!(
265 !resolver
266 .http_resolver
267 .is_allowed_domain("https://raw.githubusercontent.com/alice/other/HEAD/mod.mq")
268 );
269 }
270
271 #[cfg(feature = "http-import")]
272 #[test]
273 fn test_set_allowed_domains_normalizes_github_domains() {
274 let mut resolver = DefaultModuleResolver::new(vec![]);
275 resolver.set_allowed_domains(vec!["github.com/bob/myrepo".to_string()]);
276 assert!(
277 resolver
278 .http_resolver
279 .is_allowed_domain("https://raw.githubusercontent.com/bob/myrepo/HEAD/mod.mq")
280 );
281 assert!(
282 !resolver
283 .http_resolver
284 .is_allowed_domain("https://raw.githubusercontent.com/bob/other/HEAD/mod.mq")
285 );
286 }
287}