GH-267 - Explicitly declared empty allowed dependencies now forbids any dependency.
The default for @ApplicationModule(allowedDependencies) is now a single element list with a dedicated token we recognize as "all dependencies allowed". This allows users to declare an empty array explicitly to disallow any outgoing dependencies for an application module. Previously, such a declaration would have allowed any dependency.
This commit is contained in:
@@ -29,6 +29,8 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApplicationModule {
|
||||
|
||||
public static final String OPEN_TOKEN = "¯\\_(ツ)_/¯";
|
||||
|
||||
/**
|
||||
* The human readable name of the module to be used for display and documentation purposes.
|
||||
*
|
||||
@@ -37,14 +39,17 @@ public @interface ApplicationModule {
|
||||
String displayName() default "";
|
||||
|
||||
/**
|
||||
* List the names of modules that the module is allowed to depend on. Shared modules defined in {@link Modulith} will
|
||||
* be allowed, too. Names listed are local ones, unless the application has configured
|
||||
* {@link Modulithic#useFullyQualifiedModuleNames()} to {@literal true}. Explicit references to
|
||||
* List the names of modules that the module is allowed to depend on. Shared modules defined in
|
||||
* {@link Modulith}/{@link Modulithic} will be allowed, too. Names listed are local ones, unless the application has
|
||||
* configured {@link Modulithic#useFullyQualifiedModuleNames()} to {@literal true}. Explicit references to
|
||||
* {@link NamedInterface}s need to be separated by a double colon {@code ::}, e.g. {@code module::API} if
|
||||
* {@code module} is the logical module name and {@code API} is the name of the named interface.
|
||||
* <p>
|
||||
* Declaring an empty array will allow no dependencies to other modules. To not restrict the dependencies at all,
|
||||
* leave the attribute at its default value.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
* @see NamedInterface
|
||||
*/
|
||||
String[] allowedDependencies() default {};
|
||||
String[] allowedDependencies() default { OPEN_TOKEN };
|
||||
}
|
||||
|
||||
@@ -374,20 +374,20 @@ public class ApplicationModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all allowed module dependencies, either explicitly declared or defined as shared on the given
|
||||
* Returns all declared module dependencies, either explicitly declared or defined as shared on the given
|
||||
* {@link ApplicationModules} instance.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
DeclaredDependencies getAllowedDependencies(ApplicationModules modules) {
|
||||
DeclaredDependencies getDeclaredDependencies(ApplicationModules modules) {
|
||||
|
||||
Assert.notNull(modules, "Modules must not be null!");
|
||||
|
||||
var allowedDependencyNames = information.getAllowedDependencies();
|
||||
var allowedDependencyNames = information.getDeclaredDependencies();
|
||||
|
||||
if (allowedDependencyNames.isEmpty()) {
|
||||
return new DeclaredDependencies(Collections.emptyList());
|
||||
if (DeclaredDependencies.isOpen(allowedDependencyNames)) {
|
||||
return DeclaredDependencies.open();
|
||||
}
|
||||
|
||||
var explicitlyDeclaredModules = allowedDependencyNames.stream() //
|
||||
@@ -398,7 +398,7 @@ public class ApplicationModule {
|
||||
|
||||
return Stream.concat(explicitlyDeclaredModules, sharedDependencies) //
|
||||
.distinct() //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::new));
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::closed));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,6 +655,19 @@ public class ApplicationModule {
|
||||
return namedInterface.contains(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link DeclaredDependency} contains the given {@link Class}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
|
||||
return namedInterface.contains(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
@@ -701,38 +714,55 @@ public class ApplicationModule {
|
||||
*/
|
||||
static class DeclaredDependencies {
|
||||
|
||||
private static final String OPEN_TOKEN = "¯\\_(ツ)_/¯";
|
||||
|
||||
private final List<DeclaredDependency> dependencies;
|
||||
private final boolean closed;
|
||||
|
||||
static boolean isOpen(List<String> declaredDependencies) {
|
||||
return declaredDependencies.size() == 1 && declaredDependencies.get(0).equals(OPEN_TOKEN);
|
||||
}
|
||||
|
||||
public static DeclaredDependencies open() {
|
||||
return new DeclaredDependencies(Collections.emptyList(), false);
|
||||
}
|
||||
|
||||
public static DeclaredDependencies closed(List<DeclaredDependency> dependencies) {
|
||||
return new DeclaredDependencies(dependencies, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link DeclaredDependencies} for the given {@link List} of {@link DeclaredDependency}.
|
||||
*
|
||||
* @param dependencies must not be {@literal null}.
|
||||
*/
|
||||
public DeclaredDependencies(List<DeclaredDependency> dependencies) {
|
||||
private DeclaredDependencies(List<DeclaredDependency> dependencies, boolean closed) {
|
||||
|
||||
Assert.notNull(dependencies, "Dependencies must not be null!");
|
||||
|
||||
this.dependencies = dependencies;
|
||||
this.closed = closed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any of the dependencies contains the given {@link JavaClass}.
|
||||
* Returns whether the given {@link JavaClass} is a valid dependency.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
|
||||
Assert.notNull(type, "JavaClass must not be null!");
|
||||
|
||||
return dependencies.stream() //
|
||||
.anyMatch(it -> it.contains(type));
|
||||
public boolean isAllowedDependency(JavaClass type) {
|
||||
return isAllowedDependency(it -> it.contains(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link DeclaredDependencies} are empty.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return dependencies.isEmpty();
|
||||
public boolean isAllowedDependency(Class<?> type) {
|
||||
return isAllowedDependency(it -> it.contains(type));
|
||||
}
|
||||
|
||||
private boolean isAllowedDependency(Predicate<DeclaredDependency> predicate) {
|
||||
|
||||
Assert.notNull(predicate, "Predicate must not be null!");
|
||||
|
||||
return closed ? !dependencies.isEmpty() && contains(predicate) : dependencies.isEmpty() || contains(predicate);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -742,9 +772,9 @@ public class ApplicationModule {
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return dependencies.stream() //
|
||||
.map(DeclaredDependency::toString)
|
||||
.collect(Collectors.joining(", "));
|
||||
return dependencies.isEmpty() //
|
||||
? "none" //
|
||||
: dependencies.stream().map(DeclaredDependency::toString).collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -773,6 +803,18 @@ public class ApplicationModule {
|
||||
public int hashCode() {
|
||||
return Objects.hash(dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any of the dependencies contains the given {@link JavaClass}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
private boolean contains(Predicate<DeclaredDependency> condition) {
|
||||
|
||||
Assert.notNull(condition, "Condition must not be null!");
|
||||
|
||||
return dependencies.stream().anyMatch(condition);
|
||||
}
|
||||
}
|
||||
|
||||
static class QualifiedDependency {
|
||||
@@ -880,16 +922,15 @@ public class ApplicationModule {
|
||||
var originModule = getExistingModuleOf(source, modules);
|
||||
var targetModule = getExistingModuleOf(target, modules);
|
||||
|
||||
DeclaredDependencies allowedTargets = originModule.getAllowedDependencies(modules);
|
||||
DeclaredDependencies declaredDependencies = originModule.getDeclaredDependencies(modules);
|
||||
Violations violations = Violations.NONE;
|
||||
|
||||
// Check explicitly defined allowed targets
|
||||
|
||||
if (!allowedTargets.isEmpty() && !allowedTargets.contains(target)) {
|
||||
if (!declaredDependencies.isAllowedDependency(target)) {
|
||||
|
||||
var message = "Module '%s' depends on module '%s' via %s -> %s. Allowed targets: %s." //
|
||||
.formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(),
|
||||
allowedTargets.toString());
|
||||
declaredDependencies.toString());
|
||||
|
||||
return violations.and(new IllegalStateException(message));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
@@ -67,7 +66,7 @@ interface ApplicationModuleInformation {
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
List<String> getAllowedDependencies();
|
||||
List<String> getDeclaredDependencies();
|
||||
|
||||
/**
|
||||
* An {@link ApplicationModuleInformation} for the jMolecules {@link Module} annotation.
|
||||
@@ -106,11 +105,11 @@ interface ApplicationModuleInformation {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.ApplicationModuleInformation#getAllowedDependencies()
|
||||
* @see org.springframework.modulith.core.ApplicationModuleInformation#getDeclaredDependencies()
|
||||
*/
|
||||
@Override
|
||||
public List<String> getAllowedDependencies() {
|
||||
return Collections.emptyList();
|
||||
public List<String> getDeclaredDependencies() {
|
||||
return List.of(ApplicationModule.OPEN_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,14 +157,14 @@ interface ApplicationModuleInformation {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.model.ApplicationModuleInformation#getAllowedDependencies()
|
||||
* @see org.springframework.modulith.core.ApplicationModuleInformation#getDeclaredDependencies()
|
||||
*/
|
||||
@Override
|
||||
public List<String> getAllowedDependencies() {
|
||||
public List<String> getDeclaredDependencies() {
|
||||
|
||||
return annotation //
|
||||
.map(it -> Arrays.stream(it.allowedDependencies())) //
|
||||
.orElse(Stream.empty()) //
|
||||
.orElse(Stream.of(ApplicationModule.OPEN_TOKEN)) //
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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.
|
||||
*/
|
||||
package example.declared.first;
|
||||
|
||||
import example.declared.second.Second;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
public class First {
|
||||
|
||||
First(Second second) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// No dependencies allowed
|
||||
@org.springframework.modulith.ApplicationModule(allowedDependencies = {})
|
||||
package example.declared.first;
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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.
|
||||
*/
|
||||
package example.declared.fourth;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
public class Fourth {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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.
|
||||
*/
|
||||
package example.declared.second;
|
||||
|
||||
import example.declared.third.Third;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
public class Second {
|
||||
Second(Third third) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// No explicit allowed dependencies -> all allowed
|
||||
@org.springframework.modulith.ApplicationModule(displayName = "Second")
|
||||
package example.declared.second;
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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.
|
||||
*/
|
||||
package example.declared.third;
|
||||
|
||||
import example.declared.fourth.Fourth;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@Component
|
||||
public class Third {
|
||||
Third(Fourth fourth) {}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ package org.springframework.modulith.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import example.declared.first.First;
|
||||
import example.declared.fourth.Fourth;
|
||||
import example.declared.second.Second;
|
||||
import example.declared.third.Third;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -164,6 +169,22 @@ class ApplicationModulesIntegrationTest {
|
||||
assertThat(source).containsExactly(ServiceComponentA.class, ServiceComponentB.class, String.class);
|
||||
}
|
||||
|
||||
@Test // GH-267
|
||||
void explicitEmptyAllowedModulesResultsInAllDependenciesRejected() {
|
||||
|
||||
var modules = ApplicationModules.of("example.declared");
|
||||
var first = modules.getModuleByType(First.class).orElseThrow();
|
||||
var second = modules.getModuleByType(Second.class).orElseThrow();
|
||||
var third = modules.getModuleByType(Third.class).orElseThrow();
|
||||
|
||||
// Disallowed due to allowedDependencies = {}
|
||||
assertThat(first.getDeclaredDependencies(modules).isAllowedDependency(Second.class)).isFalse();
|
||||
|
||||
// Allowed as allowedDependencies not set
|
||||
assertThat(second.getDeclaredDependencies(modules).isAllowedDependency(Third.class)).isTrue();
|
||||
assertThat(third.getDeclaredDependencies(modules).isAllowedDependency(Fourth.class)).isTrue();
|
||||
}
|
||||
|
||||
private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class<?>... types) {
|
||||
|
||||
Stream.of(types).forEach(type -> {
|
||||
|
||||
Reference in New Issue
Block a user