Skip to content

Commit 56dd700

Browse files
committed
feat: Add OpenApiServer.Builder with extra-handler support
1 parent 86f62c1 commit 56dd700

2 files changed

Lines changed: 166 additions & 1 deletion

File tree

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.sun.net.httpserver.HttpServer;
1616
import java.io.IOException;
1717
import java.net.InetSocketAddress;
18+
import java.util.LinkedHashMap;
1819
import java.util.Map;
1920
import java.util.Optional;
2021
import org.slf4j.Logger;
@@ -45,7 +46,7 @@ public OpenApiServer(
4546
Map<String, HttpHandler> handlers,
4647
ExceptionHandler exceptionHandler)
4748
throws IOException {
48-
this(spec, jsonMapper, handlers, exceptionHandler, DEFAULT_PORT);
49+
this(spec, jsonMapper, handlers, exceptionHandler, DEFAULT_PORT, Map.of());
4950
}
5051

5152
/**
@@ -63,6 +64,17 @@ public OpenApiServer(
6364
ExceptionHandler exceptionHandler,
6465
int port)
6566
throws IOException {
67+
this(spec, jsonMapper, handlers, exceptionHandler, port, Map.of());
68+
}
69+
70+
OpenApiServer(
71+
Spec spec,
72+
JsonMapper jsonMapper,
73+
Map<String, HttpHandler> handlers,
74+
ExceptionHandler exceptionHandler,
75+
int port,
76+
Map<String, HttpHandler> extras)
77+
throws IOException {
6678

6779
requireNonNull(spec, "Spec must not be null");
6880
requireNonNull(jsonMapper, "JsonMapper must not be null");
@@ -84,6 +96,12 @@ public OpenApiServer(
8496
ctx.getFilters().add(new RequestPreparationFilter(spec, router, validator, jsonMapper));
8597
ctx.setHandler(new DispatchHandler(handlers));
8698

99+
for (Map.Entry<String, HttpHandler> e : extras.entrySet()) {
100+
HttpContext extraCtx = httpServer.createContext(e.getKey());
101+
extraCtx.getFilters().add(new ExceptionFilter(exceptionHandler));
102+
extraCtx.setHandler(e.getValue());
103+
}
104+
87105
httpServer.createContext("/", Handlers.notFoundHandler());
88106
httpServer.start();
89107

@@ -100,4 +118,70 @@ public void close() {
100118
httpServer.stop(0);
101119
}
102120
}
121+
122+
public static Builder builder() {
123+
return new Builder();
124+
}
125+
126+
/** Fluent builder for {@link OpenApiServer}. */
127+
public static final class Builder {
128+
129+
private Spec spec;
130+
private JsonMapper jsonMapper;
131+
private Map<String, HttpHandler> handlers;
132+
private ExceptionHandler exceptionHandler;
133+
private int port = DEFAULT_PORT;
134+
private final LinkedHashMap<String, HttpHandler> extras = new LinkedHashMap<>();
135+
136+
private Builder() {}
137+
138+
public Builder spec(Spec spec) {
139+
this.spec = spec;
140+
return this;
141+
}
142+
143+
public Builder jsonMapper(JsonMapper jsonMapper) {
144+
this.jsonMapper = jsonMapper;
145+
return this;
146+
}
147+
148+
public Builder handlers(Map<String, HttpHandler> handlers) {
149+
this.handlers = handlers;
150+
return this;
151+
}
152+
153+
public Builder exceptionHandler(ExceptionHandler exceptionHandler) {
154+
this.exceptionHandler = exceptionHandler;
155+
return this;
156+
}
157+
158+
public Builder port(int port) {
159+
this.port = port;
160+
return this;
161+
}
162+
163+
public Builder addHandler(String path, HttpHandler handler) {
164+
requireNonNull(path, "path must not be null");
165+
requireNonNull(handler, "handler must not be null");
166+
if (extras.containsKey(path)) {
167+
throw new IllegalStateException("duplicate extra handler path: " + path);
168+
}
169+
extras.put(path, handler);
170+
return this;
171+
}
172+
173+
public OpenApiServer build() throws IOException {
174+
requireNonNull(spec, "Spec must not be null");
175+
requireNonNull(jsonMapper, "JsonMapper must not be null");
176+
requireNonNull(handlers, "handlers must not be null");
177+
String basePath = Optional.ofNullable(spec.basePath()).orElse("/");
178+
for (String path : extras.keySet()) {
179+
if (path.equals(basePath)) {
180+
throw new IllegalStateException(
181+
"extra handler path " + path + " conflicts with spec basePath " + basePath);
182+
}
183+
}
184+
return new OpenApiServer(spec, jsonMapper, handlers, exceptionHandler, port, extras);
185+
}
186+
}
103187
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.retailsvc.http;
2+
3+
import static java.util.Collections.emptyMap;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
6+
7+
import com.retailsvc.http.spec.Spec;
8+
import java.util.List;
9+
import java.util.Map;
10+
import org.junit.jupiter.api.Test;
11+
12+
class OpenApiServerBuilderTest {
13+
14+
private final Spec spec = testSpec();
15+
private final JsonMapper jsonMapper = body -> new java.util.HashMap<String, Object>();
16+
17+
@Test
18+
void buildsWithRequiredFieldsOnly() {
19+
assertDoesNotThrow(
20+
() -> {
21+
try (var _ =
22+
OpenApiServer.builder()
23+
.spec(spec)
24+
.jsonMapper(jsonMapper)
25+
.handlers(emptyMap())
26+
.port(0)
27+
.build()) {
28+
// close on exit
29+
}
30+
});
31+
}
32+
33+
@Test
34+
void rejectsDuplicateExtraPathOnSecondAddHandler() {
35+
OpenApiServer.Builder b =
36+
OpenApiServer.builder()
37+
.spec(spec)
38+
.jsonMapper(jsonMapper)
39+
.handlers(emptyMap())
40+
.addHandler("/alive", Handlers.aliveHandler());
41+
42+
assertThatThrownBy(() -> b.addHandler("/alive", Handlers.aliveHandler()))
43+
.isInstanceOf(IllegalStateException.class)
44+
.hasMessageContaining("/alive");
45+
}
46+
47+
@Test
48+
void rejectsExtraPathEqualToSpecBasePathAtBuildTime() {
49+
// testSpec() uses "/api" as the basePath (servers[0].url = http://localhost:8080/api).
50+
assertThatThrownBy(
51+
() ->
52+
OpenApiServer.builder()
53+
.spec(spec)
54+
.jsonMapper(jsonMapper)
55+
.handlers(emptyMap())
56+
.addHandler("/api", Handlers.aliveHandler())
57+
.port(0)
58+
.build())
59+
.isInstanceOf(IllegalStateException.class)
60+
.hasMessageContaining("/api");
61+
}
62+
63+
@Test
64+
void rejectsNullSpec() {
65+
assertThatThrownBy(
66+
() ->
67+
OpenApiServer.builder().jsonMapper(jsonMapper).handlers(emptyMap()).port(0).build())
68+
.isInstanceOf(NullPointerException.class)
69+
.hasMessageContaining("Spec");
70+
}
71+
72+
private static Spec testSpec() {
73+
Map<String, Object> raw =
74+
Map.of(
75+
"openapi", "3.1.0",
76+
"info", Map.of("title", "Test API", "version", "1.0"),
77+
"servers", List.of(Map.of("url", "http://localhost:8080/api")),
78+
"paths", emptyMap());
79+
return Spec.from(raw);
80+
}
81+
}

0 commit comments

Comments
 (0)