# Archaven
Archaven is a Rust library for checking dependency rules inside Rust projects.
It scans Rust source files, extracts dependencies between module paths, runs your
rules against those dependencies, and returns a printable list of violations.
The primary use case is an architecture test that fails when code crosses a
boundary it should not cross.
```rust
use archaven::{Access, Archaven, Rule};
#[test]
fn architecture_rules_are_respected() {
let violations = Archaven::new()
.rule(
Rule::between("app::*")
.named("bounded contexts")
.deny_all()
.allow(
Access::from("*::infrastructure::adapter::**")
.to_any([
"*::application::command::**",
"*::application::query::**",
])
.because("bounded contexts communicate through adapters and command/query APIs"),
),
)
.check("./src")
.unwrap();
assert!(violations.is_empty(), "{violations}");
}
```
## What Archaven Checks
Archaven works with Rust module paths such as:
```text
app::sales::orders::domain::order
app::sales::orders::infrastructure::adapter::billing_client
app::billing::invoices::application::command::issue_invoice
```
For each dependency found in the source tree, Archaven asks:
```text
source path -> target path
```
Then every configured `Rule` decides whether that dependency is allowed. A rule
can describe boundaries between scopes, dependencies inside one scope, or a
global source-to-target policy.
## Installation
Add Archaven as a dev dependency:
```toml
[dev-dependencies]
archaven = "0.2.1"
```
## Core Concepts
### `Archaven`
`Archaven` is the checker. Add one or more rules, call `check`, and assert that
the returned `Violations` collection is empty.
```rust
let violations = Archaven::new()
.rule(/* rule */)
.check("./src")
.unwrap();
violations.assert_empty();
```
`check` returns `Result<Violations, ArchavenError>`:
- `Err(...)` means scanning, parsing, or rule compilation failed.
- `Ok(violations)` means analysis completed and the returned list contains all
architectural violations.
### `Rule::between`
`Rule::between(pattern)` checks dependencies between different instances matched
by the same scope pattern.
```rust
Rule::between("app::*")
```
With paths like `app::sales::...` and `app::billing::...`, this checks
cross-context dependencies from `sales` to `billing` and from `billing` to
`sales`. Dependencies inside `app::sales` are ignored by this rule.
Example:
```rust
Rule::between("app::*")
.named("bounded contexts")
.deny_all()
.allow(
Access::from("*::infrastructure::adapter::**")
.to_any([
"*::application::command::**",
"*::application::query::**",
]),
)
```
The `from` and `to` patterns are relative to the matched source and target
scopes. For a dependency from `app::sales` to `app::billing`, the rule above
allows:
```text
app::sales::*::infrastructure::adapter::**
->
app::billing::*::application::command::**
app::billing::*::application::query::**
```
### `Rule::within`
`Rule::within(pattern)` checks dependencies inside the same matched scope.
```rust
Rule::within("app::*::*")
```
With a scope like `app::sales::orders`, this checks dependencies from one path
under `orders` to another path under `orders`. Dependencies from `orders` to
`invoices` are ignored by this rule.
Example:
```rust
Rule::within("app::*::*")
.named("module internals")
.deny_all()
.allow(Access::from("application::**").to("domain::**"))
.allow(Access::from("infrastructure::**").to("application::**"))
.allow(Access::from("infrastructure::**").to("domain::**"))
.allow(Access::from("ui::**").to("application::**"))
```
If a file assembles a module and should not be evaluated by that rule, ignore it
with a file glob:
```rust
Rule::within("app::*::*")
.named("module internals")
.deny_all()
.ignore_files(["**/mod.rs"])
.allow(Access::from("application::**").to("domain::**"))
```
`ignore_files` is per-rule. Dependencies discovered in matching source files are
skipped for that rule before `deny`, `deny_all`, and `allow` are evaluated.
This can model layered, hexagonal, vertical-slice, plugin, or custom dependency
styles without adding architecture-specific types to the public API.
### `Rule::new`
`Rule::new()` checks absolute source and target patterns.
```rust
Rule::new()
.named("domain purity")
.deny(
Access::from("app::**::domain::**")
.to("app::**::infrastructure::**")
.because("domain code must not depend on infrastructure"),
)
```
Use this when a rule is not scoped by `between` or `within`.
## Path Patterns
Archaven matches module paths by `::` segments.
```text
* = exactly one segment
** = one or more segments
```
Examples:
```text
app::*::anything matches app::orders::anything
app::*::anything does not match app::orders::nested::anything
app::**::anything matches app::orders::nested::anything
app::**::anything does not match app::anything
app::** matches app::orders and app::orders::domain
app::** does not match app
```
Patterns are intentionally segment-based. `app::*::domain` is different from
`app::**::domain`, and `**` never means "zero segments".
## Modular Monolith Example
Given this source layout:
```text
src/app/sales/orders/domain/order.rs
src/app/sales/orders/application/command/create_order.rs
src/app/sales/orders/infrastructure/adapter/billing_client.rs
src/app/sales/orders/ui/http_controller.rs
src/app/billing/invoices/application/command/issue_invoice.rs
src/app/billing/invoices/application/query/get_invoice.rs
```
Check cross-context communication:
```rust
use archaven::{Access, Archaven, Rule};
#[test]
fn bounded_contexts_are_isolated() {
let violations = Archaven::new()
.rule(
Rule::between("app::*")
.named("bounded contexts")
.deny_all()
.allow(
Access::from("*::infrastructure::adapter::**")
.to_any([
"*::application::command::**",
"*::application::query::**",
])
.because("cross-context calls go through adapters and command/query APIs"),
),
)
.check("./src")
.unwrap();
assert!(violations.is_empty(), "{violations}");
}
```
Check dependencies inside each module:
```rust
use archaven::{Access, Archaven, Rule};
#[test]
fn module_internal_dependencies_are_valid() {
let violations = Archaven::new()
.rule(
Rule::within("app::*::*")
.named("module internals")
.deny_all()
.allow(Access::from("application::**").to("domain::**"))
.allow(Access::from("infrastructure::**").to("application::**"))
.allow(Access::from("infrastructure::**").to("domain::**"))
.allow(Access::from("ui::**").to("application::**"))
.because("dependencies inside a module must point inward"),
)
.check("./src")
.unwrap();
assert!(violations.is_empty(), "{violations}");
}
```
## Violation Output
`Violations` implements `Display`, so it can be used directly in assertions:
```rust
assert!(violations.is_empty(), "{violations}");
```
For test assertions, `assert_empty` panics with the formatted violation list and
reports the panic at the assertion call site:
```rust
violations.assert_empty();
```
You can also format violations yourself:
```rust
for violation in &violations {
println!(
"{}: {} -> {}",
violation.location().file().display(),
violation.source(),
violation.target(),
);
}
```
A violation contains:
- rule name
- reason
- source module path
- target module path
- file path
- line and column when available
## How Scanning Works
Archaven derives source module paths from Rust file paths under the directory
passed to `check`.
For example:
```text
src/app/sales/orders/domain/order.rs
```
becomes:
```text
app::sales::orders::domain::order
```
The scanner parses Rust files with `syn` and records dependencies from:
- `use crate::...`
- `crate::...`
- `self::...`
- `super::...`
- local root paths such as `app::...`
Use `Rule::ignore_files(["**/mod.rs"])` when module composition root files
intentionally wire internals that the rule should not evaluate.
This keeps Archaven fast and usable from regular tests. It also means the first
version is a source-level checker, not a full Rust compiler front-end. Macro
generated paths, complex re-exports, trait dispatch, and every possible aliasing
pattern may require explicit paths in code or future scanner improvements.
## Custom Rule Sets
The built-in `Rule` type is enough for many projects, but Archaven also exposes
`RuleSet` for custom policies.
```rust
use archaven::{ArchavenError, DependencyGraph, RuleSet, Violations};
struct MyRule;
impl RuleSet for MyRule {
fn check(&self, graph: &DependencyGraph) -> Result<Violations, ArchavenError> {
let _ = graph;
Ok(Violations::new())
}
}
```
## License
Licensed under either of:
- Apache License, Version 2.0
- MIT license
at your option.