From 51c5f0bf42ea532df0b46e934d05350309fb4e54 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 16 May 2024 11:08:45 +0200 Subject: [PATCH] GH-601 - Support for wildcard references to named interfaces in explicitly defined allowed application module dependencies. When defining allowed application module dependencies to named interfaces, the asterisk can now be used to allow referencing all named interfaces declared by the target module. --- .../modulith/core/ApplicationModule.java | 49 +++++++++++++------ .../modulith/core/NamedInterfaces.java | 20 ++++++++ .../modulith/core/ModuleUnitTest.java | 26 +++++++--- .../modulith/core/TestUtils.java | 12 +++++ .../ApplicationModulesIntegrationTest.java | 2 +- .../modules/ROOT/pages/fundamentals.adoc | 30 +++++++++++- 6 files changed, 115 insertions(+), 24 deletions(-) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java index ceafa3bd..625a2759 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java @@ -643,20 +643,20 @@ public class ApplicationModule { private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'."; private static final String INVALID_NAMED_INTERFACE_DECLARATION = "No named interface named '%s' found! Original dependency declaration: %s -> %s."; + private static final String WILDCARD = "*"; private final ApplicationModule target; - private final NamedInterface namedInterface; + private final @Nullable NamedInterface namedInterface; /** * Creates a new {@link DeclaredDependency} for the given {@link ApplicationModule} and {@link NamedInterface}. * * @param target must not be {@literal null}. - * @param namedInterface must not be {@literal null}. + * @param namedInterface can be {@literal null}. */ - private DeclaredDependency(ApplicationModule target, NamedInterface namedInterface) { + private DeclaredDependency(ApplicationModule target, @Nullable NamedInterface namedInterface) { Assert.notNull(target, "Target ApplicationModule must not be null!"); - Assert.notNull(namedInterface, "NamedInterface must not be null!"); this.target = target; this.namedInterface = namedInterface; @@ -681,17 +681,22 @@ public class ApplicationModule { var segments = identifier.split("::"); var targetModuleName = segments[0].trim(); - var namedInterfacename = segments.length > 1 ? segments[1].trim() : null; + var namedInterfaceName = segments.length > 1 ? segments[1].trim() : null; var target = modules.getModuleByName(targetModuleName) .orElseThrow(() -> new IllegalArgumentException( INVALID_EXPLICIT_MODULE_DEPENDENCY.formatted(source.getName(), targetModuleName))); - var namedInterface = namedInterfacename == null - ? target.getNamedInterfaces().getUnnamedInterface() - : target.getNamedInterfaces().getByName(segments[1]) + if (WILDCARD.equals(namedInterfaceName)) { + return new DeclaredDependency(target, null); + } + + var namedInterfaces = target.getNamedInterfaces(); + var namedInterface = namedInterfaceName == null + ? namedInterfaces.getUnnamedInterface() + : namedInterfaces.getByName(namedInterfaceName) .orElseThrow(() -> new IllegalArgumentException( - INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfacename, source.getName(), identifier))); + INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfaceName, source.getName(), identifier))); return new DeclaredDependency(target, namedInterface); } @@ -719,7 +724,9 @@ public class ApplicationModule { Assert.notNull(type, "Type must not be null!"); - return namedInterface.contains(type); + return namedInterface == null + ? target.getNamedInterfaces().containsInExplicitInterface(type) + : namedInterface.contains(type); } /** @@ -728,11 +735,13 @@ public class ApplicationModule { * @param type must not be {@literal null}. * @return */ - public boolean contains(Class type) { + boolean contains(Class type) { Assert.notNull(type, "Type must not be null!"); - return namedInterface.contains(type); + return namedInterface == null + ? target.getNamedInterfaces().containsInExplicitInterface(type) + : namedInterface.contains(type); } /* @@ -741,7 +750,18 @@ public class ApplicationModule { */ @Override public String toString() { - return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName(); + + var result = target.getName(); + + if (namedInterface == null) { + return result + " :: " + WILDCARD; + } + + if (namedInterface.isUnnamed()) { + return result; + } + + return result + " :: " + namedInterface.getName(); } /* @@ -761,7 +781,6 @@ public class ApplicationModule { return Objects.equals(this.target, that.target) // && Objects.equals(this.namedInterface, that.namedInterface); - } /* @@ -821,7 +840,7 @@ public class ApplicationModule { return isAllowedDependency(it -> it.contains(type)); } - public boolean isAllowedDependency(Class type) { + boolean isAllowedDependency(Class type) { return isAllowedDependency(it -> it.contains(type)); } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java index 5dbc4dea..e5cb0fa2 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java @@ -167,6 +167,20 @@ public class NamedInterfaces implements Iterable { .filter(it -> it.contains(type)); } + /** + * Returns whether the given type is contained in one of the explicitly named {@link NamedInterface}s. + * + * @param type must not be {@literal null}. + * @since 1.2 + */ + public boolean containsInExplicitInterface(JavaClass type) { + + Assert.notNull(type, "Type must not be null!"); + + return getNamedInterfacesContaining(type) + .anyMatch(NamedInterface::isNamed); + } + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() @@ -218,6 +232,12 @@ public class NamedInterfaces implements Iterable { .filter(it -> it.contains(type)); } + boolean containsInExplicitInterface(Class type) { + + return getNamedInterfacesContaining(type) + .anyMatch(NamedInterface::isNamed); + } + private static NamedInterfaces of(NamedInterface interfaces) { return new NamedInterfaces(List.of(interfaces)); } diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleUnitTest.java index 2e930279..44236eee 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleUnitTest.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleUnitTest.java @@ -16,16 +16,18 @@ package org.springframework.modulith.core; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; + +import example.ni.api.ApiType; +import example.ni.spi.SpiType; import java.util.List; import javax.sql.DataSource; -import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.springframework.modulith.core.ApplicationModule.DeclaredDependency; import com.acme.withatbean.SampleAggregate; import com.acme.withatbean.TestEvents.JMoleculesAnnotated; @@ -85,13 +87,25 @@ class ModuleUnitTest { .> extracting(JavaClass::reflect) .containsExactly(SampleAggregate.class); } - + @Test // GH-319 - void containsPackage() { - + void containsPackage() { + assertThat(module.containsPackage(packageName)).isTrue(); assertThat(module.containsPackage(packageName + ".foo")).isTrue(); - + assertThat(module.containsPackage(packageName + "foo")).isFalse(); } + + @Test // GH-601 + void wildcardedDeclaredDependencyAllowsDependenciesToAllNamedInterfaces() { + + var modules = TestUtils.of("example", "example.ninvalid"); + + var module = modules.getModuleByName("ni").orElseThrow(); + var dependency = DeclaredDependency.of("ni :: *", module, modules); + + assertThat(dependency.contains(SpiType.class)).isTrue(); + assertThat(dependency.contains(ApiType.class)).isTrue(); + } } diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java index 623557a5..24c43477 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java @@ -17,6 +17,7 @@ package org.springframework.modulith.core; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; +import java.util.List; import java.util.function.Supplier; import org.jmolecules.ddd.annotation.AggregateRoot; @@ -28,6 +29,7 @@ import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; /** * Utilities for testing. @@ -77,6 +79,10 @@ class TestUtils { return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName()); } + public static ApplicationModules of(String basePackage, String... ignoredPackages) { + return of(ModulithMetadata.of(basePackage), basePackage, JavaClass.Predicates.resideInAnyPackage(ignoredPackages)); + } + public static ApplicationModule getApplicationModule(String packageName) { return new ApplicationModule(getPackage(packageName), false); } @@ -92,4 +98,10 @@ class TestUtils { return Classes.of(new ClassFileImporter() .importPackages(packageName)); } + + private static ApplicationModules of(ModulithMetadata metadata, String basePackage, + DescribedPredicate ignores) { + return new ApplicationModules(metadata, List.of(basePackage), ignores, false, + new ImportOption.OnlyIncludeTests()) {}; + } } diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java index 735749b2..ce9b0450 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java @@ -106,7 +106,7 @@ class ApplicationModulesIntegrationTest { assertThat(modules.getModuleByName("invalid3")).hasValueSatisfying(it -> { assertThatExceptionOfType(Violations.class).isThrownBy(() -> it.verifyDependencies(modules)) .withMessageContaining("Allowed targets") - .withMessageContaining("complex::API"); + .withMessageContaining("complex :: API"); }); } diff --git a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc index 545eb8d0..55aa6c21 100644 --- a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc +++ b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc @@ -293,6 +293,7 @@ The effect of that declaration is twofold: first, code in other application modu Application modules are able to refer to the named interface in explicit dependency declarations. Assume the __inventory__ module was making use of that, it could refer to the above declared named interface like this: +.Defining allowed dependencies to dedicated named interfaces [tabs] ====== Java:: @@ -300,7 +301,7 @@ Java:: [source, java, role="primary", chomp="none"] ---- @org.springframework.modulith.ApplicationModule( - allowedDependencies = "order::spi" + allowedDependencies = "order :: spi" ) package example.inventory; ---- @@ -309,7 +310,7 @@ Kotlin:: [source, kotlin, role="secondary", chomp="none"] ---- @org.springframework.modulith.ApplicationModule( - allowedDependencies = "order::spi" + allowedDependencies = "order :: spi" ) package example.inventory ---- @@ -319,6 +320,31 @@ Note how we concatenate the named interface's name `spi` via the double colon `: In this setup, code in __inventory__ would be allowed to depend on `SomeSpiInterface` and other code residing in the `order.spi` interface, but not on `OrderManagement` for example. For modules without explicitly described dependencies, both the application module root package *and* the SPI one are accessible. +If you wanted to express that an application module is allowed to refer to all explicitly declared named interfaces, you can use the asterisk (``*``) as follows: + +.Using the asterisk to declare allowed dependencies to all declared named interfaces +[tabs] +====== +Java:: ++ +[source, java, role="primary", chomp="none"] +---- +@org.springframework.modulith.ApplicationModule( + allowedDependencies = "order :: *" +) +package example.inventory; +---- +Kotlin:: ++ +[source, kotlin, role="secondary", chomp="none"] +---- +@org.springframework.modulith.ApplicationModule( + allowedDependencies = "order :: *" +) +package example.inventory +---- +====== + [[customizing-modules]] === Customizing Module Detection