use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
fn allowed_deps() -> HashMap<&'static str, Vec<&'static str>> {
let mut m = HashMap::new();
m.insert("error", vec![]);
m.insert("model", vec!["error"]);
m.insert("port", vec!["model", "error"]);
m.insert("scanner", vec!["model", "error", "port"]);
m.insert("diff", vec!["model", "error", "port"]);
m.insert("export", vec!["model", "error", "port"]);
m
}
#[test]
fn enforce_layer_dependencies() {
let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let allowed = allowed_deps();
for (module, allowed_deps) in &allowed {
let module_files = collect_module_files(&src_dir, module);
for file_path in &module_files {
let content = fs::read_to_string(file_path).unwrap();
for (line_no, line) in content.lines().enumerate() {
if is_in_test_block(&content, line_no) {
continue;
}
if let Some(dep) = parse_crate_dep(line) {
assert!(
allowed_deps.contains(&dep.as_str()),
"LAYER VIOLATION: {module}/{} line {} imports crate::{dep} \
(allowed: {allowed_deps:?})",
file_path.file_name().unwrap().to_str().unwrap(),
line_no + 1,
);
}
}
}
}
}
fn collect_module_files(src_dir: &Path, module: &str) -> Vec<PathBuf> {
let mut files = vec![];
let single = src_dir.join(format!("{}.rs", module));
if single.exists() {
files.push(single);
}
let dir = src_dir.join(module);
if dir.is_dir() {
for entry in fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_some_and(|e| e == "rs") {
files.push(path);
}
}
}
files
}
fn parse_crate_dep(line: &str) -> Option<String> {
let trimmed = line.trim();
if !trimmed.starts_with("use crate::") {
return None;
}
let after = trimmed.strip_prefix("use crate::")?;
let module = after.split([':', '{', ';']).next()?.trim();
if module.is_empty() {
return None;
}
Some(module.to_string())
}
fn is_in_test_block(content: &str, target_line: usize) -> bool {
let lines: Vec<&str> = content.lines().collect();
for i in (0..target_line).rev() {
let trimmed = lines[i].trim();
if trimmed == "#[cfg(test)]" {
return true;
}
if trimmed.starts_with("pub fn ")
|| trimmed.starts_with("fn ")
|| trimmed.starts_with("pub struct ")
|| trimmed.starts_with("pub enum ")
|| trimmed.starts_with("pub trait ")
{
return false;
}
}
false
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_parse_crate_dep_simple() {
assert_eq!(
parse_crate_dep("use crate::model::Symbol;"),
Some("model".to_string())
);
}
#[test]
fn test_parse_crate_dep_braces() {
assert_eq!(
parse_crate_dep("use crate::error::{DbError, DiffError};"),
Some("error".to_string())
);
}
#[test]
fn test_parse_crate_dep_non_crate() {
assert_eq!(parse_crate_dep("use std::path::Path;"), None);
}
#[test]
fn test_parse_crate_dep_empty() {
assert_eq!(parse_crate_dep("use crate::{DbError};"), None);
}
}