diff --git a/{{cookiecutter.app_name}}/pom.xml b/{{cookiecutter.app_name}}/pom.xml
index bf39b5e..7024ef4 100644
--- a/{{cookiecutter.app_name}}/pom.xml
+++ b/{{cookiecutter.app_name}}/pom.xml
@@ -37,7 +37,8 @@
domain-api
domain
jpa-adapter
-
@@ -55,10 +56,15 @@
${project.version}
- packagename
+ {{cookiecutter.group_id}}
jpa-adapter
${project.version}
+
+ {{cookiecutter.group_id}}
+ rest-adapter
+ ${project.version}
+
net.lbruun.springboot
diff --git a/{{cookiecutter.app_name}}/rest-adapter/pom.xml b/{{cookiecutter.app_name}}/rest-adapter/pom.xml
new file mode 100644
index 0000000..02bd4b8
--- /dev/null
+++ b/{{cookiecutter.app_name}}/rest-adapter/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+ {{cookiecutter.group_id}}
+ {{cookiecutter.artifact_id}}-parent
+ 1.0-SNAPSHOT
+
+ 4.0.0
+ rest-adapter
+
+
+
+ {{cookiecutter.group_id}}
+ domain-api
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.yaml
+ snakeyaml
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc-openapi-starter.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.openapitools
+ openapi-generator-maven-plugin
+ 7.9.0
+
+
+ generate-open-api-source
+
+ generate
+
+
+ ${project.basedir}/src/main/resources/open-api.yaml
+ spring
+ spring-boot
+
+ {{cookiecutter.domain_capitalized}}={{cookiecutter.package_name}}.domain.model.{{cookiecutter.domain_capitalized}}
+
+
+ OffsetDateTime=java.time.LocalDateTime
+
+ {{cookiecutter.package_name}}.rest.generated.api
+ {{cookiecutter.package_name}}.rest.generated.model
+
+ false
+ false
+ true
+ true
+ java8
+ true
+ false
+ @lombok.AllArgsConstructor @lombok.Builder @lombok.Data @lombok.NoArgsConstructor
+
+
+
+
+
+
+
+ com.societegenerale.commons
+ arch-unit-maven-plugin
+
+
+
+ com.societegenerale.commons.plugin.rules.NoJunitAssertRuleTest
+ com.societegenerale.commons.plugin.rules.NoPowerMockRuleTest
+ com.societegenerale.commons.plugin.rules.NoTestIgnoreRuleTest
+ com.societegenerale.commons.plugin.rules.NoTestIgnoreWithoutCommentRuleTest
+
+ com.societegenerale.commons.plugin.rules.NoStandardStreamRuleTest
+ com.societegenerale.commons.plugin.rules.NoJodaTimeRuleTest
+ com.societegenerale.commons.plugin.rules.NoJavaUtilDateRuleTest
+ com.societegenerale.commons.plugin.rules.NoPrefixForInterfacesRuleTest
+ com.societegenerale.commons.plugin.rules.NoPublicFieldRuleTest
+ com.societegenerale.commons.plugin.rules.NoInjectedFieldTest
+ com.societegenerale.commons.plugin.rules.NoAutowiredFieldTest
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/exception/{{cookiecutter.domain_capitalized}}ExceptionHandler.java b/{{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/exception/{{cookiecutter.domain_capitalized}}ExceptionHandler.java
new file mode 100644
index 0000000..4f7cdb2
--- /dev/null
+++ b/{{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/exception/{{cookiecutter.domain_capitalized}}ExceptionHandler.java
@@ -0,0 +1,30 @@
+package {{cookiecutter.package_name}}.rest.exception;
+
+import java.time.LocalDateTime;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.context.request.WebRequest;
+import {{cookiecutter.package_name}}.domain.exception.{{cookiecutter.domain_capitalized}}NotFoundException;
+import {{cookiecutter.package_name}}.rest.generated.model.ProblemDetail;
+
+@RestControllerAdvice(basePackages = {"{{cookiecutter.package_name}}"})
+public class {{cookiecutter.domain_capitalized}}ExceptionHandler {
+
+ @ExceptionHandler(value = {{cookiecutter.domain_capitalized}}NotFoundException.class)
+ public final ResponseEntity handle{{cookiecutter.domain_capitalized}}NotFoundException(
+ final Exception exception, final WebRequest request) {
+ var problem =
+ ProblemDetail.builder()
+ .type("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404")
+ .status(HttpStatus.NOT_FOUND.value())
+ .title("{{cookiecutter.domain_capitalized}} not found")
+ .detail(exception.getMessage())
+ .instance(((ServletWebRequest) request).getRequest().getRequestURI())
+ .timestamp(LocalDateTime.now())
+ .build();
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problem);
+ }
+}
diff --git a/{{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}Resource.java b/{{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}Resource.java
new file mode 100644
index 0000000..6bdd2f8
--- /dev/null
+++ b/{{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}Resource.java
@@ -0,0 +1,27 @@
+package {{cookiecutter.package_name}}.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+import {{cookiecutter.package_name}}.domain.model.{{cookiecutter.domain_capitalized}};
+import {{cookiecutter.package_name}}.domain.port.Request{{cookiecutter.domain_capitalized}};
+import {{cookiecutter.package_name}}.rest.generated.api.{{cookiecutter.domain_capitalized}}Api;
+import {{cookiecutter.package_name}}.rest.generated.model.{{cookiecutter.domain_capitalized}}Info;
+
+@RestController
+public class {{cookiecutter.domain_capitalized}}Resource implements {{cookiecutter.domain_capitalized}}Api {
+
+ private final Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}};
+
+ public {{cookiecutter.domain_capitalized}}Resource(Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}}) {
+ this.request{{cookiecutter.domain_capitalized}} = request{{cookiecutter.domain_capitalized}};
+ }
+
+ public ResponseEntity<{{cookiecutter.domain_capitalized}}Info> get{{cookiecutter.domain_plural_capitalized}}() {
+ return ResponseEntity.ok({{cookiecutter.domain_capitalized}}Info.builder().{{cookiecutter.domain_plural}}(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_plural_capitalized}}()).build());
+ }
+
+ public ResponseEntity<{{cookiecutter.domain_capitalized}}> get{{cookiecutter.domain_capitalized}}ByCode(@PathVariable("code") Long code) {
+ return ResponseEntity.ok(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_capitalized}}ByCode(code));
+ }
+}
diff --git a/{{cookiecutter.app_name}}/rest-adapter/src/main/resources/open-api.yaml b/{{cookiecutter.app_name}}/rest-adapter/src/main/resources/open-api.yaml
new file mode 100644
index 0000000..1b8c987
--- /dev/null
+++ b/{{cookiecutter.app_name}}/rest-adapter/src/main/resources/open-api.yaml
@@ -0,0 +1,76 @@
+---
+openapi: 3.0.1
+info:
+ title: {{cookiecutter.domain_capitalized}} API Documentation
+ version: v1
+tags:
+ - name: {{cookiecutter.domain_capitalized}}
+ description: Resource to manage {{cookiecutter.domain}}
+paths:
+ "/api/v1/{{cookiecutter.domain_plural}}":
+ get:
+ tags:
+ - {{cookiecutter.domain_capitalized}}
+ summary: Get all {{cookiecutter.domain_plural}}
+ operationId: get{{cookiecutter.domain_plural_capitalized}}
+ responses:
+ '200':
+ description: OK
+ content:
+ "*/*":
+ schema:
+ "$ref": "#/components/schemas/{{cookiecutter.domain_capitalized}}Info"
+ "/api/v1/{{cookiecutter.domain_plural}}/{code}":
+ get:
+ tags:
+ - {{cookiecutter.domain_capitalized}}
+ summary: Get {{cookiecutter.domain}} by code
+ operationId: get{{cookiecutter.domain_capitalized}}ByCode
+ parameters:
+ - name: code
+ in: path
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: OK
+ content:
+ "*/*":
+ schema:
+ "$ref": "#/components/schemas/{{cookiecutter.domain_capitalized}}"
+components:
+ schemas:
+ {{cookiecutter.domain_capitalized}}:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int64
+ description:
+ type: string
+ {{cookiecutter.domain_capitalized}}Info:
+ type: object
+ properties:
+ {{cookiecutter.domain_plural}}:
+ type: array
+ items:
+ "$ref": "#/components/schemas/{{cookiecutter.domain_capitalized}}"
+ ProblemDetail:
+ type: object
+ properties:
+ type:
+ type: string
+ title:
+ type: string
+ status:
+ type: integer
+ format: int32
+ detail:
+ type: string
+ instance:
+ type: string
+ timestamp:
+ type: string
+ format: date-time
diff --git a/{{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}ResourceTest.java b/{{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}ResourceTest.java
new file mode 100644
index 0000000..65d7a06
--- /dev/null
+++ b/{{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}ResourceTest.java
@@ -0,0 +1,111 @@
+package {{cookiecutter.package_name}}.rest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.http.HttpStatus;
+import {{cookiecutter.package_name}}.domain.exception.{{cookiecutter.domain_capitalized}}NotFoundException;
+import {{cookiecutter.package_name}}.domain.model.{{cookiecutter.domain_capitalized}};
+import {{cookiecutter.package_name}}.domain.port.Request{{cookiecutter.domain_capitalized}};
+import {{cookiecutter.package_name}}.rest.generated.model.{{cookiecutter.domain_capitalized}}Info;
+import {{cookiecutter.package_name}}.rest.generated.model.ProblemDetail;
+
+@ExtendWith(MockitoExtension.class)
+@SpringBootTest(classes = {{cookiecutter.domain_capitalized}}RestAdapterApplication.class, webEnvironment = RANDOM_PORT)
+@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
+public class {{cookiecutter.domain_capitalized}}ResourceTest {
+
+ private static final String LOCALHOST = "http://localhost:";
+ private static final String API_URI = "/api/v1/{{cookiecutter.domain_plural}}";
+ @LocalServerPort private int port;
+ @Autowired private TestRestTemplate restTemplate;
+ @Autowired private Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}};
+
+ @Test
+ @DisplayName("should start the rest adapter application")
+ public void startup() {
+ assertThat(Boolean.TRUE).isTrue();
+ }
+
+ @Test
+ @DisplayName("should give {{cookiecutter.domain_plural}} when asked for {{cookiecutter.domain_plural}} with the support of domain stub")
+ public void obtain{{cookiecutter.domain_plural_capitalized}}FromDomainStub() {
+ // Given
+ var {{cookiecutter.domain}} = {{cookiecutter.domain_capitalized}}.builder().code(1L).description("Johnny Johnny Yes Papa !!").build();
+ Mockito.lenient().when(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_plural_capitalized}}()).thenReturn(List.of({{cookiecutter.domain}}));
+ // When
+ var url = LOCALHOST + port + API_URI;
+ var responseEntity = restTemplate.getForEntity(url, {{cookiecutter.domain_capitalized}}Info.class);
+ // Then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(responseEntity.getBody()).isNotNull();
+ assertThat(responseEntity.getBody().get{{cookiecutter.domain_plural_capitalized}}())
+ .isNotEmpty()
+ .extracting("description")
+ .contains("Johnny Johnny Yes Papa !!");
+ }
+
+ @Test
+ @DisplayName(
+ "should give the {{cookiecutter.domain}} when asked for an {{cookiecutter.domain}} by code with the support of domain stub")
+ public void obtain{{cookiecutter.domain_capitalized}}ByCodeFromDomainStub() {
+ // Given
+ var code = 1L;
+ var description = "Johnny Johnny Yes Papa !!";
+ var {{cookiecutter.domain}} = {{cookiecutter.domain_capitalized}}.builder().code(code).description(description).build();
+ Mockito.lenient().when(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_capitalized}}ByCode(code)).thenReturn({{cookiecutter.domain}});
+ // When
+ var url = LOCALHOST + port + API_URI + "/" + code;
+ var responseEntity = restTemplate.getForEntity(url, {{cookiecutter.domain_capitalized}}.class);
+ // Then
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(responseEntity.getBody()).isNotNull();
+ assertThat(responseEntity.getBody()).isEqualTo({{cookiecutter.domain}});
+ }
+
+ @Test
+ @DisplayName(
+ "should give exception when asked for an {{cookiecutter.domain}} by code that does not exists with the support of domain stub")
+ public void shouldGiveExceptionWhenAskedForAn{{cookiecutter.domain_capitalized}}ByCodeFromDomainStub() {
+ // Given
+ var code = -1000L;
+ Mockito.lenient()
+ .when(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_capitalized}}ByCode(code))
+ .thenThrow(new {{cookiecutter.domain_capitalized}}NotFoundException(code));
+ // When
+ var url = LOCALHOST + port + API_URI + "/" + code;
+ var responseEntity = restTemplate.getForEntity(url, ProblemDetail.class);
+ // Then
+ var expectedProblemDetail =
+ ProblemDetail.builder()
+ .type("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404")
+ .status(HttpStatus.NOT_FOUND.value())
+ .detail("{{cookiecutter.domain_capitalized}} with code -1000 does not exist")
+ .instance("/api/v1/{{cookiecutter.domain_plural}}/-1000")
+ .title("{{cookiecutter.domain_capitalized}} not found")
+ .build();
+ assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(responseEntity.getBody()).isNotNull();
+ assertThat(responseEntity.getBody())
+ .usingRecursiveComparison()
+ .ignoringFields("timestamp")
+ .isEqualTo(expectedProblemDetail);
+ assertThat(responseEntity.getBody().getTimestamp())
+ .isCloseTo(LocalDateTime.now(), within(100L, ChronoUnit.SECONDS));
+ }
+}
diff --git a/{{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}RestAdapterApplication.java b/{{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}RestAdapterApplication.java
new file mode 100644
index 0000000..6528c9b
--- /dev/null
+++ b/{{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}RestAdapterApplication.java
@@ -0,0 +1,18 @@
+package {{cookiecutter.package_name}}.rest;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.ComponentScan;
+import {{cookiecutter.package_name}}.domain.port.Request{{cookiecutter.domain_capitalized}};
+
+@SpringBootApplication
+@ComponentScan(basePackages = "{{cookiecutter.package_name}}")
+public class {{cookiecutter.domain_capitalized}}RestAdapterApplication {
+
+ @MockBean private Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}};
+
+ public static void main(String[] args) {
+ SpringApplication.run({{cookiecutter.domain_capitalized}}RestAdapterApplication.class, args);
+ }
+}