Skip to content

Commit

Permalink
GH-267 - Explicitly declared empty allowed dependencies now forbids a…
Browse files Browse the repository at this point in the history
…ny 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.
  • Loading branch information
odrotbohm committed Aug 15, 2023
1 parent cec759a commit 9568f29
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
@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.
*
Expand All @@ -37,14 +39,17 @@
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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -374,20 +374,20 @@ Classes getSpringBeansInternal() {
}

/**
* 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() //
Expand All @@ -398,7 +398,7 @@ DeclaredDependencies getAllowedDependencies(ApplicationModules modules) {

return Stream.concat(explicitlyDeclaredModules, sharedDependencies) //
.distinct() //
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::new));
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::closed));
}

/**
Expand Down Expand Up @@ -655,6 +655,19 @@ public boolean contains(JavaClass type) {
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()
Expand Down Expand Up @@ -701,38 +714,55 @@ public int hashCode() {
*/
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!");
public boolean isAllowedDependency(JavaClass type) {
return isAllowedDependency(it -> it.contains(type));
}

return dependencies.stream() //
.anyMatch(it -> it.contains(type));
public boolean isAllowedDependency(Class<?> type) {
return isAllowedDependency(it -> it.contains(type));
}

/**
* Returns whether the {@link DeclaredDependencies} are empty.
*/
public boolean isEmpty() {
return dependencies.isEmpty();
private boolean isAllowedDependency(Predicate<DeclaredDependency> predicate) {

Assert.notNull(predicate, "Predicate must not be null!");

return closed ? !dependencies.isEmpty() && contains(predicate) : dependencies.isEmpty() || contains(predicate);
}

/*
Expand All @@ -742,9 +772,9 @@ public boolean isEmpty() {
@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(", "));
}

/*
Expand Down Expand Up @@ -773,6 +803,18 @@ public boolean equals(Object obj) {
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 {
Expand Down Expand Up @@ -880,16 +922,15 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,7 +66,7 @@ default Optional<String> getDisplayName() {
*
* @return will never be {@literal null}.
*/
List<String> getAllowedDependencies();
List<String> getDeclaredDependencies();

/**
* An {@link ApplicationModuleInformation} for the jMolecules {@link Module} annotation.
Expand Down Expand Up @@ -106,11 +105,11 @@ public Optional<String> getDisplayName() {

/*
* (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);
}
}

Expand Down Expand Up @@ -158,14 +157,14 @@ public Optional<String> getDisplayName() {

/*
* (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();
}
}
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// No dependencies allowed
@org.springframework.modulith.ApplicationModule(allowedDependencies = {})
package example.declared.first;
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// No explicit allowed dependencies -> all allowed
@org.springframework.modulith.ApplicationModule(displayName = "Second")
package example.declared.second;
Original file line number Diff line number Diff line change
@@ -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) {}
}
Loading

0 comments on commit 9568f29

Please sign in to comment.