1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7pub trait LibraryLoader {
9 fn load_library(&self, path: &str) -> Result<String, String>;
10}
11
12#[derive(Debug, Default, Clone)]
15pub struct FileResolver {
16 files: HashMap<String, String>,
17}
18
19impl FileResolver {
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn add(&mut self, path: &str, source: &str) {
26 self.files.insert(path.to_string(), source.to_string());
27 }
28
29 pub fn with_file(mut self, path: &str, source: &str) -> Self {
31 self.add(path, source);
32 self
33 }
34}
35
36impl LibraryLoader for FileResolver {
37 fn load_library(&self, path: &str) -> Result<String, String> {
38 self.files
39 .get(path)
40 .cloned()
41 .ok_or_else(|| format!("no library registered for '{path}'"))
42 }
43}
44
45pub struct DirLoader {
47 roots: Vec<PathBuf>,
48 loaded: RefCell<HashMap<String, String>>,
49}
50
51impl DirLoader {
52 pub fn new(roots: Vec<PathBuf>) -> Self {
53 Self {
54 roots,
55 loaded: RefCell::new(HashMap::new()),
56 }
57 }
58
59 pub fn source_of(&self, path: &str) -> Option<String> {
62 self.loaded.borrow().get(path).cloned()
63 }
64
65 pub fn resolve_path(&self, path: &str) -> Option<PathBuf> {
68 self.candidates(path).into_iter().find(|c| c.is_file())
69 }
70
71 fn candidates(&self, path: &str) -> Vec<PathBuf> {
73 let trimmed = path.trim_matches('/');
74 let parts: Vec<&str> = trimmed.split('/').collect();
75
76 let mut relative = vec![
77 PathBuf::from(format!("{trimmed}.pine")),
78 PathBuf::from(trimmed),
79 ];
80 if let [user, name, _version] = parts.as_slice() {
83 relative.push(PathBuf::from(user).join(format!("{name}.pine")));
84 relative.push(PathBuf::from(format!("{name}.pine")));
85 }
86
87 self.roots
88 .iter()
89 .flat_map(|root| relative.iter().map(|rel| root.join(rel)))
90 .collect()
91 }
92}
93
94impl LibraryLoader for DirLoader {
95 fn load_library(&self, path: &str) -> Result<String, String> {
96 if let Some(cached) = self.source_of(path) {
97 return Ok(cached);
98 }
99
100 for candidate in self.candidates(path) {
101 if !candidate.is_file() {
102 continue;
103 }
104 let source = std::fs::read_to_string(&candidate)
105 .map_err(|e| format!("{}: {e}", candidate.display()))?;
106 self.loaded
107 .borrow_mut()
108 .insert(path.to_string(), source.clone());
109 return Ok(source);
110 }
111
112 if self.roots.is_empty() {
113 return Err("no library directory given (pass --lib <DIR>)".to_string());
114 }
115 Err(format!(
116 "not found under {}",
117 self.roots
118 .iter()
119 .map(|r| r.display().to_string())
120 .collect::<Vec<_>>()
121 .join(", ")
122 ))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::sync::atomic::{AtomicU32, Ordering};
130
131 struct TempDir(PathBuf);
133
134 impl TempDir {
135 fn new() -> Self {
136 static COUNTER: AtomicU32 = AtomicU32::new(0);
137 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
138 let dir =
139 std::env::temp_dir().join(format!("pine-dirloader-{}-{id}", std::process::id()));
140 std::fs::create_dir_all(&dir).unwrap();
141 TempDir(dir)
142 }
143
144 fn write(&self, rel: &str, contents: &str) {
145 let path = self.0.join(rel);
146 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
147 std::fs::write(path, contents).unwrap();
148 }
149 }
150
151 impl Drop for TempDir {
152 fn drop(&mut self) {
153 let _ = std::fs::remove_dir_all(&self.0);
154 }
155 }
156
157 #[test]
158 fn loads_a_pine_file_and_caches_it() {
159 let dir = TempDir::new();
160 dir.write("mylib.pine", "// lib source");
161 let loader = DirLoader::new(vec![dir.0.clone()]);
162
163 assert_eq!(loader.load_library("mylib").unwrap(), "// lib source");
164 assert_eq!(loader.source_of("mylib").as_deref(), Some("// lib source"));
166 }
167
168 #[test]
169 fn a_versioned_path_falls_back_to_the_bare_name() {
170 let dir = TempDir::new();
171 dir.write("Stats.pine", "// stats");
172 let loader = DirLoader::new(vec![dir.0.clone()]);
173
174 assert_eq!(loader.load_library("alice/Stats/3").unwrap(), "// stats");
176 }
177}