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 fn candidates(&self, path: &str) -> Vec<PathBuf> {
67 let trimmed = path.trim_matches('/');
68 let parts: Vec<&str> = trimmed.split('/').collect();
69
70 let mut relative = vec![
71 PathBuf::from(format!("{trimmed}.pine")),
72 PathBuf::from(trimmed),
73 ];
74 if let [user, name, _version] = parts.as_slice() {
77 relative.push(PathBuf::from(user).join(format!("{name}.pine")));
78 relative.push(PathBuf::from(format!("{name}.pine")));
79 }
80
81 self.roots
82 .iter()
83 .flat_map(|root| relative.iter().map(|rel| root.join(rel)))
84 .collect()
85 }
86}
87
88impl LibraryLoader for DirLoader {
89 fn load_library(&self, path: &str) -> Result<String, String> {
90 if let Some(cached) = self.source_of(path) {
91 return Ok(cached);
92 }
93
94 for candidate in self.candidates(path) {
95 if !candidate.is_file() {
96 continue;
97 }
98 let source = std::fs::read_to_string(&candidate)
99 .map_err(|e| format!("{}: {e}", candidate.display()))?;
100 self.loaded
101 .borrow_mut()
102 .insert(path.to_string(), source.clone());
103 return Ok(source);
104 }
105
106 if self.roots.is_empty() {
107 return Err("no library directory given (pass --lib <DIR>)".to_string());
108 }
109 Err(format!(
110 "not found under {}",
111 self.roots
112 .iter()
113 .map(|r| r.display().to_string())
114 .collect::<Vec<_>>()
115 .join(", ")
116 ))
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use std::sync::atomic::{AtomicU32, Ordering};
124
125 struct TempDir(PathBuf);
127
128 impl TempDir {
129 fn new() -> Self {
130 static COUNTER: AtomicU32 = AtomicU32::new(0);
131 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
132 let dir =
133 std::env::temp_dir().join(format!("pine-dirloader-{}-{id}", std::process::id()));
134 std::fs::create_dir_all(&dir).unwrap();
135 TempDir(dir)
136 }
137
138 fn write(&self, rel: &str, contents: &str) {
139 let path = self.0.join(rel);
140 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
141 std::fs::write(path, contents).unwrap();
142 }
143 }
144
145 impl Drop for TempDir {
146 fn drop(&mut self) {
147 let _ = std::fs::remove_dir_all(&self.0);
148 }
149 }
150
151 #[test]
152 fn loads_a_pine_file_and_caches_it() {
153 let dir = TempDir::new();
154 dir.write("mylib.pine", "// lib source");
155 let loader = DirLoader::new(vec![dir.0.clone()]);
156
157 assert_eq!(loader.load_library("mylib").unwrap(), "// lib source");
158 assert_eq!(loader.source_of("mylib").as_deref(), Some("// lib source"));
160 }
161
162 #[test]
163 fn a_versioned_path_falls_back_to_the_bare_name() {
164 let dir = TempDir::new();
165 dir.write("Stats.pine", "// stats");
166 let loader = DirLoader::new(vec![dir.0.clone()]);
167
168 assert_eq!(loader.load_library("alice/Stats/3").unwrap(), "// stats");
170 }
171}