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