ArchUnitRuby API Guide

This guide is the stable map of ArchUnitRuby’s public fluent API. The class and method reference is generated from the source, while this page explains which entry point to choose and how the stages fit together.

All rule builders are immutable and lazy. Constructing a rule does not read the project; check, measure, snapshot, and export methods are the terminals that perform work.

Common rule shape

rule = ArchUnit.project_files
               .in_folder('app/api/**')
               .should_not.depend_on_files
               .in_folder('app/database/**')

violations = rule.check
Stage Public vocabulary Result
Entry project_files, project_layers, project_slices, project_graph, metrics Immutable builder
Scope with_name, in_folder, in_path, in_file, for_classes_matching Narrower builder
Mood should, should_not Positive or negated predicate builder
Predicate File, layer, slice, or metric policy Checkable rule
Terminal check, measure, snapshot, to_*, export_as_* Data or an artifact

String patterns are anchored globs. * stays inside one path segment, ** crosses zero or more segments, and ? matches one non-separator character. A trailing folder glob such as app/api/** includes the app/api folder itself and every descendant folder. Regular expressions are accepted by all pattern selectors except the exact in_file selector.

Every pattern selector accepts except:. Pass a pattern, an array of patterns, or explicit selector keys such as { in_folder: 'generated', with_name: '*_spec.rb' }.

File rules

Every completed rule implementing ArchUnit::Checkable supports .because(reason). Append it after all selectors and modifiers to get a distinct frozen rule of the same type, with a trimmed, immutable rationale string. Non-string or blank reasons raise ArgumentError. The original rule, violations, graph cache, and check options are unchanged. The shared result formatter adds Because: ... exactly once to failed framework-neutral, RSpec, and Minitest assertions, including failed negated RSpec expectations. Successful assertions and raw violation formatting omit it.

Start with ArchUnit.project_files(project_locator = nil); ArchUnit.files is its short alias. Without a locator, ArchUnitRuby searches upward for a Gemfile or gemspec.

Discovery skips symlinked source files and directories, including aliases inside the project root. Direct FileInfo extraction validates canonical as well as lexical containment before reading. The source walker reuses a single file-status result per entry for type and symlink decisions.

The detected root’s .archignore is applied centrally to graphs and file-based metrics. It accepts blank lines, full-line # comments, basename globs, root-relative path globs, leading / anchors, and trailing / directory patterns. Backslashes normalize to /. Bare names match at any depth; patterns containing a separator match from the project root. A directory pattern excludes its descendants but not a similarly named regular file. Nested ignore files are not read.

Repository patterns combine with defaults or explicit exclude_patterns:. An explicit [] only disables defaults, never repository patterns. The normalized effective pattern set participates in graph cache identity; changing the file is noticed on the next check without clear_cache. Changing source contents still requires explicit cache clearing. Reads are bounded to 1 MiB; UTF-8 BOMs are accepted and invalid bytes replaced. Symlinks (including project-local ones), non-regular files, negation, NUL characters, and a bare / are rejected. No Ruby or gemspec code is evaluated.

Scope or predicate Purpose
with_name(pattern) Select or require a filename pattern
in_folder(pattern) Select or require a project-relative directory
in_path(pattern) Select or require a complete project-relative path
in_file(path) Select one exact project-relative file
have_no_cycles Reject cycles among selected project files
depend_on_files Allow or reject dependencies matching target selectors
depend_on_external_modules Allow or reject standard-library or gem imports
adhere_to(callable, message) Evaluate a custom predicate over immutable FileInfo values
service_scope = ArchUnit.project_files.in_folder('app/services/**')

rules = [
  service_scope.should.have_name('*_service.rb'),
  service_scope.should_not.depend_on_files.in_folder('app/controllers/**'),
  service_scope.should_not.depend_on_external_modules.matching('net/http'),
  service_scope.should.adhere_to(
    ->(file) { file.lines_of_code < 300 },
    'services must stay below 300 non-blank lines'
  )
]

Positive dependency rules are allowlists: every selected dependency must match at least one target. Negated dependency rules are blocklists: matching dependencies become violations.

Cycle projection uses iterative strongly-connected-component and elementary-cycle traversals, so long dependency chains do not consume Ruby’s call stack. It still enumerates all elementary cycles; dense graphs can have exponentially many results. Scope cycle rules to meaningful components.

Layer rules

Use ArchUnit.project_layers(project_locator = nil) to name architectural layers and then declare their allowed or forbidden relationships.

rule = ArchUnit.project_layers
               .layer('api').defined_by('app/api/**/*.rb')
               .layer('services').defined_by_folder('app/services/**')
               .layer('database').defined_by('app/database/**/*.rb')
               .where_layer('api').may_only_depend_on_layers('services')
               .where_layer('services').may_only_depend_on_layers('database')
               .where_layer('database').may_only_depend_on_layers

may_only_depend_on_layers creates an allowlist. Calling it without targets seals the layer. may_not_depend_on_layers creates a blocklist and requires at least one target. Dependencies inside one layer are allowed; edges with an unassigned endpoint are ignored.

Slices and diagrams

Use ArchUnit.project_slices(project_locator = nil) to capture one segment from project paths. (**) is the glob capture; defined_by_regex uses the first regular-expression capture.

slices = ArchUnit.project_slices.defined_by('lib/my_app/(**)/')

dependency_rule = slices.should_not.contain_dependency('api', 'database')
diagram_rule = slices.should
                     .ignoring_external_slices
                     .ignoring_orphan_slices
                     .adhere_to_diagram_in_file('docs/architecture.puml')

plantuml = slices.to_plantuml
slices.export_as_plantuml('reports/architecture.puml')

adhere_to_diagram accepts inline PlantUML. The supported subset includes components, directed dependencies, comments, @startuml, and @enduml.

Dependency graph reports

Use ArchUnit.project_graph(project_locator = nil) for queryable snapshots and reports.

report = ArchUnit.project_graph
                 .include_external_dependencies
                 .focus_on('app/services/**', 2)
                 .collapse_to_folder_depth(2)
                 .titled('Service dependencies')

summary = report.summary
snapshot = report.snapshot
report.export_as_mermaid('reports/services.mmd')
report.export_as_html('reports/services.html')

Query modifiers are focus_on(pattern, depth), reachable_from(pattern), and dependents_of(pattern). Group nodes with collapse_to_folder_depth or collapse_by_pattern(pattern, replacement). Formats are DOT, Mermaid, D2, CSV, JSON, and self-contained HTML; each provides both to_<format> and export_as_<format>.

Metrics

Start with ArchUnit.metrics(project_locator = nil). Select files with the standard file selectors and classes with for_classes_matching.

services = ArchUnit.metrics
                   .in_path('app/services/**/*.rb')
                   .for_classes_matching('*Service')

rules = [
  services.count.method_count.should_be_below_or_equal(20),
  services.lcom.lcom4.should_be(1),
  services.distance.instability.should_be_below(0.8)
]

measurements = services.count.method_count.measure
services.count.export_as_html('reports/service-counts')

Count metrics cover methods, fields, lines, statements, imports, classes, and top-level functions. Cohesion provides LCOM96a, LCOM96b, LCOM1-5, and LCOM*. Distance metrics include abstractness, instability, main-sequence distance, normalized distance, and coupling factor.

Thresholds use exactly should_be_below, should_be_above, should_be, should_be_below_or_equal, should_be_above_or_equal, and should_satisfy. Use custom_metric for a calculation over ClassInfo; zone guards are not_in_zone_of_pain and not_in_zone_of_uselessness.

Checking and test frameworks

Every architecture rule includes ArchUnit::Checkable and exposes check(options = nil). It returns an array of structured Violation values; an architecture disagreement is not an exception until an assertion adapter translates it at the test boundary.

violations = rule.check

expect(rule).to pass         # RSpec
assert_passes(rule)          # Minitest
ArchUnit.assert_passes(rule) # Framework-neutral

Use ArchUnit.format_violations or ArchUnit::ResultFactory when integrating with another test framework or command-line interface.

Check options and logging

ArchUnit::CheckOptions keeps optional execution behavior out of the fluent sentence:

logging = ArchUnit::LoggingOptions.new(
  level: :debug,
  output_directory: 'tmp/archunit-logs'
)

options = ArchUnit::CheckOptions.new(
  allow_empty_tests: false,
  clear_cache: false,
  load_paths: ['components/billing/lib'],
  dependency_analysis: :imports,
  logging: logging
)

violations = rule.check(options)

Zero selected subjects produce EmptyTestViolation by default. Set allow_empty_tests: true only when an empty scope is intentionally valid. Set clear_cache: true after modifying the analyzed project during one process. load_paths adds project-contained source roots without evaluating gemspecs or running application code.

dependency_analysis defaults to :imports. Imports accept literal arguments and preceding local literal-string assignments in supported straight-line scopes. Aliases, mutation/escape, closures, reflection, and unsupported control flow invalidate inferred values; runtime paths are not guessed.

For autoloaded projects, opt into known project constant references alongside imports:

options = ArchUnit::CheckOptions.new(dependency_analysis: :imports_and_constants)
violations = rule.check(options)
json = ArchUnit.project_graph.with_check_options(options).to_json
values = ArchUnit.metrics.distance.instability.measure(options)

Expanded analysis resolves supported lexical, compact, and root-qualified references to their definition files. Explicit superclass and mixin references also contribute edges. Missing or ambiguous definitions, inherited-member lookup, and dynamic metaprogramming stay unresolved under the bounded contract in the main guide. Namespaces that fail to stabilize within eight index passes fall back to import-only results. Unknown third-party constants do not create architecture violations.

Expanded edges include :constant_reference in import_kinds and retain DependencyEvidence with names and source lines in edge.evidence and JSON reports. An import and a constant use supporting the same file pair merge into one edge. Import-only edge evidence stays empty for compatibility; ArchUnit::Extraction.extract_imports retains import call locations in both modes. The analysis mode participates in graph-cache identity, and clear_cache: true rebuilds the symbol index.

Errors and support

Invalid API input raises ArchUnit::UserError or ArgumentError; library or environment failures raise ArchUnit::TechnicalError. Rule failures remain violation data.

See the main guide for installation and complete examples, SUPPORT.md for help, and SECURITY.md for responsible vulnerability reporting.