The files family
The file is this library’s primary node: a rule of this family selects files and then says something about what they depend on, how they are named or where they live. It is the family to reach for first, and the one the other four are variations on.
rule := archunit.ProjectFiles(nil).
InFolder("internal/api/**").
ShouldNot().
DependOnFiles().
InFolder("internal/db/**")
archunit.ProjectFiles is the entry point, archunit.Files is the same one under a shorter name, and both
take an optional *ProjectLocator.
Scope
The scope verbs are chainable and combined with AND, so each one narrows what the rule is about. Every one of them takes an exclusion.
| Verb | Selects the files whose |
|---|---|
WithName |
filename matches the pattern |
InFolder |
folder — the identifier without the filename — matches the pattern |
InPath |
whole identifier matches the pattern |
InFile |
identifier is this string, taken literally |
| Exclusion | Reads its patterns against |
|---|---|
Except |
whatever the selector in front of it reads |
ExceptWithName |
the filename |
ExceptInFolder |
the folder |
ExceptInPath |
the whole identifier |
To see what a scope resolves to before you judge it, ask the scope rather than the rule. SelectFiles hands
back the identifiers, sorted:
files, err := archunit.ProjectFiles(nil).
InFolder("internal/api/**").
ExceptWithName("*_gen.go").
SelectFiles(nil)
The predicates
Should and ShouldNot open the mood stage, and these are the predicates behind them:
| Predicate | Says that a selected file | Moods | Reports |
|---|---|---|---|
DependOnFiles |
depends on the files the object names | both | FileDependencyViolation |
DependOnExternalModules |
depends on the third-party modules the object names | both | FileExternalDependencyViolation |
HaveNoCycles |
is in no dependency circle | Should only |
FileCycleViolation |
HaveName |
is named as the pattern says | both | FileNamingViolation |
BeInFolder |
sits where the pattern says | both | FileNamingViolation |
BeInPath |
has the identifier the pattern says | both | FileNamingViolation |
AdhereTo |
satisfies a function you wrote | both | FileAdherenceViolation |
Every one of them ends in Check, and each violation carries the offending file, the requirement as a
compiled pattern and what was found instead — never a sentence. Running a rule is where the
prose gets built.
A boundary between two folders
DependOnFiles opens an object stage: the files the rule is about depending on. Its verbs are the scope’s
three pattern verbs, chainable and combined with AND, and each takes the same four exclusions:
rule := archunit.ProjectFiles(nil).
InFolder("internal/api/**").
Except("**/generated").
ShouldNot().
DependOnFiles().
InFolder("internal/db/**").
ExceptInFolder("internal/db/dto/**")
That reads as the api does not touch the database, except for its data-transfer types — a boundary with one documented door in it. In the positive mood the same chain is a requirement: every selected file has to depend on at least one file the object matches, which is how every handler uses the service layer is written.
| Object verb | Matches against |
|---|---|
WithName |
the filename of the file depended on |
InFolder |
its folder |
InPath |
its whole identifier |
Third-party modules
DependOnExternalModules is the same shape for the code outside your project, and its one object verb is
Matching, which is read against the import path:
rule := archunit.ProjectFiles(nil).
InFolder("internal/domain/**").
ShouldNot().
DependOnExternalModules().
Matching("*.*/**").
Except("gopkg.in/yaml*/**")
Matching is repeatable and combined with OR — the one chain in this library that widens rather than
narrows, because any of these modules is what a third-party policy means. *.*/** is the glob for
anything with a dot in its first segment, which is every module path that is not a standard library
package.
Cycles
rule := archunit.ProjectFiles(nil).InFolder("internal/**").Should().HaveNoCycles()
One FileCycleViolation per cycle, each carrying a Circuit — the chain of dependencies that leaves a file,
comes back to it and touches nothing twice on the way, printable as a.go -> b.go -> a.go. The predicate is
offered on the positive mood alone: the negation would ask a project to contain a circle somewhere, which is
not a rule anybody means.
Naming and placement
naming := archunit.ProjectFiles(nil).InFolder("internal/api/**").Should().HaveName("*_handler.go")
placement := archunit.ProjectFiles(nil).WithName("*_test.go").ShouldNot().BeInFolder("internal/db/**")
HaveName reads the filename, BeInFolder the folder and BeInPath the whole identifier — the predicate
forms of the three scope verbs, matching against exactly the same parts.
Your own predicate
AdhereTo is the escape hatch: a question about one file, answered yes or no, and the words to report it by.
Should requires every selected file to answer yes; ShouldNot forbids any of them from doing so.
rule := archunit.ProjectFiles(nil).
InFolder("internal/**").
Should().
AdhereTo(func(file archunit.FileInfo) bool {
return file.NonBlankLineCount <= 400
}, "be at most 400 lines long")
The function is a FilePredicate and the value it is handed is a FileInfo, so it can ask about the text of
a file as well as its place:
| Field | Is |
|---|---|
Path |
the file’s identifier |
Name |
its name without the extension |
Extension |
that extension |
Directory |
its folder |
Source |
its whole source text |
NonBlankLineCount |
how many of its lines carry something |
The second argument is the requirement in your own words, and it is what the failure message says the file should have done — so write it as the predicate’s own sentence, be at most 400 lines long, and the report reads as one.
Next
The other four families are the same grammar over a different node: layers name their sets of files up front, slices cut them out of identifiers, metrics count rather than judge, and the dependency graph draws instead of judging at all.