diff --git a/docs/Dockerfile b/docs/Dockerfile index 7735dbc36..7647d5103 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,10 +1,15 @@ -FROM jekyll/jekyll:stable +FROM ruby:3.3-slim -MAINTAINER Peter Gafert +LABEL maintainer="Peter Gafert " + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + && rm -rf /var/lib/apt/lists/* WORKDIR /srv/jekyll -COPY Gemfile . -COPY Gemfile.lock . +COPY Gemfile Gemfile.lock ./ -RUN bundle install \ No newline at end of file +RUN gem install bundler -v 2.3.25 \ + && bundle install diff --git a/docs/README.md b/docs/README.md index d3dae43fb..394419856 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,9 +10,22 @@ Install [Docker](https://docs.docker.com/engine/installation/) and [Docker Compo then execute ``` -docker-compose up +docker compose up ``` -The site will be served on [localhost:4000](http://localhost:4000) + +The site will be served on [localhost:4000](http://localhost:4000). + +### Rebuilding the User Guide + +Jekyll does not render the AsciiDoc user guide (`userguide/*.adoc`) — Gradle +does. After editing any `.adoc` file, run from the repository root: + +``` +./gradlew :docs:renderUserGuide +``` + +This regenerates `userguide/html/` and `_pages/use-cases.md`, which the +running Jekyll container will pick up on the next browser refresh. ## Credits diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml index 5026690e8..86282854d 100644 --- a/docs/docker-compose.yml +++ b/docs/docker-compose.yml @@ -1,14 +1,12 @@ -version: "3.0" - services: site: build: . image: archunit/jekyll:latest - command: bundle exec jekyll serve --verbose --trace --host 0.0.0.0 --watch --incremental + command: bundle exec jekyll serve --verbose --trace --host 0.0.0.0 --watch --force_polling environment: - JEKYLL_ENV=development volumes: - ./:/srv/jekyll - ./_config-dev.yml:/srv/jekyll/_config.yml ports: - - 4000:4000 + - "4000:4000" diff --git a/docs/userguide/003_Getting_Started.adoc b/docs/userguide/003_Getting_Started.adoc index c22853eca..5af17725e 100644 --- a/docs/userguide/003_Getting_Started.adoc +++ b/docs/userguide/003_Getting_Started.adoc @@ -50,8 +50,9 @@ ArchRule myRule = classes() .should().onlyBeAccessed().byAnyPackage("..controller..", "..service.."); ---- -The two dots represent any number of packages (compare AspectJ Pointcuts). The returned -object of type `ArchRule` can now be evaluated against a set of imported classes: +The two dots represent any number of packages (compare AspectJ Pointcuts); for details on +the supported package pattern syntax see <>. The returned object of +type `ArchRule` can now be evaluated against a set of imported classes: [source,java,options="nowrap"] ---- diff --git a/docs/userguide/006_The_Core_API.adoc b/docs/userguide/006_The_Core_API.adoc index 1cb287c74..2a419ea56 100644 --- a/docs/userguide/006_The_Core_API.adoc +++ b/docs/userguide/006_The_Core_API.adoc @@ -396,3 +396,120 @@ ArchUnit's own rule APIs (compare <>) never rely on the classpath though. Thus the evaluation of default rules and syntax combinations, described in the next section, does not depend on whether the classes were imported from the classpath or some JAR / folder. + +=== Package Identifiers + +Several ArchUnit methods accept a `String` package identifier to describe a set of packages, +for example `resideInAPackage(..)` and `resideInAnyPackage(..)` (and their negated variants) +in the Lang API, `slices().matching(..)` and `modules().definedByPackages(..)` in the Library +API, or the component stereotypes of a PlantUML component diagram. All of them delegate to +the same underlying https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/core/domain/PackageMatcher.html[`PackageMatcher`], whose syntax is inspired by AspectJ type patterns and +extended with capturing groups. + +==== Wildcards + +The syntax is built from the following elements: + +[cols="1,4"] +|=== +| Pattern | Meaning + +| `*` +| Matches any sequence of characters **not containing** the dot `.`, +i.e. exactly one package segment. + +| `..` +| Matches any sequence of characters that **may contain** the dot `.`, +i.e. any number of package segments (including zero). + +| `(*)` +| Like `*`, but additionally _captures_ the matched segment, +so it can be referenced later (e.g. as a slice identifier, see <>). + +| `(**)` +| Like `..`, but additionally _captures_ the matched segments. + +| `[a\|b]` +| Alternation: matches either `a` or `b`. Alternations are only allowed inside brackets +`[...]` or inside capturing groups `(...)`. +|=== + +Note that `(..)` is not a valid capturing group — use `(**)` instead. `()` and `[]` cannot +be nested inside each other. + +The segments matched by capturing groups can be retrieved from the result of +https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/core/domain/PackageMatcher.html#match(java.lang.String)[`PackageMatcher#match(String)`] +via +https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/core/domain/PackageMatcher.Result.html#getGroup(int)[`PackageMatcher.Result#getGroup(int)`] +(groups are 1-based, in the order the capturing groups appear in the pattern). + +==== Worked Example + +Consider the following five classes and the packages they reside in: + +[cols="2,2"] +|=== +| Class | Package + +| `com.myapp.controller.SomeController` | `com.myapp.controller` +| `com.myapp.service.SomeService` | `com.myapp.service` +| `com.myapp.service.impl.SomeServiceImpl` | `com.myapp.service.impl` +| `com.myapp.persistence.dao.SomeDao` | `com.myapp.persistence.dao` +| `com.myapp.persistence.dao.jpa.SomeJpa` | `com.myapp.persistence.dao.jpa` +|=== + +The following table shows a range of package identifiers and which of the five packages +above each of them matches: + +[cols="2,3"] +|=== +| Package Identifier | Matches + +| `com.myapp.service` +| `com.myapp.service` only (exact match). + +| `com.myapp.*` +| `com.myapp.controller` and `com.myapp.service` +(exactly one more segment after `com.myapp`). + +| `com.myapp..` +| All five packages — `com.myapp..` matches `com.myapp` itself and every subpackage of it. + +| `..service` +| `com.myapp.service` only (segment `service` at the end). + +| `..service..` +| `com.myapp.service` and `com.myapp.service.impl` +(any package containing a segment `service`). + +| `..dao..` +| `com.myapp.persistence.dao` and `com.myapp.persistence.dao.jpa`. + +| `..impl` +| `com.myapp.service.impl` only. + +| `com.myapp.(*)` +| `com.myapp.controller` (group 1 = `controller`) and +`com.myapp.service` (group 1 = `service`). +The deeper packages do not match because `(*)` allows only a single segment. + +| `com.myapp.(*)..` +| All five packages; group 1 captures the first sub-package below `com.myapp` +(`controller`, `service`, `service`, `persistence`, `persistence` respectively). +This is the typical pattern used by `slices().matching(..)`. + +| `..[service\|controller]..` +| `com.myapp.controller`, `com.myapp.service` and `com.myapp.service.impl`. +|=== + +==== What is Matched Against the Pattern + +An important detail is that the package identifier is matched against the +**package name of a class**, not against the fully qualified class name. +So for `com.myapp.service.SomeService` the string that is checked against +the pattern is `com.myapp.service`, not `com.myapp.service.SomeService`. + +This means that a pattern like `..SomeService` will _not_ match a class named `SomeService`, +because `SomeService` is the simple class name, not a package segment. To match by class +name use a name-based predicate such as +`haveSimpleName("SomeService")` or `haveNameMatching(".*SomeService")` instead. diff --git a/docs/userguide/008_The_Library_API.adoc b/docs/userguide/008_The_Library_API.adoc index 49419c37b..6746b9262 100644 --- a/docs/userguide/008_The_Library_API.adoc +++ b/docs/userguide/008_The_Library_API.adoc @@ -130,7 +130,10 @@ com.tngtech.archunit.library.dependencies.SlicesRuleDefinition ---- The API is based on the idea to sort classes into slices according to one or several package -infixes, and then write assertions against those slices. At the moment this is for example: +infixes, and then write assertions against those slices. The `matching(..)` argument follows +the package pattern syntax described in <>, where the parentheses +`+++(*)+++` / `+++(**)+++` mark the captured segment(s) that are used as slice identifiers. +At the moment this is for example: [source,java,options="nowrap"] ----