bend/imports/
loader.rs

1use super::{BoundSource, Import, ImportType};
2use crate::fun::Name;
3use indexmap::IndexMap;
4use std::{
5  collections::HashSet,
6  path::{Component, Path, PathBuf},
7};
8
9pub type Sources = IndexMap<Name, String>;
10
11/// Trait to load packages from various sources.
12pub trait PackageLoader {
13  /// Load a package specified by the `import` parameter.
14  ///
15  /// # Parameters
16  ///
17  /// - `import`: A mutable reference to an `Import` structure, which contains:
18  ///   - `path`: The path to the package or directory to be imported.
19  ///   - `imp_type`: The type of import, which can specify a single name, a list of names, or all names in a path.
20  ///   - `relative`: A boolean indicating if the path is relative to the current directory.
21  ///   - `src`: A `BoundSource` to be updated with the names of the located files.
22  ///
23  /// # Behavior
24  ///
25  /// The `load` method is responsible for locating and loading the requested package(s).
26  /// The loaded packages are returned as a `Sources` map, where the key is the package name and the value is its content.
27  /// Implementers must:
28  ///
29  /// - Track already loaded sources to avoid loading and returning them again.
30  /// - Update `import.src` with the names of the found packages, even if they are not included in the `Sources` map.
31  ///
32  /// The implementation should handle the following import types:
33  /// - **Single**: Load a specific file by its name.
34  /// - **List**: Load a list of specified files or names from a specific file.
35  /// - **Glob**: Load all files in a directory or all names from a specific file.
36  fn load(&mut self, import: &mut Import) -> Result<Sources, String>;
37}
38
39/// Default implementation of `PackageLoader` that loads packages from the local directory.
40pub struct DefaultLoader {
41  local_path: PathBuf,
42  loaded: HashSet<Name>,
43  entrypoint: Name,
44}
45
46impl DefaultLoader {
47  pub fn new(local_path: &Path) -> Self {
48    let entrypoint = Name::new(local_path.file_stem().unwrap().to_string_lossy());
49    let local_path = local_path.parent().unwrap().to_path_buf();
50    Self { local_path, loaded: HashSet::new(), entrypoint }
51  }
52
53  fn read_file(&mut self, path: &Path, file_path: &str, src: &mut Sources) -> Result<Option<Name>, String> {
54    let normalized = normalize_path(&PathBuf::from(file_path));
55    let file_path = Name::new(normalized.to_string_lossy());
56
57    if self.entrypoint == file_path {
58      return Err("Can not import the entry point of the program.".to_string());
59    };
60
61    if !self.is_loaded(&file_path) {
62      self.loaded.insert(file_path.clone());
63
64      let path = path.with_extension("bend");
65      let Some(code) = std::fs::read_to_string(path).ok() else { return Ok(None) };
66      src.insert(file_path.clone(), code);
67    }
68
69    Ok(Some(file_path))
70  }
71
72  fn read_file_in_folder(
73    &mut self,
74    full_path: &Path,
75    folder: &str,
76    file_name: &str,
77    src: &mut Sources,
78  ) -> Result<Option<Name>, String> {
79    let full_path = full_path.join(file_name);
80
81    if folder.is_empty() {
82      self.read_file(&full_path, file_name, src)
83    } else {
84      let file_name = &format!("{}/{}", folder, file_name);
85      self.read_file(&full_path, file_name, src)
86    }
87  }
88
89  fn read_path(
90    &mut self,
91    base_path: &Path,
92    path: &Name,
93    imp_type: &ImportType,
94  ) -> Result<Option<(BoundSource, Sources)>, String> {
95    let full_path = base_path.join(path.as_ref());
96    let mut src = IndexMap::new();
97    let (mut file, mut dir) = (None, None);
98
99    if full_path.with_extension("bend").is_file() {
100      file = self.read_file(&full_path, path.as_ref(), &mut src)?;
101    }
102
103    if full_path.is_dir() || path.is_empty() {
104      let mut names = IndexMap::new();
105
106      match imp_type {
107        ImportType::Single(file, _) => {
108          if let Some(name) = self.read_file_in_folder(&full_path, path, file, &mut src)? {
109            names.insert(file.clone(), name);
110          }
111        }
112        ImportType::List(list) => {
113          for (file, _) in list {
114            if let Some(name) = self.read_file_in_folder(&full_path, path, file, &mut src)? {
115              names.insert(file.clone(), name);
116            }
117          }
118        }
119        ImportType::Glob => {
120          for entry in full_path.read_dir().unwrap().flatten() {
121            let file = PathBuf::from(&entry.file_name());
122
123            if let Some("bend") = file.extension().and_then(|f| f.to_str()) {
124              let file = file.file_stem().unwrap().to_string_lossy();
125              if let Some(name) = self.read_file_in_folder(&full_path, path, &file, &mut src)? {
126                names.insert(Name::new(file), name);
127              }
128            }
129          }
130        }
131      }
132
133      if !names.is_empty() {
134        dir = Some(names);
135      }
136    }
137
138    let src = match (file, dir) {
139      (Some(f), None) => Some((BoundSource::File(f), src)),
140      (None, Some(d)) => Some((BoundSource::Dir(d), src)),
141      (Some(f), Some(d)) => Some((BoundSource::Either(f, d), src)),
142      (None, None) => None,
143    };
144
145    Ok(src)
146  }
147
148  fn is_loaded(&self, name: &Name) -> bool {
149    self.loaded.contains(name)
150  }
151}
152
153pub const BEND_PATH: &[&str] = &[""];
154
155impl PackageLoader for DefaultLoader {
156  fn load(&mut self, import: &mut Import) -> Result<Sources, String> {
157    let mut sources = Sources::new();
158
159    let Import { path, imp_type, relative, src } = import;
160
161    let folders = if *relative {
162      vec![self.local_path.clone()]
163    } else {
164      BEND_PATH.iter().map(|p| self.local_path.join(p)).collect()
165    };
166
167    for base in folders {
168      let Some((names, new_pkgs)) = self.read_path(&base, path, imp_type)? else { continue };
169
170      *src = names;
171      sources.extend(new_pkgs);
172      break;
173    }
174
175    if let BoundSource::None = src {
176      return Err(format!("Failed to import '{}' from '{}'", imp_type, path).to_string());
177    }
178
179    Ok(sources)
180  }
181}
182
183// Taken from 'cargo/util/paths.rs'
184pub fn normalize_path(path: &Path) -> PathBuf {
185  let mut components = path.components().peekable();
186  let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
187    components.next();
188    PathBuf::from(c.as_os_str())
189  } else {
190    PathBuf::new()
191  };
192
193  for component in components {
194    match component {
195      Component::Prefix(..) => unreachable!(),
196      Component::RootDir => {
197        ret.push(component.as_os_str());
198      }
199      Component::CurDir => {}
200      Component::ParentDir => {
201        ret.pop();
202      }
203      Component::Normal(c) => {
204        ret.push(c);
205      }
206    }
207  }
208  ret
209}