microcad_lang/resolve/
externals.rs1use crate::{resolve::*, syntax::*, MICROCAD_EXTENSIONS};
7use derive_more::Deref;
8
9#[derive(Default, Deref)]
14pub struct Externals(std::collections::HashMap<QualifiedName, std::path::PathBuf>);
15
16impl Externals {
17 pub fn new(search_paths: &[impl AsRef<std::path::Path>]) -> ResolveResult<Self> {
23 if search_paths.is_empty() {
24 log::info!("No external search paths were given");
25 Ok(Externals::default())
26 } else {
27 let new = Self(Self::search_externals(search_paths)?);
28 if new.is_empty() {
29 log::warn!("Did not find any externals in any search path");
30 } else {
31 log::info!("Found {} external module(s): {new}", new.len());
32 log::trace!("Externals:\n{new:?}");
33 }
34 Ok(new)
35 }
36 }
37
38 pub fn fetch_external(
43 &self,
44 name: &QualifiedName,
45 ) -> ResolveResult<(QualifiedName, std::path::PathBuf)> {
46 log::trace!("fetching {name} from externals");
47
48 if let Some(found) = self
49 .0
50 .iter()
51 .filter(|(n, _)| name.is_within(n))
53 .max_by_key(|(name, _)| name.len())
55 .map(|(name, path)| ((*name).clone(), (*path).clone()))
57 {
58 return Ok(found);
59 }
60
61 Err(ResolveError::ExternalSymbolNotFound(name.clone()))
62 }
63
64 pub fn get_name(&self, path: &std::path::Path) -> ResolveResult<&QualifiedName> {
66 match self.0.iter().find(|(_, p)| p.as_path() == path) {
67 Some((name, _)) => {
68 log::trace!("got name of {path:?} => {name}");
69 Ok(name)
70 }
71 None => Err(ResolveError::ExternalPathNotFound(path.to_path_buf())),
72 }
73 }
74
75 fn search_externals(
77 search_paths: &[impl AsRef<std::path::Path>],
78 ) -> ResolveResult<std::collections::HashMap<QualifiedName, std::path::PathBuf>> {
79 search_paths
80 .iter()
81 .inspect(|p| log::trace!("Searching externals in: {:?}", p.as_ref()))
82 .map(|search_path| {
83 scan_dir::ScanDir::all()
84 .read(search_path.as_ref(), |iter| {
85 iter.map(|(entry, _)| entry.path())
86 .map(find_external_mod)
87 .collect::<Result<Vec<_>, _>>()?
89 .into_iter()
90 .flatten()
91 .map(|file| {
92 let name = make_symbol_name(
93 file.strip_prefix(search_path)
94 .expect("cannot strip search path from file name"),
95 );
96 let path = file.canonicalize().expect("path not found");
97 log::trace!("Found external: {name} {path:?}");
98 Ok((name, path))
99 })
100 .collect::<Result<Vec<_>, _>>()
101 })
102 .into_iter()
103 .collect::<Result<Vec<_>, _>>()
104 .map(|v| v.into_iter().flatten())
105 })
106 .collect::<Result<Vec<_>, _>>()
107 .map(|v| v.into_iter().flatten().collect())
108 }
109}
110
111impl std::fmt::Display for Externals {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 let mut v = self.0.iter().collect::<Vec<_>>();
114 v.sort();
116 write!(
117 f,
118 "{}",
119 v.iter()
120 .map(|file| file.0.to_string())
121 .collect::<Vec<_>>()
122 .join(", ")
123 )
124 }
125}
126
127impl std::fmt::Debug for Externals {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 let mut v = self.0.iter().collect::<Vec<_>>();
130 v.sort();
132 v.iter()
133 .try_for_each(|file| writeln!(f, "{} => {}", file.0, file.1.to_string_lossy()))
134 }
135}
136
137fn make_symbol_name(relative_path: impl AsRef<std::path::Path>) -> QualifiedName {
138 let path = relative_path.as_ref();
139 let stem = path.file_stem().map(|s| s.to_string_lossy().to_string());
140 let name = if stem == Some("mod".into()) {
141 path.parent().expect("mod file without parent folder")
142 } else {
143 path
144 };
145 name.iter()
146 .map(|id| Identifier::no_ref(id.to_string_lossy().as_ref()))
147 .collect()
148}
149
150fn search_mod_dir_file(
151 path: impl AsRef<std::path::Path>,
152) -> ResolveResult<Option<std::path::PathBuf>> {
153 log::trace!("search_mod_dir_file: {:?}", path.as_ref());
154 let files = scan_dir::ScanDir::files().read(path, |iter| {
155 iter.map(|(ref entry, _)| entry.path())
156 .filter(is_mod_file)
157 .collect::<Vec<_>>()
158 })?;
159 if let Some(file) = files.first() {
160 match files.len() {
161 1 => Ok(Some(file.clone())),
162 _ => Err(ResolveError::AmbiguousExternals(files)),
163 }
164 } else {
165 Ok(None)
166 }
167}
168
169#[allow(clippy::ptr_arg)]
171fn is_microcad_file(p: &std::path::PathBuf) -> bool {
172 p.is_file()
173 && p.extension()
174 .map(|ext| {
175 MICROCAD_EXTENSIONS
176 .iter()
177 .any(|extension| *extension == ext)
178 })
179 .unwrap_or(false)
180}
181
182fn is_mod_file(p: &std::path::PathBuf) -> bool {
184 is_microcad_file(p)
185 && p.file_stem()
186 .and_then(|s| s.to_str())
187 .is_some_and(|s| s == "mod")
188}
189
190pub fn find_mod_file_by_id(
199 path: impl AsRef<std::path::Path>,
200 id: &Identifier,
201) -> ResolveResult<std::path::PathBuf> {
202 let path = path.as_ref();
203 log::trace!("find_mod_file_by_id: {path:?} {id:?}");
204 match (
205 search_mod_file_by_id(path, id),
206 search_mod_dir_file(path.join(id.to_string())),
207 ) {
208 (Ok(file), Ok(Some(dir))) => Err(ResolveError::AmbiguousExternals(vec![file, dir])),
209 (Ok(file), Err(_) | Ok(None)) | (Err(_), Ok(Some(file))) => Ok(file),
210 (Err(err), _) => Err(err),
211 }
212}
213
214fn find_external_mod(
215 path: impl AsRef<std::path::Path>,
216) -> ResolveResult<Option<std::path::PathBuf>> {
217 log::trace!("find mod file ex: {:?}", path.as_ref());
218 let path = path.as_ref().to_path_buf();
219 if path.is_dir() {
220 return search_mod_dir_file(&path);
221 }
222 if is_microcad_file(&path) {
223 Ok(Some(path))
224 } else {
225 Ok(None)
226 }
227}
228
229fn search_mod_file_by_id(
230 path: impl AsRef<std::path::Path>,
231 id: &Identifier,
232) -> ResolveResult<std::path::PathBuf> {
233 let path = path.as_ref();
234
235 let path = if std::fs::exists(path.join(".test")).expect("file access failure") {
237 path.join(".test")
238 } else {
239 path.into()
240 };
241
242 log::trace!("search_mod_file_by_id: {path:?} {id}");
243 if let Some(path) = scan_dir::ScanDir::files().read(&path, |iter| {
244 iter.map(|(entry, _)| entry.path())
245 .filter(is_microcad_file)
246 .find(|p| {
247 p.file_stem()
248 .map(|stem| *stem == *id.to_string())
249 .unwrap_or(false)
250 })
251 })? {
252 Ok(path)
253 } else {
254 Err(ResolveError::SourceFileNotFound(
255 id.clone(),
256 path.to_path_buf(),
257 ))
258 }
259}
260
261#[test]
262fn resolve_external_file() {
263 let externals = Externals::new(&["../lib"]).expect("test error");
264
265 assert!(!externals.is_empty());
266
267 log::trace!("{externals}");
268
269 assert!(externals
270 .fetch_external(&"std::geo2d::Circle".into())
271 .is_ok());
272
273 assert!(externals
274 .fetch_external(&"non_std::geo2d::Circle".into())
275 .is_err());
276}