Skip to content

Commit bededc7

Browse files
committed
feat: rest-adapter
1 parent 699746e commit bededc7

File tree

7 files changed

+372
-2
lines changed

7 files changed

+372
-2
lines changed

{{cookiecutter.app_name}}/pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
<module>domain-api</module>
3838
<module>domain</module>
3939
<module>jpa-adapter</module>
40-
<!--<module>rest-adapter</module>
40+
<module>rest-adapter</module>
41+
<!--
4142
<module>bootstrap</module>
4243
<module>acceptance-test</module>-->
4344
</modules>
@@ -55,10 +56,15 @@
5556
<version>${project.version}</version>
5657
</dependency>
5758
<dependency>
58-
<groupId>packagename</groupId>
59+
<groupId>{{cookiecutter.group_id}}</groupId>
5960
<artifactId>jpa-adapter</artifactId>
6061
<version>${project.version}</version>
6162
</dependency>
63+
<dependency>
64+
<groupId>{{cookiecutter.group_id}}</groupId>
65+
<artifactId>rest-adapter</artifactId>
66+
<version>${project.version}</version>
67+
</dependency>
6268
<!-- Frameworks & Libraries -->
6369
<dependency>
6470
<groupId>net.lbruun.springboot</groupId>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<groupId>{{cookiecutter.group_id}}</groupId>
7+
<artifactId>{{cookiecutter.artifact_id}}-parent</artifactId>
8+
<version>1.0-SNAPSHOT</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
<artifactId>rest-adapter</artifactId>
12+
<dependencies>
13+
<!-- Domain -->
14+
<dependency>
15+
<groupId>{{cookiecutter.group_id}}</groupId>
16+
<artifactId>domain-api</artifactId>
17+
</dependency>
18+
<!-- Frameworks & Libraries -->
19+
<dependency>
20+
<groupId>org.springframework.boot</groupId>
21+
<artifactId>spring-boot-starter-web</artifactId>
22+
</dependency>
23+
<dependency>
24+
<groupId>org.yaml</groupId>
25+
<artifactId>snakeyaml</artifactId>
26+
</dependency>
27+
<dependency>
28+
<groupId>org.springdoc</groupId>
29+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
30+
<version>${springdoc-openapi-starter.version}</version>
31+
</dependency>
32+
<!-- Test -->
33+
<dependency>
34+
<groupId>org.springframework.boot</groupId>
35+
<artifactId>spring-boot-starter-test</artifactId>
36+
<scope>test</scope>
37+
</dependency>
38+
</dependencies>
39+
<build>
40+
<plugins>
41+
<plugin>
42+
<groupId>org.openapitools</groupId>
43+
<artifactId>openapi-generator-maven-plugin</artifactId>
44+
<version>7.9.0</version>
45+
<executions>
46+
<execution>
47+
<id>generate-open-api-source</id>
48+
<goals>
49+
<goal>generate</goal>
50+
</goals>
51+
<configuration>
52+
<inputSpec>${project.basedir}/src/main/resources/open-api.yaml</inputSpec>
53+
<generatorName>spring</generatorName>
54+
<library>spring-boot</library>
55+
<schemaMappings>
56+
<schemaMapping>{{cookiecutter.domain_capitalized}}={{cookiecutter.package_name}}.domain.model.{{cookiecutter.domain_capitalized}}</schemaMapping>
57+
</schemaMappings>
58+
<typeMappings>
59+
<typeMapping>OffsetDateTime=java.time.LocalDateTime</typeMapping>
60+
</typeMappings>
61+
<apiPackage>{{cookiecutter.package_name}}.rest.generated.api</apiPackage>
62+
<modelPackage>{{cookiecutter.package_name}}.rest.generated.model</modelPackage>
63+
<configOptions>
64+
<useBeanValidation>false</useBeanValidation>
65+
<performBeanValidation>false</performBeanValidation>
66+
<useSpringBoot3>true</useSpringBoot3>
67+
<interfaceOnly>true</interfaceOnly>
68+
<dateLibrary>java8</dateLibrary>
69+
<useTags>true</useTags>
70+
<openApiNullable>false</openApiNullable>
71+
<additionalModelTypeAnnotations>@lombok.AllArgsConstructor @lombok.Builder @lombok.Data @lombok.NoArgsConstructor
72+
</additionalModelTypeAnnotations>
73+
</configOptions>
74+
</configuration>
75+
</execution>
76+
</executions>
77+
</plugin>
78+
<plugin>
79+
<groupId>com.societegenerale.commons</groupId>
80+
<artifactId>arch-unit-maven-plugin</artifactId>
81+
<configuration>
82+
<rules>
83+
<preConfiguredRules>
84+
<rule>com.societegenerale.commons.plugin.rules.NoJunitAssertRuleTest</rule>
85+
<rule>com.societegenerale.commons.plugin.rules.NoPowerMockRuleTest</rule>
86+
<rule>com.societegenerale.commons.plugin.rules.NoTestIgnoreRuleTest</rule>
87+
<rule>com.societegenerale.commons.plugin.rules.NoTestIgnoreWithoutCommentRuleTest
88+
</rule>
89+
<rule>com.societegenerale.commons.plugin.rules.NoStandardStreamRuleTest</rule>
90+
<rule>com.societegenerale.commons.plugin.rules.NoJodaTimeRuleTest</rule>
91+
<rule>com.societegenerale.commons.plugin.rules.NoJavaUtilDateRuleTest</rule>
92+
<rule>com.societegenerale.commons.plugin.rules.NoPrefixForInterfacesRuleTest</rule>
93+
<rule>com.societegenerale.commons.plugin.rules.NoPublicFieldRuleTest</rule>
94+
<rule>com.societegenerale.commons.plugin.rules.NoInjectedFieldTest</rule>
95+
<rule>com.societegenerale.commons.plugin.rules.NoAutowiredFieldTest</rule>
96+
</preConfiguredRules>
97+
</rules>
98+
</configuration>
99+
</plugin>
100+
</plugins>
101+
</build>
102+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package {{cookiecutter.package_name}}.rest.exception;
2+
3+
import java.time.LocalDateTime;
4+
import org.springframework.http.HttpStatus;
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.ExceptionHandler;
7+
import org.springframework.web.bind.annotation.RestControllerAdvice;
8+
import org.springframework.web.context.request.ServletWebRequest;
9+
import org.springframework.web.context.request.WebRequest;
10+
import {{cookiecutter.package_name}}.domain.exception.{{cookiecutter.domain_capitalized}}NotFoundException;
11+
import {{cookiecutter.package_name}}.rest.generated.model.ProblemDetail;
12+
13+
@RestControllerAdvice(basePackages = {"{{cookiecutter.package_name}}"})
14+
public class {{cookiecutter.domain_capitalized}}ExceptionHandler {
15+
16+
@ExceptionHandler(value = {{cookiecutter.domain_capitalized}}NotFoundException.class)
17+
public final ResponseEntity<ProblemDetail> handle{{cookiecutter.domain_capitalized}}NotFoundException(
18+
final Exception exception, final WebRequest request) {
19+
var problem =
20+
ProblemDetail.builder()
21+
.type("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404")
22+
.status(HttpStatus.NOT_FOUND.value())
23+
.title("{{cookiecutter.domain_capitalized}} not found")
24+
.detail(exception.getMessage())
25+
.instance(((ServletWebRequest) request).getRequest().getRequestURI())
26+
.timestamp(LocalDateTime.now())
27+
.build();
28+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problem);
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package {{cookiecutter.package_name}}.rest;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.web.bind.annotation.PathVariable;
5+
import org.springframework.web.bind.annotation.RestController;
6+
import {{cookiecutter.package_name}}.domain.model.{{cookiecutter.domain_capitalized}};
7+
import {{cookiecutter.package_name}}.domain.port.Request{{cookiecutter.domain_capitalized}};
8+
import {{cookiecutter.package_name}}.rest.generated.api.{{cookiecutter.domain_capitalized}}Api;
9+
import {{cookiecutter.package_name}}.rest.generated.model.{{cookiecutter.domain_capitalized}}Info;
10+
11+
@RestController
12+
public class {{cookiecutter.domain_capitalized}}Resource implements {{cookiecutter.domain_capitalized}}Api {
13+
14+
private final Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}};
15+
16+
public {{cookiecutter.domain_capitalized}}Resource(Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}}) {
17+
this.request{{cookiecutter.domain_capitalized}} = request{{cookiecutter.domain_capitalized}};
18+
}
19+
20+
public ResponseEntity<{{cookiecutter.domain_capitalized}}Info> get{{cookiecutter.domain_plural_capitalized}}() {
21+
return ResponseEntity.ok({{cookiecutter.domain_capitalized}}Info.builder().{{cookiecutter.domain_plural}}(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_plural_capitalized}}()).build());
22+
}
23+
24+
public ResponseEntity<{{cookiecutter.domain_capitalized}}> get{{cookiecutter.domain_capitalized}}ByCode(@PathVariable("code") Long code) {
25+
return ResponseEntity.ok(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_capitalized}}ByCode(code));
26+
}
27+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
openapi: 3.0.1
3+
info:
4+
title: {{cookiecutter.domain_capitalized}} API Documentation
5+
version: v1
6+
tags:
7+
- name: {{cookiecutter.domain_capitalized}}
8+
description: Resource to manage {{cookiecutter.domain}}
9+
paths:
10+
"/api/v1/{{cookiecutter.domain_plural}}":
11+
get:
12+
tags:
13+
- {{cookiecutter.domain_capitalized}}
14+
summary: Get all {{cookiecutter.domain_plural}}
15+
operationId: get{{cookiecutter.domain_plural_capitalized}}
16+
responses:
17+
'200':
18+
description: OK
19+
content:
20+
"*/*":
21+
schema:
22+
"$ref": "#/components/schemas/{{cookiecutter.domain_capitalized}}Info"
23+
"/api/v1/{{cookiecutter.domain_plural}}/{code}":
24+
get:
25+
tags:
26+
- {{cookiecutter.domain_capitalized}}
27+
summary: Get {{cookiecutter.domain}} by code
28+
operationId: get{{cookiecutter.domain_capitalized}}ByCode
29+
parameters:
30+
- name: code
31+
in: path
32+
required: true
33+
schema:
34+
type: integer
35+
format: int64
36+
responses:
37+
'200':
38+
description: OK
39+
content:
40+
"*/*":
41+
schema:
42+
"$ref": "#/components/schemas/{{cookiecutter.domain_capitalized}}"
43+
components:
44+
schemas:
45+
{{cookiecutter.domain_capitalized}}:
46+
type: object
47+
properties:
48+
code:
49+
type: integer
50+
format: int64
51+
description:
52+
type: string
53+
{{cookiecutter.domain_capitalized}}Info:
54+
type: object
55+
properties:
56+
{{cookiecutter.domain_plural}}:
57+
type: array
58+
items:
59+
"$ref": "#/components/schemas/{{cookiecutter.domain_capitalized}}"
60+
ProblemDetail:
61+
type: object
62+
properties:
63+
type:
64+
type: string
65+
title:
66+
type: string
67+
status:
68+
type: integer
69+
format: int32
70+
detail:
71+
type: string
72+
instance:
73+
type: string
74+
timestamp:
75+
type: string
76+
format: date-time
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package {{cookiecutter.package_name}}.rest;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.within;
5+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
6+
7+
import java.time.LocalDateTime;
8+
import java.time.temporal.ChronoUnit;
9+
import java.util.List;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.mockito.Mockito;
14+
import org.mockito.junit.jupiter.MockitoExtension;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
17+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
18+
import org.springframework.boot.test.context.SpringBootTest;
19+
import org.springframework.boot.test.web.client.TestRestTemplate;
20+
import org.springframework.boot.test.web.server.LocalServerPort;
21+
import org.springframework.http.HttpStatus;
22+
import {{cookiecutter.package_name}}.domain.exception.{{cookiecutter.domain_capitalized}}NotFoundException;
23+
import {{cookiecutter.package_name}}.domain.model.{{cookiecutter.domain_capitalized}};
24+
import {{cookiecutter.package_name}}.domain.port.Request{{cookiecutter.domain_capitalized}};
25+
import {{cookiecutter.package_name}}.rest.generated.model.{{cookiecutter.domain_capitalized}}Info;
26+
import {{cookiecutter.package_name}}.rest.generated.model.ProblemDetail;
27+
28+
@ExtendWith(MockitoExtension.class)
29+
@SpringBootTest(classes = {{cookiecutter.domain_capitalized}}RestAdapterApplication.class, webEnvironment = RANDOM_PORT)
30+
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
31+
public class {{cookiecutter.domain_capitalized}}ResourceTest {
32+
33+
private static final String LOCALHOST = "http://localhost:";
34+
private static final String API_URI = "/api/v1/{{cookiecutter.domain_plural}}";
35+
@LocalServerPort private int port;
36+
@Autowired private TestRestTemplate restTemplate;
37+
@Autowired private Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}};
38+
39+
@Test
40+
@DisplayName("should start the rest adapter application")
41+
public void startup() {
42+
assertThat(Boolean.TRUE).isTrue();
43+
}
44+
45+
@Test
46+
@DisplayName("should give {{cookiecutter.domain_plural}} when asked for {{cookiecutter.domain_plural}} with the support of domain stub")
47+
public void obtain{{cookiecutter.domain_plural_capitalized}}FromDomainStub() {
48+
// Given
49+
var {{cookiecutter.domain}} = {{cookiecutter.domain_capitalized}}.builder().code(1L).description("Johnny Johnny Yes Papa !!").build();
50+
Mockito.lenient().when(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_plural_capitalized}}()).thenReturn(List.of({{cookiecutter.domain}}));
51+
// When
52+
var url = LOCALHOST + port + API_URI;
53+
var responseEntity = restTemplate.getForEntity(url, {{cookiecutter.domain_capitalized}}Info.class);
54+
// Then
55+
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
56+
assertThat(responseEntity.getBody()).isNotNull();
57+
assertThat(responseEntity.getBody().get{{cookiecutter.domain_plural_capitalized}}())
58+
.isNotEmpty()
59+
.extracting("description")
60+
.contains("Johnny Johnny Yes Papa !!");
61+
}
62+
63+
@Test
64+
@DisplayName(
65+
"should give the {{cookiecutter.domain}} when asked for an {{cookiecutter.domain}} by code with the support of domain stub")
66+
public void obtain{{cookiecutter.domain_capitalized}}ByCodeFromDomainStub() {
67+
// Given
68+
var code = 1L;
69+
var description = "Johnny Johnny Yes Papa !!";
70+
var {{cookiecutter.domain}} = {{cookiecutter.domain_capitalized}}.builder().code(code).description(description).build();
71+
Mockito.lenient().when(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_capitalized}}ByCode(code)).thenReturn({{cookiecutter.domain}});
72+
// When
73+
var url = LOCALHOST + port + API_URI + "/" + code;
74+
var responseEntity = restTemplate.getForEntity(url, {{cookiecutter.domain_capitalized}}.class);
75+
// Then
76+
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
77+
assertThat(responseEntity.getBody()).isNotNull();
78+
assertThat(responseEntity.getBody()).isEqualTo({{cookiecutter.domain}});
79+
}
80+
81+
@Test
82+
@DisplayName(
83+
"should give exception when asked for an {{cookiecutter.domain}} by code that does not exists with the support of domain stub")
84+
public void shouldGiveExceptionWhenAskedForAn{{cookiecutter.domain_capitalized}}ByCodeFromDomainStub() {
85+
// Given
86+
var code = -1000L;
87+
Mockito.lenient()
88+
.when(request{{cookiecutter.domain_capitalized}}.get{{cookiecutter.domain_capitalized}}ByCode(code))
89+
.thenThrow(new {{cookiecutter.domain_capitalized}}NotFoundException(code));
90+
// When
91+
var url = LOCALHOST + port + API_URI + "/" + code;
92+
var responseEntity = restTemplate.getForEntity(url, ProblemDetail.class);
93+
// Then
94+
var expectedProblemDetail =
95+
ProblemDetail.builder()
96+
.type("https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404")
97+
.status(HttpStatus.NOT_FOUND.value())
98+
.detail("{{cookiecutter.domain_capitalized}} with code -1000 does not exist")
99+
.instance("/api/v1/{{cookiecutter.domain_plural}}/-1000")
100+
.title("{{cookiecutter.domain_capitalized}} not found")
101+
.build();
102+
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
103+
assertThat(responseEntity.getBody()).isNotNull();
104+
assertThat(responseEntity.getBody())
105+
.usingRecursiveComparison()
106+
.ignoringFields("timestamp")
107+
.isEqualTo(expectedProblemDetail);
108+
assertThat(responseEntity.getBody().getTimestamp())
109+
.isCloseTo(LocalDateTime.now(), within(100L, ChronoUnit.SECONDS));
110+
}
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package {{cookiecutter.package_name}}.rest;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.boot.test.mock.mockito.MockBean;
6+
import org.springframework.context.annotation.ComponentScan;
7+
import {{cookiecutter.package_name}}.domain.port.Request{{cookiecutter.domain_capitalized}};
8+
9+
@SpringBootApplication
10+
@ComponentScan(basePackages = "{{cookiecutter.package_name}}")
11+
public class {{cookiecutter.domain_capitalized}}RestAdapterApplication {
12+
13+
@MockBean private Request{{cookiecutter.domain_capitalized}} request{{cookiecutter.domain_capitalized}};
14+
15+
public static void main(String[] args) {
16+
SpringApplication.run({{cookiecutter.domain_capitalized}}RestAdapterApplication.class, args);
17+
}
18+
}

0 commit comments

Comments
 (0)