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