The grammar

The fluent API constrains which words can legally follow one another. A completed check rule has this shape:

entry point -> subject selectors -> mood -> condition -> optional object selectors -> terminal

Graph reports and raw measurements are queries rather than verdicts, so they omit the mood and end in a rendering or measurement terminal.

Entry points

The entry point chooses both a feature family and where Cargo discovery begins.

Family Auto-discover Explicit directory or Cargo.toml Alias
Files project_files() project_files_in(path) files() / files_in(path)
Layers project_layers() project_layers_in(path) layers() / layers_in(path)
Slices project_slices() project_slices_in(path) slices() / slices_in(path)
Metrics metrics() metrics_in(path)
Graph reports project_graph() project_graph_in(path) dependency_graph() / dependency_graph_in(path)

Auto-discovery starts at the process working directory and walks to the relevant Cargo manifest. The explicit forms are useful when a test runner starts outside the project being checked.

Subject selectors

File selectors are with_name, in_folder, in_path, and literal in_file. Chaining selectors narrows the subject with AND semantics:

use archunit::project_files;

let scope = project_files()
    .in_path("crates/**")
    .in_folder("**/services")
    .with_name("*_service.rs");

assert_eq!(scope.filters().len(), 3);

Metrics add for_types_matching. Layers name each selection with layer(...).defined_by(...), and slices derive component names with defined_by, defined_by_regex, or with_projection.

Mood and condition

should() enters the positive mood; should_not() enters the negative mood. Naming and placement conditions invert directly. Dependency conditions have the more useful architecture meaning:

Cycle freedom is positive only. Slice diagram adherence is positive only, while a forbidden pair of slices uses should_not().contain_dependency(source, target).

Object selectors

Some conditions need an object before the sentence is complete. depend_on_files() accepts with_name, in_folder, in_path, and in_file; repeated object selectors are OR alternatives. depend_on_external_modules() accepts repeated matching crate-name globs.

use archunit::{Checkable, project_files};

let rule = project_files()
    .in_path("src/api/**")
    .should()
    .depend_on_files()
    .in_path("src/domain/**")
    .in_path("src/common/**");

let _: &dyn Checkable = &rule;

This permits API files to depend only on the two named internal areas. Dependencies outside the analyzed workspace remain external and are governed by a separate external-module condition.

Terminals

Every architecture condition implements Checkable. Use check() for typed data, check_with for per-check options, or assert_passes! at the test boundary:

use archunit::{ArchUnitError, Checkable, project_files};

fn inspect_rule() -> Result<(), ArchUnitError> {
    let rule = project_files().in_path("src/**").should().have_no_cycles();
    let violations = rule.check()?;
    assert!(violations.is_empty());
    Ok(())
}

Metrics may instead end in measure, analyze, or export_as_html. Graph queries end in snapshot, summary, a to_* renderer, or an export_as_* writer. Slice scopes can render the actual component graph with to_plantuml or export_as_plantuml.

Builders are values

Builder methods consume and return Self; they do not mutate shared global configuration. Clone a partially built value to branch it:

use archunit::project_files;

let source = project_files().in_path("src/**");
let services = source.clone().with_name("*_service.rs");
let repositories = source.with_name("*_repository.rs");

assert_eq!(services.filters().len(), 2);
assert_eq!(repositories.filters().len(), 2);

Continue with patterns and identifiers, then choose a family from the landing page.