Skip to content

Commit 6414e06

Browse files
committed
docs: modernize all docs for new unified { data, meta } model and nested generics architecture
### Why The previous documentation described only `ServiceResponse<T>` without the new meta layer and nested generic support. ### What Changed - Updated root README to introduce the `{ data, meta }` response structure - Added details about full nested generic handling (`ServiceResponse<Page<T>>`) - Revised adoption guides (server & client) to match new architecture - Clarified OpenAPI extensions (`x-api-wrapper`, `x-data-container`, `x-data-item`) - Improved Quick Start and examples to reflect current generator output ### Result All docs now reflect the latest end-to-end generics-aware design and align with the production-ready OpenAPI Generator 7.16.0 setup.
1 parent 7653b70 commit 6414e06

File tree

15 files changed

+1211
-1399
lines changed

15 files changed

+1211
-1399
lines changed

README.md

Lines changed: 99 additions & 247 deletions
Large diffs are not rendered by default.

customer-service-client/README.md

Lines changed: 170 additions & 470 deletions
Large diffs are not rendered by default.

customer-service-client/src/main/java/io/github/bsayli/openapi/client/adapter/config/CustomerApiClientConfig.java

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,44 +24,44 @@ public class CustomerApiClientConfig {
2424
@Bean
2525
RestClientCustomizer problemDetailStatusHandler(ObjectMapper om) {
2626
return builder ->
27-
builder.defaultStatusHandler(
28-
HttpStatusCode::isError,
29-
(request, response) -> {
30-
ProblemDetail pd = null;
31-
try (var is = response.getBody()) {
32-
pd = om.readValue(is, ProblemDetail.class);
33-
} catch (Exception ignore) {
34-
}
35-
throw new ClientProblemException(pd, response.getStatusCode().value());
36-
});
27+
builder.defaultStatusHandler(
28+
HttpStatusCode::isError,
29+
(request, response) -> {
30+
ProblemDetail pd = null;
31+
try (var is = response.getBody()) {
32+
pd = om.readValue(is, ProblemDetail.class);
33+
} catch (Exception ignore) {
34+
}
35+
throw new ClientProblemException(pd, response.getStatusCode().value());
36+
});
3737
}
3838

3939
@Bean(destroyMethod = "close")
4040
CloseableHttpClient customerHttpClient(
41-
@Value("${customer.api.max-connections-total:64}") int maxTotal,
42-
@Value("${customer.api.max-connections-per-route:16}") int maxPerRoute) {
41+
@Value("${customer.api.max-connections-total:64}") int maxTotal,
42+
@Value("${customer.api.max-connections-per-route:16}") int maxPerRoute) {
4343

4444
var cm =
45-
PoolingHttpClientConnectionManagerBuilder.create()
46-
.setMaxConnTotal(maxTotal)
47-
.setMaxConnPerRoute(maxPerRoute)
48-
.build();
45+
PoolingHttpClientConnectionManagerBuilder.create()
46+
.setMaxConnTotal(maxTotal)
47+
.setMaxConnPerRoute(maxPerRoute)
48+
.build();
4949

5050
return HttpClients.custom()
51-
.setConnectionManager(cm)
52-
.evictExpiredConnections()
53-
.evictIdleConnections(org.apache.hc.core5.util.TimeValue.ofSeconds(30))
54-
.setUserAgent("customer-service-client")
55-
.disableAutomaticRetries()
56-
.build();
51+
.setConnectionManager(cm)
52+
.evictExpiredConnections()
53+
.evictIdleConnections(org.apache.hc.core5.util.TimeValue.ofSeconds(30))
54+
.setUserAgent("customer-service-client")
55+
.disableAutomaticRetries()
56+
.build();
5757
}
5858

5959
@Bean
6060
HttpComponentsClientHttpRequestFactory customerRequestFactory(
61-
CloseableHttpClient customerHttpClient,
62-
@Value("${customer.api.connect-timeout-seconds:10}") long connect,
63-
@Value("${customer.api.connection-request-timeout-seconds:10}") long connReq,
64-
@Value("${customer.api.read-timeout-seconds:15}") long read) {
61+
CloseableHttpClient customerHttpClient,
62+
@Value("${customer.api.connect-timeout-seconds:10}") long connect,
63+
@Value("${customer.api.connection-request-timeout-seconds:10}") long connReq,
64+
@Value("${customer.api.read-timeout-seconds:15}") long read) {
6565

6666
var f = new HttpComponentsClientHttpRequestFactory(customerHttpClient);
6767
f.setConnectTimeout(Duration.ofSeconds(connect));
@@ -72,10 +72,9 @@ HttpComponentsClientHttpRequestFactory customerRequestFactory(
7272

7373
@Bean
7474
RestClient customerRestClient(
75-
RestClient.Builder builder,
76-
HttpComponentsClientHttpRequestFactory customerRequestFactory,
77-
List<RestClientCustomizer> customizers
78-
) {
75+
RestClient.Builder builder,
76+
HttpComponentsClientHttpRequestFactory customerRequestFactory,
77+
List<RestClientCustomizer> customizers) {
7978
builder.requestFactory(customerRequestFactory);
8079
if (customizers != null) {
8180
customizers.forEach(c -> c.customize(builder));
@@ -85,12 +84,12 @@ RestClient customerRestClient(
8584

8685
@Bean
8786
ApiClient customerApiClient(
88-
RestClient customerRestClient, @Value("${customer.api.base-url}") String baseUrl) {
87+
RestClient customerRestClient, @Value("${customer.api.base-url}") String baseUrl) {
8988
return new ApiClient(customerRestClient).setBasePath(baseUrl);
9089
}
9190

9291
@Bean
9392
CustomerControllerApi customerControllerApi(ApiClient customerApiClient) {
9493
return new CustomerControllerApi(customerApiClient);
9594
}
96-
}
95+
}

customer-service-client/src/main/java/io/github/bsayli/openapi/client/common/ClientMeta.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import java.time.Instant;
55
import java.util.List;
66

7-
public record ClientMeta(String requestId, Instant serverTime, List<ClientSort> sort) {
7+
public record ClientMeta(Instant serverTime, List<ClientSort> sort) {
88
public ClientMeta {
99
sort = (sort == null) ? List.of() : List.copyOf(sort);
1010
}

customer-service-client/src/main/resources/customer-api-docs.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,6 @@ components:
296296
Meta:
297297
type: object
298298
properties:
299-
requestId:
300-
type: string
301299
serverTime:
302300
type: string
303301
format: date-time

customer-service-client/src/test/java/io/github/bsayli/openapi/client/adapter/CustomerClientIT.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ void createCustomer_shouldReturn201_andMapBody() {
4646
"""
4747
{
4848
"data": { "customerId": 1, "name": "Jane Doe", "email": "jane@example.com" },
49-
"meta": { "requestId": "req-1", "serverTime": "2025-01-01T12:34:56Z", "sort": [] }
49+
"meta": { "serverTime": "2025-01-01T12:34:56Z", "sort": [] }
5050
}
5151
""";
5252

@@ -66,7 +66,6 @@ void createCustomer_shouldReturn201_andMapBody() {
6666
assertEquals("jane@example.com", resp.getData().getEmail());
6767

6868
assertNotNull(resp.getMeta());
69-
assertEquals("req-1", resp.getMeta().requestId());
7069
assertNotNull(resp.getMeta().serverTime());
7170
}
7271

@@ -95,7 +94,6 @@ void getCustomer_shouldReturn200_andMapBody() {
9594
assertEquals("Jane Doe", resp.getData().getName());
9695

9796
assertNotNull(resp.getMeta());
98-
assertEquals("req-2", resp.getMeta().requestId());
9997
assertNotNull(resp.getMeta().serverTime());
10098
}
10199

@@ -117,7 +115,7 @@ void getCustomers_shouldReturn200_andMapPage() {
117115
"hasNext": false,
118116
"hasPrev": false
119117
},
120-
"meta": { "requestId": "req-3", "serverTime": "2025-01-03T10:00:00Z", "sort": [] }
118+
"meta": { "serverTime": "2025-01-03T10:00:00Z", "sort": [] }
121119
}
122120
""";
123121

@@ -145,7 +143,6 @@ void getCustomers_shouldReturn200_andMapPage() {
145143
assertEquals(1, page.content().getFirst().getCustomerId());
146144

147145
assertNotNull(resp.getMeta());
148-
assertEquals("req-3", resp.getMeta().requestId());
149146
assertNotNull(resp.getMeta().serverTime());
150147
}
151148

@@ -156,7 +153,7 @@ void updateCustomer_shouldReturn200_andMapBody() {
156153
"""
157154
{
158155
"data": { "customerId": 1, "name": "Jane Updated", "email": "jane.updated@example.com" },
159-
"meta": { "requestId": "req-4", "serverTime": "2025-01-04T12:00:00Z", "sort": [] }
156+
"meta": { "serverTime": "2025-01-04T12:00:00Z", "sort": [] }
160157
}
161158
""";
162159

@@ -176,7 +173,6 @@ void updateCustomer_shouldReturn200_andMapBody() {
176173
assertEquals("jane.updated@example.com", resp.getData().getEmail());
177174

178175
assertNotNull(resp.getMeta());
179-
assertEquals("req-4", resp.getMeta().requestId());
180176
assertNotNull(resp.getMeta().serverTime());
181177
}
182178

@@ -187,7 +183,7 @@ void deleteCustomer_shouldReturn200_andMapBody() {
187183
"""
188184
{
189185
"data": { "customerId": 1 },
190-
"meta": { "requestId": "req-5", "serverTime": "2025-01-05T08:00:00Z", "sort": [] }
186+
"meta": { "serverTime": "2025-01-05T08:00:00Z", "sort": [] }
191187
}
192188
""";
193189

@@ -204,7 +200,6 @@ void deleteCustomer_shouldReturn200_andMapBody() {
204200
assertEquals(1, resp.getData().getCustomerId());
205201

206202
assertNotNull(resp.getMeta());
207-
assertEquals("req-5", resp.getMeta().requestId());
208203
assertNotNull(resp.getMeta().serverTime());
209204
}
210205

customer-service-client/src/test/java/io/github/bsayli/openapi/client/adapter/config/CustomerApiClientConfigStatusHandlerTest.java

Lines changed: 72 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,23 @@
1919
@DisplayName("Unit: CustomerApiClientConfig.problemDetailStatusHandler")
2020
class CustomerApiClientConfigStatusHandlerTest {
2121

22-
@Test
23-
@DisplayName("400 with application/problem+json -> throws ClientProblemException with parsed ProblemDetail")
24-
void handler_parses_problem_detail_on_4xx() {
25-
// Arrange
26-
var om = new ObjectMapper();
27-
RestClient.Builder builder = RestClient.builder().baseUrl("http://localhost");
28-
29-
// apply the status handler customizer
30-
RestClientCustomizer customizer = new CustomerApiClientConfig().problemDetailStatusHandler(om);
31-
customizer.customize(builder);
32-
33-
// bind mock server to this builder
34-
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
35-
36-
String body = """
22+
@Test
23+
@DisplayName(
24+
"400 with application/problem+json -> throws ClientProblemException with parsed ProblemDetail")
25+
void handler_parses_problem_detail_on_4xx() {
26+
// Arrange
27+
var om = new ObjectMapper();
28+
RestClient.Builder builder = RestClient.builder().baseUrl("http://localhost");
29+
30+
// apply the status handler customizer
31+
RestClientCustomizer customizer = new CustomerApiClientConfig().problemDetailStatusHandler(om);
32+
customizer.customize(builder);
33+
34+
// bind mock server to this builder
35+
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
36+
37+
String body =
38+
"""
3739
{
3840
"type":"https://example.org/problem/bad-request",
3941
"title":"Bad Request",
@@ -45,53 +47,58 @@ void handler_parses_problem_detail_on_4xx() {
4547
}
4648
""";
4749

48-
server.expect(once(), requestTo("http://localhost/err400"))
49-
.andRespond(withStatus(HttpStatus.BAD_REQUEST)
50-
.contentType(MediaType.valueOf("application/problem+json"))
51-
.body(body));
52-
53-
RestClient client = builder.build();
54-
55-
// Act + Assert
56-
ClientProblemException ex =
57-
assertThrows(ClientProblemException.class,
58-
() -> client.get().uri("/err400").retrieve().body(String.class));
59-
60-
assertEquals(400, ex.getStatus());
61-
ProblemDetail pd = ex.getProblem();
62-
assertNotNull(pd);
63-
assertEquals("Bad Request", pd.getTitle());
64-
assertEquals("Validation failed", pd.getDetail());
65-
assertEquals("VAL_001", pd.getErrorCode());
66-
67-
server.verify();
68-
}
69-
70-
@Test
71-
@DisplayName("500 with empty body -> throws ClientProblemException with null ProblemDetail")
72-
void handler_handles_empty_body_on_5xx() {
73-
// Arrange
74-
var om = new ObjectMapper();
75-
RestClient.Builder builder = RestClient.builder().baseUrl("http://localhost");
76-
77-
RestClientCustomizer customizer = new CustomerApiClientConfig().problemDetailStatusHandler(om);
78-
customizer.customize(builder);
79-
80-
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
81-
82-
server.expect(once(), requestTo("http://localhost/err500"))
83-
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); // no body, no content-type
84-
85-
RestClient client = builder.build();
86-
87-
// Act + Assert
88-
ClientProblemException ex =
89-
assertThrows(ClientProblemException.class,
90-
() -> client.get().uri("/err500").retrieve().body(String.class));
91-
92-
assertEquals(500, ex.getStatus());
93-
assertNull(ex.getProblem()); // no problem body parsed
94-
95-
server.verify();
96-
}
97-
}
50+
server
51+
.expect(once(), requestTo("http://localhost/err400"))
52+
.andRespond(
53+
withStatus(HttpStatus.BAD_REQUEST)
54+
.contentType(MediaType.valueOf("application/problem+json"))
55+
.body(body));
56+
57+
RestClient client = builder.build();
58+
59+
// Act + Assert
60+
ClientProblemException ex =
61+
assertThrows(
62+
ClientProblemException.class,
63+
() -> client.get().uri("/err400").retrieve().body(String.class));
64+
65+
assertEquals(400, ex.getStatus());
66+
ProblemDetail pd = ex.getProblem();
67+
assertNotNull(pd);
68+
assertEquals("Bad Request", pd.getTitle());
69+
assertEquals("Validation failed", pd.getDetail());
70+
assertEquals("VAL_001", pd.getErrorCode());
71+
72+
server.verify();
73+
}
74+
75+
@Test
76+
@DisplayName("500 with empty body -> throws ClientProblemException with null ProblemDetail")
77+
void handler_handles_empty_body_on_5xx() {
78+
// Arrange
79+
var om = new ObjectMapper();
80+
RestClient.Builder builder = RestClient.builder().baseUrl("http://localhost");
81+
82+
RestClientCustomizer customizer = new CustomerApiClientConfig().problemDetailStatusHandler(om);
83+
customizer.customize(builder);
84+
85+
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
86+
87+
server
88+
.expect(once(), requestTo("http://localhost/err500"))
89+
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); // no body, no content-type
90+
91+
RestClient client = builder.build();
92+
93+
// Act + Assert
94+
ClientProblemException ex =
95+
assertThrows(
96+
ClientProblemException.class,
97+
() -> client.get().uri("/err500").retrieve().body(String.class));
98+
99+
assertEquals(500, ex.getStatus());
100+
assertNull(ex.getProblem()); // no problem body parsed
101+
102+
server.verify();
103+
}
104+
}

0 commit comments

Comments
 (0)