use std::collections::HashMap;
use std::path::Path;
use crate::engine::config::Target;
use crate::engine::targets::{normalise_separators, path_comparison_key, topmost_targets};
pub(crate) type ModuleId = u16;
#[derive(Debug,Default)]
pub(crate) struct Modules {
names: Vec<Option<String>>,
dir_boundaries: HashMap<String, ModuleId>,
file_boundaries: HashMap<String, ModuleId>
}
impl Modules {
pub(crate) fn of(targets: &[Target]) -> Self {
if targets.iter().all(|x| x.module.is_none()) {
return Modules::default();
}
let mut names : Vec<Option<String>> = Vec::new();
for target in targets {
if !names.contains(&target.module) {
names.push(target.module.clone());
}
}
if let Some(position) = names.iter().position(Option::is_none) {
let unnamed = names.remove(position);
names.push(unnamed);
}
let roots = topmost_targets(targets);
let mut modules = Modules { names, ..Default::default() };
for target in targets {
if roots.contains(target) {
continue;
}
let id = modules.id_of(&target.module);
let key = path_comparison_key(target.path.trim_end_matches('/'));
if Path::new(&target.path).is_dir() {
modules.dir_boundaries.insert(key, id);
} else {
modules.file_boundaries.insert(key, id);
}
}
modules
}
fn id_of(&self, module: &Option<String>) -> ModuleId {
self.names.iter().position(|x| x == module).unwrap_or(0) as ModuleId
}
pub(crate) fn count(&self) -> usize {
self.names.len().max(1)
}
pub(crate) fn is_used(&self) -> bool {
!self.names.is_empty()
}
pub(crate) fn name_of(&self, id: ModuleId) -> Option<&str> {
self.names.get(id as usize).and_then(|x| x.as_deref())
}
pub(crate) fn of_target(&self, target: &Target) -> ModuleId {
if self.is_used() {self.id_of(&target.module)} else {0}
}
pub(crate) fn has_dir_boundaries(&self) -> bool {
!self.dir_boundaries.is_empty()
}
pub(crate) fn has_file_boundaries(&self) -> bool {
!self.file_boundaries.is_empty()
}
pub(crate) fn at_dir(&self, path: &Path, inherited: ModuleId) -> ModuleId {
self.at(&self.dir_boundaries, path, inherited)
}
pub(crate) fn at_file(&self, path: &Path, inherited: ModuleId) -> ModuleId {
self.at(&self.file_boundaries, path, inherited)
}
fn at(&self, boundaries: &HashMap<String, ModuleId>, path: &Path, inherited: ModuleId) -> ModuleId {
let Some(path) = path.to_str() else { return inherited };
let path = normalise_separators(path);
boundaries.get(&path_comparison_key(&path)).copied().unwrap_or(inherited)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn modules_of(entries: &[&str]) -> Modules {
let targets = entries.iter().map(|entry| match entry.split_once(' ') {
Some((name, path)) => Target::named(name, path),
None => Target::of(*entry)
}).collect::<Vec<_>>();
Modules::of(&targets)
}
#[test]
fn a_run_that_names_nothing_has_one_bucket_and_no_lookups() {
let modules = modules_of(&["./src", "./tests"]);
assert!(!modules.is_used());
assert_eq!(1, modules.count());
assert_eq!(None, modules.name_of(0));
assert!(!modules.has_dir_boundaries() && !modules.has_file_boundaries());
}
#[test]
fn the_leftovers_are_a_bucket_of_their_own_and_come_last() {
let modules = modules_of(&["./src", "code ./src/lib.rs", "docs ./data"]);
assert!(modules.is_used());
assert_eq!(3, modules.count());
assert_eq!(Some("code"), modules.name_of(0));
assert_eq!(Some("docs"), modules.name_of(1));
assert_eq!(None, modules.name_of(2));
let modules = modules_of(&["code ./src"]);
assert!(modules.is_used());
assert_eq!(1, modules.count());
assert_eq!(Some("code"), modules.name_of(0));
}
#[test]
fn only_a_target_inside_another_target_is_a_boundary() {
let unrelated = modules_of(&["code ./src", "suite ./tests"]);
assert!(!unrelated.has_dir_boundaries() && !unrelated.has_file_boundaries());
let nested_dir = modules_of(&["./", "fixtures ./tests/fixtures"]);
assert!(nested_dir.has_dir_boundaries() && !nested_dir.has_file_boundaries());
let nested_file = modules_of(&["./src", "entry ./src/main.rs"]);
assert!(!nested_file.has_dir_boundaries() && nested_file.has_file_boundaries());
}
#[test]
fn a_boundary_answers_for_its_own_path_and_leaves_the_rest_inherited() {
let modules = modules_of(&["./", "fixtures ./tests/fixtures"]);
let fixtures = modules.id_of(&Some("fixtures".to_owned()));
assert_eq!(fixtures, modules.at_dir(Path::new("./tests/fixtures"), 7));
assert_eq!(7, modules.at_dir(Path::new("./tests"), 7));
assert_eq!(7, modules.at_dir(Path::new("./tests/fixtures/lang"), 7));
if cfg!(windows) {
assert_eq!(fixtures, modules.at_dir(Path::new(".\\tests\\fixtures"), 7));
assert_eq!(fixtures, modules.at_dir(Path::new("./TESTS/Fixtures"), 7));
}
}
}