The slices family
A slice is a name cut out of a file’s identifier. Nothing declares the slices of a project: the capture in the slicing pattern does, so a slicing is a way of talking about a codebase rather than a list of its parts.
rule := archunit.ProjectSlices(nil).
DefinedBy("internal/(**)/**").
ShouldNot().
ContainDependency("api", "db")
internal/(**)/** says the slices of this project are its folders under internal, so
internal/api/handler.go is in the slice api and a file the pattern does not match is in no slice at all.
That is the difference from a layer policy, where every layer is named before any file is read —
and the reason the two families exist side by side.
archunit.ProjectSlices is the entry point. It has no shorter alias, because slices alone is a standard
library package and a chain starting with it would read as one.
The slicing
The scope of this family is exactly one verb, not the usual chain of them:
| Verb | Reads its pattern as |
|---|---|
DefinedBy |
a glob, whose one capture — (**) or (*) — names the slice |
DefinedByRegex |
Go’s own regular expression syntax, whose one capturing group names the slice |
internal/(**)/** and internal/([^/]+)/.* are the same slicing said twice. Both need exactly one capture,
because a slice with two names is not a name; see patterns and identifiers for the rest of the
syntax.
A second slicing is a user error rather than a narrower rule — two slicings would be two vocabularies for the
same project — so a chain that spells one twice reports ErrSlicedTwice, and a chain that spells none reports
ErrNoSlicing.
One pair of slices
ContainDependency takes the two slices as they come out of the capture, in either mood:
slicing := archunit.ProjectSlices(nil).DefinedBy("internal/(**)/**")
noDatabase := slicing.ShouldNot().ContainDependency("api", "db")
viaDomain := slicing.Should().ContainDependency("api", "domain")
Both rules are values over one stored slicing, which is what makes a suite of them cheap to write. Check
reports SliceDependencyViolation values, carrying the two slices and the concrete file dependencies behind
the pair, because api depends on db is one fact however many imports made it true.
The whole architecture at once
Forty pairwise rules nobody keeps up to date are better written as the drawing everybody already has.
AdhereToDiagram takes the diagram as text and AdhereToDiagramInFile reads it from a file beside the code:
rule := archunit.ProjectSlices(nil).
DefinedBy("internal/(**)/**").
Should().
AdhereToDiagramInFile("docs/architecture.puml")
rule := archunit.ProjectSlices(nil).
DefinedBy("internal/(**)/**").
Should().
AdhereToDiagram(`
@startuml
' the architecture we agreed on
component [api]
component [domain]
component [db]
[api] --> [domain]
[api] --> [db]
@enduml
`)
The predicate is on the positive mood alone: should not adhere to the diagram is not a rule anybody means.
The dialect is the component-diagram subset of PlantUML and no more of it — component declarations, arrows,
comments, and the frame. component [api], component api and [api] each declare a component; [api] -->
[db] and [api] -> [db] each draw a dependency, and a : label after an arrow is read and dropped. A line
outside that grammar is refused with its number and its text rather than skipped, because a diagram whose
arrows are quietly half-read is worse than no diagram: it becomes rules nobody wrote. A styling directive a
drawing needs for its looks can be commented out with ' for this library’s benefit. A text with no component
in it at all is ErrEmptyDiagram.
A project and a drawing can disagree in three ways, and one check reports every one of them it finds:
SliceDiagramFinding |
Says |
|---|---|
FindingUndrawnDependency |
the project has a dependency between two slices that the diagram does not draw |
FindingUndeclaredSlice |
the project has a slice the diagram does not declare |
FindingAbsentComponent |
the diagram declares a component the project has no slice for |
They are one violation type, SliceDiagramViolation, under one kind: a reader checking a project against a
drawing wants one list of the ways the two do not match. Only the first carries the file dependencies, because
the other two report that something is not there at all.
Two modifiers switch off one finding each, and they are chainable in either order:
| Modifier | Leaves out |
|---|---|
IgnoringOrphanSlices |
the slices no dependency reaches, which an architect drawing the architecture rather than the folder tree may reasonably not draw |
IgnoringExternalSlices |
the components this project has no slice for, which is what a drawing of a whole system full of sibling modules is made of |
Neither touches the dependencies, and a slice that is an end of an arrow and missing from the drawing is a hole in it whatever the modifiers say.
Drawing the project as it is
The reverse of the rule is a terminal on the slicing itself — a drawing states what a project is, and no rule about it has been written yet:
diagram, err := archunit.ProjectSlices(nil).DefinedBy("internal/(**)/**").ToPlantUML(nil)
err := archunit.ProjectSlices(nil).
DefinedBy("internal/(**)/**").
ExportAsPlantUML("docs/architecture.puml", nil)
ToPlantUML hands back the document as a string and ExportAsPlantUML writes it to a file, in exactly the
dialect AdhereToDiagram reads. So the way to start using this family on a codebase nobody has drawn yet is
to export the diagram, read it, delete the arrows that should not be there, and check the rest in as the
architecture. A slicing that found no slice at all is ErrNothingToDraw rather than an empty frame.
Asking what a slice holds
SelectSliceFiles resolves the slicing and hands back the files of each slice by name, which is how a rule
about a slice that does not exist — a typo, a renamed folder — is found in one line:
files, err := archunit.ProjectSlices(nil).DefinedBy("internal/(**)/**").SelectSliceFiles(nil)
Next
Metrics is the family that counts instead of judging what depends on what, and dependency-graph reports is the one that draws the project’s files rather than its components.