Skip to content

Commit

Permalink
Add rule SpringBootRules.AllTypesInApplicationPackage
Browse files Browse the repository at this point in the history
  • Loading branch information
rweisleder committed Nov 26, 2023
1 parent 567bd2b commit b7cf1dc
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 5 deletions.
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
<artifactId>spring-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -78,6 +84,11 @@
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/de/rweisleder/archunit/spring/SpringBootRules.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package de.rweisleder.archunit.spring;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.conditions.ArchConditions;
import org.springframework.boot.SpringBootConfiguration;

import java.util.Collection;
import java.util.List;

import static com.tngtech.archunit.lang.conditions.ArchConditions.resideInAnyPackage;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static de.rweisleder.archunit.spring.MergedAnnotationPredicates.springAnnotatedWith;
import static java.util.stream.Collectors.toList;

/**
* Collection of {@link ArchRule rules} that can be used to check the structure of Spring Boot applications.
*
* @author Roland Weisleder
*/
public class SpringBootRules {

/**
* A rule that checks that all classes are located in the same package or a sub-package of the application class.
* The application class is the one annotated with {@code @SpringBootApplication} or {@code @SpringBootConfiguration}
* and must be within the given classes.
*
* @see #beInApplicationPackage()
*/
public static final ArchRule AllTypesInApplicationPackage = classes()
.should(beInApplicationPackage())
.as("all types of a Spring Boot application should be located in the same package or a sub-package of the application class");

/**
* A condition that checks that the given classes are located in the same package or a sub-package of the application class.
* The application class is the one annotated with {@code @SpringBootApplication} or {@code @SpringBootConfiguration}
* and must be within the given classes.
* <p>
* In case the application class is not within the given classes, consider using {@link ArchConditions#resideInAnyPackage(String...)} directly.
*
* @see #AllTypesInApplicationPackage
*/
public static ArchCondition<JavaClass> beInApplicationPackage() {
return new ArchCondition<JavaClass>("be located in the same package or a sub-package of the application class") {

private ArchCondition<JavaClass> inApplicationPackageCondition;

@Override
public void init(Collection<JavaClass> javaClasses) {
List<JavaClass> springBootApplicationClasses = javaClasses.stream()
.filter(springAnnotatedWith(SpringBootConfiguration.class))
.collect(toList());

if (springBootApplicationClasses.isEmpty()) {
throw new AssertionError("Could not locate a class annotated with @SpringBootApplication or @SpringBootConfiguration");
}

String[] applicationPackageIdentifiers = springBootApplicationClasses.stream()
.map(javaClass -> javaClass.getPackageName() + "..")
.distinct().toArray(String[]::new);
inApplicationPackageCondition = resideInAnyPackage(applicationPackageIdentifiers);
}

@Override
public void check(JavaClass javaClass, ConditionEvents events) {
inApplicationPackageCondition.check(javaClass, events);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaParameter;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
Expand All @@ -24,6 +23,7 @@

import static com.tngtech.archunit.base.DescribedPredicate.describe;
import static de.rweisleder.archunit.spring.MergedAnnotationPredicates.springAnnotatedWith;
import static de.rweisleder.archunit.spring.TestUtils.importClass;
import static org.assertj.core.api.Assertions.assertThat;

class MergedAnnotationPredicatesTest {
Expand Down Expand Up @@ -236,10 +236,6 @@ void springAnnotatedWith_with_MergedAnnotations_predicate_rejects_non_matching_c
assertThat(predicate).rejects(controllerClass);
}

private JavaClass importClass(Class<?> classToImport) {
return new ClassFileImporter().importClass(classToImport);
}

@Controller
static class ControllerClass {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package de.rweisleder.archunit.spring;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.lang.EvaluationResult;
import de.rweisleder.archunit.spring.testclasses.boot.app1.FirstSpringBootApplication;
import de.rweisleder.archunit.spring.testclasses.boot.app1.subpackage.FirstAppClassInSubpackage;
import de.rweisleder.archunit.spring.testclasses.boot.app2.SecondSpringBootApplication;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static de.rweisleder.archunit.spring.TestUtils.importClasses;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class SpringBootRulesTest {

@Nested
class Rule_AllTypesInApplicationPackage {

@Test
void provides_a_description() {
String description = SpringBootRules.AllTypesInApplicationPackage.getDescription();
assertThat(description).isEqualTo("all types of a Spring Boot application should be located in the same package or a sub-package of the application class");
}

@Test
void has_no_violation_if_all_classes_are_in_application_package_or_subpackage() {
JavaClasses classes = importClasses(FirstSpringBootApplication.class, FirstAppClassInSubpackage.class);
EvaluationResult evaluationResult = SpringBootRules.AllTypesInApplicationPackage.evaluate(classes);
assertThat(evaluationResult.hasViolation()).isFalse();
}

@Test
void has_violation_for_class_outside_application_package() {
Class<?> classOutsideApplicationPackage = String.class;
JavaClasses classes = importClasses(FirstSpringBootApplication.class, classOutsideApplicationPackage);
EvaluationResult evaluationResult = SpringBootRules.AllTypesInApplicationPackage.evaluate(classes);
assertThat(evaluationResult.hasViolation()).isTrue();
assertThat(evaluationResult.getFailureReport().getDetails()).anySatisfy(detail -> {
assertThat(detail).contains(classOutsideApplicationPackage.getName(), "does not reside in any package");
});
}

@Test
void fails_if_classes_do_not_contain_a_Spring_Boot_application() {
JavaClasses classes = importClasses(String.class);
assertThatThrownBy(() -> SpringBootRules.AllTypesInApplicationPackage.evaluate(classes))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Could not locate a class annotated with @SpringBootApplication or @SpringBootConfiguration");
}

@Test
void has_no_violation_if_classes_contain_multiple_distinct_Spring_Boot_applications() {
JavaClasses classes = importClasses(FirstSpringBootApplication.class, SecondSpringBootApplication.class);
EvaluationResult evaluationResult = SpringBootRules.AllTypesInApplicationPackage.evaluate(classes);
assertThat(evaluationResult.hasViolation()).isFalse();
}
}
}
16 changes: 16 additions & 0 deletions src/test/java/de/rweisleder/archunit/spring/TestUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.rweisleder.archunit.spring;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;

public class TestUtils {

public static JavaClasses importClasses(Class<?>... classes) {
return new ClassFileImporter().importClasses(classes);
}

public static JavaClass importClass(Class<?> classToImport) {
return new ClassFileImporter().importClass(classToImport);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.rweisleder.archunit.spring.testclasses.boot.app1;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FirstSpringBootApplication {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package de.rweisleder.archunit.spring.testclasses.boot.app1.subpackage;

public class FirstAppClassInSubpackage {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.rweisleder.archunit.spring.testclasses.boot.app2;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecondSpringBootApplication {
}

0 comments on commit b7cf1dc

Please sign in to comment.