Skip to content

Commit b9c2915

Browse files
committed
feat!: Switch handlers to RequestHandler receiving Request
1 parent fd23f8a commit b9c2915

19 files changed

Lines changed: 133 additions & 210 deletions

src/main/java/com/retailsvc/http/OpenApiServer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class OpenApiServer implements AutoCloseable {
4343
OpenApiServer(
4444
Spec spec,
4545
Map<String, TypeMapper> bodyMappers,
46-
Map<String, HttpHandler> handlers,
46+
Map<String, RequestHandler> handlers,
4747
ExceptionHandler exceptionHandler,
4848
int port,
4949
Map<String, HttpHandler> extras,
@@ -117,7 +117,7 @@ public static final class Builder {
117117

118118
private Spec spec;
119119
private final LinkedHashMap<String, TypeMapper> bodyMappers = new LinkedHashMap<>();
120-
private Map<String, HttpHandler> handlers;
120+
private Map<String, RequestHandler> handlers;
121121
private ExceptionHandler exceptionHandler;
122122
private int port = DEFAULT_PORT;
123123
private int shutdownTimeoutSeconds = 0;
@@ -137,7 +137,7 @@ public Builder bodyMapper(String mediaType, TypeMapper mapper) {
137137
return this;
138138
}
139139

140-
public Builder handlers(Map<String, HttpHandler> handlers) {
140+
public Builder handlers(Map<String, RequestHandler> handlers) {
141141
this.handlers = handlers;
142142
return this;
143143
}
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
package com.retailsvc.http.internal;
22

33
import com.retailsvc.http.MissingOperationHandlerException;
4+
import com.retailsvc.http.Request;
5+
import com.retailsvc.http.RequestHandler;
46
import com.sun.net.httpserver.HttpExchange;
57
import com.sun.net.httpserver.HttpHandler;
68
import java.io.IOException;
79
import java.util.Map;
810

911
public final class DispatchHandler implements HttpHandler {
10-
private final Map<String, HttpHandler> handlers;
1112

12-
public DispatchHandler(Map<String, HttpHandler> handlers) {
13+
public static final ScopedValue<Request> CURRENT = ScopedValue.newInstance();
14+
15+
private final Map<String, RequestHandler> handlers;
16+
17+
public DispatchHandler(Map<String, RequestHandler> handlers) {
1318
this.handlers = Map.copyOf(handlers);
1419
}
1520

1621
@Override
1722
public void handle(HttpExchange exchange) throws IOException {
18-
String opId = LegacyRequestAccess.operationId();
19-
HttpHandler h = handlers.get(opId);
23+
Request request = CURRENT.get();
24+
RequestHandler h = handlers.get(request.operationId());
2025
if (h == null) {
21-
throw new MissingOperationHandlerException(opId);
26+
throw new MissingOperationHandlerException(request.operationId());
2227
}
23-
h.handle(exchange);
28+
h.handle(request);
2429
}
2530
}

src/main/java/com/retailsvc/http/internal/RequestPreparationFilter.java

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.retailsvc.http.MethodNotAllowedException;
44
import com.retailsvc.http.NotFoundException;
5+
import com.retailsvc.http.Request;
56
import com.retailsvc.http.TypeMapper;
67
import com.retailsvc.http.ValidationException;
78
import com.retailsvc.http.spec.HttpMethod;
@@ -61,34 +62,24 @@ public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
6162
validateParameters(exchange, op, match.pathParameters());
6263
Object parsedBody = validateAndParseBody(exchange, op, body);
6364

64-
RequestContext ctx =
65-
new RequestContext(body, parsedBody, op.operationId(), match.pathParameters());
65+
Request request =
66+
new Request(
67+
exchange, body, parsedBody, op.operationId(), match.pathParameters(), bodyMappers);
6668

67-
runWithRequestContext(ctx, () -> chain.doFilter(exchange));
68-
}
69-
70-
private static void runWithRequestContext(RequestContext ctx, IORunnable work)
71-
throws IOException {
7269
try {
73-
ScopedValue.where(LegacyRequestAccess.CONTEXT, ctx)
70+
ScopedValue.where(DispatchHandler.CURRENT, request)
7471
.call(
7572
() -> {
76-
work.run();
73+
chain.doFilter(exchange);
7774
return null;
7875
});
7976
} catch (IOException | RuntimeException e) {
8077
throw e;
8178
} catch (Exception e) {
82-
// Callable.call() throws Exception; nothing else can actually be thrown by the chain.
8379
throw new IOException(e);
8480
}
8581
}
8682

87-
@FunctionalInterface
88-
private interface IORunnable {
89-
void run() throws IOException;
90-
}
91-
9283
private String stripBasePath(String path) {
9384
String base = spec.basePath();
9485
if (base == null || base.isEmpty() || base.equals("/")) {

src/test/java/com/retailsvc/http/NonJsonBodyIT.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import com.retailsvc.http.start.FormEchoHandler;
77
import com.retailsvc.http.start.TextEchoHandler;
8-
import com.sun.net.httpserver.HttpHandler;
98
import java.net.URI;
109
import java.net.http.HttpRequest;
1110
import java.net.http.HttpResponse.BodyHandlers;
@@ -16,7 +15,7 @@ class NonJsonBodyIT extends ServerBaseTest {
1615

1716
@Test
1817
void formUrlEncodedBodyParsedAndCoerced() throws Exception {
19-
Map<String, HttpHandler> handlers = Map.of("form-echo", new FormEchoHandler());
18+
Map<String, RequestHandler> handlers = Map.of("form-echo", new FormEchoHandler());
2019
try (var s = newServer(handlers);
2120
var client = httpClient()) {
2221
var req = postForm(s, "/form-echo", "name=foo&age=30");
@@ -28,7 +27,7 @@ void formUrlEncodedBodyParsedAndCoerced() throws Exception {
2827

2928
@Test
3029
void formArrayProperty() throws Exception {
31-
Map<String, HttpHandler> handlers = Map.of("form-echo", new FormEchoHandler());
30+
Map<String, RequestHandler> handlers = Map.of("form-echo", new FormEchoHandler());
3231
try (var s = newServer(handlers);
3332
var client = httpClient()) {
3433
var req = postForm(s, "/form-echo", "tags=a&tags=b");
@@ -40,7 +39,7 @@ void formArrayProperty() throws Exception {
4039

4140
@Test
4241
void formCoercionFailureReturns400() throws Exception {
43-
Map<String, HttpHandler> handlers = Map.of("form-echo", new FormEchoHandler());
42+
Map<String, RequestHandler> handlers = Map.of("form-echo", new FormEchoHandler());
4443
try (var s = newServer(handlers);
4544
var client = httpClient()) {
4645
var req = postForm(s, "/form-echo", "age=abc");
@@ -52,7 +51,7 @@ void formCoercionFailureReturns400() throws Exception {
5251

5352
@Test
5453
void textPlainBodyParsedAsString() throws Exception {
55-
Map<String, HttpHandler> handlers = Map.of("text-echo", new TextEchoHandler());
54+
Map<String, RequestHandler> handlers = Map.of("text-echo", new TextEchoHandler());
5655
try (var s = newServer(handlers);
5756
var client = httpClient()) {
5857
var req = postWithContentType(s, "/text-echo", "hello", "text/plain; charset=utf-8");

src/test/java/com/retailsvc/http/OpenApiServerIT.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import com.retailsvc.http.start.EchoHandler;
1010
import com.retailsvc.http.start.GetDataHandler;
11-
import com.sun.net.httpserver.HttpHandler;
1211
import java.io.IOException;
1312
import java.net.http.HttpResponse.BodyHandlers;
1413
import java.util.Map;
@@ -130,7 +129,7 @@ void postDataShouldReturnJsonBody() {
130129

131130
@Test
132131
void postDataShouldReturnBadRequestOnMissingRequiredProperties() {
133-
Map<String, HttpHandler> handlers = Map.of("post-data", new EchoHandler());
132+
Map<String, RequestHandler> handlers = Map.of("post-data", new EchoHandler());
134133

135134
try (var server = newServer(handlers);
136135
var client = httpClient()) {
@@ -606,8 +605,7 @@ class FormatEmail {
606605

607606
@Test
608607
void formatEmailShouldReturnBadRequestOnInvalidEmail() {
609-
try (var server =
610-
newServer(Map.of("format-email", exchange -> exchange.sendResponseHeaders(200, -1)));
608+
try (var server = newServer(Map.of("format-email", req -> req.respond(200).empty()));
611609
var client = httpClient()) {
612610

613611
var request = newRequest(server, path + "?addr=not-an-email", "GET", noBody());
@@ -631,8 +629,7 @@ void formatEmailShouldReturnBadRequestOnInvalidEmail() {
631629

632630
@Test
633631
void formatEmailShouldReturnOkOnValidEmail() {
634-
try (var server =
635-
newServer(Map.of("format-email", exchange -> exchange.sendResponseHeaders(200, -1)));
632+
try (var server = newServer(Map.of("format-email", req -> req.respond(200).empty()));
636633
var client = httpClient()) {
637634

638635
var request = newRequest(server, path + "?addr=user%40example.com", "GET", noBody());
@@ -658,8 +655,7 @@ class FormatByte {
658655

659656
@Test
660657
void formatByteShouldReturnBadRequestOnInvalidBase64() {
661-
try (var server =
662-
newServer(Map.of("format-byte", exchange -> exchange.sendResponseHeaders(200, -1)));
658+
try (var server = newServer(Map.of("format-byte", req -> req.respond(200).empty()));
663659
var client = httpClient()) {
664660

665661
var request = newRequest(server, path + "?data=not%20base64!!", "GET", noBody());
@@ -683,8 +679,7 @@ void formatByteShouldReturnBadRequestOnInvalidBase64() {
683679

684680
@Test
685681
void formatByteShouldReturnOkOnValidBase64() {
686-
try (var server =
687-
newServer(Map.of("format-byte", exchange -> exchange.sendResponseHeaders(200, -1)));
682+
try (var server = newServer(Map.of("format-byte", req -> req.respond(200).empty()));
688683
var client = httpClient()) {
689684

690685
var request = newRequest(server, path + "?data=aGVsbG8%3D", "GET", noBody());
@@ -710,8 +705,7 @@ class FormatInt32 {
710705

711706
@Test
712707
void formatInt32ShouldReturnBadRequestOnOverflow() {
713-
try (var server =
714-
newServer(Map.of("format-int32", exchange -> exchange.sendResponseHeaders(200, -1)));
708+
try (var server = newServer(Map.of("format-int32", req -> req.respond(200).empty()));
715709
var client = httpClient()) {
716710

717711
var request = newRequest(server, path + "?n=2147483648", "GET", noBody());
@@ -735,8 +729,7 @@ void formatInt32ShouldReturnBadRequestOnOverflow() {
735729

736730
@Test
737731
void formatInt32ShouldReturnOkOnValidValue() {
738-
try (var server =
739-
newServer(Map.of("format-int32", exchange -> exchange.sendResponseHeaders(200, -1)));
732+
try (var server = newServer(Map.of("format-int32", req -> req.respond(200).empty()));
740733
var client = httpClient()) {
741734

742735
var request = newRequest(server, path + "?n=42", "GET", noBody());

src/test/java/com/retailsvc/http/RequestResponseGatewayTest.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,17 @@
1010
import java.net.http.HttpRequest;
1111
import java.net.http.HttpRequest.BodyPublishers;
1212
import java.util.Map;
13-
import org.junit.jupiter.api.Disabled;
1413
import org.junit.jupiter.api.Test;
1514

16-
// TODO Task 9: remove @Disabled once Builder.handlers() accepts Map<String, RequestHandler>
17-
@Disabled("Enabled in Task 9 once handlers() takes RequestHandler")
1815
class RequestResponseGatewayTest extends ServerBaseTest {
1916

20-
/**
21-
* Casts a {@code Map<String, RequestHandler>} to the raw {@code Map} type so callers compile
22-
* against the existing {@code Builder.handlers(Map<String, HttpHandler>)} signature. This cast is
23-
* safe to write here because the class is {@link Disabled} — it never runs until Task 9 replaces
24-
* the stub with a real {@code handlers(Map<String, RequestHandler>)} overload.
25-
*/
26-
@SuppressWarnings("unchecked")
27-
private static Map asRawHandlers(Map<String, RequestHandler> handlers) {
28-
return handlers;
29-
}
30-
3117
@Test
3218
void respondJsonWritesBodyAndContentType() throws Exception {
3319
RequestHandler echo = req -> req.respond(200).json(Map.of("op", req.operationId()));
3420
server =
3521
OpenApiServer.builder()
3622
.spec(spec)
37-
.handlers(asRawHandlers(Map.of("getRoot", echo, "postData", echo)))
23+
.handlers(Map.of("get-data", echo, "post-data", echo))
3824
.port(0)
3925
.build();
4026
HttpClient client =
@@ -47,12 +33,12 @@ void respondJsonWritesBodyAndContentType() throws Exception {
4733
HttpRequest.newBuilder()
4834
.uri(URI.create("http://localhost:%d/api/v1/data".formatted(server.listenPort())))
4935
.header("Content-Type", "application/json")
50-
.POST(BodyPublishers.ofString("{\"n\":1}"))
36+
.POST(BodyPublishers.ofString("{\"aList\":[\"x\"],\"feelingGood\":true}"))
5137
.build(),
5238
ofString());
5339
assertThat(resp.statusCode()).isEqualTo(200);
5440
assertThat(resp.headers().firstValue("Content-Type")).contains("application/json");
55-
assertThat(resp.body()).contains("\"op\":\"postData\"");
41+
assertThat(resp.body()).contains("\"op\":\"post-data\"");
5642
}
5743

5844
@Test
@@ -61,14 +47,16 @@ void respondEmptyUses204Style() throws Exception {
6147
server =
6248
OpenApiServer.builder()
6349
.spec(spec)
64-
.handlers(asRawHandlers(Map.of("getRoot", ok, "postData", ok)))
50+
.handlers(Map.of("get-data", ok, "post-data", ok))
6551
.port(0)
6652
.build();
6753
var resp =
6854
HttpClient.newHttpClient()
6955
.send(
7056
HttpRequest.newBuilder()
71-
.uri(URI.create("http://localhost:%d/api/v1/".formatted(server.listenPort())))
57+
.uri(
58+
URI.create(
59+
"http://localhost:%d/api/v1/data".formatted(server.listenPort())))
7260
.GET()
7361
.build(),
7462
ofString());
@@ -88,14 +76,16 @@ void respondStreamUsesChunkedEncoding() throws Exception {
8876
server =
8977
OpenApiServer.builder()
9078
.spec(spec)
91-
.handlers(asRawHandlers(Map.of("getRoot", streamer, "postData", streamer)))
79+
.handlers(Map.of("get-data", streamer, "post-data", streamer))
9280
.port(0)
9381
.build();
9482
var resp =
9583
HttpClient.newHttpClient()
9684
.send(
9785
HttpRequest.newBuilder()
98-
.uri(URI.create("http://localhost:%d/api/v1/".formatted(server.listenPort())))
86+
.uri(
87+
URI.create(
88+
"http://localhost:%d/api/v1/data".formatted(server.listenPort())))
9989
.GET()
10090
.build(),
10191
ofString());
Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
11
package com.retailsvc.http;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4-
import static org.junit.jupiter.api.Assertions.assertThrows;
4+
import static org.mockito.Mockito.mock;
55

6-
import com.retailsvc.http.internal.LegacyRequestAccess;
7-
import com.retailsvc.http.internal.RequestContext;
6+
import com.retailsvc.http.internal.DispatchHandler;
7+
import com.sun.net.httpserver.HttpExchange;
88
import java.util.Map;
9-
import java.util.NoSuchElementException;
109
import java.util.concurrent.atomic.AtomicReference;
1110
import org.junit.jupiter.api.Test;
1211

1312
class RequestTest {
1413

1514
@Test
1615
void readsBoundContext() throws Exception {
17-
RequestContext ctx =
18-
new RequestContext(new byte[] {1, 2, 3}, Map.of("k", "v"), "get-x", Map.of("id", "42"));
16+
HttpExchange exchange = mock(HttpExchange.class);
17+
Request req =
18+
new Request(
19+
exchange,
20+
new byte[] {1, 2, 3},
21+
Map.of("k", "v"),
22+
"get-x",
23+
Map.of("id", "42"),
24+
Map.of());
1925

2026
AtomicReference<byte[]> seenBytes = new AtomicReference<>();
2127
AtomicReference<Object> seenParsed = new AtomicReference<>();
2228
AtomicReference<String> seenOpId = new AtomicReference<>();
2329
AtomicReference<Map<String, String>> seenPathParams = new AtomicReference<>();
2430

25-
ScopedValue.where(LegacyRequestAccess.CONTEXT, ctx)
31+
ScopedValue.where(DispatchHandler.CURRENT, req)
2632
.call(
2733
() -> {
28-
seenBytes.set(LegacyRequestAccess.bytes());
29-
seenParsed.set(LegacyRequestAccess.parsed());
30-
seenOpId.set(LegacyRequestAccess.operationId());
31-
seenPathParams.set(LegacyRequestAccess.pathParams());
34+
Request r = DispatchHandler.CURRENT.get();
35+
seenBytes.set(r.bytes());
36+
seenParsed.set(r.parsed());
37+
seenOpId.set(r.operationId());
38+
seenPathParams.set(r.pathParams());
3239
return null;
3340
});
3441

@@ -37,9 +44,4 @@ void readsBoundContext() throws Exception {
3744
assertThat(seenOpId.get()).isEqualTo("get-x");
3845
assertThat(seenPathParams.get()).containsEntry("id", "42");
3946
}
40-
41-
@Test
42-
void readingOutsideScopeThrows() {
43-
assertThrows(NoSuchElementException.class, LegacyRequestAccess::bytes);
44-
}
4547
}

src/test/java/com/retailsvc/http/ServerBaseTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import com.google.gson.Gson;
88
import com.retailsvc.http.spec.Spec;
9-
import com.sun.net.httpserver.HttpHandler;
109
import java.io.InputStream;
1110
import java.net.URI;
1211
import java.net.http.HttpClient;
@@ -44,7 +43,7 @@ void tearDown() {
4443
Optional.ofNullable(server).ifPresent(OpenApiServer::close);
4544
}
4645

47-
protected OpenApiServer newServer(Map<String, HttpHandler> handlers) {
46+
protected OpenApiServer newServer(Map<String, RequestHandler> handlers) {
4847
try {
4948
server = OpenApiServer.builder().spec(spec).handlers(handlers).port(0).build();
5049
return server;

0 commit comments

Comments
 (0)