Skip to content

Commit 42ac911

Browse files
committed
docs: Add end-to-end example using YAML spec, Gson default, one interceptor + decorator
1 parent 98a78d3 commit 42ac911

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,70 @@ public class GetPromotionHandler implements RequestHandler {
228228
}
229229
```
230230

231+
### End-to-end example
232+
233+
Gson on the classpath for request/response JSON, SnakeYAML on the classpath for the spec, one interceptor binding a request-scoped tenant + correlation id, one decorator stamping the correlation id on every response, one handler. No extra wiring.
234+
235+
``` java
236+
package com.example.promotions;
237+
238+
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
239+
import static java.net.HttpURLConnection.HTTP_OK;
240+
241+
import com.retailsvc.http.OpenApiServer;
242+
import com.retailsvc.http.Request;
243+
import com.retailsvc.http.RequestHandler;
244+
import com.retailsvc.http.Response;
245+
import com.retailsvc.http.spec.Spec;
246+
import java.nio.file.Path;
247+
import java.util.Map;
248+
import java.util.Optional;
249+
import java.util.UUID;
250+
251+
public final class App {
252+
253+
static final ScopedValue<String> TENANT = ScopedValue.newInstance();
254+
static final ScopedValue<String> CORRELATION_ID = ScopedValue.newInstance();
255+
256+
public static void main(String[] args) throws Exception {
257+
Spec spec = Spec.fromPath(Path.of("openapi.yaml")); // SnakeYAML parses the spec
258+
259+
RequestHandler getPromotion = req -> {
260+
String id = req.pathParams().get("id");
261+
return PromotionService.find(TENANT.get(), id) // uses bound tenant
262+
.<Response>map(p -> Response.of(HTTP_OK, p)) // 200 + JSON via Gson
263+
.orElseGet(() -> Response.status(HTTP_NOT_FOUND)); // 404, no body
264+
};
265+
266+
OpenApiServer.builder()
267+
.spec(spec)
268+
.handlers(Map.of("get-promotion", getPromotion))
269+
// Bind tenant + correlation id once per request.
270+
.interceptor((req, next) -> {
271+
String tenant = req.header("X-Tenant-Id");
272+
String correlationId =
273+
Optional.ofNullable(req.header("X-Correlation-Id"))
274+
.orElseGet(() -> UUID.randomUUID().toString());
275+
return ScopedValue.where(TENANT, tenant)
276+
.where(CORRELATION_ID, correlationId)
277+
.call(next::proceed);
278+
})
279+
// Stamp the correlation id on every response.
280+
.responseDecorator((req, resp) -> resp.withHeader("X-Correlation-Id", CORRELATION_ID.get()))
281+
.port(8080)
282+
.build();
283+
}
284+
}
285+
```
286+
287+
What the example demonstrates:
288+
289+
- **Gson is the default JSON serializer.** No explicit `bodyMapper(...)` call — the library auto-registers `GsonJsonMapper` for request and response JSON because Gson is on the classpath.
290+
- **SnakeYAML parses the spec.** `Spec.fromPath(...)` picks the parser by file extension; `.yaml` here means SnakeYAML, and Gson would handle `.json` the same way.
291+
- **One interceptor sets cross-cutting context.** `ScopedValue.where(...).call(next::proceed)` runs the handler (and any inner interceptors and decorators) inside the binding, so `TENANT.get()` and `CORRELATION_ID.get()` work anywhere they're called.
292+
- **One decorator stamps a response header.** `Response.withHeader(...)` is non-destructive — the handler's `Response` is replaced with one that has the extra header.
293+
- **Handler is a pure function.** Reads from `Request`, returns a `Response` value. No `HttpExchange`, no try/catch IOException, no builder.
294+
231295
### Request body content types
232296

233297
The server reads `requestBody.content` from the spec and selects a mapper by the request's media type (the bare `type/subtype` from `Content-Type`, e.g. `application/json`; lookup is case-insensitive):

0 commit comments

Comments
 (0)