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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ plugins {
id("org.jreleaser") version "1.19.0"
}

extra["springModulithVersion"] = "1.4.1"
extra["springBootVersion"] = "3.5.3"
extra["springModulithVersion"] = "2.0.3"
extra["springBootVersion"] = "4.0.2"
extra["archUnitVersion"] = "1.4.1"

@kirill-pyatunin kirill-pyatunin Apr 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

142 ver


allprojects {
group = "dev.clutcher.modulith"
version = "1.0.1"
version = "2.0.0"
}

configureJReleaser()
Expand Down Expand Up @@ -44,6 +44,7 @@ subprojects {

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

dependencyManagement {
Expand All @@ -55,9 +56,11 @@ subprojects {

configurePublishing()

signing {
useGpgCmd()
sign(publishing.publications["default"])
if (project.hasProperty("sign")) {
signing {
useGpgCmd()
sign(publishing.publications["default"])
}
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package dev.clutcher.modulith.archunit.starter;

import dev.clutcher.modulith.archunit.rules.app.api.ApiForArchRuleCreation;
import dev.clutcher.modulith.archunit.rules.app.domain.services.ArchRuleRegistry;
import dev.clutcher.modulith.archunit.rules.app.domain.services.HexagonalArchRuleCreationService;
import dev.clutcher.modulith.archunit.rules.app.domain.services.library.CodeConventionsRulesLibrary;
import dev.clutcher.modulith.archunit.rules.app.domain.services.library.HexagonalArchitectureRulesLibrary;
import dev.clutcher.modulith.archunit.rules.app.spi.ArchRuleToggleSettings;
import dev.clutcher.modulith.archunit.rules.app.spi.HexagonalArchitectureSettings;
import dev.clutcher.modulith.archunit.rules.app.spi.NamedArchRule;
import dev.clutcher.modulith.archunit.rules.out.spring.ArchRuleToggleSettingsUsingSpringProperties;
import dev.clutcher.modulith.archunit.rules.out.spring.HexagonalPackageSettingsUsingSpringProperties;
import dev.clutcher.modulith.archunit.verifier.app.api.ApiForModuleArchitectureVerification;
import dev.clutcher.modulith.archunit.verifier.app.domain.services.ModuleArchitectureVerificationService;
Expand All @@ -25,9 +31,28 @@ public HexagonalArchitectureSettings hexagonalArchitectureVerificationProperties
}

@Bean
@ConfigurationProperties(prefix = "dev.clutcher.modulith.archunit.rules")
@ConditionalOnMissingBean
public ApiForArchRuleCreation hexagonalArchRuleCreationService(HexagonalArchitectureSettings properties) {
return new HexagonalArchRuleCreationService(properties);
public ArchRuleToggleSettings archRuleToggleSettings() {
return new ArchRuleToggleSettingsUsingSpringProperties();
}

@Bean
@ConditionalOnMissingBean
public ArchRuleRegistry archRuleRegistry(ArchRuleToggleSettings toggleSettings,
List<NamedArchRule> customRules) {
ArchRuleRegistry registry = new ArchRuleRegistry(toggleSettings);
HexagonalArchitectureRulesLibrary.allRules().forEach(registry::register);
CodeConventionsRulesLibrary.allRules().forEach(registry::register);
customRules.forEach(registry::register);
return registry;
}

@Bean
@ConditionalOnMissingBean
public ApiForArchRuleCreation hexagonalArchRuleCreationService(HexagonalArchitectureSettings properties,
ArchRuleRegistry registry) {
return new HexagonalArchRuleCreationService(properties, registry);
}

@Bean
Expand Down
2 changes: 2 additions & 0 deletions spring-modulith-module-archunit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ dependencies {
implementation("org.springframework.modulith:spring-modulith-core")

testImplementation("org.springframework.modulith:spring-modulith-starter-test")
testCompileOnly("org.mapstruct:mapstruct:1.6.3")
testCompileOnly("org.springframework:spring-web")
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface ApiForArchRuleCreation {

ArchRule createDevStandardsRule(ApplicationModule applicationModule);

ArchRule createCodeConventionsRule(ApplicationModule applicationModule);

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ static ApiForCustomizingArchRuleCreation forApplicabilityChecker(RuleApplicabili

ApiForCustomizingArchRuleCreation withDevStandardsRule(RuleProvider provider);

ApiForCustomizingArchRuleCreation withCodeConventionsRule(ApiForArchRuleCreation apiForArchRuleCreation);

ApiForCustomizingArchRuleCreation withCodeConventionsRule(RuleProvider provider);

ApiForArchRuleCreation create();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.clutcher.modulith.archunit.rules.app.domain.model;

public enum RuleGroup {

LAYER("layer"),
PACKAGE_STRUCTURE("package-structure"),
DEV_STANDARDS("dev-standards"),
CODE_CONVENTIONS("code-conventions");

private final String value;

RuleGroup(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.clutcher.modulith.archunit.rules.app.domain.services;

import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.CompositeArchRule;
import dev.clutcher.modulith.archunit.rules.app.domain.model.RuleGroup;
import dev.clutcher.modulith.archunit.rules.app.spi.ArchRuleToggleSettings;
import dev.clutcher.modulith.archunit.rules.app.spi.HexagonalArchitectureSettings;
import dev.clutcher.modulith.archunit.rules.app.spi.NamedArchRule;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class ArchRuleRegistry {

private final Map<String, NamedArchRule> rules = new LinkedHashMap<>();
private final ArchRuleToggleSettings toggleSettings;

public ArchRuleRegistry(ArchRuleToggleSettings toggleSettings) {
this.toggleSettings = toggleSettings;
}

public void register(NamedArchRule rule) {
rules.put(rule.getId(), rule);
}

public ArchRule buildGroupRule(RuleGroup group, String moduleBasePackage, HexagonalArchitectureSettings settings) {
List<ArchRule> enabledRules = rules.values().stream()
.filter(r -> r.getGroup() == group)
.filter(r -> toggleSettings.isRuleEnabled(r.getId()))
.map(r -> r.create(moduleBasePackage, settings))
.filter(Objects::nonNull)
.toList();

if (enabledRules.isEmpty()) {
return null;
}

CompositeArchRule composite = CompositeArchRule.of(enabledRules.get(0));
for (int i = 1; i < enabledRules.size(); i++) {
composite = composite.and(enabledRules.get(i));
}
return composite;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ public class DelegatingArchRuleCreationService implements ApiForArchRuleCreation
private final RuleProvider layerRuleProvider;
private final RuleProvider packageRuleProvider;
private final RuleProvider devStandardsRuleProvider;
private final RuleProvider codeConventionsRuleProvider;

public DelegatingArchRuleCreationService(
RuleApplicabilityChecker ruleApplicabilityChecker,
RuleProvider layerRuleProvider,
RuleProvider packageRuleProvider,
RuleProvider devStandardsRuleProvider
RuleProvider devStandardsRuleProvider,
RuleProvider codeConventionsRuleProvider
) {
this.ruleApplicabilityChecker = ruleApplicabilityChecker;
this.layerRuleProvider = layerRuleProvider;
this.packageRuleProvider = packageRuleProvider;
this.devStandardsRuleProvider = devStandardsRuleProvider;
this.codeConventionsRuleProvider = codeConventionsRuleProvider;
}

@Override
Expand All @@ -46,4 +49,9 @@ public ArchRule createDevStandardsRule(ApplicationModule applicationModule) {
return devStandardsRuleProvider.provide(applicationModule);
}

@Override
public ArchRule createCodeConventionsRule(ApplicationModule applicationModule) {
return codeConventionsRuleProvider.provide(applicationModule);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.CompositeArchRule;
import dev.clutcher.modulith.archunit.rules.app.api.ApiForArchRuleCreation;
import dev.clutcher.modulith.archunit.rules.app.domain.services.library.HexagonalArchitectureRulesLibrary;
import dev.clutcher.modulith.archunit.rules.app.domain.model.RuleGroup;
import dev.clutcher.modulith.archunit.rules.app.spi.HexagonalArchitectureSettings;
import org.springframework.modulith.core.ApplicationModule;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;

public class HexagonalArchRuleCreationService implements ApiForArchRuleCreation {

private HexagonalArchitectureSettings properties;
private final HexagonalArchitectureSettings properties;
private final ArchRuleRegistry registry;

public HexagonalArchRuleCreationService(HexagonalArchitectureSettings properties) {
public HexagonalArchRuleCreationService(HexagonalArchitectureSettings properties, ArchRuleRegistry registry) {
this.properties = properties;
this.registry = registry;
}

@Override
Expand All @@ -29,27 +30,22 @@ public boolean isApplicable(ApplicationModule module, JavaClasses allClassesRela

@Override
public ArchRule createLayerRule(ApplicationModule module) {
String moduleBasePackage = module.getBasePackage().getName();
return HexagonalArchitectureRulesLibrary.createLayerDefinitionRule(moduleBasePackage, properties);
return registry.buildGroupRule(RuleGroup.LAYER, module.getBasePackage().getName(), properties);
}

@Override
public ArchRule createPackageStructureRule(ApplicationModule applicationModule) {
String moduleBasePackage = applicationModule.getBasePackage().getName();
return CompositeArchRule
.of(HexagonalArchitectureRulesLibrary.ruleForModuleRootPackageStructure(moduleBasePackage, properties))
.and(HexagonalArchitectureRulesLibrary.ruleForApplicationPortsPackageStructure(moduleBasePackage, properties))
.and(HexagonalArchitectureRulesLibrary.ruleForAdaptersPackageStructure(moduleBasePackage, properties));
return registry.buildGroupRule(RuleGroup.PACKAGE_STRUCTURE, applicationModule.getBasePackage().getName(), properties);
}

@Override
public ArchRule createDevStandardsRule(ApplicationModule applicationModule) {
String moduleBasePackage = applicationModule.getBasePackage().getName();
return CompositeArchRule
.of(HexagonalArchitectureRulesLibrary.ruleForApplicationServices(moduleBasePackage, properties))
.and(HexagonalArchitectureRulesLibrary.ruleForDrivingPorts(moduleBasePackage, properties))
.and(HexagonalArchitectureRulesLibrary.ruleForDrivenPorts(moduleBasePackage, properties))
.and(HexagonalArchitectureRulesLibrary.ruleForDrivenAdapters(moduleBasePackage, properties));
return registry.buildGroupRule(RuleGroup.DEV_STANDARDS, applicationModule.getBasePackage().getName(), properties);
}

@Override
public ArchRule createCodeConventionsRule(ApplicationModule applicationModule) {
return registry.buildGroupRule(RuleGroup.CODE_CONVENTIONS, applicationModule.getBasePackage().getName(), properties);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ public class DelegatingArchRuleCreationServiceBuilder implements ApiForCustomizi
private RuleProvider layerRuleProvider;
private RuleProvider packageRuleProvider;
private RuleProvider devStandardsRuleProvider;
private RuleProvider codeConventionsRuleProvider;

public DelegatingArchRuleCreationServiceBuilder(ApiForArchRuleCreation apiForArchRuleCreation) {
this.ruleApplicabilityChecker = apiForArchRuleCreation::isApplicable;
this.layerRuleProvider = apiForArchRuleCreation::createLayerRule;
this.packageRuleProvider = apiForArchRuleCreation::createPackageStructureRule;
this.devStandardsRuleProvider = apiForArchRuleCreation::createDevStandardsRule;
this.codeConventionsRuleProvider = apiForArchRuleCreation::createCodeConventionsRule;
}

public DelegatingArchRuleCreationServiceBuilder(RuleApplicabilityChecker ruleApplicabilityChecker) {
this.ruleApplicabilityChecker = ruleApplicabilityChecker;
this.layerRuleProvider = (module) -> null;
this.packageRuleProvider = (module) -> null;
this.devStandardsRuleProvider = (module) -> null;
this.codeConventionsRuleProvider = (module) -> null;
}

@Override
Expand Down Expand Up @@ -75,13 +78,26 @@ public ApiForCustomizingArchRuleCreation withDevStandardsRule(RuleProvider provi
return this;
}

@Override
public ApiForCustomizingArchRuleCreation withCodeConventionsRule(ApiForArchRuleCreation apiForArchRuleCreation) {
this.codeConventionsRuleProvider = apiForArchRuleCreation::createCodeConventionsRule;
return this;
}

@Override
public ApiForCustomizingArchRuleCreation withCodeConventionsRule(RuleProvider provider) {
this.codeConventionsRuleProvider = provider;
return this;
}

@Override
public ApiForArchRuleCreation create() {
return new DelegatingArchRuleCreationService(
this.ruleApplicabilityChecker,
this.layerRuleProvider,
this.packageRuleProvider,
this.devStandardsRuleProvider
this.devStandardsRuleProvider,
this.codeConventionsRuleProvider
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.clutcher.modulith.archunit.rules.app.domain.services.library;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;

import java.util.Set;

public final class ArchRulePredicates {

private ArchRulePredicates() {
}

public static DescribedPredicate<JavaClass> areNotAnnotatedWithAnyOf(String[] annotationNames) {
return new DescribedPredicate<>("are not annotated with any of the generated class annotations") {
@Override
public boolean test(JavaClass javaClass) {
Set<? extends JavaAnnotation<? extends JavaClass>> annotations = javaClass.getAnnotations();
for (String annotationName : annotationNames) {
for (JavaAnnotation<? extends JavaClass> annotation : annotations) {
if (annotation.getRawType().getName().equals(annotationName)) {
return false;
}
}
}
return true;
}
};
}
}
Loading