i spent the last year just spinning
This commit is contained in:
@@ -1,14 +1,44 @@
|
||||
# flash-ext-jackson
|
||||
|
||||
Jackson JSON integration for the Flash HTTP server.
|
||||
Jackson JSON integration for Flash with an opinionated auto-marshal middleware.
|
||||
|
||||
## What it provides
|
||||
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| `JacksonExtension` | Installs Jackson into the extension layer |
|
||||
| `JacksonHandler` | Base class for handlers that need JSON I/O |
|
||||
| Global exception handler | Maps `HttpException` → JSON error; all other exceptions → 500 |
|
||||
| `JacksonExtension` | Registers JSON services into `FlashContext` |
|
||||
| `Json` | JSON read/write helper (`body`, `bodyFrom`, `write`, `writeView`) |
|
||||
| `ObjectMapper` | Raw mapper escape hatch for advanced usage |
|
||||
| `JacksonMiddleware` | `autoJson()` middleware for automatic outbound JSON marshalling |
|
||||
|
||||
Default mapper behavior (`new JacksonExtension()`):
|
||||
|
||||
- auto-discovers Jackson modules on classpath (`findAndAddModules()`)
|
||||
- includes Java Time support (`jackson-datatype-jsr310`)
|
||||
- writes date/time values as ISO-8601 strings (not numeric timestamps)
|
||||
|
||||
## Recommended default
|
||||
|
||||
Install the extension, then apply `autoJson()` once at app or scope level.
|
||||
|
||||
```java
|
||||
JacksonExtension jackson = new JacksonExtension();
|
||||
|
||||
FlashApp app = FlashApp.create(8080)
|
||||
.install(jackson)
|
||||
.use(jackson.autoJson());
|
||||
|
||||
app.startAndBlock();
|
||||
```
|
||||
|
||||
Behavior of `autoJson()`:
|
||||
|
||||
- pass-through: `null`, `Response`, `byte[]`, `String`, `CharSequence`
|
||||
- any other return value: serialize to JSON `byte[]`
|
||||
- sets `Content-Type: application/json` for marshalled responses
|
||||
- serialization failures throw `IllegalStateException`
|
||||
|
||||
This keeps handlers concise while preserving Flash's direct byte write path.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -16,21 +46,43 @@ Jackson JSON integration for the Flash HTTP server.
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-ext-jackson</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.1-indev2</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## Json helper API
|
||||
|
||||
Use `Json` when you want explicit, local control in a handler.
|
||||
|
||||
```java
|
||||
FlashApp.create(8080)
|
||||
.install(new JacksonExtension())
|
||||
.install(new OpenApiExtension("/openapi", "My API", "1.0.0"))
|
||||
.start();
|
||||
@POST("/users")
|
||||
public final class CreateUser extends RequestHandler {
|
||||
private Json json;
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
json = require(Json.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handle(Request req, Response res) throws Exception {
|
||||
CreateUserBody body = json.body(req, CreateUserBody.class);
|
||||
UserDto created = service.create(body);
|
||||
res.status(201);
|
||||
return json.write(res, created);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Install order is irrelevant — Flash's two-phase extension model ensures `ObjectMapper` is
|
||||
resolved before any extension that calls `ctx.require(ObjectMapper.class)` needs it.
|
||||
Methods:
|
||||
|
||||
### Custom mapper
|
||||
- `body(req, Type.class)` -> parse from `req.body().bytes()`
|
||||
- `bodyFrom(req, Type.class)` -> parse from `req.body().stream()`
|
||||
- `write(res, obj)` -> writes JSON string and sets JSON content type
|
||||
- `writeView(res, obj, View.class)` -> JSON with Jackson `@JsonView`
|
||||
- `mapper()` -> raw `ObjectMapper`
|
||||
|
||||
## Custom mapper
|
||||
|
||||
```java
|
||||
ObjectMapper mapper = JsonMapper.builder()
|
||||
@@ -42,63 +94,23 @@ FlashApp.create(8080)
|
||||
.install(new JacksonExtension(mapper));
|
||||
```
|
||||
|
||||
## JacksonHandler
|
||||
## Scope usage
|
||||
|
||||
Extend `JacksonHandler` to get `bodyAs` and `json` helpers without constructor boilerplate.
|
||||
The `ObjectMapper` is injected once at startup and shared across all subclasses.
|
||||
`autoJson()` works the same at scope level:
|
||||
|
||||
```java
|
||||
@Route(method = HttpMethod.POST, path = "/api/blogs")
|
||||
@ApiOperation(summary = "Create blog")
|
||||
@ApiResponse(status = 201, description = "Created", schema = Blog.class)
|
||||
@ApiResponse(status = 400, description = "Invalid body")
|
||||
public class CreateBlog extends JacksonHandler {
|
||||
JacksonExtension jackson = new JacksonExtension();
|
||||
|
||||
@Override
|
||||
public Object handle(Request req, Response res) throws Exception {
|
||||
CreateBlogRequest body = bodyAs(req, CreateBlogRequest.class);
|
||||
Blog created = service.create(body);
|
||||
res.status(201);
|
||||
return json(res, created); // sets Content-Type: application/json
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `bodyAs(req, Type.class)` | Deserializes the request body; throws `HttpException` 400 on parse errors |
|
||||
| `json(res, obj)` | Serializes `obj`, sets `Content-Type: application/json`, returns the JSON string |
|
||||
| `jsonView(res, obj, View.class)` | Like `json` but applies a Jackson `@JsonView` filter |
|
||||
|
||||
## Exception handling
|
||||
|
||||
`JacksonExtension` installs a global exception handler that applies to every route:
|
||||
|
||||
```
|
||||
HttpException(status, message) → HTTP <status> {"error": "<message>"}
|
||||
Any other Throwable → HTTP 500 {"error": "Internal Server Error"}
|
||||
```
|
||||
|
||||
To throw a handled HTTP error from any handler:
|
||||
|
||||
```java
|
||||
throw HttpException.notFound("Blog not found");
|
||||
throw HttpException.badRequest("Missing field: title");
|
||||
throw HttpException.unauthorized();
|
||||
throw HttpException.forbidden();
|
||||
```
|
||||
|
||||
## Lambda routes
|
||||
|
||||
For lambda-style routes, use the `ObjectMapper` directly from the context:
|
||||
|
||||
```java
|
||||
ObjectMapper mapper = app.ctx().require(ObjectMapper.class);
|
||||
|
||||
app.get("/api/status", (req, res) -> {
|
||||
res.type(ContentType.JSON);
|
||||
return mapper.writeValueAsString(Map.of("status", "ok"));
|
||||
app.mount("/api", api -> {
|
||||
api.use(jackson.autoJson());
|
||||
api.get("/health", (req, res) -> Map.of("ok", true));
|
||||
});
|
||||
```
|
||||
|
||||
If you need to pull it from context, `JacksonMiddleware` is also provided as a service
|
||||
after the app boots (same lifecycle model as other extension-provided services).
|
||||
|
||||
## Notes
|
||||
|
||||
- Install order is irrelevant (Flash two-phase extension lifecycle).
|
||||
- `autoJson()` and OpenAPI are intentionally decoupled.
|
||||
|
||||
@@ -7,11 +7,15 @@
|
||||
<parent>
|
||||
<groupId>dev.relism</groupId>
|
||||
<artifactId>flash-extensions</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.1-indev5</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>flash-ext-jackson</artifactId>
|
||||
|
||||
<properties>
|
||||
<jacoco.version>0.8.12</jacoco.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>dev.relism</groupId>
|
||||
@@ -21,6 +25,10 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@@ -31,4 +39,44 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>jacoco-prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>jacoco-report-and-check</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>LINE</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.80</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
+31
-4
@@ -1,9 +1,11 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import dev.relism.extension.FlashContext;
|
||||
import dev.relism.extension.FlashExtension;
|
||||
import dev.relism.routing.Middleware;
|
||||
|
||||
/**
|
||||
* Registers JSON support into the Flash extension layer.
|
||||
@@ -15,6 +17,9 @@ import dev.relism.extension.FlashExtension;
|
||||
* <p>The raw {@link ObjectMapper} is also registered under {@code ObjectMapper.class}
|
||||
* for extensions that need direct mapper access (e.g. OpenAPI schema generation).
|
||||
*
|
||||
* <p>{@link JacksonMiddleware} is provided under {@code JacksonMiddleware.class} and
|
||||
* exposes opinionated JSON auto-marshalling middleware via {@link JacksonMiddleware#autoJson()}.
|
||||
*
|
||||
* <h3>Usage — composition (preferred)</h3>
|
||||
* <pre>{@code
|
||||
* public class MyHandler extends RequestHandler {
|
||||
@@ -45,21 +50,43 @@ import dev.relism.extension.FlashExtension;
|
||||
public class JacksonExtension implements FlashExtension {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final JacksonMiddleware middleware;
|
||||
|
||||
/** Installs with a default {@link JsonMapper} (no extra modules). */
|
||||
/**
|
||||
* Installs with an opinionated default {@link JsonMapper}:
|
||||
* auto-discovers modules on classpath (e.g. Java Time) and writes dates as ISO strings.
|
||||
*/
|
||||
public JacksonExtension() {
|
||||
this(JsonMapper.builder().build());
|
||||
this(JsonMapper.builder()
|
||||
.findAndAddModules()
|
||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.build());
|
||||
}
|
||||
|
||||
/** Installs with a fully configured custom {@link ObjectMapper}. */
|
||||
public JacksonExtension(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
this.middleware = new JacksonMiddleware(mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opinionated outbound JSON middleware factory.
|
||||
*
|
||||
* <p>Use for app/scope-level registration:
|
||||
* <pre>{@code
|
||||
* JacksonExtension jackson = new JacksonExtension();
|
||||
* app.install(jackson).use(jackson.autoJson());
|
||||
* }</pre>
|
||||
*/
|
||||
public Middleware autoJson() {
|
||||
return middleware.autoJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void provide(FlashContext ctx) {
|
||||
Json json = new Json(mapper);
|
||||
ctx.provide(Json.class, json);
|
||||
ctx.provide(ObjectMapper.class, mapper);
|
||||
ctx.provide(Json.class, json);
|
||||
ctx.provide(ObjectMapper.class, mapper);
|
||||
ctx.provide(JacksonMiddleware.class, middleware);
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.models.Response;
|
||||
import dev.relism.routing.Middleware;
|
||||
|
||||
/**
|
||||
* Outbound JSON marshalling middleware for class-based and lambda routes.
|
||||
*
|
||||
* <p>{@link #autoJson()} marshals any non-body-native return value to JSON bytes,
|
||||
* writes {@code Content-Type: application/json}, and returns {@code byte[]} so
|
||||
* the Flash write path stays direct.
|
||||
*
|
||||
* <p>Pass-through return types:
|
||||
* <ul>
|
||||
* <li>{@code null}</li>
|
||||
* <li>{@link Response}</li>
|
||||
* <li>{@code byte[]}</li>
|
||||
* <li>{@link String}</li>
|
||||
* <li>{@link CharSequence}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class JacksonMiddleware {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
JacksonMiddleware(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic JSON marshalling policy.
|
||||
*
|
||||
* <p>For non-pass-through return values, serializes with Jackson directly to
|
||||
* {@code byte[]} and sets response content type to JSON.
|
||||
*
|
||||
* @throws IllegalStateException when serialization fails
|
||||
*/
|
||||
public Middleware autoJson() {
|
||||
return next -> (req, res) -> {
|
||||
Object out = next.handle(req, res);
|
||||
if (isPassThrough(out)) return out;
|
||||
|
||||
res.type(ContentType.JSON);
|
||||
try {
|
||||
return mapper.writeValueAsBytes(out);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException(
|
||||
"Failed to serialize handler result as JSON: " + out.getClass().getName(), e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isPassThrough(Object out) {
|
||||
return out == null
|
||||
|| out instanceof Response
|
||||
|| out instanceof byte[]
|
||||
|| out instanceof CharSequence;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.relism.extension.FlashContext;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JacksonExtensionTest {
|
||||
|
||||
@Test
|
||||
void provide_registers_json_mapper_and_middleware() {
|
||||
FlashContext ctx = new FlashContext();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JacksonExtension ext = new JacksonExtension(mapper);
|
||||
|
||||
ext.provide(ctx);
|
||||
|
||||
assertNotNull(ctx.require(Json.class));
|
||||
assertNotNull(ctx.require(JacksonMiddleware.class));
|
||||
assertSame(mapper, ctx.require(ObjectMapper.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoJson_factory_delegates_to_middleware_policy() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JacksonExtension ext = new JacksonExtension(mapper);
|
||||
RequestHandler next = new RequestHandler() {
|
||||
@Override
|
||||
public Object handle(Request request, Response response) {
|
||||
return new Payload("ok");
|
||||
}
|
||||
};
|
||||
RequestHandler wrapped = new RequestHandler() {
|
||||
private final dev.relism.models.SimpleHandler.FunctionalHandler delegate = ext.autoJson().wrap(next);
|
||||
|
||||
@Override
|
||||
public Object handle(Request request, Response response) throws Exception {
|
||||
return delegate.handle(request, response);
|
||||
}
|
||||
};
|
||||
|
||||
Response res = new Response(200, ContentType.TEXT_PLAIN);
|
||||
Object out = wrapped.handle(null, res);
|
||||
|
||||
assertTrue(out instanceof byte[]);
|
||||
assertEquals("application/json", new String(res.getContentType(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private record Payload(String status) {}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JacksonMiddlewareTest {
|
||||
|
||||
private static final Request REQ = null;
|
||||
|
||||
@Test
|
||||
void autoJson_marshalsPojo_toJsonBytes_and_setsJsonContentType() throws Exception {
|
||||
JacksonMiddleware mw = new JacksonMiddleware(new ObjectMapper());
|
||||
RequestHandler wrapped = wrap(mw, new UserDto("u1", "alice"));
|
||||
Response res = new Response(200, ContentType.TEXT_PLAIN);
|
||||
|
||||
Object out = wrapped.handle(REQ, res);
|
||||
|
||||
assertInstanceOf(byte[].class, out);
|
||||
assertEquals("application/json", new String(res.getContentType(), StandardCharsets.UTF_8));
|
||||
assertEquals("{\"id\":\"u1\",\"name\":\"alice\"}", new String((byte[]) out, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoJson_passThrough_for_response_string_charSequence_bytes_and_null() throws Exception {
|
||||
JacksonMiddleware mw = new JacksonMiddleware(new ObjectMapper());
|
||||
|
||||
Response payloadResponse = new Response(201, ContentType.TEXT_PLAIN).body("ok");
|
||||
RequestHandler wrappedResponse = wrap(mw, payloadResponse);
|
||||
Response res = new Response(200, ContentType.TEXT_PLAIN);
|
||||
assertSame(payloadResponse, wrappedResponse.handle(REQ, res));
|
||||
|
||||
String s = "hello";
|
||||
assertSame(s, wrap(mw, s).handle(REQ, res));
|
||||
|
||||
CharSequence cs = new StringBuilder("hello-cs");
|
||||
assertSame(cs, wrap(mw, cs).handle(REQ, res));
|
||||
|
||||
byte[] bytes = new byte[]{1, 2, 3};
|
||||
assertSame(bytes, wrap(mw, bytes).handle(REQ, res));
|
||||
|
||||
assertSame(null, wrap(mw, null).handle(REQ, res));
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoJson_wraps_serialization_errors_as_illegal_state() {
|
||||
JacksonMiddleware mw = new JacksonMiddleware(new ObjectMapper());
|
||||
RequestHandler wrapped = wrap(mw, new CyclicDto());
|
||||
Response res = new Response(200, ContentType.TEXT_PLAIN);
|
||||
|
||||
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> wrapped.handle(REQ, res));
|
||||
assertEquals("application/json", new String(res.getContentType(), StandardCharsets.UTF_8));
|
||||
assertTrue(ex.getMessage().startsWith("Failed to serialize handler result as JSON:"));
|
||||
}
|
||||
|
||||
private static RequestHandler wrap(JacksonMiddleware mw, Object fixedReturn) {
|
||||
RequestHandler next = new RequestHandler() {
|
||||
@Override
|
||||
public Object handle(Request request, Response response) {
|
||||
return fixedReturn;
|
||||
}
|
||||
};
|
||||
return new RequestHandler() {
|
||||
private final dev.relism.models.SimpleHandler.FunctionalHandler delegate = mw.autoJson().wrap(next);
|
||||
|
||||
@Override
|
||||
public Object handle(Request request, Response response) throws Exception {
|
||||
return delegate.handle(request, response);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private record UserDto(String id, String name) {}
|
||||
|
||||
private static final class CyclicDto {
|
||||
CyclicDto self = this;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package dev.relism.ext.jackson;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.relism.exceptions.HttpException;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.http.HttpMethod;
|
||||
import dev.relism.models.HeaderMap;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestLine;
|
||||
import dev.relism.models.Response;
|
||||
import dev.relism.routing.routers.fastpathrouter.FastPathViews;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JsonTest {
|
||||
|
||||
@Test
|
||||
void body_parses_valid_json_and_maps_bad_payload_to_http_400() throws Exception {
|
||||
Json json = new Json(new ObjectMapper());
|
||||
|
||||
Request ok = request("{\"id\":\"u1\",\"name\":\"alice\"}");
|
||||
UserDto dto = json.body(ok, UserDto.class);
|
||||
assertEquals("u1", dto.id);
|
||||
assertEquals("alice", dto.name);
|
||||
|
||||
Request bad = request("not-json");
|
||||
HttpException ex = assertThrows(HttpException.class, () -> json.body(bad, UserDto.class));
|
||||
assertEquals(400, ex.status());
|
||||
assertTrue(ex.getMessage().startsWith("Invalid request body:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bodyFrom_parses_stream_and_maps_bad_payload_to_http_400() throws Exception {
|
||||
Json json = new Json(new ObjectMapper());
|
||||
|
||||
Request ok = request("{\"id\":\"u2\",\"name\":\"bob\"}");
|
||||
UserDto dto = json.bodyFrom(ok, UserDto.class);
|
||||
assertEquals("u2", dto.id);
|
||||
assertEquals("bob", dto.name);
|
||||
|
||||
Request bad = request("[");
|
||||
HttpException ex = assertThrows(HttpException.class, () -> json.bodyFrom(bad, UserDto.class));
|
||||
assertEquals(400, ex.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void write_and_writeView_set_content_type_and_render_expected_payload() throws Exception {
|
||||
Json json = new Json(new ObjectMapper());
|
||||
Response res = new Response(200, ContentType.TEXT_PLAIN);
|
||||
|
||||
String payload = json.write(res, new UserDto("u3", "carol"));
|
||||
assertEquals("application/json", new String(res.getContentType(), StandardCharsets.UTF_8));
|
||||
assertEquals("{\"id\":\"u3\",\"name\":\"carol\"}", payload);
|
||||
|
||||
Response viewRes = new Response(200, ContentType.TEXT_PLAIN);
|
||||
String viewed = json.writeView(viewRes, new ViewDto("u4", "hidden"), PublicView.class);
|
||||
assertEquals("application/json", new String(viewRes.getContentType(), StandardCharsets.UTF_8));
|
||||
assertEquals("{\"id\":\"u4\"}", viewed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapper_returns_underlying_object_mapper_instance() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Json json = new Json(mapper);
|
||||
assertSame(mapper, json.mapper());
|
||||
}
|
||||
|
||||
private static Request request(String body) {
|
||||
return request(body.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static Request request(byte[] body) {
|
||||
RequestLine line = new RequestLine(
|
||||
HttpMethod.POST,
|
||||
new FastPathViews.StringByteView("/json"),
|
||||
null,
|
||||
new FastPathViews.StringByteView("HTTP/1.1"),
|
||||
new HeaderMap()
|
||||
);
|
||||
return new Request(line, body);
|
||||
}
|
||||
|
||||
private static final class UserDto {
|
||||
public String id;
|
||||
public String name;
|
||||
|
||||
public UserDto() {}
|
||||
|
||||
private UserDto(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
private interface PublicView {}
|
||||
private interface InternalView {}
|
||||
|
||||
private static final class ViewDto {
|
||||
@JsonView(PublicView.class)
|
||||
public String id;
|
||||
@JsonView(InternalView.class)
|
||||
public String secret;
|
||||
|
||||
private ViewDto(String id, String secret) {
|
||||
this.id = id;
|
||||
this.secret = secret;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user