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.canonicalize().map_err(|e| FetchError::Io(e));
26 }
27 }
28 }
29
30 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 if ctx.parent_is_remote {
65 return Err(FetchError::LocalFromRemote(path.clone()));
66 }
67
68 let resolved_path = self.resolve_path(path, ctx)?;
70
71 let content = std::fs::read_to_string(&resolved_path)?;
73
74 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, 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}