use std::path::PathBuf;
#[expect(
clippy::exhaustive_enums,
reason = "matched exhaustively by consumer code"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModuleError {
NotFound {
path: Vec<String>,
searched_paths: Vec<PathBuf>,
},
CircularImport { cycle: Vec<String> },
ReadError { path: PathBuf, error: String },
PrivateItem { item: String, module: String },
ItemNotFound {
item: String,
module: String,
available: Vec<String>,
},
}
pub trait ModuleResolver {
fn resolve(
&self,
path: &[String],
current_file: Option<&PathBuf>,
) -> Result<(String, PathBuf), ModuleError>;
}
#[derive(Debug)]
pub struct FileSystemResolver {
root_dir: PathBuf,
}
impl FileSystemResolver {
#[must_use]
pub const fn new(root_dir: PathBuf) -> Self {
Self { root_dir }
}
fn find_module_file(&self, path: &[String], current_file: Option<&PathBuf>) -> Option<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
if let Some(cf) = current_file {
if let Some(parent) = cf.parent() {
if parent.as_os_str().is_empty() {
roots.push(PathBuf::from("."));
} else {
roots.push(parent.to_path_buf());
}
}
}
if !roots.iter().any(|r| r == &self.root_dir) {
roots.push(self.root_dir.clone());
}
for root in &roots {
let mut file_path = root.clone();
for segment in path {
file_path.push(segment);
}
file_path.set_extension("fv");
if file_path.exists() {
return Some(file_path);
}
if let Some((_last, init)) = path.split_last() {
if !init.is_empty() {
let mut dir_path = root.clone();
for segment in init {
dir_path.push(segment);
}
dir_path.set_extension("fv");
if dir_path.exists() {
return Some(dir_path);
}
}
}
}
None
}
}
impl ModuleResolver for FileSystemResolver {
fn resolve(
&self,
path: &[String],
current_file: Option<&PathBuf>,
) -> Result<(String, PathBuf), ModuleError> {
let module_file = self.find_module_file(path, current_file).ok_or_else(|| {
let mut searched = vec![];
let mut search_roots: Vec<PathBuf> = Vec::new();
if let Some(cf) = current_file {
if let Some(parent) = cf.parent() {
search_roots.push(if parent.as_os_str().is_empty() {
PathBuf::from(".")
} else {
parent.to_path_buf()
});
}
}
if !search_roots.iter().any(|r| r == &self.root_dir) {
search_roots.push(self.root_dir.clone());
}
for root in &search_roots {
let mut file_path = root.clone();
for segment in path {
file_path.push(segment);
}
file_path.set_extension("fv");
searched.push(file_path);
if let Some((_last, init)) = path.split_last() {
if !init.is_empty() {
let mut dir_path = root.clone();
for segment in init {
dir_path.push(segment);
}
dir_path.set_extension("fv");
searched.push(dir_path);
}
}
}
ModuleError::NotFound {
path: path.to_vec(),
searched_paths: searched,
}
})?;
let source = std::fs::read_to_string(&module_file).map_err(|e| ModuleError::ReadError {
path: module_file.clone(),
error: e.to_string(),
})?;
Ok((source, module_file))
}
}