diff --git a/.gitignore b/.gitignore index 4b9c4e7..b7826d4 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,7 @@ build/ # IntelliJ IDEA *.iml *.iws -out/ +/out/ # VS Code /worktrees/ @@ -55,3 +55,4 @@ out/ gradle-plugin-publishing.md sedr-library-maven-central-publishing.md +/**/*prompts.md \ No newline at end of file diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/README.adoc b/examples/architecture-validator/hexagonal-spring-cycle-detection/README.adoc new file mode 100644 index 0000000..451be94 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/README.adoc @@ -0,0 +1,82 @@ += Hexagonal Spring Cycle Detection Example +:toc: left + +This example demonstrates package cycle detection in adapter packages via `CycleFreedomTest`. +It also documents the `CycleFreedomTest` slice-pattern bug fix that was required to make this check +effective in realistically nested base packages — see "Why this example exists" below. + +== Files and Purpose + +[cols="1,2"] +|=== +|File |Purpose + +|`src/main/java/.../cycledetection/adapter/persistence/OrderRepositoryAdapter.java` +|Adapter package A. Depends on `adapter.notification.OrderNotifier`. + +|`src/main/java/.../cycledetection/adapter/notification/OrderNotifier.java` +|Adapter package B. Depends back on `OrderRepositoryAdapter`, closing the cycle. + +|`src/main/java/.../cycledetection/application/service/OrderService.java` +|Plain application service that depends only on an outbound port — kept compliant so the only failure is the +cycle itself. +|=== + +== Configuration + +[source,groovy] +---- +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.cycledetection' + useBuiltInHexagonalRulePack = false +} +---- + +No special properties are needed — `CycleFreedomTest` runs against the plugin's default `adapters` pattern +(`..adapter..`/`..adapters..`). + +== Why this example exists + +`CycleFreedomTest` builds an ArchUnit slice pattern from the configured `adapters` package root. The +original implementation stripped a leading `..` from that pattern (e.g. `..adapters..` became `adapters`), +which anchors the resulting pattern to the *start* of a class's fully-qualified name. For any realistic, +multi-segment base package like `com.arc_e_tect.example.spring.cycledetection`, no class's FQN actually +starts with `adapters.` — so the check matched zero classes and passed vacuously, regardless of real cycles. +This example's base package is deliberately nested several segments deep so it would have silently passed +under the old, buggy pattern; it now correctly fails. + +== Run + +[source,bash] +---- +./gradlew build +./gradlew testArchitecture +---- + +== Expected violations + +The two adapter classes depend on each other, forming a two-package cycle: + +[source,java] +---- +// adapter.persistence.OrderRepositoryAdapter +public OrderRepositoryAdapter(OrderNotifier notifier) { ... } // depends on adapter.notification + +// adapter.notification.OrderNotifier +public OrderNotifier(OrderRepositoryAdapter repositoryAdapter) { ... } // depends back on adapter.persistence +---- + +`CycleFreedomTest.adapterPackagesShouldBeFreeOfCycles` fails and reports the cycle between +`adapter.persistence` and `adapter.notification`. Every other rule class passes for this fixture. + +== Reports + +The Gradle HTML report is under `build/reports/architecture-validator/html/`. +The XML report is under `build/reports/architecture-validator/xml/`. + +== Fix + +Break the dependency loop so `adapter.notification` no longer depends on `adapter.persistence` — for +example, have `OrderRepositoryAdapter` publish through a port or event mechanism instead of calling into +`OrderNotifier` directly, or move the shared collaboration point into a package neither adapter owns. Keep +adapter package dependencies acyclic to preserve maintainable boundaries. diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/build.gradle b/examples/architecture-validator/hexagonal-spring-cycle-detection/build.gradle new file mode 100644 index 0000000..352bccf --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'jacoco' + id 'java' + alias(libs.plugins.architecture.validator.iff) +} + +group = 'com.arc-e-tect.example.spring' +version = '0.0.1' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation libs.spring.context.iff + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.cycledetection' + useBuiltInHexagonalRulePack = false + +} + +tasks.named('jacocoTestReport') { + reports { + xml.required = true + html.required = true + } +} + +tasks.named('testArchitecture', Test) { + ignoreFailures = false +} + +tasks.named('check') { + dependsOn tasks.named('testArchitecture') +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/libs.versions.toml b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/libs.versions.toml new file mode 100644 index 0000000..58fd4a4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/libs.versions.toml @@ -0,0 +1,16 @@ +## Generated by $ ./gradlew refreshVersionsCatalog + +[plugins] + +architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" } + +[versions] + +architecture-validator-iff = "0.0.1" +architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE" +spring-context-iff = "7.0.8" + +[libraries] + +architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" } +spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" } diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/wrapper/gradle-wrapper.jar b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/wrapper/gradle-wrapper.properties b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/gradlew b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/gradlew.bat b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/settings.gradle b/examples/architecture-validator/hexagonal-spring-cycle-detection/settings.gradle new file mode 100644 index 0000000..08c02b7 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'de.fayard.refreshVersions' version '0.60.6' +} + +refreshVersions { + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = 'architecture-validator-hexagonal-spring-cycle-detection' diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/notification/OrderNotifier.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/notification/OrderNotifier.java new file mode 100644 index 0000000..0e7332e --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/notification/OrderNotifier.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.cycledetection.adapter.notification; + +import com.arc_e_tect.example.spring.cycledetection.adapter.persistence.OrderRepositoryAdapter; +import org.springframework.stereotype.Component; + +@Component +public class OrderNotifier { + + private final OrderRepositoryAdapter repositoryAdapter; + + public OrderNotifier(OrderRepositoryAdapter repositoryAdapter) { + this.repositoryAdapter = repositoryAdapter; + } + + public void notifySaved(String id) { + repositoryAdapter.recordNotification(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/persistence/OrderRepositoryAdapter.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/persistence/OrderRepositoryAdapter.java new file mode 100644 index 0000000..a5e922d --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/persistence/OrderRepositoryAdapter.java @@ -0,0 +1,25 @@ +package com.arc_e_tect.example.spring.cycledetection.adapter.persistence; + +import com.arc_e_tect.example.spring.cycledetection.adapter.notification.OrderNotifier; +import com.arc_e_tect.example.spring.cycledetection.application.domain.Order; +import com.arc_e_tect.example.spring.cycledetection.application.port.outbound.OrderPort; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderRepositoryAdapter implements OrderPort { + + private final OrderNotifier notifier; + + public OrderRepositoryAdapter(OrderNotifier notifier) { + this.notifier = notifier; + } + + @Override + public void save(Order order) { + notifier.notifySaved(order.id()); + } + + public void recordNotification(String id) { + // Example back-reference target used to complete the package cycle. + } +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/web/OrderController.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/web/OrderController.java new file mode 100644 index 0000000..3d1827f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/adapter/web/OrderController.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.cycledetection.adapter.web; + +import com.arc_e_tect.example.spring.cycledetection.application.port.inbound.OrderUseCase; +import org.springframework.stereotype.Controller; + +@Controller +public class OrderController { + + private final OrderUseCase orderUseCase; + + public OrderController(OrderUseCase orderUseCase) { + this.orderUseCase = orderUseCase; + } + + public void submit(String id) { + orderUseCase.createOrder(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/domain/Order.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/domain/Order.java new file mode 100644 index 0000000..9160648 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/domain/Order.java @@ -0,0 +1,4 @@ +package com.arc_e_tect.example.spring.cycledetection.application.domain; + +public record Order(String id) { +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/port/inbound/OrderUseCase.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/port/inbound/OrderUseCase.java new file mode 100644 index 0000000..132aa9a --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/port/inbound/OrderUseCase.java @@ -0,0 +1,6 @@ +package com.arc_e_tect.example.spring.cycledetection.application.port.inbound; + +public interface OrderUseCase { + + void createOrder(String id); +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/port/outbound/OrderPort.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/port/outbound/OrderPort.java new file mode 100644 index 0000000..918a34a --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/port/outbound/OrderPort.java @@ -0,0 +1,8 @@ +package com.arc_e_tect.example.spring.cycledetection.application.port.outbound; + +import com.arc_e_tect.example.spring.cycledetection.application.domain.Order; + +public interface OrderPort { + + void save(Order order); +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/service/OrderService.java b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/service/OrderService.java new file mode 100644 index 0000000..c5d7ed4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/src/main/java/com/arc_e_tect/example/spring/cycledetection/application/service/OrderService.java @@ -0,0 +1,19 @@ +package com.arc_e_tect.example.spring.cycledetection.application.service; + +import com.arc_e_tect.example.spring.cycledetection.application.domain.Order; +import com.arc_e_tect.example.spring.cycledetection.application.port.inbound.OrderUseCase; +import com.arc_e_tect.example.spring.cycledetection.application.port.outbound.OrderPort; + +public class OrderService implements OrderUseCase { + + private final OrderPort orderPort; + + public OrderService(OrderPort orderPort) { + this.orderPort = orderPort; + } + + @Override + public void createOrder(String id) { + orderPort.save(new Order(id)); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-cycle-detection/versions.properties b/examples/architecture-validator/hexagonal-spring-cycle-detection/versions.properties new file mode 100644 index 0000000..a07c552 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-cycle-detection/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.6 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/README.adoc b/examples/architecture-validator/hexagonal-spring-field-injection/README.adoc new file mode 100644 index 0000000..320c284 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/README.adoc @@ -0,0 +1,96 @@ += Hexagonal Spring Field Injection Example +:toc: left + +This example demonstrates the no-field-injection rule from `DependencyInjectionStyleTest`. +It intentionally uses `@Autowired` field injection on an adapter class, instead of constructor injection. + +== Files and Purpose + +[cols="1,2"] +|=== +|File |Purpose + +|`src/main/java/.../fieldinjection/adapter/persistence/OrderRepositoryAdapter.java` +|Repository adapter with an `@Autowired` field, to trigger `DependencyInjectionStyleTest.fieldsShouldNotBeAutowired`. + +|`src/main/java/.../fieldinjection/application/service/OrderService.java` +|Plain application service that depends only on an outbound port — kept compliant so the only failure is +the field injection. + +|`src/main/java/.../fieldinjection/adapter/web/OrderController.java` +|Inbound controller that depends only on the inbound port. +|=== + +== Configuration + +[source,groovy] +---- +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.fieldinjection' + useBuiltInHexagonalRulePack = false +} +---- + +`DependencyInjectionStyleTest` needs no extra configuration — it checks fields in whatever packages are +already configured as `domainModel`, `adapters`, and `applicationServices`. + +== Run + +[source,bash] +---- +./gradlew build +./gradlew testArchitecture +---- + +== Expected violations + +`OrderRepositoryAdapter` injects its collaborator via an `@Autowired` field instead of a constructor +parameter: + +[source,java] +---- +@Repository +public class OrderRepositoryAdapter implements OrderPort { + + @Autowired + private PersistenceGateway gateway; // <1> + + @Override + public void save(Order order) { + gateway.persist(order); + } +} +---- +<1> Field injection hides a required dependency and prevents constructing this class without a Spring +context — the rule this example is designed to trip. + +`DependencyInjectionStyleTest.fieldsShouldNotBeAutowired` fails, naming the specific class and field. + +== Reports + +The Gradle HTML report is under `build/reports/architecture-validator/html/`. +The XML report is under `build/reports/architecture-validator/xml/`. + +== Fix + +Replace the `@Autowired` field with a constructor parameter: + +[source,java] +---- +@Repository +public class OrderRepositoryAdapter implements OrderPort { + + private final PersistenceGateway gateway; + + public OrderRepositoryAdapter(PersistenceGateway gateway) { + this.gateway = gateway; + } + + @Override + public void save(Order order) { + gateway.persist(order); + } +} +---- + +This keeps the dependency explicit and lets the class be constructed and tested without a Spring context. diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/build.gradle b/examples/architecture-validator/hexagonal-spring-field-injection/build.gradle new file mode 100644 index 0000000..bff31e1 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'jacoco' + id 'java' + alias(libs.plugins.architecture.validator.iff) +} + +group = 'com.arc-e-tect.example.spring' +version = '0.0.1' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation libs.spring.context.iff + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.fieldinjection' + useBuiltInHexagonalRulePack = false + +} + +tasks.named('jacocoTestReport') { + reports { + xml.required = true + html.required = true + } +} + +tasks.named('testArchitecture', Test) { + ignoreFailures = false +} + +tasks.named('check') { + dependsOn tasks.named('testArchitecture') +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/gradle/libs.versions.toml b/examples/architecture-validator/hexagonal-spring-field-injection/gradle/libs.versions.toml new file mode 100644 index 0000000..58fd4a4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/gradle/libs.versions.toml @@ -0,0 +1,16 @@ +## Generated by $ ./gradlew refreshVersionsCatalog + +[plugins] + +architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" } + +[versions] + +architecture-validator-iff = "0.0.1" +architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE" +spring-context-iff = "7.0.8" + +[libraries] + +architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" } +spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" } diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/gradle/wrapper/gradle-wrapper.jar b/examples/architecture-validator/hexagonal-spring-field-injection/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/examples/architecture-validator/hexagonal-spring-field-injection/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/gradle/wrapper/gradle-wrapper.properties b/examples/architecture-validator/hexagonal-spring-field-injection/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/gradlew b/examples/architecture-validator/hexagonal-spring-field-injection/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/gradlew.bat b/examples/architecture-validator/hexagonal-spring-field-injection/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/settings.gradle b/examples/architecture-validator/hexagonal-spring-field-injection/settings.gradle new file mode 100644 index 0000000..920a471 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'de.fayard.refreshVersions' version '0.60.6' +} + +refreshVersions { + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = 'architecture-validator-hexagonal-spring-field-injection' diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/persistence/OrderRepositoryAdapter.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/persistence/OrderRepositoryAdapter.java new file mode 100644 index 0000000..0e780b9 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/persistence/OrderRepositoryAdapter.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.fieldinjection.adapter.persistence; + +import com.arc_e_tect.example.spring.fieldinjection.application.domain.Order; +import com.arc_e_tect.example.spring.fieldinjection.application.port.outbound.OrderPort; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderRepositoryAdapter implements OrderPort { + + @Autowired + private PersistenceGateway gateway; + + @Override + public void save(Order order) { + gateway.persist(order); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/persistence/PersistenceGateway.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/persistence/PersistenceGateway.java new file mode 100644 index 0000000..0c30ed1 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/persistence/PersistenceGateway.java @@ -0,0 +1,10 @@ +package com.arc_e_tect.example.spring.fieldinjection.adapter.persistence; + +import com.arc_e_tect.example.spring.fieldinjection.application.domain.Order; + +public class PersistenceGateway { + + public void persist(Order order) { + // Example collaborator used to demonstrate field injection. + } +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/web/OrderController.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/web/OrderController.java new file mode 100644 index 0000000..8913fdc --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/adapter/web/OrderController.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.fieldinjection.adapter.web; + +import com.arc_e_tect.example.spring.fieldinjection.application.port.inbound.OrderUseCase; +import org.springframework.stereotype.Controller; + +@Controller +public class OrderController { + + private final OrderUseCase orderUseCase; + + public OrderController(OrderUseCase orderUseCase) { + this.orderUseCase = orderUseCase; + } + + public void submit(String id) { + orderUseCase.createOrder(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/domain/Order.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/domain/Order.java new file mode 100644 index 0000000..a3cffdf --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/domain/Order.java @@ -0,0 +1,4 @@ +package com.arc_e_tect.example.spring.fieldinjection.application.domain; + +public record Order(String id) { +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/port/inbound/OrderUseCase.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/port/inbound/OrderUseCase.java new file mode 100644 index 0000000..b87d38c --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/port/inbound/OrderUseCase.java @@ -0,0 +1,6 @@ +package com.arc_e_tect.example.spring.fieldinjection.application.port.inbound; + +public interface OrderUseCase { + + void createOrder(String id); +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/port/outbound/OrderPort.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/port/outbound/OrderPort.java new file mode 100644 index 0000000..c6c7aa4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/port/outbound/OrderPort.java @@ -0,0 +1,8 @@ +package com.arc_e_tect.example.spring.fieldinjection.application.port.outbound; + +import com.arc_e_tect.example.spring.fieldinjection.application.domain.Order; + +public interface OrderPort { + + void save(Order order); +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/service/OrderService.java b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/service/OrderService.java new file mode 100644 index 0000000..7171a3f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/src/main/java/com/arc_e_tect/example/spring/fieldinjection/application/service/OrderService.java @@ -0,0 +1,19 @@ +package com.arc_e_tect.example.spring.fieldinjection.application.service; + +import com.arc_e_tect.example.spring.fieldinjection.application.domain.Order; +import com.arc_e_tect.example.spring.fieldinjection.application.port.inbound.OrderUseCase; +import com.arc_e_tect.example.spring.fieldinjection.application.port.outbound.OrderPort; + +public class OrderService implements OrderUseCase { + + private final OrderPort orderPort; + + public OrderService(OrderPort orderPort) { + this.orderPort = orderPort; + } + + @Override + public void createOrder(String id) { + orderPort.save(new Order(id)); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-field-injection/versions.properties b/examples/architecture-validator/hexagonal-spring-field-injection/versions.properties new file mode 100644 index 0000000..a07c552 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-field-injection/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.6 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/README.adoc b/examples/architecture-validator/hexagonal-spring-misconfigured/README.adoc new file mode 100644 index 0000000..0993be2 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/README.adoc @@ -0,0 +1,80 @@ += Hexagonal Spring Misconfigured Example +:toc: left + +This example intentionally leaves `architectureValidator.basePackage` blank. +It demonstrates the fail-fast validation added in `RulePackConfigurationTest`: before this check existed, a +misconfigured consumer would see every architecture rule pass vacuously (`ClassFileImporter` imports zero +classes, so nothing is ever checked) — a silent false-green build. This example shows the loud failure that +replaces that silence. + +== Files and Purpose + +[cols="1,2"] +|=== +|File |Purpose + +|`build.gradle` +|Configures `architectureValidator` with a blank `basePackage` to trigger fail-fast misconfiguration behavior. + +|`src/main/java/com/arc_e_tect/example/spring/misconfigured/SampleDomain.java` +|Minimal placeholder class so the project compiles. Its content is irrelevant — the point is that +`basePackage` never resolves to it. +|=== + +== Configuration + +[source,groovy] +---- +architectureValidator { + basePackage = '' + useBuiltInHexagonalRulePack = false // <1> +} +---- +<1> Disabled so the failure comes only from the external rule pack (`hexagonal-spring-rules`), not the +plugin's own generated built-in test — keeps the demonstration focused on `RulePackConfigurationTest`. + +== Run + +[source,bash] +---- +./gradlew build +./gradlew testArchitecture +---- + +== Expected violations + +With `basePackage` blank, `RulePackConfiguration.requireConfigured()` throws before `ClassFileImporter` ever +runs. Every rule class's `classes` field is initialized the same way +(`new ClassFileImporter()...importPackages(RulePackConfiguration.basePackage())`), and `basePackage()` now +calls `requireConfigured()` itself — so the same `AssertionError` propagates out of *every* rule class's +field initializer, not just `RulePackConfigurationTest`. All 23 architecture tests fail with the identical +root cause: + +[source] +---- +java.lang.AssertionError: architectureValidator.basePackage is not set. Configure it via the Architecture +Validator plugin's architectureValidator { basePackage = 'com.example.myapp' } extension. +---- + +The one that matters is `RulePackConfigurationTest.requiredArchitecturePropertiesShouldBeConfigured` — its +message is the actionable one, telling you exactly which property to set and how. The other 22 failures are +the same exception surfacing everywhere else; seeing all of them fail together (instead of quietly passing) +is the point of this example. + +== Reports + +The Gradle HTML report is under `build/reports/architecture-validator/html/`. +The XML report is under `build/reports/architecture-validator/xml/`. + +== Fix + +Set `architectureValidator.basePackage` to the project's real root package. Keep `inPorts`, `outPorts`, and +`domainModel` configured (or leave them at the plugin's Hexagonal defaults) so the rules validate real +classes instead of an empty import scope: + +[source,groovy] +---- +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.misconfigured' +} +---- diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/build.gradle b/examples/architecture-validator/hexagonal-spring-misconfigured/build.gradle new file mode 100644 index 0000000..5b7ab89 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'jacoco' + id 'java' + alias(libs.plugins.architecture.validator.iff) +} + +group = 'com.arc-e-tect.example.spring' +version = '0.0.1' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation libs.spring.context.iff + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = '' + useBuiltInHexagonalRulePack = false + +} + +tasks.named('jacocoTestReport') { + reports { + xml.required = true + html.required = true + } +} + +tasks.named('testArchitecture', Test) { + ignoreFailures = false +} + +tasks.named('check') { + dependsOn tasks.named('testArchitecture') +} diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/libs.versions.toml b/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/libs.versions.toml new file mode 100644 index 0000000..58fd4a4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/libs.versions.toml @@ -0,0 +1,16 @@ +## Generated by $ ./gradlew refreshVersionsCatalog + +[plugins] + +architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" } + +[versions] + +architecture-validator-iff = "0.0.1" +architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE" +spring-context-iff = "7.0.8" + +[libraries] + +architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" } +spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" } diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/wrapper/gradle-wrapper.jar b/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/wrapper/gradle-wrapper.properties b/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/gradlew b/examples/architecture-validator/hexagonal-spring-misconfigured/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/gradlew.bat b/examples/architecture-validator/hexagonal-spring-misconfigured/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/settings.gradle b/examples/architecture-validator/hexagonal-spring-misconfigured/settings.gradle new file mode 100644 index 0000000..027e85b --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'de.fayard.refreshVersions' version '0.60.6' +} + +refreshVersions { + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = 'architecture-validator-hexagonal-spring-misconfigured' diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/src/main/java/com/arc_e_tect/example/spring/misconfigured/SampleDomain.java b/examples/architecture-validator/hexagonal-spring-misconfigured/src/main/java/com/arc_e_tect/example/spring/misconfigured/SampleDomain.java new file mode 100644 index 0000000..b33ff1f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/src/main/java/com/arc_e_tect/example/spring/misconfigured/SampleDomain.java @@ -0,0 +1,4 @@ +package com.arc_e_tect.example.spring.misconfigured; + +public record SampleDomain(String id) { +} diff --git a/examples/architecture-validator/hexagonal-spring-misconfigured/versions.properties b/examples/architecture-validator/hexagonal-spring-misconfigured/versions.properties new file mode 100644 index 0000000..a07c552 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-misconfigured/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.6 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/README.adoc b/examples/architecture-validator/hexagonal-spring-naming-conventions/README.adoc new file mode 100644 index 0000000..0a318a2 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/README.adoc @@ -0,0 +1,92 @@ += Hexagonal Spring Naming Conventions Example +:toc: left + +This example demonstrates the opt-in naming convention rules from `NamingConventionTest`. +It intentionally uses non-compliant suffixes for an in-port, an out-port, and a repository adapter. + +`NamingConventionTest` is disabled by default — unlike the other example projects in this repository, simply +adding the rule pack dependency is not enough here. It must be explicitly turned on. + +== Files and Purpose + +[cols="1,2"] +|=== +|File |Purpose + +|`src/main/java/.../namingconventions/application/port/inbound/OrderCommands.java` +|Inbound port that intentionally does not end with `UseCase` or `Port`. + +|`src/main/java/.../namingconventions/application/port/outbound/OrderStore.java` +|Outbound port that intentionally does not end with `Port`. + +|`src/main/java/.../namingconventions/adapter/persistence/OrderPersistence.java` +|Repository adapter (`@Repository`) that intentionally does not end with `Repository` or `Adapter`. + +|`build.gradle` +|Opts in to naming convention rules via `hexagonalArchitecture { namingConventionsEnabled = true }`. +|=== + +== Configuration + +[source,groovy] +---- +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.namingconventions' + hexagonalArchitecture { + namingConventionsEnabled = true // <1> + } + useBuiltInHexagonalRulePack = false +} +---- +<1> Required. Without this, all three `NamingConventionTest` rules are reported as *SKIPPED* rather than +failed — try commenting this line out and re-running `./gradlew testArchitecture` to see the difference. + +== Run + +[source,bash] +---- +./gradlew build +./gradlew testArchitecture +---- + +== Expected violations + +Three types deliberately break their expected suffix: + +[source,java] +---- +public interface OrderCommands { void createOrder(String id); } // should end with UseCase or Port +public interface OrderStore { void save(Order order); } // should end with Port + +@Repository +public class OrderPersistence implements OrderStore { ... } // should end with Repository or Adapter +---- + +All three fail, each naming the specific offending class: + +* `NamingConventionTest.inputPortsShouldHaveConsistentSuffix` fails for `OrderCommands`. +* `NamingConventionTest.outputPortsShouldHaveConsistentSuffix` fails for `OrderStore`. +* `NamingConventionTest.adaptersShouldHaveConsistentSuffix` fails for `OrderPersistence`. + +== Reports + +The Gradle HTML report is under `build/reports/architecture-validator/html/`. +The XML report is under `build/reports/architecture-validator/xml/`. + +== Fix + +Rename each type to match its role's expected suffix: + +[cols="1,1"] +|=== +|From |To + +|`OrderCommands` +|`OrderUseCase` (or `OrderCommandsUseCase`) + +|`OrderStore` +|`OrderPort` + +|`OrderPersistence` +|`OrderRepositoryAdapter` (or `OrderRepository`) +|=== diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/build.gradle b/examples/architecture-validator/hexagonal-spring-naming-conventions/build.gradle new file mode 100644 index 0000000..af54c0f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'jacoco' + id 'java' + alias(libs.plugins.architecture.validator.iff) +} + +group = 'com.arc-e-tect.example.spring' +version = '0.0.1' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation libs.spring.context.iff + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.namingconventions' + hexagonalArchitecture { + namingConventionsEnabled = true + } + useBuiltInHexagonalRulePack = false + +} + +tasks.named('jacocoTestReport') { + reports { + xml.required = true + html.required = true + } +} + +tasks.named('testArchitecture', Test) { + ignoreFailures = false +} + +tasks.named('check') { + dependsOn tasks.named('testArchitecture') +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/libs.versions.toml b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/libs.versions.toml new file mode 100644 index 0000000..58fd4a4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/libs.versions.toml @@ -0,0 +1,16 @@ +## Generated by $ ./gradlew refreshVersionsCatalog + +[plugins] + +architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" } + +[versions] + +architecture-validator-iff = "0.0.1" +architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE" +spring-context-iff = "7.0.8" + +[libraries] + +architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" } +spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" } diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/wrapper/gradle-wrapper.jar b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/wrapper/gradle-wrapper.properties b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/gradlew b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/gradlew.bat b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/settings.gradle b/examples/architecture-validator/hexagonal-spring-naming-conventions/settings.gradle new file mode 100644 index 0000000..9f35ba5 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'de.fayard.refreshVersions' version '0.60.6' +} + +refreshVersions { + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = 'architecture-validator-hexagonal-spring-naming-conventions' diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/adapter/persistence/OrderPersistence.java b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/adapter/persistence/OrderPersistence.java new file mode 100644 index 0000000..0f6c383 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/adapter/persistence/OrderPersistence.java @@ -0,0 +1,14 @@ +package com.arc_e_tect.example.spring.namingconventions.adapter.persistence; + +import com.arc_e_tect.example.spring.namingconventions.application.domain.Order; +import com.arc_e_tect.example.spring.namingconventions.application.port.outbound.OrderStore; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderPersistence implements OrderStore { + + @Override + public void save(Order order) { + // Example persistence operation. + } +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/adapter/web/OrderController.java b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/adapter/web/OrderController.java new file mode 100644 index 0000000..855b040 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/adapter/web/OrderController.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.namingconventions.adapter.web; + +import com.arc_e_tect.example.spring.namingconventions.application.port.inbound.OrderCommands; +import org.springframework.stereotype.Controller; + +@Controller +public class OrderController { + + private final OrderCommands orderCommands; + + public OrderController(OrderCommands orderCommands) { + this.orderCommands = orderCommands; + } + + public void submit(String id) { + orderCommands.createOrder(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/domain/Order.java b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/domain/Order.java new file mode 100644 index 0000000..e23af1a --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/domain/Order.java @@ -0,0 +1,4 @@ +package com.arc_e_tect.example.spring.namingconventions.application.domain; + +public record Order(String id) { +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/port/inbound/OrderCommands.java b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/port/inbound/OrderCommands.java new file mode 100644 index 0000000..f29afa2 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/port/inbound/OrderCommands.java @@ -0,0 +1,6 @@ +package com.arc_e_tect.example.spring.namingconventions.application.port.inbound; + +public interface OrderCommands { + + void createOrder(String id); +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/port/outbound/OrderStore.java b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/port/outbound/OrderStore.java new file mode 100644 index 0000000..b789cc6 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/port/outbound/OrderStore.java @@ -0,0 +1,8 @@ +package com.arc_e_tect.example.spring.namingconventions.application.port.outbound; + +import com.arc_e_tect.example.spring.namingconventions.application.domain.Order; + +public interface OrderStore { + + void save(Order order); +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/service/OrderService.java b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/service/OrderService.java new file mode 100644 index 0000000..3af7833 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/src/main/java/com/arc_e_tect/example/spring/namingconventions/application/service/OrderService.java @@ -0,0 +1,19 @@ +package com.arc_e_tect.example.spring.namingconventions.application.service; + +import com.arc_e_tect.example.spring.namingconventions.application.domain.Order; +import com.arc_e_tect.example.spring.namingconventions.application.port.inbound.OrderCommands; +import com.arc_e_tect.example.spring.namingconventions.application.port.outbound.OrderStore; + +public class OrderService implements OrderCommands { + + private final OrderStore orderStore; + + public OrderService(OrderStore orderStore) { + this.orderStore = orderStore; + } + + @Override + public void createOrder(String id) { + orderStore.save(new Order(id)); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-naming-conventions/versions.properties b/examples/architecture-validator/hexagonal-spring-naming-conventions/versions.properties new file mode 100644 index 0000000..a07c552 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-naming-conventions/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.6 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/README.adoc b/examples/architecture-validator/hexagonal-spring-rules-disabled/README.adoc new file mode 100644 index 0000000..f9ad8b6 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/README.adoc @@ -0,0 +1,84 @@ += Hexagonal Spring Rules Disabled Example +:toc: left + +This example demonstrates per-rule opt-out using `architectureValidator.rulesDisabled`. +It intentionally introduces two violations of two different rules and disables only one of them, so the +build still fails — proving the opt-out is selective, not a blanket "ignore everything" switch. + +== Files and Purpose + +[cols="1,2"] +|=== +|File |Purpose + +|`src/main/java/.../rulesdisabled/application/service/adapter/OrderService.java` +|Annotated `@Service` and depends directly on a `@Repository` — both intentional violations (see below). + +|`src/main/java/.../rulesdisabled/adapter/persistence/OrderRepository.java` +|Spring repository adapter reached directly by the service instead of through an outbound port. + +|`build.gradle` +|Disables `DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes` via `rulesDisabled`. +|=== + +== Configuration + +[source,groovy] +---- +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.rulesdisabled' + rulesDisabled = ['DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes'] // <1> + useBuiltInHexagonalRulePack = false +} +---- +<1> Rule identifiers are `ClassSimpleName.methodName`, matching the `@Test` method names in +`hexagonal-spring-rules`'s rule classes exactly. + +== Run + +[source,bash] +---- +./gradlew build +./gradlew testArchitecture +---- + +== Expected violations + +`OrderService` carries `@Service` *and* depends directly on `OrderRepository`: + +[source,java] +---- +@Service +public class OrderService implements OrderUseCase { + + private final OrderRepository repository; // <1> + + public OrderService(OrderRepository repository) { + this.repository = repository; + } + ... +} +---- +<1> Direct dependency on an adapter-layer `@Repository`, bypassing an outbound port. + +This is structurally two separate violations: + +* `DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes` — flags the `@Service` + annotation itself. This is the one disabled via `rulesDisabled`, so it's reported as *SKIPPED*, not + passed and not absent. +* `SpringHexagonalArchitectureTest.servicesShouldNotAccessRepositoriesDirectly` — flags the direct + `OrderRepository` dependency. This one stays enabled and *fails* the build. + +All other enabled rule classes pass for this fixture — only the one deliberately-left-enabled violation +should be red. + +== Reports + +The Gradle HTML report is under `build/reports/architecture-validator/html/`. +The XML report is under `build/reports/architecture-validator/xml/`. + +== Fix + +Remove direct service-to-repository coupling by introducing an outbound port, and remove the `rulesDisabled` +override once the project is aligned with the architecture rules — `rulesDisabled` is meant as a deliberate, +temporary or team-specific exception, not a permanent way to silence a rule you plan to leave violated. diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/build.gradle b/examples/architecture-validator/hexagonal-spring-rules-disabled/build.gradle new file mode 100644 index 0000000..0253fad --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'jacoco' + id 'java' + alias(libs.plugins.architecture.validator.iff) +} + +group = 'com.arc-e-tect.example.spring' +version = '0.0.1' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation libs.spring.context.iff + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.rulesdisabled' + rulesDisabled = ['DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes'] + useBuiltInHexagonalRulePack = false + +} + +tasks.named('jacocoTestReport') { + reports { + xml.required = true + html.required = true + } +} + +tasks.named('testArchitecture', Test) { + ignoreFailures = false +} + +tasks.named('check') { + dependsOn tasks.named('testArchitecture') +} diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/libs.versions.toml b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/libs.versions.toml new file mode 100644 index 0000000..58fd4a4 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/libs.versions.toml @@ -0,0 +1,16 @@ +## Generated by $ ./gradlew refreshVersionsCatalog + +[plugins] + +architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" } + +[versions] + +architecture-validator-iff = "0.0.1" +architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE" +spring-context-iff = "7.0.8" + +[libraries] + +architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" } +spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" } diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/wrapper/gradle-wrapper.jar b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/wrapper/gradle-wrapper.properties b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/gradlew b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/gradlew.bat b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/settings.gradle b/examples/architecture-validator/hexagonal-spring-rules-disabled/settings.gradle new file mode 100644 index 0000000..18a776b --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'de.fayard.refreshVersions' version '0.60.6' +} + +refreshVersions { + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = 'architecture-validator-hexagonal-spring-rules-disabled' diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/adapter/persistence/OrderRepository.java b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/adapter/persistence/OrderRepository.java new file mode 100644 index 0000000..637e33a --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/adapter/persistence/OrderRepository.java @@ -0,0 +1,12 @@ +package com.arc_e_tect.example.spring.rulesdisabled.adapter.persistence; + +import com.arc_e_tect.example.spring.rulesdisabled.application.domain.Order; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderRepository { + + public void save(Order order) { + // Example persistence operation. + } +} diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/adapter/web/OrderController.java b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/adapter/web/OrderController.java new file mode 100644 index 0000000..8e6e217 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/adapter/web/OrderController.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.rulesdisabled.adapter.web; + +import com.arc_e_tect.example.spring.rulesdisabled.application.port.inbound.OrderUseCase; +import org.springframework.stereotype.Controller; + +@Controller +public class OrderController { + + private final OrderUseCase orderUseCase; + + public OrderController(OrderUseCase orderUseCase) { + this.orderUseCase = orderUseCase; + } + + public void submit(String id) { + orderUseCase.createOrder(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/domain/Order.java b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/domain/Order.java new file mode 100644 index 0000000..8ae3445 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/domain/Order.java @@ -0,0 +1,4 @@ +package com.arc_e_tect.example.spring.rulesdisabled.application.domain; + +public record Order(String id) { +} diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/port/inbound/OrderUseCase.java b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/port/inbound/OrderUseCase.java new file mode 100644 index 0000000..6815d93 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/port/inbound/OrderUseCase.java @@ -0,0 +1,6 @@ +package com.arc_e_tect.example.spring.rulesdisabled.application.port.inbound; + +public interface OrderUseCase { + + void createOrder(String id); +} diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/service/adapter/OrderService.java b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/service/adapter/OrderService.java new file mode 100644 index 0000000..5f01e14 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/src/main/java/com/arc_e_tect/example/spring/rulesdisabled/application/service/adapter/OrderService.java @@ -0,0 +1,21 @@ +package com.arc_e_tect.example.spring.rulesdisabled.application.service.adapter; + +import com.arc_e_tect.example.spring.rulesdisabled.adapter.persistence.OrderRepository; +import com.arc_e_tect.example.spring.rulesdisabled.application.domain.Order; +import com.arc_e_tect.example.spring.rulesdisabled.application.port.inbound.OrderUseCase; +import org.springframework.stereotype.Service; + +@Service +public class OrderService implements OrderUseCase { + + private final OrderRepository repository; + + public OrderService(OrderRepository repository) { + this.repository = repository; + } + + @Override + public void createOrder(String id) { + repository.save(new Order(id)); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-rules-disabled/versions.properties b/examples/architecture-validator/hexagonal-spring-rules-disabled/versions.properties new file mode 100644 index 0000000..a07c552 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-rules-disabled/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.6 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/README.adoc b/examples/architecture-validator/hexagonal-spring-type-leakage/README.adoc new file mode 100644 index 0000000..577f242 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/README.adoc @@ -0,0 +1,100 @@ += Hexagonal Spring Type Leakage Example +:toc: left + +This example demonstrates a JPA entity leaking into an outbound port contract. +It is designed to trigger `TypeLeakageTest.portsShouldNotExposeJpaEntities`. + +== Files and Purpose + +[cols="1,2"] +|=== +|File |Purpose + +|`src/main/java/.../typeleakage/application/port/outbound/OrderPort.java` +|Outbound port that intentionally returns `OrderEntity` to demonstrate entity leakage across the application boundary. + +|`src/main/java/.../typeleakage/persistence/OrderEntity.java` +|JPA entity type that must not appear in port method signatures. + +|`src/main/java/.../typeleakage/adapter/persistence/OrderRepositoryAdapter.java` +|Repository adapter implementation behind the outbound port. + +|`build.gradle` +|Disables `PortContractTest.portsShouldOnlyDependOnJavaCoreAndDomainModel` so this example isolates +`TypeLeakageTest` behavior (that older, coarser rule would otherwise flag the same leak as generic +framework-type exposure and muddy the demonstration). +|=== + +== Configuration + +[source,groovy] +---- +dependencies { + implementation libs.jakarta.persistence.api.iff // <1> + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.typeleakage' + useBuiltInHexagonalRulePack = false + rulesDisabled = ['PortContractTest.portsShouldOnlyDependOnJavaCoreAndDomainModel'] // <2> +} +---- +<1> Needed only so `jakarta.persistence.Entity` is on the compile classpath for `OrderEntity`; a real +project would already depend on a JPA provider. +<2> The per-rule opt-out feature (see the `hexagonal-spring-rules-disabled` example for its own dedicated +demonstration) — used here purely to keep this example's failure output limited to `TypeLeakageTest`. + +== Run + +[source,bash] +---- +./gradlew build +./gradlew testArchitecture +---- + +== Expected violations + +The out-port method returns the JPA entity directly instead of a domain type: + +[source,java] +---- +public interface OrderPort { + + OrderEntity findById(String id); // <1> +} +---- +<1> `OrderEntity` (`persistence.OrderEntity`, annotated `@jakarta.persistence.Entity`) is a persistence +detail; exposing it here leaks that detail across the port boundary into anything calling `OrderPort`. + +`TypeLeakageTest.portsShouldNotExposeJpaEntities` fails, naming the exact method and leaked type: + +[source] +---- +OrderPort#findById exposes JPA entity com.arc_e_tect.example.spring.typeleakage.persistence.OrderEntity as return type +---- + +`PortContractTest.portsShouldOnlyDependOnJavaCoreAndDomainModel` is reported as SKIPPED (via `rulesDisabled`, +not because it wouldn't also catch this) — see the Configuration section above for why. + +== Reports + +The Gradle HTML report is under `build/reports/architecture-validator/html/`. +The XML report is under `build/reports/architecture-validator/xml/`. + +== Fix + +Change `OrderPort` to return a domain type instead of `OrderEntity`: + +[source,java] +---- +public interface OrderPort { + + Order findById(String id); +} +---- + +Keep `OrderEntity` confined to `adapter.persistence`, mapping between it and `Order` inside +`OrderRepositoryAdapter`. Once the port signature no longer exposes `OrderEntity`, remove the temporary +`rulesDisabled` override in `build.gradle` — `PortContractTest.portsShouldOnlyDependOnJavaCoreAndDomainModel` +should pass cleanly on its own again. diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/build.gradle b/examples/architecture-validator/hexagonal-spring-type-leakage/build.gradle new file mode 100644 index 0000000..993fd88 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'jacoco' + id 'java' + alias(libs.plugins.architecture.validator.iff) +} + +group = 'com.arc-e-tect.example.spring' +version = '0.0.1' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation libs.jakarta.persistence.api.iff + implementation libs.spring.context.iff + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring.typeleakage' + useBuiltInHexagonalRulePack = false + rulesDisabled = ['PortContractTest.portsShouldOnlyDependOnJavaCoreAndDomainModel'] + +} + +tasks.named('jacocoTestReport') { + reports { + xml.required = true + html.required = true + } +} + +tasks.named('testArchitecture', Test) { + ignoreFailures = false +} + +tasks.named('check') { + dependsOn tasks.named('testArchitecture') +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/libs.versions.toml b/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/libs.versions.toml new file mode 100644 index 0000000..60908ee --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +## Generated by $ ./gradlew refreshVersionsCatalog + +[plugins] + +architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" } + +[versions] + +architecture-validator-iff = "0.0.1" +architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE" +jakarta-persistence-api-iff = "3.2.0" +spring-context-iff = "7.0.8" + +[libraries] + +architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" } +jakarta-persistence-api-iff = { module = "jakarta.persistence:jakarta.persistence-api", version.ref = "jakarta-persistence-api-iff" } +spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" } diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/wrapper/gradle-wrapper.jar b/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/wrapper/gradle-wrapper.properties b/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a9db115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/gradlew b/examples/architecture-validator/hexagonal-spring-type-leakage/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/gradlew.bat b/examples/architecture-validator/hexagonal-spring-type-leakage/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/settings.gradle b/examples/architecture-validator/hexagonal-spring-type-leakage/settings.gradle new file mode 100644 index 0000000..aa1e2e1 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/settings.gradle @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'de.fayard.refreshVersions' version '0.60.6' +} + +refreshVersions { + rejectVersionIf { + candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) + } +} + +rootProject.name = 'architecture-validator-hexagonal-spring-type-leakage' diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/adapter/persistence/OrderRepositoryAdapter.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/adapter/persistence/OrderRepositoryAdapter.java new file mode 100644 index 0000000..0886265 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/adapter/persistence/OrderRepositoryAdapter.java @@ -0,0 +1,16 @@ +package com.arc_e_tect.example.spring.typeleakage.adapter.persistence; + +import com.arc_e_tect.example.spring.typeleakage.application.port.outbound.OrderPort; +import com.arc_e_tect.example.spring.typeleakage.persistence.OrderEntity; +import org.springframework.stereotype.Repository; + +@Repository +public class OrderRepositoryAdapter implements OrderPort { + + @Override + public OrderEntity findById(String id) { + OrderEntity entity = new OrderEntity(); + entity.setId(id); + return entity; + } +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/adapter/web/OrderController.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/adapter/web/OrderController.java new file mode 100644 index 0000000..5139115 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/adapter/web/OrderController.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.typeleakage.adapter.web; + +import com.arc_e_tect.example.spring.typeleakage.application.port.inbound.OrderUseCase; +import org.springframework.stereotype.Controller; + +@Controller +public class OrderController { + + private final OrderUseCase orderUseCase; + + public OrderController(OrderUseCase orderUseCase) { + this.orderUseCase = orderUseCase; + } + + public void submit(String id) { + orderUseCase.createOrder(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/domain/Order.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/domain/Order.java new file mode 100644 index 0000000..2601b69 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/domain/Order.java @@ -0,0 +1,4 @@ +package com.arc_e_tect.example.spring.typeleakage.application.domain; + +public record Order(String id) { +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/port/inbound/OrderUseCase.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/port/inbound/OrderUseCase.java new file mode 100644 index 0000000..a4c9a7a --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/port/inbound/OrderUseCase.java @@ -0,0 +1,6 @@ +package com.arc_e_tect.example.spring.typeleakage.application.port.inbound; + +public interface OrderUseCase { + + void createOrder(String id); +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/port/outbound/OrderPort.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/port/outbound/OrderPort.java new file mode 100644 index 0000000..3d9d680 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/port/outbound/OrderPort.java @@ -0,0 +1,8 @@ +package com.arc_e_tect.example.spring.typeleakage.application.port.outbound; + +import com.arc_e_tect.example.spring.typeleakage.persistence.OrderEntity; + +public interface OrderPort { + + OrderEntity findById(String id); +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/service/OrderService.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/service/OrderService.java new file mode 100644 index 0000000..d3ce72c --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/application/service/OrderService.java @@ -0,0 +1,18 @@ +package com.arc_e_tect.example.spring.typeleakage.application.service; + +import com.arc_e_tect.example.spring.typeleakage.application.port.inbound.OrderUseCase; +import com.arc_e_tect.example.spring.typeleakage.application.port.outbound.OrderPort; + +public class OrderService implements OrderUseCase { + + private final OrderPort orderPort; + + public OrderService(OrderPort orderPort) { + this.orderPort = orderPort; + } + + @Override + public void createOrder(String id) { + orderPort.findById(id); + } +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/persistence/OrderEntity.java b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/persistence/OrderEntity.java new file mode 100644 index 0000000..2e070c2 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/src/main/java/com/arc_e_tect/example/spring/typeleakage/persistence/OrderEntity.java @@ -0,0 +1,17 @@ +package com.arc_e_tect.example.spring.typeleakage.persistence; + +import jakarta.persistence.Entity; + +@Entity +public class OrderEntity { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/examples/architecture-validator/hexagonal-spring-type-leakage/versions.properties b/examples/architecture-validator/hexagonal-spring-type-leakage/versions.properties new file mode 100644 index 0000000..a07c552 --- /dev/null +++ b/examples/architecture-validator/hexagonal-spring-type-leakage/versions.properties @@ -0,0 +1,10 @@ +#### Dependencies and Plugin versions with their available updates. +#### Generated by `./gradlew refreshVersions` version 0.60.6 +#### +#### Don't manually edit or split the comments that start with four hashtags (####), +#### they will be overwritten by refreshVersions. +#### +#### suppress inspection "SpellCheckingInspection" for whole file +#### suppress inspection "UnusedProperty" for whole file +#### +#### NOTE: Some versions are filtered by the rejectVersionIf predicate. See the settings.gradle.kts file. diff --git a/examples/architecture-validator/single-module-spring/README.adoc b/examples/architecture-validator/single-module-spring/README.adoc index 58d25b2..24b7236 100644 --- a/examples/architecture-validator/single-module-spring/README.adoc +++ b/examples/architecture-validator/single-module-spring/README.adoc @@ -3,7 +3,7 @@ // tag::root[] This example enables the companion Spring rule pack. -It demonstrates a direct dependency from a Spring service to a Spring repository in the adapter layer. +It demonstrates a direct dependency from an application service to a Spring repository adapter, bypassing the outbound port. == Files and Purpose @@ -12,7 +12,7 @@ It demonstrates a direct dependency from a Spring service to a Spring repository |File |Purpose |`.../application/service/OrderService.java` -|A Spring `@Service` that depends directly on `OrderRepository` and triggers the rule violation under test. +|A plain application service (deliberately not annotated `@Service` — see `DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes`) that depends directly on `OrderRepository`, triggering the rule violations under test. |`.../adapter/persistence/OrderRepository.java` |Adapter-layer Spring repository reached directly by the service instead of through an out-port. @@ -20,13 +20,32 @@ It demonstrates a direct dependency from a Spring service to a Spring repository |`.../adapter/web/OrderController.java` |Inbound web adapter calling the use case. -|`.../application/port/in/OrderUseCase.java` +|`.../application/port/inbound/OrderUseCase.java` |Inbound port interface. |`.../application/domain/model/Order.java` |Domain model. |=== +== Configuration + +The only setup this rule pack needs is a `basePackage` and the `testArchitectureImplementation` dependency; +package boundaries (`inPorts`, `outPorts`, `domainModel`, `adapters`, `applicationServices`) fall back to the +plugin's Hexagonal defaults (`..application.port.inbound..`, `..application.port.outbound..`, +`..application.domain..`, `..adapter..`/`..adapters..`, `..application.service..`), which this project's +package layout already follows. + +[source,groovy] +---- +dependencies { + testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff +} + +architectureValidator { + basePackage = 'com.arc_e_tect.example.spring' +} +---- + == Run [source,bash] @@ -44,8 +63,33 @@ The current sample is intentionally non-compliant, so both commands are expected == Expected violations -`OrderService` is a Spring `@Service` that depends directly on `OrderRepository`. -That violates the Spring-flavoured Hexagonal rules because the service bypasses an outbound port and reaches into the adapter layer directly. +`OrderService` depends directly on `OrderRepository`, an adapter-layer Spring `@Repository`, instead of going through an outbound port: + +[source,java] +---- +public class OrderService implements OrderUseCase { + + private final OrderRepository repository; // <1> + + public OrderService(OrderRepository repository) { + this.repository = repository; + } + + @Override + public void createOrder(String id) { + repository.save(new Order(id)); + } +} +---- +<1> `OrderRepository` lives in `adapter.persistence`, not behind an `application.port.outbound` interface — this is the violation. + +That single design mistake trips several rules from different angles: + +* `DependencyDirectionTest.coreApplicationLayerShouldNotDependOnAdapters` — the application core must not depend on adapter-layer classes. +* `SpringHexagonalArchitectureTest.repositoriesShouldOnlyBeAccessedViaOutPorts` — `@Repository` classes may only be reached through an outbound port, an adapter, or configuration. +* The plugin's built-in generated `HexagonalArchitectureTest.application_services_must_not_depend_on_adapters` rule, which checks the same thing independently of the external rule pack. + +Note: `SpringHexagonalArchitectureTest.servicesShouldNotAccessRepositoriesDirectly` does not fire here because it only inspects `@Service`-annotated classes, and this rule pack expects application services to stay plain (see `DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes`) — so `OrderService` intentionally carries no Spring stereotype. == Reports @@ -54,7 +98,7 @@ The XML report is under `build/reports/architecture-validator/xml/`. == Fix -Introduce an outbound port in `application.port.out`. +Introduce an outbound port in `application.port.outbound`. Make the service depend on that port. Move the repository implementation behind an adapter that satisfies the port contract. // end::root[] \ No newline at end of file diff --git a/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/adapter/web/OrderController.java b/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/adapter/web/OrderController.java index edcacdf..4fe9da6 100644 --- a/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/adapter/web/OrderController.java +++ b/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/adapter/web/OrderController.java @@ -1,14 +1,14 @@ package com.arc_e_tect.example.spring.adapter.web; -import com.arc_e_tect.example.spring.application.service.OrderService; +import com.arc_e_tect.example.spring.application.port.inbound.OrderUseCase; import org.springframework.stereotype.Controller; @Controller public class OrderController { - private final OrderService orderService; + private final OrderUseCase orderService; - public OrderController(OrderService orderService) { + public OrderController(OrderUseCase orderService) { this.orderService = orderService; } diff --git a/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/port/in/OrderUseCase.java b/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/port/inbound/OrderUseCase.java similarity index 51% rename from examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/port/in/OrderUseCase.java rename to examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/port/inbound/OrderUseCase.java index 174a72c..5967a05 100644 --- a/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/port/in/OrderUseCase.java +++ b/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/port/inbound/OrderUseCase.java @@ -1,4 +1,4 @@ -package com.arc_e_tect.example.spring.application.port.in; +package com.arc_e_tect.example.spring.application.port.inbound; public interface OrderUseCase { diff --git a/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/service/OrderService.java b/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/service/OrderService.java index c86b132..4b31f84 100644 --- a/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/service/OrderService.java +++ b/examples/architecture-validator/single-module-spring/src/main/java/com/arc_e_tect/example/spring/application/service/OrderService.java @@ -2,10 +2,8 @@ import com.arc_e_tect.example.spring.adapter.persistence.OrderRepository; import com.arc_e_tect.example.spring.application.domain.model.Order; -import com.arc_e_tect.example.spring.application.port.in.OrderUseCase; -import org.springframework.stereotype.Service; +import com.arc_e_tect.example.spring.application.port.inbound.OrderUseCase; -@Service public class OrderService implements OrderUseCase { private final OrderRepository repository; diff --git a/examples/sedr-library/jacoco-marker/README.adoc b/examples/sedr-library/jacoco-marker/README.adoc index 68f0394..8453495 100644 --- a/examples/sedr-library/jacoco-marker/README.adoc +++ b/examples/sedr-library/jacoco-marker/README.adoc @@ -4,7 +4,15 @@ :icons: font :source-highlighter: rouge -This self-contained example demonstrates compliant and non-compliant usage of `@ExcludeFromJacocoGeneratedCodeCoverage`. +`@ExcludeFromJacocoGeneratedCodeCoverage` (from `sedr-library`'s `com.arc_e_tect.sedr.utils.jacoco.marker` +package) marks a constructor, method, or type as intentionally excluded from JaCoCo coverage measurement — +for example, a framework entry point that only the container ever calls, which unit tests cannot reasonably +exercise. `AbstractCoverageExclusionConventionsTest` is an ArchUnit-based convention check that enforces the +one rule that makes the annotation trustworthy: every use must carry a non-blank `justification`, so a +reviewer can tell a deliberate exclusion from someone quietly hiding untested code. + +This self-contained example demonstrates both sides of that check: compliant usage (justification present, +passes) and non-compliant usage (justification missing, fails). // tag::root[] == Files and Purpose @@ -20,9 +28,45 @@ This self-contained example demonstrates compliant and non-compliant usage of `@ |Uses `@ExcludeFromJacocoGeneratedCodeCoverage` without a justification, so the ArchUnit test reports a violation. |`src/test/java/.../CoverageExclusionConventionsTest.java` -|Extends `AbstractCoverageExclusionConventionsTest` targeting the example package. +|Extends `AbstractCoverageExclusionConventionsTest` targeting the example package. This is the entire +integration point — see the snippet below. |=== +== The Two Cases + +[source,java] +---- +// CompliantService.java +@ExcludeFromJacocoGeneratedCodeCoverage( + justification = "Stub entry point executed only by the container, not unit-testable") +public void start() { + // framework entry point — excluded from coverage by design +} +---- + +[source,java] +---- +// NonCompliantService.java +@ExcludeFromJacocoGeneratedCodeCoverage // <1> +public void stop() { + // annotation has no justification — the ArchUnit test will fail here +} +---- +<1> No `justification` — this is what the convention check rejects. + +Wiring the check into a project is a three-line subclass naming the package to scan: + +[source,java] +---- +class CoverageExclusionConventionsTest extends AbstractCoverageExclusionConventionsTest { + + @Override + protected String getBasePackage() { + return "com.arc_e_tect.sedr.example.jacoco.marker"; + } +} +---- + == Running the Example This example requires `sedr-library` to be published to your local Maven repository first. @@ -36,9 +80,15 @@ cd sedr-library && ./gradlew clean build publishToMavenLocal cd ../examples/sedr-library/jacoco-marker && ./gradlew test ---- -The test run reports exactly one violation. -`Method NonCompliantService.stop() uses @ExcludeFromJacocoGeneratedCodeCoverage without a justification`. -`CompliantService` is not mentioned. +The test run reports exactly one violation: + +[source] +---- +Method com.arc_e_tect.sedr.example.jacoco.marker.NonCompliantService.stop() uses +@ExcludeFromJacocoGeneratedCodeCoverage without a justification +---- + +`CompliantService` is not mentioned — only the annotation usage missing a `justification` is flagged. // end::root[] == License diff --git a/hexagonal-spring-rules/README.adoc b/hexagonal-spring-rules/README.adoc index 123b7a3..553fc17 100644 --- a/hexagonal-spring-rules/README.adoc +++ b/hexagonal-spring-rules/README.adoc @@ -81,6 +81,21 @@ The default rules target Spring-specific architectural boundaries, including dep |`PortContractTest` |In-ports and out-ports are interfaces, and port signatures reference only Java core and domain model types with no framework leakage into port contracts. + +|`RulePackConfigurationTest` +|Fails fast when required Architecture Validator properties are missing so the rule pack cannot pass with empty class imports. + +|`TypeLeakageTest` +|Ports must not expose JPA entities or web DTO packages, and the domain model must not reference JPA entities. + +|`CycleFreedomTest` +|Adapter and domain-model package trees must be free of circular package dependencies. + +|`DependencyInjectionStyleTest` +|Classes in adapters, application services, and domain model must not use field injection via `@Autowired`. + +|`NamingConventionTest` +|Optional naming checks enforce consistent suffixes for in-ports, out-ports, repositories, and web controllers. |=== == Architecture Validator Properties @@ -108,12 +123,28 @@ The rules read package boundaries from system properties set by the Architecture |`architectureValidator.applicationServices` |Comma-separated application service packages. + +|`architectureValidator.rules.disabled` +|Comma-separated rule identifiers to skip in the format `ClassSimpleName.methodName`. +Example: `DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes`. + +|`architectureValidator.namingConventions.enabled` +|Boolean flag for optional naming convention rules; defaults to `false` and must be explicitly set to `true`. |=== == Example Project -A self-contained example demonstrating a Spring Hexagonal architecture violation is at link:../examples/architecture-validator/single-module-spring/[`examples/architecture-validator/single-module-spring/`]. -See link:../examples/architecture-validator/single-module-spring/README.adoc[its README] for full details. +The following self-contained example projects demonstrate each rule-pack behavior in isolation-oriented fixtures. + +* link:../examples/architecture-validator/single-module-spring/[examples/architecture-validator/single-module-spring/] demonstrates the original SpringHexagonalArchitectureTest service-to-repository violation. +* link:../examples/architecture-validator/hexagonal-spring-misconfigured/[examples/architecture-validator/hexagonal-spring-misconfigured/] demonstrates fail-fast configuration errors from RulePackConfigurationTest. +* link:../examples/architecture-validator/hexagonal-spring-type-leakage/[examples/architecture-validator/hexagonal-spring-type-leakage/] demonstrates TypeLeakageTest detection of JPA entity leakage in a port contract. +* link:../examples/architecture-validator/hexagonal-spring-cycle-detection/[examples/architecture-validator/hexagonal-spring-cycle-detection/] demonstrates CycleFreedomTest detection of adapter-package cycles in nested base packages. +* link:../examples/architecture-validator/hexagonal-spring-rules-disabled/[examples/architecture-validator/hexagonal-spring-rules-disabled/] demonstrates selective per-rule opt-out via architectureValidator.rulesDisabled. +* link:../examples/architecture-validator/hexagonal-spring-field-injection/[examples/architecture-validator/hexagonal-spring-field-injection/] demonstrates DependencyInjectionStyleTest detection of @Autowired field injection. +* link:../examples/architecture-validator/hexagonal-spring-naming-conventions/[examples/architecture-validator/hexagonal-spring-naming-conventions/] demonstrates opt-in NamingConventionTest failures for inconsistent suffixes. + +Each example README contains runnable commands, expected violations, and remediation guidance. == Contributing diff --git a/hexagonal-spring-rules/build.gradle b/hexagonal-spring-rules/build.gradle index 7ad04cf..0a75663 100644 --- a/hexagonal-spring-rules/build.gradle +++ b/hexagonal-spring-rules/build.gradle @@ -7,6 +7,7 @@ plugins { id 'jacoco' id 'java-library' + id 'java-test-fixtures' id 'maven-publish' alias(libs.plugins.owasp.dependency.check.iff) alias(libs.plugins.jreleaser.iff) @@ -92,7 +93,10 @@ dependencies { compileOnly libs.spring.context.iff compileOnly libs.junit.jupiter.iff + testFixturesImplementation libs.spring.context.iff + testImplementation libs.junit.jupiter.iff + testImplementation(testFixtures(project)) testRuntimeOnly libs.junit.platform.launcher.iff } diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/CycleFreedomTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/CycleFreedomTest.java new file mode 100644 index 0000000..1afdf91 --- /dev/null +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/CycleFreedomTest.java @@ -0,0 +1,92 @@ +package com.arc_e_tect.gradle.architecture.spring; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + +/** + * Package cycle validation rules for Hexagonal architecture. + * + *
This rule class enforces cycle freedom inside configured adapter and domain-model + * package roots. Layer boundary checks can miss cycles inside a single layer, so these + * rules prevent tightly coupled package graphs that are hard to evolve and test. + * + *
This class is discovered and included in the rule-pack suite by the Architecture Validator + * plugin. Each test method defines a separate validation rule. + * + * @see RulePackConfiguration + * @since 1.0.0 + */ +class CycleFreedomTest { + + private final JavaClasses classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(RulePackConfiguration.basePackage()); + + /** + * Validates that configured adapter package trees are free of package cycles. + */ + @Test + void adapterPackagesShouldBeFreeOfCycles() { + assertConfiguredSlicesAreCycleFree( + RulePackConfiguration.adapters(), + "Adapter packages should remain acyclic to keep infrastructure boundaries maintainable"); + } + + /** + * Validates that configured domain model package trees are free of package cycles. + */ + @Test + void domainModelShouldBeFreeOfCycles() { + assertConfiguredSlicesAreCycleFree( + RulePackConfiguration.domainModel(), + "Domain model packages should remain acyclic to preserve a clear core model structure"); + } + + private void assertConfiguredSlicesAreCycleFree(String[] packageRoots, String reason) { + if (packageRoots.length == 0) { + return; + } + + Arrays.stream(packageRoots) + .map(CycleFreedomTest::toSlicePattern) + .forEach(slicePattern -> slices() + .matching(slicePattern) + .should().beFreeOfCycles() + .because(reason) + .allowEmptyShould(true) + .check(classes)); + } + + private static String toSlicePattern(String packageRoot) { + String trimmed = packageRoot.trim(); + boolean matchesAnyPrefix = trimmed.startsWith(".."); + + String normalized = trimmed; + while (normalized.startsWith("..")) { + normalized = normalized.substring(2); + } + + while (normalized.endsWith("..")) { + normalized = normalized.substring(0, normalized.length() - 2); + } + + if (normalized.endsWith(".")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + + if (normalized.isEmpty()) { + return "(**)"; + } + + // ArchUnit slice patterns anchor to the start of the fully-qualified class name unless + // prefixed with "..", so a leading ".." in the configured package root must be preserved, + // otherwise nested packages (the normal case) never match and the check passes vacuously. + return (matchesAnyPrefix ? ".." : "") + normalized + ".(**)"; + } +} diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyDirectionTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyDirectionTest.java index 88b747b..2d82e0f 100644 --- a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyDirectionTest.java +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyDirectionTest.java @@ -3,6 +3,7 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; @@ -43,6 +44,10 @@ class DependencyDirectionTest { */ @Test void coreApplicationLayerShouldNotDependOnAdapters() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("DependencyDirectionTest.coreApplicationLayerShouldNotDependOnAdapters"), + "Rule disabled via architectureValidator.rules.disabled" + ); noClasses() .that().resideOutsideOfPackage("..adapter..") .and().resideOutsideOfPackage("..adapters..") @@ -60,6 +65,10 @@ void coreApplicationLayerShouldNotDependOnAdapters() { */ @Test void adaptersShouldNotDependOnServiceImplementations() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("DependencyDirectionTest.adaptersShouldNotDependOnServiceImplementations"), + "Rule disabled via architectureValidator.rules.disabled" + ); noClasses() .that().resideInAnyPackage(RulePackConfiguration.adapters()) .should().dependOnClassesThat().resideInAnyPackage(RulePackConfiguration.applicationServices()) @@ -78,6 +87,10 @@ void adaptersShouldNotDependOnServiceImplementations() { */ @Test void onlyConfigurationMayDependOnServiceImplementations() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("DependencyDirectionTest.onlyConfigurationMayDependOnServiceImplementations"), + "Rule disabled via architectureValidator.rules.disabled" + ); noClasses() .that().resideOutsideOfPackage("..configuration..") .and().resideOutsideOfPackage("..application.domain.service..") diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyInjectionStyleTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyInjectionStyleTest.java new file mode 100644 index 0000000..0f5ebde --- /dev/null +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DependencyInjectionStyleTest.java @@ -0,0 +1,57 @@ +package com.arc_e_tect.gradle.architecture.spring; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields; + +/** + * Dependency injection style validation rules for Hexagonal architecture. + * + *
This rule class enforces constructor-injection-friendly design by forbidding + * Spring field injection annotations in core application and adapter layers. + * + *
Rules validate: + *
This class is discovered and included in the rule-pack suite by the Architecture Validator + * plugin. Each test method defines a separate validation rule. + * + * @see RulePackConfiguration + * @since 0.4.0 + */ +class DependencyInjectionStyleTest { + + private final JavaClasses classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(RulePackConfiguration.basePackage()); + + private String[] targetPackages() { + return RulePackConfiguration.merge( + RulePackConfiguration.domainModel(), + RulePackConfiguration.merge( + RulePackConfiguration.adapters(), + RulePackConfiguration.applicationServices()) + ); + } + + /** + * Validates that classes in core and adapter layers do not use field injection. + * + *
Field injection hides required dependencies and makes classes harder to test in isolation. + * Constructor injection keeps dependencies explicit and supports framework-agnostic design. + */ + @Test + void fieldsShouldNotBeAutowired() { + noFields() + .that().areDeclaredInClassesThat().resideInAnyPackage(targetPackages()) + .should().beAnnotatedWith("org.springframework.beans.factory.annotation.Autowired") + .because("Constructor injection keeps dependencies explicit and testable without a Spring context") + .allowEmptyShould(true) + .check(classes); + } +} diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DomainIsolationTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DomainIsolationTest.java index e6430e1..8ee464f 100644 --- a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DomainIsolationTest.java +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/DomainIsolationTest.java @@ -3,6 +3,7 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @@ -43,11 +44,22 @@ class DomainIsolationTest { */ @Test void domainModelShouldOnlyDependOnJavaCoreAndDomainModel() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("DomainIsolationTest.domainModelShouldOnlyDependOnJavaCoreAndDomainModel"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().resideInAnyPackage(RulePackConfiguration.domainModel()) .should().onlyDependOnClassesThat() .resideInAnyPackage(RulePackConfiguration.merge( RulePackConfiguration.domainModel(), + "java.util..", + "java.lang..", + "java.time..")) + .because("Domain model classes must stay framework free") + .check(classes); + } + /** * Validates that the core application layer has no Spring or persistence framework dependencies. * @@ -56,15 +68,12 @@ void domainModelShouldOnlyDependOnJavaCoreAndDomainModel() { * This ensures the business logic remains framework-agnostic and can be evolved * independently of infrastructure choices. */ - "java.util..", - "java.lang..", - "java.time..")) - .because("Domain model classes must stay framework free") - .check(classes); - } - @Test void coreApplicationLayerShouldHaveNoFrameworkDependencies() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("DomainIsolationTest.coreApplicationLayerShouldHaveNoFrameworkDependencies"), + "Rule disabled via architectureValidator.rules.disabled" + ); noClasses() .that().resideInAnyPackage(RulePackConfiguration.domainModel()) .should().dependOnClassesThat() @@ -89,6 +98,10 @@ void coreApplicationLayerShouldHaveNoFrameworkDependencies() { */ @Test void applicationServicesShouldNotCarrySpringStereotypes() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("DomainIsolationTest.applicationServicesShouldNotCarrySpringStereotypes"), + "Rule disabled via architectureValidator.rules.disabled" + ); noClasses() .that().resideInAnyPackage(RulePackConfiguration.applicationServices()) .should().beAnnotatedWith("org.springframework.stereotype.Service") diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/NamingConventionTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/NamingConventionTest.java new file mode 100644 index 0000000..0b2d905 --- /dev/null +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/NamingConventionTest.java @@ -0,0 +1,108 @@ +package com.arc_e_tect.gradle.architecture.spring; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +/** + * Optional naming convention validation rules for Spring Hexagonal architecture. + * + *
This rule class enforces naming suffix conventions for ports and adapters to keep + * architectural roles obvious in code review and IDE navigation. + * + *
Rules validate: + *
This class is discovered and included in the rule-pack suite by the Architecture Validator + * plugin. Each test method defines a separate validation rule. + * + * @see RulePackConfiguration + * @since 0.4.0 + */ +class NamingConventionTest { + + private static final String OPT_IN_MESSAGE = "Naming convention rules are opt-in; set architectureValidator.namingConventions.enabled=true to activate"; + + private final JavaClasses classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(RulePackConfiguration.basePackage()); + + /** + * Validates that in-port types use a consistent role suffix. + * + *
Using {@code UseCase} or {@code Port} suffixes makes inbound application + * contracts instantly recognizable and reinforces Hexagonal boundaries. + */ + @Test + void inputPortsShouldHaveConsistentSuffix() { + Assumptions.assumeTrue(RulePackConfiguration.namingConventionsEnabled(), OPT_IN_MESSAGE); + classes() + .that().resideInAnyPackage(RulePackConfiguration.inPorts()) + .should().haveNameMatching(".*UseCase$") + .orShould().haveNameMatching(".*Port$") + .because("Consistent in-port naming makes application entry-point contracts explicit") + .allowEmptyShould(true) + .check(classes); + } + + /** + * Validates that out-port types end with {@code Port}. + * + *
A stable suffix for outbound contracts clarifies that adapter implementations + * are behind an abstraction owned by the application core. + */ + @Test + void outputPortsShouldHaveConsistentSuffix() { + Assumptions.assumeTrue(RulePackConfiguration.namingConventionsEnabled(), OPT_IN_MESSAGE); + classes() + .that().resideInAnyPackage(RulePackConfiguration.outPorts()) + .should().haveSimpleNameEndingWith("Port") + .because("Consistent out-port naming keeps infrastructure boundaries self-documenting") + .allowEmptyShould(true) + .check(classes); + } + + /** + * Validates that adapter stereotypes use role-aligned suffixes. + * + *
Repository stereotypes should read as repository/adapter implementations, + * and web controllers should read as controller entry points. + */ + @Test + void adaptersShouldHaveConsistentSuffix() { + Assumptions.assumeTrue(RulePackConfiguration.namingConventionsEnabled(), OPT_IN_MESSAGE); + + classes() + .that().resideInAnyPackage(RulePackConfiguration.adapters()) + .and().areAnnotatedWith("org.springframework.stereotype.Repository") + .should().haveSimpleNameEndingWith("Repository") + .orShould().haveSimpleNameEndingWith("Adapter") + .because("Repository adapters should use repository-oriented suffixes to signal their role") + .allowEmptyShould(true) + .check(classes); + + classes() + .that().resideInAnyPackage(RulePackConfiguration.adapters()) + .and().areAnnotatedWith("org.springframework.web.bind.annotation.RestController") + .should().haveSimpleNameEndingWith("Controller") + .because("Web adapters should use Controller suffixes to make request-entry components obvious") + .allowEmptyShould(true) + .check(classes); + + classes() + .that().resideInAnyPackage(RulePackConfiguration.adapters()) + .and().areAnnotatedWith("org.springframework.stereotype.Controller") + .should().haveSimpleNameEndingWith("Controller") + .because("Web adapters should use Controller suffixes to make request-entry components obvious") + .allowEmptyShould(true) + .check(classes); + } +} diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/PortContractTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/PortContractTest.java index 49062a6..0daf099 100644 --- a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/PortContractTest.java +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/PortContractTest.java @@ -3,6 +3,7 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @@ -43,6 +44,10 @@ class PortContractTest { */ @Test void inputPortsShouldBeInterfaces() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("PortContractTest.inputPortsShouldBeInterfaces"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().resideInAnyPackage(RulePackConfiguration.inPorts()) .should().beInterfaces() @@ -61,6 +66,10 @@ void inputPortsShouldBeInterfaces() { */ @Test void outputPortsShouldBeInterfaces() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("PortContractTest.outputPortsShouldBeInterfaces"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().resideInAnyPackage(RulePackConfiguration.outPorts()) .should().beInterfaces() @@ -79,6 +88,10 @@ void outputPortsShouldBeInterfaces() { */ @Test void portsShouldOnlyDependOnJavaCoreAndDomainModel() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("PortContractTest.portsShouldOnlyDependOnJavaCoreAndDomainModel"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().resideInAnyPackage(RulePackConfiguration.inPorts()) .or().resideInAnyPackage(RulePackConfiguration.outPorts()) diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/RulePackConfiguration.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/RulePackConfiguration.java index 31ed607..377da98 100644 --- a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/RulePackConfiguration.java +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/RulePackConfiguration.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -19,8 +20,12 @@ *
This rule ensures the consuming project configures the minimum + * Architecture Validator properties so architecture checks do not pass + * vacuously with empty class imports. + * + * @see RulePackConfiguration + * @since 1.0.0 + */ +class RulePackConfigurationTest { + + /** + * Validates that required architecture properties are configured. + */ + @Test + void requiredArchitecturePropertiesShouldBeConfigured() { + RulePackConfiguration.requireConfigured(); + } +} diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/SpringHexagonalArchitectureTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/SpringHexagonalArchitectureTest.java index 43a5ef9..8618355 100644 --- a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/SpringHexagonalArchitectureTest.java +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/SpringHexagonalArchitectureTest.java @@ -3,9 +3,11 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; /** * Spring Hexagonal architecture validation rules. @@ -56,6 +58,10 @@ private String[] mergeAll(String[] first, String[] second, String... fixed) { */ @Test void controllersShouldOnlyCallInPorts() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("SpringHexagonalArchitectureTest.controllersShouldOnlyCallInPorts"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().areAnnotatedWith("org.springframework.stereotype.Controller") .or().areAnnotatedWith("org.springframework.web.bind.annotation.RestController") @@ -71,22 +77,27 @@ void controllersShouldOnlyCallInPorts() { /** * Validates that Spring services do not directly access repositories or adapters. - * + * *
Services must communicate with external systems through out-ports only. * Direct repository access couples the business logic to persistence details * and prevents flexibility in choosing adapter implementations. + * + *
This is a deny-list check on the adapter packages specifically, rather than an + * allow-list of every legitimate dependency: a {@code @Service} implementing its own + * in-port (the standard Hexagonal pattern) is a real dependency ArchUnit can see, and an + * allow-list would have to special-case it. The actual concern is adapter/repository + * access, so only that is checked. */ @Test void servicesShouldNotAccessRepositoriesDirectly() { - classes() + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("SpringHexagonalArchitectureTest.servicesShouldNotAccessRepositoriesDirectly"), + "Rule disabled via architectureValidator.rules.disabled" + ); + noClasses() .that().areAnnotatedWith("org.springframework.stereotype.Service") - .should().onlyDependOnClassesThat() - .resideInAnyPackage(mergeAll( - RulePackConfiguration.outPorts(), - RulePackConfiguration.applicationServices(), - RulePackConfiguration.domainModel(), - "java..", - "org.springframework..")) + .should().dependOnClassesThat() + .resideInAnyPackage(RulePackConfiguration.adapters()) .because("Spring services should not access repositories directly; use out-ports instead") .allowEmptyShould(true) .check(classes); @@ -101,6 +112,10 @@ void servicesShouldNotAccessRepositoriesDirectly() { */ @Test void repositoriesShouldOnlyBeAccessedViaOutPorts() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("SpringHexagonalArchitectureTest.repositoriesShouldOnlyBeAccessedViaOutPorts"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().areAnnotatedWith("org.springframework.stereotype.Repository") .should().onlyBeAccessed().byClassesThat() @@ -122,6 +137,10 @@ void repositoriesShouldOnlyBeAccessedViaOutPorts() { */ @Test void springComponentsShouldFollowHexagonalLayers() { + Assumptions.assumeFalse( + RulePackConfiguration.isRuleDisabled("SpringHexagonalArchitectureTest.springComponentsShouldFollowHexagonalLayers"), + "Rule disabled via architectureValidator.rules.disabled" + ); classes() .that().areAnnotatedWith("org.springframework.stereotype.Component") .or().areAnnotatedWith("org.springframework.stereotype.Service") diff --git a/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/TypeLeakageTest.java b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/TypeLeakageTest.java new file mode 100644 index 0000000..b32cc44 --- /dev/null +++ b/hexagonal-spring-rules/src/main/java/com/arc_e_tect/gradle/architecture/spring/TypeLeakageTest.java @@ -0,0 +1,166 @@ +package com.arc_e_tect.gradle.architecture.spring; + +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; + +/** + * Type leakage validation rules for Spring Hexagonal architecture. + * + *
This rule class enforces that persistence and web transport details do not leak + * into ports or the domain model. Ports and the core model must depend on stable + * business types, not JPA entities or web DTOs. + * + *
Rules validate: + *
This class is discovered and included in the rule-pack suite by the Architecture Validator + * plugin. Each test method defines a separate validation rule. + * + * @see RulePackConfiguration + * @since 1.0.0 + */ +class TypeLeakageTest { + + private static final String JAKARTA_ENTITY = "jakarta.persistence.Entity"; + private static final String JAVAX_ENTITY = "javax.persistence.Entity"; + + private final JavaClasses classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages(RulePackConfiguration.basePackage()); + + /** + * Validates that ports do not expose JPA entity types. + * + *
Ports define application contracts and must stay persistence-agnostic. + * Referencing JPA entities in port contracts leaks adapter concerns into + * the application boundary and couples core use cases to persistence models. + */ + @Test + void portsShouldNotExposeJpaEntities() { + methods() + .that().areDeclaredInClassesThat().resideInAnyPackage(RulePackConfiguration.inPorts()) + .or().areDeclaredInClassesThat().resideInAnyPackage(RulePackConfiguration.outPorts()) + .should(notExposeJpaEntityTypesInMethodSignatures()) + .because("Ports must not expose JPA entity types; use domain model contracts instead") + .allowEmptyShould(true) + .check(classes); + } + + /** + * Validates that domain model types do not reference JPA entities. + * + *
The domain model must remain persistence-independent so business + * behavior can evolve independently from ORM mappings and storage concerns. + */ + @Test + void domainModelShouldNotReferenceJpaEntities() { + classes() + .that().resideInAnyPackage(RulePackConfiguration.domainModel()) + .should(notDependOnJpaEntityTypes()) + .because("Domain model must not depend on JPA entity types") + .allowEmptyShould(true) + .check(classes); + } + + /** + * Validates that ports do not expose web DTO/request/response transport types. + * + *
Port contracts should model business interactions, not HTTP transport payloads.
+ * Using web DTO packages in ports leaks adapter concerns into the application core.
+ */
+ @Test
+ void portsShouldNotExposeWebDtos() {
+ methods()
+ .that().areDeclaredInClassesThat().resideInAnyPackage(RulePackConfiguration.inPorts())
+ .or().areDeclaredInClassesThat().resideInAnyPackage(RulePackConfiguration.outPorts())
+ .should(notExposeWebDtoTypesInMethodSignatures())
+ .because("Ports must not expose web transport DTOs; map them at adapter boundaries")
+ .allowEmptyShould(true)
+ .check(classes);
+ }
+
+ private static ArchCondition This test suite executes rule classes against in-repo fixture packages to prove
+ * that compliant structures pass while targeted violations fail.
+ *
+ * @since 0.4.0
+ */
+class RulePackSelfTest {
+
+ private static final String BASE_PACKAGE_KEY = "architectureValidator.basePackage";
+ private static final String IN_PORTS_KEY = "architectureValidator.inPorts";
+ private static final String OUT_PORTS_KEY = "architectureValidator.outPorts";
+ private static final String DOMAIN_MODEL_KEY = "architectureValidator.domainModel";
+ private static final String ADAPTERS_KEY = "architectureValidator.adapters";
+ private static final String INBOUND_ADAPTERS_KEY = "architectureValidator.inboundAdapters";
+ private static final String OUTBOUND_ADAPTERS_KEY = "architectureValidator.outboundAdapters";
+ private static final String APPLICATION_SERVICES_KEY = "architectureValidator.applicationServices";
+
+ private static final String COMPLIANT_BASE = "com.arc_e_tect.fixtures.compliant";
+ private static final String SERVICE_IMPLEMENTS_PORT_BASE = "com.arc_e_tect.fixtures.regression.serviceImplementsPort";
+
+ private static final String SPRING_CONTROLLERS_BASE = "com.arc_e_tect.fixtures.violating.spring.controllers";
+ private static final String SPRING_SERVICES_BASE = "com.arc_e_tect.fixtures.violating.spring.services";
+ private static final String SPRING_REPOSITORIES_BASE = "com.arc_e_tect.fixtures.violating.spring.repositories";
+ private static final String SPRING_COMPONENTS_BASE = "com.arc_e_tect.fixtures.violating.spring.components";
+
+ private static final String DOMAIN_DEPENDENCY_BASE = "com.arc_e_tect.fixtures.violating.domain.domainDependency";
+ private static final String DOMAIN_FRAMEWORK_BASE = "com.arc_e_tect.fixtures.violating.domain.frameworkDependency";
+ private static final String DOMAIN_SERVICE_STEREOTYPE_BASE = "com.arc_e_tect.fixtures.violating.domain.applicationServiceStereotype";
+
+ private static final String DEPENDENCY_CORE_BASE = "com.arc_e_tect.fixtures.violating.dependency.coreDependsOnAdapter";
+ private static final String DEPENDENCY_ADAPTER_BASE = "com.arc_e_tect.fixtures.violating.dependency.adapterDependsOnService";
+ private static final String DEPENDENCY_NON_CONFIG_BASE = "com.arc_e_tect.fixtures.violating.dependency.nonConfigDependsOnService";
+
+ private static final String PORT_INPUT_BASE = "com.arc_e_tect.fixtures.violating.port.inputNotInterface";
+ private static final String PORT_OUTPUT_BASE = "com.arc_e_tect.fixtures.violating.port.outputNotInterface";
+ private static final String PORT_SIGNATURE_BASE = "com.arc_e_tect.fixtures.violating.port.signatureLeak";
+
+ private static final String CYCLE_BASE = "com.arc_e_tect.fixtures.violating.cycle";
+
+ private final Map