Skip to content

Commit eebc70b

Browse files
committed
feat: Expose HTTP method on Request
1 parent 6588176 commit eebc70b

3 files changed

Lines changed: 92 additions & 7 deletions

File tree

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

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.retailsvc.http;
22

3+
import com.retailsvc.http.spec.HttpMethod;
34
import java.net.URLDecoder;
45
import java.nio.charset.StandardCharsets;
56
import java.util.LinkedHashMap;
@@ -9,8 +10,8 @@
910
import java.util.function.UnaryOperator;
1011

1112
/**
12-
* Read-only per-request handle passed to {@link RequestHandler}. Carries the parsed body, path
13-
* parameters, query parameters, headers, and operation ID.
13+
* Read-only per-request handle passed to {@link RequestHandler}. Carries the HTTP method, parsed
14+
* body, path parameters, query parameters, headers, and operation ID.
1415
*
1516
* <p>{@code Request} is transport-neutral: it holds the body bytes, the raw query string, the path
1617
* parameter map, and a header lookup function. The transport adapter (today the built-in JDK {@code
@@ -28,6 +29,7 @@ public final class Request {
2829
private final String operationId;
2930
private final Map<String, String> pathParameters;
3031
private final String rawQuery;
32+
private final HttpMethod method;
3133
private final UnaryOperator<String> headerLookup;
3234
private final Map<String, Object> principals;
3335
private Map<String, String> queryParamCache;
@@ -36,6 +38,8 @@ public final class Request {
3638
* Builds a {@code Request} from transport-neutral primitives. Adapters call this; handlers
3739
* receive the constructed instance.
3840
*
41+
* <p>{@code method} is {@code null}; use the 9-arg constructor to supply it.
42+
*
3943
* @param body raw request body bytes; never {@code null}, may be empty
4044
* @param parsed loose structural view of the body (Map / List / boxed primitive), or {@code null}
4145
* @param bodyMapper {@link TypeMapper} that produced {@code parsed}, used for typed conversion;
@@ -53,12 +57,23 @@ public Request(
5357
Map<String, String> pathParameters,
5458
String rawQuery,
5559
UnaryOperator<String> headerLookup) {
56-
this(body, parsed, bodyMapper, operationId, pathParameters, rawQuery, headerLookup, Map.of());
60+
this(
61+
body,
62+
parsed,
63+
bodyMapper,
64+
operationId,
65+
pathParameters,
66+
rawQuery,
67+
headerLookup,
68+
Map.of(),
69+
null);
5770
}
5871

5972
/**
6073
* Builds a {@code Request} from transport-neutral primitives with an explicit principals map.
6174
*
75+
* <p>{@code method} is {@code null}; use the 9-arg constructor to supply it.
76+
*
6277
* @param body raw request body bytes; never {@code null}, may be empty
6378
* @param parsed loose structural view of the body (Map / List / boxed primitive), or {@code null}
6479
* @param bodyMapper {@link TypeMapper} that produced {@code parsed}, used for typed conversion;
@@ -69,9 +84,47 @@ public Request(
6984
* @param headerLookup first-value, case-insensitive header lookup; returns {@code null} if absent
7085
* @param principals principals stashed by the security filter, keyed by scheme name
7186
*/
87+
@SuppressWarnings("java:S107")
88+
public Request(
89+
byte[] body,
90+
Object parsed,
91+
TypeMapper bodyMapper,
92+
String operationId,
93+
Map<String, String> pathParameters,
94+
String rawQuery,
95+
UnaryOperator<String> headerLookup,
96+
Map<String, Object> principals) {
97+
this(
98+
body,
99+
parsed,
100+
bodyMapper,
101+
operationId,
102+
pathParameters,
103+
rawQuery,
104+
headerLookup,
105+
principals,
106+
null);
107+
}
108+
109+
/**
110+
* Builds a {@code Request} from transport-neutral primitives with explicit principals and method.
111+
*
112+
* @param body raw request body bytes; never {@code null}, may be empty
113+
* @param parsed loose structural view of the body (Map / List / boxed primitive), or {@code null}
114+
* @param bodyMapper {@link TypeMapper} that produced {@code parsed}, used for typed conversion;
115+
* may be {@code null} if there is no body
116+
* @param operationId the OpenAPI {@code operationId} the request was routed to
117+
* @param pathParameters path variables extracted by the router
118+
* @param rawQuery raw (percent-encoded) query string, or {@code null} if absent
119+
* @param headerLookup first-value, case-insensitive header lookup; returns {@code null} if absent
120+
* @param principals principals stashed by the security filter, keyed by scheme name
121+
* @param method the HTTP method of the request. Never {@code null} when constructed through the
122+
* normal request pipeline. {@code null} only when constructed via the legacy 7- or 8-argument
123+
* constructors (kept for backward compatibility).
124+
*/
72125
// Request is transport-neutral and assembled from primitives at the adapter boundary; collapsing
73126
// these into a holder type would just move the parameter count one level out without simplifying
74-
// the call site, so the 8-arg constructor is preferred over the rule's 7-param limit.
127+
// the call site, so the 9-arg constructor is preferred over the rule's 7-param limit.
75128
@SuppressWarnings("java:S107")
76129
public Request(
77130
byte[] body,
@@ -81,13 +134,15 @@ public Request(
81134
Map<String, String> pathParameters,
82135
String rawQuery,
83136
UnaryOperator<String> headerLookup,
84-
Map<String, Object> principals) {
137+
Map<String, Object> principals,
138+
HttpMethod method) {
85139
this.body = body;
86140
this.parsed = parsed;
87141
this.bodyMapper = bodyMapper;
88142
this.operationId = operationId;
89143
this.pathParameters = pathParameters;
90144
this.rawQuery = rawQuery;
145+
this.method = method;
91146
this.headerLookup = headerLookup;
92147
this.principals = Map.copyOf(principals);
93148
}
@@ -207,14 +262,31 @@ public Optional<Object> principal(String schemeName) {
207262
return Optional.ofNullable(principals.get(schemeName));
208263
}
209264

265+
/**
266+
* HTTP method of the request. Never {@code null} for requests routed through the standard
267+
* pipeline; {@code null} only when the {@code Request} was constructed via a legacy constructor
268+
* without a method.
269+
*/
270+
public HttpMethod method() {
271+
return method;
272+
}
273+
210274
/**
211275
* Returns a new {@code Request} identical to this one except with the supplied principals. Used
212276
* by {@code SecurityFilter} on success; the returned instance carries the principals through to
213277
* the {@link RequestHandler}.
214278
*/
215279
public Request withPrincipals(Map<String, Object> principals) {
216280
return new Request(
217-
body, parsed, bodyMapper, operationId, pathParameters, rawQuery, headerLookup, principals);
281+
body,
282+
parsed,
283+
bodyMapper,
284+
operationId,
285+
pathParameters,
286+
rawQuery,
287+
headerLookup,
288+
principals,
289+
method);
218290
}
219291

220292
private static Map<String, String> parseQuery(String query) {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
7373
op.operationId(),
7474
match.pathParameters(),
7575
exchange.getRequestURI().getRawQuery(),
76-
headers::getFirst);
76+
headers::getFirst,
77+
Map.of(),
78+
method);
7779

7880
try {
7981
ScopedValue.where(DispatchHandler.CURRENT, request)

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

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

66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import com.retailsvc.http.internal.DispatchHandler;
8+
import com.retailsvc.http.spec.HttpMethod;
89
import java.nio.charset.StandardCharsets;
910
import java.util.HashMap;
1011
import java.util.Map;
@@ -231,6 +232,16 @@ void withPrincipalsDoesNotShareUnderlyingMap() {
231232
assertThat(copy.principal("a")).contains("b");
232233
}
233234

235+
@Test
236+
void exposesMethod() {
237+
Request req =
238+
new Request(
239+
new byte[0], null, null, "op", Map.of(), null, NO_HEADERS, Map.of(), HttpMethod.POST);
240+
241+
assertThat(req.method()).isEqualTo(HttpMethod.POST);
242+
assertThat(req.withPrincipals(Map.of("k", "v")).method()).isEqualTo(HttpMethod.POST);
243+
}
244+
234245
@Test
235246
void headerReturnsOptionalAndBlankIsAbsent() {
236247
Request req =

0 commit comments

Comments
 (0)