Skip to main content

abi_loader/fetcher/
path.rs

1//! Path-based Import Fetcher
2//!
3//! Fetches ABI files from local filesystem paths.
4
5use crate::fetcher::{FetchContext, FetchError, FetchResult, ImportFetcher};
6use crate::file::ImportSource;
7use std::path::PathBuf;
8
9/* Local filesystem path fetcher */
10pub struct PathFetcher;
11
12impl PathFetcher {
13    /* Create a new path fetcher */
14    pub fn new() -> Self {
15        Self
16    }
17
18    /* Resolve an import path relative to base file or include directories */
19    fn resolve_path(&self, import_path: &str, ctx: &FetchContext) -> Result<PathBuf, FetchError> {
20        /* First try relative to the base file's directory */
21        if let Some(base) = &ctx.base_path {
22            if let Some(parent) = base.parent() {
23                let relative_path = parent.join(import_path);
24                if relative_path.exists() {
25                    return relative_path
26                        .canonicalize()
27                        .map_err(|e| FetchError::Io(e));
28                }
29            }
30        }
31
32        /* Then try each include directory */
33        for include_dir in &ctx.include_dirs {
34            let include_path = include_dir.join(import_path);
35            if include_path.exists() {
36                return include_path
37                    .canonicalize()
38                    .map_err(|e| FetchError::Io(e));
39            }
40        }
41
42        Err(FetchError::NotFound(format!(
43            "Import '{}' not found relative to {:?} or in include directories",
44            import_path,
45            ctx.base_path
46        )))
47    }
48}
49
50impl Default for PathFetcher {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl ImportFetcher for PathFetcher {
57    fn handles(&self, source: &ImportSource) -> bool {
58        matches!(source, ImportSource::Path { .. })
59    }
60
61    fn fetch(&self, source: &ImportSource, ctx: &FetchContext) -> Result<FetchResult, FetchError> {
62        let ImportSource::Path { path } = source else {
63            return Err(FetchError::UnsupportedSource(
64                "PathFetcher only handles Path imports".to_string(),
65            ));
66        };
67
68        /* Enforce remote import restriction: local imports not allowed from remote parents */
69        if ctx.parent_is_remote {
70            return Err(FetchError::LocalFromRemote(path.clone()));
71        }
72
73        /* Resolve the path */
74        let resolved_path = self.resolve_path(path, ctx)?;
75
76        /* Read the file content */
77        let content = std::fs::read_to_string(&resolved_path)?;
78
79        /* Create canonical location for caching/cycle detection */
80        let canonical_location = resolved_path.to_string_lossy().to_string();
81
82        Ok(FetchResult {
83            content,
84            canonical_location,
85            is_remote: false,
86            resolved_path: Some(resolved_path),
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use std::io::Write;
95    use tempfile::TempDir;
96
97    fn create_test_abi(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
98        let path = dir.join(name);
99        let mut file = std::fs::File::create(&path).unwrap();
100        file.write_all(content.as_bytes()).unwrap();
101        path
102    }
103
104    #[test]
105    fn test_path_fetcher_handles() {
106        let fetcher = PathFetcher::new();
107
108        let path_import = ImportSource::Path {
109            path: "test.abi.yaml".to_string(),
110        };
111        let git_import = ImportSource::Git {
112            url: "https://github.com/test/repo".to_string(),
113            git_ref: "main".to_string(),
114            path: "abi.yaml".to_string(),
115        };
116
117        assert!(fetcher.handles(&path_import));
118        assert!(!fetcher.handles(&git_import));
119    }
120
121    #[test]
122    fn test_path_fetcher_relative_import() {
123        let temp_dir = TempDir::new().unwrap();
124        let abi_content = r#"
125abi:
126  package: "test.package"
127  abi-version: 1
128  package-version: "1.0.0"
129  description: "Test ABI"
130types: []
131"#;
132
133        let abi_path = create_test_abi(temp_dir.path(), "test.abi.yaml", abi_content);
134
135        let fetcher = PathFetcher::new();
136        let source = ImportSource::Path {
137            path: "test.abi.yaml".to_string(),
138        };
139        let ctx = FetchContext {
140            base_path: Some(temp_dir.path().join("main.abi.yaml")),
141            parent_is_remote: false,
142            include_dirs: vec![],
143        };
144
145        let result = fetcher.fetch(&source, &ctx).unwrap();
146        assert!(!result.is_remote);
147        assert!(result.content.contains("test.package"));
148        assert_eq!(
149            result.resolved_path.unwrap().canonicalize().unwrap(),
150            abi_path.canonicalize().unwrap()
151        );
152    }
153
154    #[test]
155    fn test_path_fetcher_include_dir() {
156        let temp_dir = TempDir::new().unwrap();
157        let include_dir = temp_dir.path().join("include");
158        std::fs::create_dir(&include_dir).unwrap();
159
160        let abi_content = r#"
161abi:
162  package: "include.package"
163  abi-version: 1
164  package-version: "1.0.0"
165  description: "Include ABI"
166types: []
167"#;
168        create_test_abi(&include_dir, "include.abi.yaml", abi_content);
169
170        let fetcher = PathFetcher::new();
171        let source = ImportSource::Path {
172            path: "include.abi.yaml".to_string(),
173        };
174        let ctx = FetchContext {
175            base_path: Some(temp_dir.path().join("other").join("main.abi.yaml")),
176            parent_is_remote: false,
177            include_dirs: vec![include_dir],
178        };
179
180        let result = fetcher.fetch(&source, &ctx).unwrap();
181        assert!(result.content.contains("include.package"));
182    }
183
184    #[test]
185    fn test_path_fetcher_rejects_local_from_remote() {
186        let fetcher = PathFetcher::new();
187        let source = ImportSource::Path {
188            path: "test.abi.yaml".to_string(),
189        };
190        let ctx = FetchContext {
191            base_path: None,
192            parent_is_remote: true, /* Parent was remote */
193            include_dirs: vec![],
194        };
195
196        let result = fetcher.fetch(&source, &ctx);
197        assert!(matches!(result, Err(FetchError::LocalFromRemote(_))));
198    }
199
200    #[test]
201    fn test_path_fetcher_not_found() {
202        let fetcher = PathFetcher::new();
203        let source = ImportSource::Path {
204            path: "nonexistent.abi.yaml".to_string(),
205        };
206        let ctx = FetchContext {
207            base_path: Some(PathBuf::from("/tmp/test.abi.yaml")),
208            parent_is_remote: false,
209            include_dirs: vec![],
210        };
211
212        let result = fetcher.fetch(&source, &ctx);
213        assert!(matches!(result, Err(FetchError::NotFound(_))));
214    }
215}