use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ResolvedModule {
pub name: String,
pub path: PathBuf,
pub content: Option<String>,
}
pub struct ModuleResolver {
root: PathBuf,
src_dir: PathBuf,
cache: HashMap<String, ResolvedModule>,
}
impl ModuleResolver {
pub fn new(root: PathBuf) -> Self {
let src_dir = root.join("src");
Self {
root,
src_dir,
cache: HashMap::new(),
}
}
pub fn from_source_file(source_path: &Path) -> Option<Self> {
let mut current = source_path.parent()?;
loop {
if current.join("fastc.toml").exists() {
return Some(Self::new(current.to_path_buf()));
}
if current.file_name().map(|n| n == "src").unwrap_or(false) {
if let Some(parent) = current.parent() {
if parent.join("fastc.toml").exists() {
return Some(Self::new(parent.to_path_buf()));
}
}
}
current = current.parent()?;
}
}
pub fn resolve(&mut self, module_name: &str) -> Result<&ResolvedModule, ModuleError> {
if self.cache.contains_key(module_name) {
return Ok(self.cache.get(module_name).unwrap());
}
let module_file = self.find_module_file(module_name)?;
let resolved = ResolvedModule {
name: module_name.to_string(),
path: module_file,
content: None,
};
self.cache.insert(module_name.to_string(), resolved);
Ok(self.cache.get(module_name).unwrap())
}
pub fn resolve_path(&mut self, path: &[String]) -> Result<&ResolvedModule, ModuleError> {
if path.is_empty() {
return Err(ModuleError::EmptyPath);
}
self.resolve(&path[0])
}
pub fn find_module_file(&self, module_name: &str) -> Result<PathBuf, ModuleError> {
let direct_path = self.src_dir.join(format!("{}.fc", module_name));
if direct_path.exists() {
return Ok(direct_path);
}
let dir_path = self.src_dir.join(module_name).join("mod.fc");
if dir_path.exists() {
return Ok(dir_path);
}
Err(ModuleError::NotFound {
name: module_name.to_string(),
searched: vec![direct_path, dir_path],
})
}
pub fn load_content(&mut self, module_name: &str) -> Result<String, ModuleError> {
if !self.cache.contains_key(module_name) {
self.resolve(module_name)?;
}
let module = self.cache.get_mut(module_name).unwrap();
if module.content.is_none() {
let content = std::fs::read_to_string(&module.path).map_err(|e| ModuleError::Io {
path: module.path.clone(),
error: e.to_string(),
})?;
module.content = Some(content);
}
Ok(module.content.clone().unwrap())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn src_dir(&self) -> &Path {
&self.src_dir
}
}
#[derive(Debug)]
pub enum ModuleError {
NotFound {
name: String,
searched: Vec<PathBuf>,
},
EmptyPath,
Io { path: PathBuf, error: String },
}
impl std::fmt::Display for ModuleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModuleError::NotFound { name, searched } => {
write!(f, "module '{}' not found, searched: ", name)?;
for (i, path) in searched.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", path.display())?;
}
Ok(())
}
ModuleError::EmptyPath => write!(f, "empty module path"),
ModuleError::Io { path, error } => {
write!(f, "failed to read {}: {}", path.display(), error)
}
}
}
}
impl std::error::Error for ModuleError {}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup_test_project() -> TempDir {
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("src")).unwrap();
fs::write(
dir.path().join("fastc.toml"),
r#"[package]
name = "test_project"
"#,
)
.unwrap();
fs::write(
dir.path().join("src/main.fc"),
"fn main() -> i32 { return 0; }",
)
.unwrap();
fs::write(
dir.path().join("src/utils.fc"),
"fn helper() -> i32 { return 42; }",
)
.unwrap();
fs::create_dir_all(dir.path().join("src/math")).unwrap();
fs::write(
dir.path().join("src/math/mod.fc"),
"fn add(a: i32, b: i32) -> i32 { return (a + b); }",
)
.unwrap();
dir
}
#[test]
fn test_resolve_direct_module() {
let project = setup_test_project();
let mut resolver = ModuleResolver::new(project.path().to_path_buf());
let module = resolver.resolve("utils").unwrap();
assert_eq!(module.name, "utils");
assert!(module.path.ends_with("utils.fc"));
}
#[test]
fn test_resolve_directory_module() {
let project = setup_test_project();
let mut resolver = ModuleResolver::new(project.path().to_path_buf());
let module = resolver.resolve("math").unwrap();
assert_eq!(module.name, "math");
assert!(module.path.ends_with("mod.fc"));
}
#[test]
fn test_resolve_not_found() {
let project = setup_test_project();
let mut resolver = ModuleResolver::new(project.path().to_path_buf());
let result = resolver.resolve("nonexistent");
assert!(matches!(result, Err(ModuleError::NotFound { .. })));
}
#[test]
fn test_load_content() {
let project = setup_test_project();
let mut resolver = ModuleResolver::new(project.path().to_path_buf());
let content = resolver.load_content("utils").unwrap();
assert!(content.contains("helper"));
}
}