diff --git a/pom.xml b/pom.xml
index 6bb1772..0994827 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,6 +26,7 @@
3.4.3
1.13.4
+ 1.3.0
@@ -35,6 +36,11 @@
micrometer-registry-prometheus
${micrometer-registry-prometheus.version}
+
+ com.tngtech.archunit
+ archunit-junit5
+ ${archunit-junit5.version}
+
@@ -58,6 +64,11 @@
spring-boot-starter-test
test
+
+ com.tngtech.archunit
+ archunit-junit5
+ test
+
diff --git a/src/main/java/tk/antoine/roux/Main.java b/src/main/java/tk/antoine/roux/Main.java
index 7045d2c..e842613 100644
--- a/src/main/java/tk/antoine/roux/Main.java
+++ b/src/main/java/tk/antoine/roux/Main.java
@@ -5,6 +5,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import java.lang.invoke.MethodHandles;
+
@SpringBootApplication
public class Main {
diff --git a/src/main/java/tk/antoine/roux/domain/model/Either.java b/src/main/java/tk/antoine/roux/domain/model/Either.java
index 20d1272..360f6a5 100644
--- a/src/main/java/tk/antoine/roux/domain/model/Either.java
+++ b/src/main/java/tk/antoine/roux/domain/model/Either.java
@@ -1,10 +1,10 @@
package tk.antoine.roux.domain.model;
-public final class Either {
- private final Exception error;
+public final class Either {
+ private final E error;
private final T success;
- private Either(Exception error, T success) {
+ private Either(E error, T success) {
this.error = error;
this.success = success;
}
@@ -17,11 +17,11 @@ public final class Either {
return success;
}
- public static Either right(T success) {
+ public static Either right(T success) {
return new Either<>(null, success);
}
- public static Either left(Exception error) {
+ public static Either left(E error) {
return new Either<>(error, null);
}
diff --git a/src/main/java/tk/antoine/roux/infrastructure/Manager.java b/src/main/java/tk/antoine/roux/infrastructure/Manager.java
index 10e74c5..b3338ce 100644
--- a/src/main/java/tk/antoine/roux/infrastructure/Manager.java
+++ b/src/main/java/tk/antoine/roux/infrastructure/Manager.java
@@ -3,14 +3,23 @@ package tk.antoine.roux.infrastructure;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
+import org.springframework.web.ErrorResponse;
import tk.antoine.roux.domain.model.Either;
public interface Manager {
- static ResponseEntity eitherToResponseEntity(Either either, String errorMessage) {
+ static ResponseEntity eitherToResponseEntity(Either either, String errorMessage) {
ResponseEntity response;
- if (either.isLeft()) {
- ProblemDetail problemDetail = exceptionToProblemDetail(either, errorMessage);
+ if (either.isLeft() ) {
+
+ final ProblemDetail problemDetail;
+ if (either.left() instanceof ErrorResponse exception) {
+ problemDetail = exception.getBody();
+ } else {
+ problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, either.left().getMessage());
+ }
+
+ problemDetail.setTitle(errorMessage);
response = ResponseEntity.of(problemDetail).build();
} else {
response = ResponseEntity.ok(either.right());
@@ -18,10 +27,4 @@ public interface Manager {
return response;
}
- private static ProblemDetail exceptionToProblemDetail(Either> either, String errorMessage) {
- ProblemDetail problemDetail = ProblemDetail
- .forStatusAndDetail(HttpStatus.BAD_REQUEST, either.left().getMessage());
- problemDetail.setTitle(errorMessage);
- return problemDetail;
- }
}
diff --git a/src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java b/src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java
new file mode 100644
index 0000000..15dc2eb
--- /dev/null
+++ b/src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java
@@ -0,0 +1,46 @@
+package tk.antoine.roux.architecture;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeArchives;
+import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars;
+import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludePackageInfos;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.library.Architectures;
+
+/**
+ * Test of clean architecture package dependencies
+ */
+@AnalyzeClasses(packages = "tk.antoine.roux", importOptions = {
+ DoNotIncludeArchives.class,
+ DoNotIncludeJars.class,
+ DoNotIncludePackageInfos.class
+})
+class CleanArchitectureTest {
+
+ @ArchTest
+ void should_respect_clean_architecture_application_layer(JavaClasses classes) {
+ Architectures.layeredArchitecture()
+ .consideringAllDependencies()
+
+ //@formatter:off
+ .layer(Layer.DOMAIN.name).definedBy(Layer.DOMAIN.path)
+ .layer(Layer.MODEL.name).definedBy(Layer.MODEL.path)
+ .optionalLayer(Layer.REPOSITORIES.name).definedBy(Layer.REPOSITORIES.path)
+ .optionalLayer(Layer.USECASES.name).definedBy(Layer.USECASES.path)
+ .layer(Layer.INFRASTRUCTURE.name).definedBy(Layer.INFRASTRUCTURE.path)
+ .optionalLayer(Layer.ADAPTER_IN.name).definedBy(Layer.ADAPTER_IN.path)
+ .optionalLayer(Layer.ADAPTER_OUT.name).definedBy(Layer.ADAPTER_OUT.path)
+ //@formatter:on
+
+ .whereLayer(Layer.INFRASTRUCTURE.name).mayNotBeAccessedByAnyLayer()
+ .whereLayer(Layer.USECASES.name).mayOnlyBeAccessedByLayers(Layer.ADAPTER_IN.name)
+ .whereLayer(Layer.REPOSITORIES.name).mayOnlyBeAccessedByLayers(Layer.USECASES.name)
+ .whereLayer(Layer.MODEL.name).mayOnlyBeAccessedByLayers(Layer.INFRASTRUCTURE.name)
+ .whereLayer(Layer.ADAPTER_OUT.name).mayNotBeAccessedByAnyLayer()
+ .whereLayer(Layer.ADAPTER_OUT.name).mayNotAccessAnyLayer()
+
+ .check(classes);
+ }
+
+}
diff --git a/src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java b/src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java
new file mode 100644
index 0000000..56a45ae
--- /dev/null
+++ b/src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java
@@ -0,0 +1,67 @@
+package tk.antoine.roux.architecture;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.lang.CompositeArchRule;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES;
+import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_JARS;
+import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_PACKAGE_INFOS;
+import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS;
+import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
+import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION;
+import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;
+import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JODATIME;
+
+/**
+ * Tests of good practice coding usage
+ */
+class CodingGoodPracticeTest {
+ private static final String BASE_PACKAGE = "tk.antoine.roux";
+
+ ClassFileImporter classFileImporter = new ClassFileImporter()
+ .withImportOption(DO_NOT_INCLUDE_ARCHIVES)
+ .withImportOption(DO_NOT_INCLUDE_JARS)
+ .withImportOption(DO_NOT_INCLUDE_PACKAGE_INFOS);
+
+ JavaClasses classes = classFileImporter
+ .importPackages(BASE_PACKAGE);
+
+ JavaClasses classesWithoutTest = classFileImporter
+ .withImportOption(DO_NOT_INCLUDE_TESTS)
+ .importPackages(BASE_PACKAGE);
+
+ @Test
+ @DisplayName("Should prevent field injection everywhere, except in test")
+ void preventFieldInjection() {
+ CompositeArchRule commonUsageRules = CompositeArchRule
+ .of(NO_CLASSES_SHOULD_USE_FIELD_INJECTION);
+ commonUsageRules.check(classesWithoutTest);
+ }
+
+ @Test
+ @DisplayName("Should prevent usage of system stream like out, err, in")
+ void preventSystemStream() {
+ CompositeArchRule commonUsageRules = CompositeArchRule
+ .of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS);
+ commonUsageRules.check(classes);
+ }
+
+ @Test
+ @DisplayName("Should prevent usage of Java.util.logging classes")
+ void preventJavaUtilLogging() {
+ CompositeArchRule commonUsageRules = CompositeArchRule
+ .of(NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING);
+ commonUsageRules.check(classes);
+ }
+
+ @Test
+ @DisplayName("Should prevent usage of JodaTime library")
+ void preventJodaTime() {
+ CompositeArchRule commonUsageRules = CompositeArchRule
+ .of(NO_CLASSES_SHOULD_USE_JODATIME);
+ commonUsageRules.check(classes);
+ }
+}
diff --git a/src/test/java/tk/antoine/roux/architecture/Layer.java b/src/test/java/tk/antoine/roux/architecture/Layer.java
new file mode 100644
index 0000000..7e3d47b
--- /dev/null
+++ b/src/test/java/tk/antoine/roux/architecture/Layer.java
@@ -0,0 +1,20 @@
+package tk.antoine.roux.architecture;
+
+public enum Layer {
+ DOMAIN("domain", "tk.antoine.roux.domain.."),
+ MODEL("model", "tk.antoine.roux.domain.model.."),
+ REPOSITORIES("repositories", "tk.antoine.roux.domain.repositories.."),
+ USECASES("usecases", "tk.antoine.roux.domain.usecases.."),
+ INFRASTRUCTURE("infrastructure", "tk.antoine.roux.infrastructure.."),
+ ADAPTER_IN("adapter_in", "tk.antoine.roux.infrastructure.in.."),
+ ADAPTER_OUT("adapter_out", "tk.antoine.roux.infrastructure.out..")
+ ;
+
+ public final String name;
+ public final String path;
+
+ Layer(String name, String path) {
+ this.name = name;
+ this.path = path;
+ }
+}
diff --git a/src/test/resources/archunit.properties b/src/test/resources/archunit.properties
new file mode 100644
index 0000000..d93e375
--- /dev/null
+++ b/src/test/resources/archunit.properties
@@ -0,0 +1 @@
+junit.displayName.replaceUnderscoresBySpaces=true