Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,49 @@
import static java.util.stream.Collectors.toList;

/**
* Matches packages with a syntax similar to AspectJ. In particular '*' stands for any sequence of
* characters but not the dot '.', while '..' stands for any sequence of packages, including zero packages.<br>
* Matches packages with a syntax similar to AspectJ. In particular
* <ul>
* <li><b>{@code *}</b> stands for any non-empty sequence of characters but not the dot '.', while</li>
* <li><b>{@code ..}</b> stands for any sequence of packages, including zero packages.</li>
* </ul>
* For example
* <ul>
* <li><b>{@code '..pack..'}</b> matches <b>{@code 'a.pack'}</b>, <b>{@code 'a.pack.b'}</b> or <b>{@code 'a.b.pack.c.d'}</b>,
* but not <b>{@code 'a.packa.b'}</b></li>
* <li><b>{@code '*.pack.*'}</b> matches <b>{@code 'a.pack.b'}</b>, but not <b>{@code 'a.b.pack.c'}</b></li>
* <li><b>{@code '..*pack*..'}</b> matches <b>{@code 'a.prepackfix.b'}</b></li>
* <li><b>{@code '*.*.pack*..'}</b> matches <b>{@code 'a.b.packfix.c.d'}</b>,
* but neither <b>{@code 'a.packfix.b'}</b> nor <b>{@code 'a.b.prepack.d'}</b></li>
* <li>{@code "..pack.."} matches {@code a.pack}, {@code a.pack.b} or {@code a.b.pack.c.d},
* but not {@code a.packa.b}</li>
* <li>{@code "*.pack.*"} matches {@code a.pack.b},
* but not {@code a.b.pack.c}</li>
* <li>{@code "..*pack*.."} matches {@code a.prepackfix.b},
* but not {@code a.prepack.b}</li>
* <li>{@code "*.*.pack*.."} matches {@code a.b.packfix.c.d},
* but neither {@code a.packfix.b} nor {@code a.b.prepack.d}</li>
* </ul>
* You can also use alternations with the '|' operator within brackets. For example
* <p>
* You can also use alternations with the <b>{@code |}</b> operator within brackets. For example
* <ul>
* <li><b>{@code 'pack.[a.c|b*].d'}</b> matches <b>{@code 'pack.a.c.d'} or <b>{@code 'pack.bar.d'}</b>, but neither
* <b>{@code 'pack.a.d'}</b> nor <b>{@code 'pack.b.c.d'}</b></li>
* <li>{@code "pack.[a.c|b*].d"} matches {@code pack.a.c.d} or {@code pack.bar.d},
* but neither {@code pack.a.d} nor {@code pack.b.c.d}</li>
* </ul>
* <p>
* Furthermore, the use of capturing groups is supported. In this case '(*)' matches any sequence of characters,
* but not the dot '.', while '(**)' matches any sequence including the dot. <br>
* Furthermore, the use of capturing groups is supported:
* <ul>
* <li><b>{@code (*)}</b> matches any sequence of characters, * but not the dot '.', while</li>
* <li><b>{@code (**)}</b> matches any sequence including the dot.</li>
* </ul>
* For example
* <ul>
* <li><b>{@code '..service.(*)..'}</b> matches <b>{@code 'a.service.hello.b'}</b> and group 1 would be <b>{@code 'hello'}</b></li>
* <li><b>{@code '..service.(**)'}</b> matches <b>{@code 'a.service.hello.more'}</b> and group 1 would be <b>{@code 'hello.more'}</b></li>
* <li><b>{@code 'my.(*)..service.(**)'}</b> matches <b>{@code 'my.company.some.service.hello.more'}</b>
* and group 1 would be <b>{@code 'company'}</b>, while group 2 would be <b>{@code 'hello.more'}</b></li>
* <li><b>{@code '..service.(a|b*)..'}</b> matches <b>{@code 'a.service.bar.more'}</b> and group 1 would be <b>{@code 'bar'}</b></li>
* <li>{@code "..service.(*).."} matches {@code a.service.hello.b},
* and group 1 would be {@code "hello"}</li>
* <li>{@code "..service.(**)"} matches {@code a.service.hello.more},
* and group 1 would be {@code "hello.more"}</li>
* <li>{@code "my.(*)..service.(**)"} matches {@code my.company.some.service.hello.more},
* group 1 would be {@code "company"}, and group 2 would be {@code "hello.more"}</li>
* <li>{@code "..service.(a|b*).."} matches {@code a.service.bar.more},
* and group 1 would be {@code "bar"}</li>
* </ul>
* Create via {@link PackageMatcher#of(String) PackageMatcher.of(packageIdentifier)}
*The segments matched by capturing groups can be retrieved from the result of {@link #match(String)}
* via {@link Result#getGroup(int)}.
*
* @see PackageMatcher#of(String) PackageMatcher.of(packageIdentifier)
*/
@PublicAPI(usage = ACCESS)
public final class PackageMatcher {
Expand Down Expand Up @@ -204,6 +219,11 @@ public int getNumberOfGroups() {
return matcher.groupCount();
}

/**
* @param number 1-based number of the capturing group
* @return The subsequence captured by the given group during the previous match operation.
* @throws IndexOutOfBoundsException if there is no capturing group in the pattern with the given number.
*/
@PublicAPI(usage = ACCESS)
public String getGroup(int number) {
return matcher.group(number);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,23 @@ public void match(String matcher, String target, boolean matches) {
"..[application|domain.*|infrastructure].(*).. , com.example.infrastructure.a.file , a"
})
public void capture_groups(String matcher, String target, String groupString) {
assertThat(PackageMatcher.of(matcher).match(target).isPresent())
Optional<Result> match = PackageMatcher.of(matcher).match(target);
assertThat(match.isPresent())
.as("'%s' matching '%s'", matcher, target)
.isEqualTo(groupString != null);

String[] groups = groupString != null ? groupString.split(":") : new String[0];
for (int i = 0; i < groups.length; i++) {
assertThat(PackageMatcher.of(matcher).match(target).get().getGroup(i + 1))
.as("group number %d matches when matching '%s' against '%s'", i + 1, matcher, target)
.isEqualTo(groups[i]);
}
match.ifPresent(result -> {
String[] groups = groupString.split(":");
for (int i = 0; i < groups.length; i++) {
assertThat(result.getGroup(i + 1))
.as("group number %d matches when matching '%s' against '%s'", i + 1, matcher, target)
.isEqualTo(groups[i]);
}
assertThatThrownBy(() -> result.getGroup(-1))
.isInstanceOf(IndexOutOfBoundsException.class);
assertThatThrownBy(() -> result.getGroup(groups.length + 1))
.isInstanceOf(IndexOutOfBoundsException.class);
});
}

@Test
Expand Down
15 changes: 10 additions & 5 deletions docs/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
FROM jekyll/jekyll:stable
FROM ruby:3.3-slim

MAINTAINER Peter Gafert <peter.gafert@archunit.org>
LABEL maintainer="Peter Gafert <peter.gafert@archunit.org>"

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
RUN gem install bundler -v 2.3.25 \
&& bundle install
17 changes: 15 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/_config-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title : "ArchUnit"
title_separator : "-"
name : "Peter Gafert"
description : "A Java architecture test library, to specify and assert architecture rules in plain Java"
url : "http://localhost"
url : "http://localhost:4000"
baseurl : ""
repository : "codecholeric/archunit"
teaser : # path of fallback teaser image, e.g. "/assets/images/500x300.png"
Expand All @@ -33,6 +33,8 @@ include:
- .htaccess
- _pages
exclude:
- _site
- .sass-cache
- vendor
- assets/js/plugins
- assets/js/_main.js
Expand All @@ -54,7 +56,7 @@ markdown: kramdown
highlighter: rouge
lsi: false
excerpt_separator: "\n\n"
incremental: false
incremental: true


# Markdown Processing
Expand Down
6 changes: 2 additions & 4 deletions docs/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 3 additions & 2 deletions docs/userguide/003_Getting_Started.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<Package Identifiers>>. The returned object of
Comment thread
StefanGraeber marked this conversation as resolved.
type `ArchRule` can now be evaluated against a set of imported classes:

[source,java,options="nowrap"]
----
Expand Down
113 changes: 113 additions & 0 deletions docs/userguide/006_The_Core_API.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,116 @@ ArchUnit's own rule APIs (compare <<The Lang API>>) 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="1l,4"]
|===
|Pattern | Meaning

|*
| Matches any non-empty sequence of characters not containing the dot `+.+`, i.e. exactly one package segment.

|..
| Matches any (possibly empty) 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 <<Slices>>).

|(**)
| 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.
11 changes: 7 additions & 4 deletions docs/userguide/008_The_Library_API.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<Package Identifiers>>, 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"]
----
Expand Down Expand Up @@ -245,7 +248,7 @@ ModuleRuleDefinition.modules().definedByPackages("..example.(*)..").should().beF
----

As the example shows, it shares some concepts with the <<Slices>> API. For example `definedByPackages(..)`
follows the same semantics as `slices().matching(..)`.
follows the same semantics as `slices().matching(..)`, using <<Package Identifiers>>.
Also, the configuration options for cycle detection mentioned in the last section are shared by these APIs.
But, it also offers several powerful concepts beyond that API to express many different modularization scenarios.

Expand Down Expand Up @@ -417,8 +420,8 @@ A PlantUML diagram used with ArchUnit must abide by a certain set of rules:

1. Components must be declared in the bracket notation (i.e. `[Some Component]`)
2. Components must have at least one (possible multiple) stereotype(s). Each stereotype in the diagram
must be unique and represent a valid package identifier (e.g. `\<<..example..>>` where `..` represents
an arbitrary number of packages; compare the core API)
must be unique and represent a valid <<Package Identifiers,package identifier>>
(e.g. `\<<..example..>>` where `..` represents an arbitrary number of packages; compare the core API)
3. Components may have an optional alias (e.g. `[Some Component] \<<..example..>> as myalias`). The alias must be alphanumeric and must not be quoted.
4. Components may have an optional color (e.g. `[Some Component] \<<..example..>> #OrangeRed`)
5. Dependencies must use arrows only consisting of dashes (e.g. `-\->`)
Expand Down
7 changes: 7 additions & 0 deletions docs/userguide/archunit.css
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,11 @@ h3 {
h4 {
margin-top: 1.2em !important;
margin-bottom: 0.55em !important;
}
/* Restore code box for single inline-code inside table cells */
p.tableblock > code:only-child {
background-color: #f7f7f8 !important;
padding: .1em .5ex !important;
border-radius: 4px !important;
border: 1px solid #e0e0e0; /* makes a lone dot's box clearly visible */
}
Loading