From 44e16f4aa6b245ecee7e47f621a1f445fcffd1ca Mon Sep 17 00:00:00 2001 From: Paul Williams Date: Mon, 11 Nov 2024 14:07:17 +0530 Subject: [PATCH] feat: rest-adapter --- {{cookiecutter.app_name}}/pom.xml | 10 +- .../rest-adapter/pom.xml | 102 ++++++++++++++++ ....domain_capitalized}}ExceptionHandler.java | 30 +++++ ...iecutter.domain_capitalized}}Resource.java | 27 +++++ .../src/main/resources/open-api.yaml | 76 ++++++++++++ ...tter.domain_capitalized}}ResourceTest.java | 111 ++++++++++++++++++ ...n_capitalized}}RestAdapterApplication.java | 18 +++ 7 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 {{cookiecutter.app_name}}/rest-adapter/pom.xml create mode 100644 {{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/exception/{{cookiecutter.domain_capitalized}}ExceptionHandler.java create mode 100644 {{cookiecutter.app_name}}/rest-adapter/src/main/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}Resource.java create mode 100644 {{cookiecutter.app_name}}/rest-adapter/src/main/resources/open-api.yaml create mode 100644 {{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}ResourceTest.java create mode 100644 {{cookiecutter.app_name}}/rest-adapter/src/test/java/{{cookiecutter.package_name}}/rest/{{cookiecutter.domain_capitalized}}RestAdapterApplication.java 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); + } +}