candela/compiler/
imports.rs1use smol_strc::SmolStr;
11use std::path::Path;
12use std::path::PathBuf;
13
14pub const DEFAULT_PACKAGE_ENTRY: &str = "src/main.cdl";
17
18#[derive(Clone, Debug)]
26pub struct ImportResolver {
27 lib_dir: Option<PathBuf>,
28 packages: Vec<PackageRoot>,
29}
30
31#[derive(Clone, Debug)]
38struct PackageRoot {
39 name: SmolStr,
40 dir: PathBuf,
41 entry: PathBuf,
42}
43
44impl PackageRoot {
45 fn source_dir(&self) -> PathBuf {
47 match self.entry.parent() {
48 Some(parent) if !parent.as_os_str().is_empty() => self.dir.join(parent),
49 _ => self.dir.clone(),
50 }
51 }
52}
53
54impl Default for ImportResolver {
55 fn default() -> Self {
56 Self {
57 lib_dir: default_lib_dir(),
58 packages: Vec::new(),
59 }
60 }
61}
62
63impl ImportResolver {
64 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn set_lib_dir(&mut self, dir: PathBuf) {
75 self.lib_dir = Some(dir);
76 }
77
78 pub fn add_root(&mut self, name: &str, dir: PathBuf, entry: impl Into<PathBuf>) {
87 let entry = entry.into();
88 if let Some(existing) = self.packages.iter_mut().find(|p| p.name == name) {
89 existing.dir = dir;
90 existing.entry = entry;
91 return;
92 }
93 self.packages.push(PackageRoot {
94 name: SmolStr::from(name),
95 dir,
96 entry,
97 });
98 }
99
100 #[must_use]
102 pub fn lib_dir(&self) -> Option<&Path> {
103 self.lib_dir.as_deref()
104 }
105
106 #[must_use]
108 pub fn roots(&self) -> Vec<(SmolStr, PathBuf)> {
109 self.packages
110 .iter()
111 .map(|p| (p.name.clone(), p.dir.clone()))
112 .collect()
113 }
114
115 #[must_use]
119 pub fn root_dirs(&self) -> Vec<PathBuf> {
120 self.packages.iter().map(|p| p.dir.clone()).collect()
121 }
122
123 #[must_use]
132 pub fn library_path(&self, path: &str) -> Option<PathBuf> {
133 let (head, rest) = split_first_segment(path);
134 if let Some(package) = self.packages.iter().find(|p| p.name == head) {
135 return Some(match rest {
136 Some(rest) => package.source_dir().join(rest),
137 None => package.dir.join(&package.entry),
138 });
139 }
140 self.lib_dir.as_ref().map(|dir| dir.join(path))
141 }
142}
143
144fn split_first_segment(path: &str) -> (&str, Option<&str>) {
150 match path.find(['/', '\\']) {
151 Some(sep) => (&path[..sep], Some(&path[sep + 1..])),
152 None => (path.strip_suffix(".cdl").unwrap_or(path), None),
153 }
154}
155
156fn default_lib_dir() -> Option<PathBuf> {
164 crate::rt::std_lib_dir()
165}
166
167#[cfg(test)]
168mod tests {
169 use super::DEFAULT_PACKAGE_ENTRY;
170 use super::ImportResolver;
171 use std::path::Path;
172 use std::path::PathBuf;
173
174 fn resolver() -> ImportResolver {
175 let mut resolver = ImportResolver {
176 lib_dir: Some(PathBuf::from("/toolchain/libs")),
177 packages: Vec::new(),
178 };
179 resolver.add_root(
180 "shapes",
181 PathBuf::from("/cache/shapes/1.2.3"),
182 DEFAULT_PACKAGE_ENTRY,
183 );
184 resolver
185 }
186
187 #[test]
188 fn a_std_path_reads_from_the_library_directory() {
189 assert_eq!(
190 resolver().library_path("std/string.cdl").as_deref(),
191 Some(Path::new("/toolchain/libs/std/string.cdl"))
192 );
193 }
194
195 #[test]
196 fn a_package_by_its_own_name_reads_its_entry() {
197 assert_eq!(
198 resolver().library_path("shapes.cdl").as_deref(),
199 Some(Path::new("/cache/shapes/1.2.3/src/main.cdl"))
200 );
201 }
202
203 #[test]
204 fn a_path_inside_a_package_reads_from_the_entry_directory() {
205 assert_eq!(
206 resolver().library_path("shapes/circle.cdl").as_deref(),
207 Some(Path::new("/cache/shapes/1.2.3/src/circle.cdl"))
208 );
209 assert_eq!(
210 resolver().library_path("shapes/two/deep.cdl").as_deref(),
211 Some(Path::new("/cache/shapes/1.2.3/src/two/deep.cdl"))
212 );
213 }
214
215 #[test]
216 fn a_manifest_entry_moves_both_forms() {
217 let mut resolver = resolver();
218 resolver.add_root("geom", PathBuf::from("/cache/geom/0.3.1"), "lib/geom.cdl");
219 assert_eq!(
220 resolver.library_path("geom.cdl").as_deref(),
221 Some(Path::new("/cache/geom/0.3.1/lib/geom.cdl"))
222 );
223 assert_eq!(
224 resolver.library_path("geom/arc.cdl").as_deref(),
225 Some(Path::new("/cache/geom/0.3.1/lib/arc.cdl"))
226 );
227 }
228
229 #[test]
230 fn an_entry_at_the_root_reads_paths_from_the_root() {
231 let mut resolver = resolver();
232 resolver.add_root("flat", PathBuf::from("/cache/flat/1.0.0"), "flat.cdl");
233 assert_eq!(
234 resolver.library_path("flat.cdl").as_deref(),
235 Some(Path::new("/cache/flat/1.0.0/flat.cdl"))
236 );
237 assert_eq!(
238 resolver.library_path("flat/more.cdl").as_deref(),
239 Some(Path::new("/cache/flat/1.0.0/more.cdl"))
240 );
241 }
242
243 #[test]
244 fn a_package_root_shadows_the_library_directory() {
245 let mut resolver = resolver();
246 resolver.add_root(
247 "std",
248 PathBuf::from("/cache/std/9.9.9"),
249 DEFAULT_PACKAGE_ENTRY,
250 );
251 assert_eq!(
252 resolver.library_path("std/string.cdl").as_deref(),
253 Some(Path::new("/cache/std/9.9.9/src/string.cdl"))
254 );
255 }
256
257 #[test]
258 fn a_root_added_twice_keeps_the_last_directory() {
259 let mut resolver = resolver();
260 resolver.add_root("shapes", PathBuf::from("/cache/shapes/2.0.0"), "shapes.cdl");
261 assert_eq!(resolver.roots().len(), 1);
262 assert_eq!(
263 resolver.library_path("shapes.cdl").as_deref(),
264 Some(Path::new("/cache/shapes/2.0.0/shapes.cdl"))
265 );
266 }
267
268 #[test]
269 fn without_a_library_directory_only_packages_resolve() {
270 let mut resolver = ImportResolver {
271 lib_dir: None,
272 packages: Vec::new(),
273 };
274 assert_eq!(resolver.library_path("std/string.cdl"), None);
275 resolver.add_root(
276 "shapes",
277 PathBuf::from("/cache/shapes/1.2.3"),
278 DEFAULT_PACKAGE_ENTRY,
279 );
280 assert_eq!(
281 resolver.library_path("shapes.cdl").as_deref(),
282 Some(Path::new("/cache/shapes/1.2.3/src/main.cdl"))
283 );
284 }
285
286 #[test]
287 fn the_root_directories_come_back_in_the_order_they_went_in() {
288 let mut resolver = resolver();
289 resolver.add_root(
290 "geom",
291 PathBuf::from("/cache/geom/0.3.1"),
292 DEFAULT_PACKAGE_ENTRY,
293 );
294 assert_eq!(
295 resolver.root_dirs(),
296 vec![
297 PathBuf::from("/cache/shapes/1.2.3"),
298 PathBuf::from("/cache/geom/0.3.1")
299 ]
300 );
301 }
302}