init: archlens — architecture diagram generator
Some checks failed
CI / Check / Test (push) Failing after 1m24s
Some checks failed
CI / Check / Test (push) Failing after 1m24s
Hex arch + DDD, tree-sitter parsing, Mermaid/ASCII output. Supports Rust + Python. 92 tests. CI, diff, --check for staleness detection.
This commit is contained in:
15
crates/adapters/walkdir/Cargo.toml
Normal file
15
crates/adapters/walkdir/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "archlens-walkdir"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
archlens-domain.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
walkdir.workspace = true
|
||||
ignore.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
3
crates/adapters/walkdir/src/lib.rs
Normal file
3
crates/adapters/walkdir/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod walkdir_discovery;
|
||||
|
||||
pub use walkdir_discovery::WalkdirDiscovery;
|
||||
100
crates/adapters/walkdir/src/walkdir_discovery.rs
Normal file
100
crates/adapters/walkdir/src/walkdir_discovery.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::path::Path;
|
||||
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
use archlens_domain::{
|
||||
AnalysisConfig, DomainError, FilePath, Language, SourceFile, ports::FileDiscovery,
|
||||
};
|
||||
|
||||
const DEFAULT_EXCLUDES: &[&str] = &[
|
||||
".venv",
|
||||
"venv",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
".git",
|
||||
"target",
|
||||
"bin",
|
||||
"obj",
|
||||
"dist",
|
||||
".tox",
|
||||
".eggs",
|
||||
];
|
||||
|
||||
pub struct WalkdirDiscovery;
|
||||
|
||||
impl Default for WalkdirDiscovery {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl WalkdirDiscovery {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn detect_language(path: &Path) -> Option<Language> {
|
||||
match path.extension()?.to_str()? {
|
||||
"rs" => Some(Language::Rust),
|
||||
"py" => Some(Language::Python),
|
||||
"cs" => Some(Language::CSharp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_excluded(path: &Path, root: &Path, excludes: &[String]) -> bool {
|
||||
let relative = path.strip_prefix(root).unwrap_or(path);
|
||||
let relative_str = relative.to_string_lossy();
|
||||
|
||||
for component in relative.components() {
|
||||
let name = component.as_os_str().to_string_lossy();
|
||||
if DEFAULT_EXCLUDES.iter().any(|e| name == *e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
excludes
|
||||
.iter()
|
||||
.any(|exclude| relative_str.contains(exclude.trim_end_matches('/')))
|
||||
}
|
||||
}
|
||||
|
||||
impl FileDiscovery for WalkdirDiscovery {
|
||||
fn discover(
|
||||
&self,
|
||||
root: &Path,
|
||||
config: &AnalysisConfig,
|
||||
) -> Result<Vec<SourceFile>, DomainError> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
let walker = WalkBuilder::new(root).hidden(true).git_ignore(true).build();
|
||||
|
||||
for entry in walker.filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if Self::is_excluded(path, root, config.excludes()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(scope) = config.scope() {
|
||||
let relative = path.strip_prefix(root).unwrap_or(path);
|
||||
if !relative.starts_with(scope) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(language) = Self::detect_language(path) {
|
||||
let absolute = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let file_path = FilePath::new(&absolute.to_string_lossy())
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))?;
|
||||
files.push(SourceFile::new(file_path, language));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
71
crates/adapters/walkdir/tests/walkdir_discovery_tests.rs
Normal file
71
crates/adapters/walkdir/tests/walkdir_discovery_tests.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::fs;
|
||||
|
||||
use archlens_domain::{AnalysisConfig, Language, ports::FileDiscovery};
|
||||
use archlens_walkdir::WalkdirDiscovery;
|
||||
|
||||
fn create_test_tree(dir: &std::path::Path) {
|
||||
fs::create_dir_all(dir.join("src/orders")).unwrap();
|
||||
fs::create_dir_all(dir.join("src/billing")).unwrap();
|
||||
fs::write(dir.join("src/orders/service.rs"), "struct OrderService;").unwrap();
|
||||
fs::write(dir.join("src/orders/model.py"), "class Order: pass").unwrap();
|
||||
fs::write(dir.join("src/billing/invoice.cs"), "class Invoice {}").unwrap();
|
||||
fs::write(dir.join("src/readme.txt"), "not source code").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_rust_python_and_csharp_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
create_test_tree(dir.path());
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 3);
|
||||
|
||||
let languages: Vec<Language> = files.iter().map(|f| f.language()).collect();
|
||||
assert!(languages.contains(&Language::Rust));
|
||||
assert!(languages.contains(&Language::Python));
|
||||
assert!(languages.contains(&Language::CSharp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_non_source_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
create_test_tree(dir.path());
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
let paths: Vec<&str> = files.iter().map(|f| f.path().as_str()).collect();
|
||||
assert!(!paths.iter().any(|p| p.ends_with(".txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_exclude_patterns() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
create_test_tree(dir.path());
|
||||
|
||||
let config = AnalysisConfig::default().with_excludes(vec!["billing".to_string()]);
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery.discover(dir.path(), &config).unwrap();
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
assert!(!files.iter().any(|f| f.path().as_str().contains("billing")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_directory_returns_no_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let discovery = WalkdirDiscovery::new();
|
||||
let files = discovery
|
||||
.discover(dir.path(), &AnalysisConfig::default())
|
||||
.unwrap();
|
||||
|
||||
assert!(files.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user