abi_loader/fetcher/
path.rs1use crate::fetcher::{FetchContext, FetchError, FetchResult, ImportFetcher};
6use crate::file::ImportSource;
7use std::path::PathBuf;
8
9pub struct PathFetcher;
11
12impl PathFetcher {
13 pub fn new() -> Self {
15 Self
16 }
17
18 fn resolve_path(&self, import_path: &str, ctx: &FetchContext) -> Result<PathBuf, FetchError> {
20 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 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 if ctx.parent_is_remote {
70 return Err(FetchError::LocalFromRemote(path.clone()));
71 }
72
73 let resolved_path = self.resolve_path(path, ctx)?;
75
76 let content = std::fs::read_to_string(&resolved_path)?;
78
79 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, 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}