orrery_parser/
source_provider.rs1use std::path::{Path, PathBuf};
15
16use orrery_core::identifier::Id;
17
18use crate::error::SourceError;
19
20pub trait SourceProvider {
25 fn resolve_path(&self, from: &Path, import_path: &str) -> Result<PathBuf, SourceError>;
42
43 fn read_source(&self, path: &Path) -> Result<String, SourceError>;
52
53 fn derive_namespace(&self, import_path: &Path) -> Result<Id, SourceError> {
68 let name = import_path
69 .file_stem()
70 .and_then(|s| s.to_str())
71 .ok_or_else(|| {
72 SourceError::new(
73 import_path,
74 "cannot derive namespace: path has no valid file stem",
75 )
76 })?;
77 Ok(Id::new(name))
78 }
79}
80
81impl<P: SourceProvider> SourceProvider for &P {
86 fn resolve_path(&self, from: &Path, import_path: &str) -> Result<PathBuf, SourceError> {
87 (**self).resolve_path(from, import_path)
88 }
89
90 fn read_source(&self, path: &Path) -> Result<String, SourceError> {
91 (**self).read_source(path)
92 }
93
94 fn derive_namespace(&self, import_path: &Path) -> Result<Id, SourceError> {
95 (**self).derive_namespace(import_path)
96 }
97}
98
99#[derive(Debug, Clone, Default)]
109pub struct InMemorySourceProvider {
110 files: std::collections::HashMap<PathBuf, String>,
111}
112
113impl InMemorySourceProvider {
114 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn add_file(&mut self, path: impl Into<PathBuf>, source: impl Into<String>) {
121 self.files.insert(path.into(), source.into());
122 }
123}
124
125impl SourceProvider for InMemorySourceProvider {
126 fn resolve_path(&self, from: &Path, import_path: &str) -> Result<PathBuf, SourceError> {
127 let dir = from.parent().unwrap_or_else(|| Path::new(""));
128
129 let mut target = dir.join(import_path);
130 target.set_extension("orr");
131
132 if self.files.contains_key(&target) {
133 Ok(target)
134 } else {
135 Err(SourceError::new(
136 &target,
137 format!("file not found: {}", target.display()),
138 ))
139 }
140 }
141
142 fn read_source(&self, path: &Path) -> Result<String, SourceError> {
143 self.files
144 .get(path)
145 .cloned()
146 .ok_or_else(|| SourceError::new(path, format!("file not found: {}", path.display())))
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn resolve_path_same_directory() {
156 let mut provider = InMemorySourceProvider::new();
157 provider.add_file("styles.orr", "library;");
158
159 let resolved = provider
160 .resolve_path(Path::new("main.orr"), "styles")
161 .unwrap();
162 assert_eq!(resolved, PathBuf::from("styles.orr"));
163 }
164
165 #[test]
166 fn resolve_path_subdirectory() {
167 let mut provider = InMemorySourceProvider::new();
168 provider.add_file("shared/styles.orr", "library;");
169
170 let resolved = provider
171 .resolve_path(Path::new("main.orr"), "shared/styles")
172 .unwrap();
173 assert_eq!(resolved, PathBuf::from("shared/styles.orr"));
174 }
175
176 #[test]
177 fn resolve_path_from_nested_file() {
178 let mut provider = InMemorySourceProvider::new();
179 provider.add_file("shared/base.orr", "library;");
180
181 let resolved = provider
183 .resolve_path(Path::new("shared/ext.orr"), "base")
184 .unwrap();
185 assert_eq!(resolved, PathBuf::from("shared/base.orr"));
186 }
187
188 #[test]
189 fn resolve_path_file_not_found() {
190 let provider = InMemorySourceProvider::new();
191
192 let err = provider
193 .resolve_path(Path::new("main.orr"), "missing")
194 .unwrap_err();
195 assert_eq!(err.path(), Path::new("missing.orr"));
196 assert!(err.message().contains("file not found"));
197 }
198
199 #[test]
200 fn resolve_path_appends_orr_extension() {
201 let mut provider = InMemorySourceProvider::new();
202 provider.add_file("lib.orr", "library;");
203
204 let resolved = provider.resolve_path(Path::new("main.orr"), "lib").unwrap();
205 assert_eq!(resolved, PathBuf::from("lib.orr"));
206 }
207
208 #[test]
209 fn read_source_existing_file() {
210 let mut provider = InMemorySourceProvider::new();
211 provider.add_file("styles.orr", "library;\ntype Box = Rectangle;");
212
213 let source = provider.read_source(Path::new("styles.orr")).unwrap();
214 assert_eq!(source, "library;\ntype Box = Rectangle;");
215 }
216
217 #[test]
218 fn read_source_missing_file() {
219 let provider = InMemorySourceProvider::new();
220
221 let err = provider.read_source(Path::new("missing.orr")).unwrap_err();
222 assert_eq!(err.path(), Path::new("missing.orr"));
223 assert!(err.message().contains("file not found"));
224 }
225
226 #[test]
227 fn resolve_then_read_round_trip() {
228 let mut provider = InMemorySourceProvider::new();
229 provider.add_file("shared/styles.orr", "library;\ntype S = Rectangle;");
230 provider.add_file("main.orr", "diagram component;");
231
232 let resolved = provider
233 .resolve_path(Path::new("main.orr"), "shared/styles")
234 .unwrap();
235 let source = provider.read_source(&resolved).unwrap();
236 assert!(source.contains("type S = Rectangle"));
237 }
238
239 #[test]
240 fn resolve_chained_imports() {
241 let mut provider = InMemorySourceProvider::new();
242 provider.add_file("base.orr", "library;");
243 provider.add_file("ext.orr", "library;");
244 provider.add_file("main.orr", "diagram component;");
245
246 let ext_path = provider.resolve_path(Path::new("main.orr"), "ext").unwrap();
248 assert_eq!(ext_path, PathBuf::from("ext.orr"));
249
250 let base_path = provider.resolve_path(&ext_path, "base").unwrap();
251 assert_eq!(base_path, PathBuf::from("base.orr"));
252
253 let base_source = provider.read_source(&base_path).unwrap();
254 assert_eq!(base_source, "library;");
255 }
256
257 #[test]
258 fn resolve_nested_directory_structure() {
259 let mut provider = InMemorySourceProvider::new();
260 provider.add_file("shared/base/types.orr", "library;");
261 provider.add_file("shared/ext.orr", "library;");
262
263 let types = provider
265 .resolve_path(Path::new("shared/ext.orr"), "base/types")
266 .unwrap();
267 assert_eq!(types, PathBuf::from("shared/base/types.orr"));
268 }
269
270 #[test]
271 fn empty_import_path_is_error() {
272 let provider = InMemorySourceProvider::new();
273
274 let err = provider
275 .resolve_path(Path::new("main.orr"), "")
276 .unwrap_err();
277 assert!(err.message().contains("file not found"));
278 }
279
280 #[test]
281 fn derive_namespace() {
282 let provider = InMemorySourceProvider::new();
283 let id = provider.derive_namespace(Path::new("simple")).unwrap();
284 assert!(id == "simple");
285
286 let id = provider
287 .derive_namespace(Path::new("shared/nested"))
288 .unwrap();
289 assert!(id == "nested");
290
291 let id = provider
292 .derive_namespace(Path::new("../relative/path"))
293 .unwrap();
294 assert!(id == "path");
295
296 let id = provider
297 .derive_namespace(Path::new("shared/extension.orr"))
298 .unwrap();
299 assert!(id == "extension");
300 }
301
302 #[test]
303 fn derive_namespace_empty_path_is_error() {
304 let provider = InMemorySourceProvider::new();
305 let err = provider.derive_namespace(Path::new("")).unwrap_err();
306 assert!(
307 err.message().contains("cannot derive namespace"),
308 "expected namespace error, got: {}",
309 err.message()
310 );
311 }
312
313 #[test]
314 fn overwrite_file() {
315 let mut provider = InMemorySourceProvider::new();
316 provider.add_file("a.orr", "version 1");
317 provider.add_file("a.orr", "version 2");
318
319 let source = provider.read_source(Path::new("a.orr")).unwrap();
320 assert_eq!(source, "version 2");
321 }
322}