Skip to content

Commit 51c5f0b

Browse files
committed
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.
1 parent e9203ca commit 51c5f0b

File tree

6 files changed

+115
-24
lines changed

6 files changed

+115
-24
lines changed

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -643,20 +643,20 @@ static class DeclaredDependency {
643643

644644
private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'.";
645645
private static final String INVALID_NAMED_INTERFACE_DECLARATION = "No named interface named '%s' found! Original dependency declaration: %s -> %s.";
646+
private static final String WILDCARD = "*";
646647

647648
private final ApplicationModule target;
648-
private final NamedInterface namedInterface;
649+
private final @Nullable NamedInterface namedInterface;
649650

650651
/**
651652
* Creates a new {@link DeclaredDependency} for the given {@link ApplicationModule} and {@link NamedInterface}.
652653
*
653654
* @param target must not be {@literal null}.
654-
* @param namedInterface must not be {@literal null}.
655+
* @param namedInterface can be {@literal null}.
655656
*/
656-
private DeclaredDependency(ApplicationModule target, NamedInterface namedInterface) {
657+
private DeclaredDependency(ApplicationModule target, @Nullable NamedInterface namedInterface) {
657658

658659
Assert.notNull(target, "Target ApplicationModule must not be null!");
659-
Assert.notNull(namedInterface, "NamedInterface must not be null!");
660660

661661
this.target = target;
662662
this.namedInterface = namedInterface;
@@ -681,17 +681,22 @@ public static DeclaredDependency of(String identifier, ApplicationModule source,
681681

682682
var segments = identifier.split("::");
683683
var targetModuleName = segments[0].trim();
684-
var namedInterfacename = segments.length > 1 ? segments[1].trim() : null;
684+
var namedInterfaceName = segments.length > 1 ? segments[1].trim() : null;
685685

686686
var target = modules.getModuleByName(targetModuleName)
687687
.orElseThrow(() -> new IllegalArgumentException(
688688
INVALID_EXPLICIT_MODULE_DEPENDENCY.formatted(source.getName(), targetModuleName)));
689689

690-
var namedInterface = namedInterfacename == null
691-
? target.getNamedInterfaces().getUnnamedInterface()
692-
: target.getNamedInterfaces().getByName(segments[1])
690+
if (WILDCARD.equals(namedInterfaceName)) {
691+
return new DeclaredDependency(target, null);
692+
}
693+
694+
var namedInterfaces = target.getNamedInterfaces();
695+
var namedInterface = namedInterfaceName == null
696+
? namedInterfaces.getUnnamedInterface()
697+
: namedInterfaces.getByName(namedInterfaceName)
693698
.orElseThrow(() -> new IllegalArgumentException(
694-
INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfacename, source.getName(), identifier)));
699+
INVALID_NAMED_INTERFACE_DECLARATION.formatted(namedInterfaceName, source.getName(), identifier)));
695700

696701
return new DeclaredDependency(target, namedInterface);
697702
}
@@ -719,7 +724,9 @@ public boolean contains(JavaClass type) {
719724

720725
Assert.notNull(type, "Type must not be null!");
721726

722-
return namedInterface.contains(type);
727+
return namedInterface == null
728+
? target.getNamedInterfaces().containsInExplicitInterface(type)
729+
: namedInterface.contains(type);
723730
}
724731

725732
/**
@@ -728,11 +735,13 @@ public boolean contains(JavaClass type) {
728735
* @param type must not be {@literal null}.
729736
* @return
730737
*/
731-
public boolean contains(Class<?> type) {
738+
boolean contains(Class<?> type) {
732739

733740
Assert.notNull(type, "Type must not be null!");
734741

735-
return namedInterface.contains(type);
742+
return namedInterface == null
743+
? target.getNamedInterfaces().containsInExplicitInterface(type)
744+
: namedInterface.contains(type);
736745
}
737746

738747
/*
@@ -741,7 +750,18 @@ public boolean contains(Class<?> type) {
741750
*/
742751
@Override
743752
public String toString() {
744-
return namedInterface.isUnnamed() ? target.getName() : target.getName() + "::" + namedInterface.getName();
753+
754+
var result = target.getName();
755+
756+
if (namedInterface == null) {
757+
return result + " :: " + WILDCARD;
758+
}
759+
760+
if (namedInterface.isUnnamed()) {
761+
return result;
762+
}
763+
764+
return result + " :: " + namedInterface.getName();
745765
}
746766

747767
/*
@@ -761,7 +781,6 @@ public boolean equals(Object obj) {
761781

762782
return Objects.equals(this.target, that.target) //
763783
&& Objects.equals(this.namedInterface, that.namedInterface);
764-
765784
}
766785

767786
/*
@@ -821,7 +840,7 @@ public boolean isAllowedDependency(JavaClass type) {
821840
return isAllowedDependency(it -> it.contains(type));
822841
}
823842

824-
public boolean isAllowedDependency(Class<?> type) {
843+
boolean isAllowedDependency(Class<?> type) {
825844
return isAllowedDependency(it -> it.contains(type));
826845
}
827846

spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,20 @@ public Stream<NamedInterface> getNamedInterfacesContaining(JavaClass type) {
167167
.filter(it -> it.contains(type));
168168
}
169169

170+
/**
171+
* Returns whether the given type is contained in one of the explicitly named {@link NamedInterface}s.
172+
*
173+
* @param type must not be {@literal null}.
174+
* @since 1.2
175+
*/
176+
public boolean containsInExplicitInterface(JavaClass type) {
177+
178+
Assert.notNull(type, "Type must not be null!");
179+
180+
return getNamedInterfacesContaining(type)
181+
.anyMatch(NamedInterface::isNamed);
182+
}
183+
170184
/*
171185
* (non-Javadoc)
172186
* @see java.lang.Iterable#iterator()
@@ -218,6 +232,12 @@ Stream<NamedInterface> getNamedInterfacesContaining(Class<?> type) {
218232
.filter(it -> it.contains(type));
219233
}
220234

235+
boolean containsInExplicitInterface(Class<?> type) {
236+
237+
return getNamedInterfacesContaining(type)
238+
.anyMatch(NamedInterface::isNamed);
239+
}
240+
221241
private static NamedInterfaces of(NamedInterface interfaces) {
222242
return new NamedInterfaces(List.of(interfaces));
223243
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleUnitTest.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@
1616
package org.springframework.modulith.core;
1717

1818
import static org.assertj.core.api.Assertions.*;
19-
import static org.junit.jupiter.api.Assertions.*;
19+
20+
import example.ni.api.ApiType;
21+
import example.ni.spi.SpiType;
2022

2123
import java.util.List;
2224

2325
import javax.sql.DataSource;
2426

25-
import org.junit.jupiter.api.DynamicTest;
2627
import org.junit.jupiter.api.Test;
2728
import org.junit.jupiter.api.TestInstance;
2829
import org.junit.jupiter.api.TestInstance.Lifecycle;
30+
import org.springframework.modulith.core.ApplicationModule.DeclaredDependency;
2931

3032
import com.acme.withatbean.SampleAggregate;
3133
import com.acme.withatbean.TestEvents.JMoleculesAnnotated;
@@ -85,13 +87,25 @@ void detectsAggregates() {
8587
.<Class<?>> extracting(JavaClass::reflect)
8688
.containsExactly(SampleAggregate.class);
8789
}
88-
90+
8991
@Test // GH-319
90-
void containsPackage() {
91-
92+
void containsPackage() {
93+
9294
assertThat(module.containsPackage(packageName)).isTrue();
9395
assertThat(module.containsPackage(packageName + ".foo")).isTrue();
94-
96+
9597
assertThat(module.containsPackage(packageName + "foo")).isFalse();
9698
}
99+
100+
@Test // GH-601
101+
void wildcardedDeclaredDependencyAllowsDependenciesToAllNamedInterfaces() {
102+
103+
var modules = TestUtils.of("example", "example.ninvalid");
104+
105+
var module = modules.getModuleByName("ni").orElseThrow();
106+
var dependency = DeclaredDependency.of("ni :: *", module, modules);
107+
108+
assertThat(dependency.contains(SpiType.class)).isTrue();
109+
assertThat(dependency.contains(ApiType.class)).isTrue();
110+
}
97111
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/TestUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
1919

20+
import java.util.List;
2021
import java.util.function.Supplier;
2122

2223
import org.jmolecules.ddd.annotation.AggregateRoot;
@@ -28,6 +29,7 @@
2829
import com.tngtech.archunit.core.domain.JavaClass;
2930
import com.tngtech.archunit.core.domain.JavaClasses;
3031
import com.tngtech.archunit.core.importer.ClassFileImporter;
32+
import com.tngtech.archunit.core.importer.ImportOption;
3133

3234
/**
3335
* Utilities for testing.
@@ -77,6 +79,10 @@ public static JavaPackage getPackage(Class<?> packageType) {
7779
return JavaPackage.of(TestUtils.getClasses(packageType), packageType.getPackageName());
7880
}
7981

82+
public static ApplicationModules of(String basePackage, String... ignoredPackages) {
83+
return of(ModulithMetadata.of(basePackage), basePackage, JavaClass.Predicates.resideInAnyPackage(ignoredPackages));
84+
}
85+
8086
public static ApplicationModule getApplicationModule(String packageName) {
8187
return new ApplicationModule(getPackage(packageName), false);
8288
}
@@ -92,4 +98,10 @@ private static Classes getClasses(String packageName) {
9298
return Classes.of(new ClassFileImporter()
9399
.importPackages(packageName));
94100
}
101+
102+
private static ApplicationModules of(ModulithMetadata metadata, String basePackage,
103+
DescribedPredicate<JavaClass> ignores) {
104+
return new ApplicationModules(metadata, List.of(basePackage), ignores, false,
105+
new ImportOption.OnlyIncludeTests()) {};
106+
}
95107
}

spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ void detectsReferenceToUndeclaredNamedInterface() {
106106
assertThat(modules.getModuleByName("invalid3")).hasValueSatisfying(it -> {
107107
assertThatExceptionOfType(Violations.class).isThrownBy(() -> it.verifyDependencies(modules))
108108
.withMessageContaining("Allowed targets")
109-
.withMessageContaining("complex::API");
109+
.withMessageContaining("complex :: API");
110110
});
111111
}
112112

src/docs/antora/modules/ROOT/pages/fundamentals.adoc

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,15 @@ The effect of that declaration is twofold: first, code in other application modu
293293
Application modules are able to refer to the named interface in explicit dependency declarations.
294294
Assume the __inventory__ module was making use of that, it could refer to the above declared named interface like this:
295295

296+
.Defining allowed dependencies to dedicated named interfaces
296297
[tabs]
297298
======
298299
Java::
299300
+
300301
[source, java, role="primary", chomp="none"]
301302
----
302303
@org.springframework.modulith.ApplicationModule(
303-
allowedDependencies = "order::spi"
304+
allowedDependencies = "order :: spi"
304305
)
305306
package example.inventory;
306307
----
@@ -309,7 +310,7 @@ Kotlin::
309310
[source, kotlin, role="secondary", chomp="none"]
310311
----
311312
@org.springframework.modulith.ApplicationModule(
312-
allowedDependencies = "order::spi"
313+
allowedDependencies = "order :: spi"
313314
)
314315
package example.inventory
315316
----
@@ -319,6 +320,31 @@ Note how we concatenate the named interface's name `spi` via the double colon `:
319320
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.
320321
For modules without explicitly described dependencies, both the application module root package *and* the SPI one are accessible.
321322

323+
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:
324+
325+
.Using the asterisk to declare allowed dependencies to all declared named interfaces
326+
[tabs]
327+
======
328+
Java::
329+
+
330+
[source, java, role="primary", chomp="none"]
331+
----
332+
@org.springframework.modulith.ApplicationModule(
333+
allowedDependencies = "order :: *"
334+
)
335+
package example.inventory;
336+
----
337+
Kotlin::
338+
+
339+
[source, kotlin, role="secondary", chomp="none"]
340+
----
341+
@org.springframework.modulith.ApplicationModule(
342+
allowedDependencies = "order :: *"
343+
)
344+
package example.inventory
345+
----
346+
======
347+
322348
[[customizing-modules]]
323349
=== Customizing Module Detection
324350

0 commit comments

Comments
 (0)