Skip to content

Commit

Permalink
[http-client] Add SingleBodyAdapter for client API with only a single…
Browse files Browse the repository at this point in the history
… body type (#533)

This provides a way to minimise the dependencies of an HttpClient that only has a single body type to support.
  • Loading branch information
rbygrave authored Dec 16, 2024
1 parent 5d0852c commit 97fe4b6
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 11 deletions.
9 changes: 8 additions & 1 deletion http-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,17 @@
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jsonb</artifactId>
<version>2.4</version>
<version>3.0-RC5</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-node</artifactId>
<version>3.0-RC5</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
Expand Down
97 changes: 97 additions & 0 deletions http-client/src/main/java/io/avaje/http/client/DSingleAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.avaje.http.client;

import io.avaje.http.client.SingleBodyAdapter.JsonBodyAdapter;
import io.avaje.json.simple.SimpleMapper;

import java.util.List;

@SuppressWarnings("unchecked")
final class DSingleAdapter implements BodyAdapter {

private final ReaderWriter<?> adapter;

static BodyAdapter of(SimpleMapper.Type<?> jsonType) {
return new DSingleAdapter(toAdapter(jsonType));
}

static BodyAdapter of(JsonBodyAdapter<?> source) {
return new DSingleAdapter(source);
}

private DSingleAdapter(JsonBodyAdapter<?> source) {
this.adapter = new ReaderWriter<>(source);
}

private static <T> JsonBodyAdapter<T> toAdapter(SimpleMapper.Type<T> jsonType) {
return new SimpleJsonAdapter<>(jsonType);
}

@Override
public <T> BodyWriter<T> beanWriter(Class<?> aClass) {
return (BodyWriter<T>) adapter;
}

@Override
public <T> BodyReader<T> beanReader(Class<T> aClass) {
return (BodyReader<T>) adapter;
}

@Override
public <T> BodyReader<List<T>> listReader(Class<T> aClass) {
return (BodyReader<List<T>>) adapter;
}

private static final class ReaderWriter<T> implements BodyReader<T>, BodyWriter<T> {

private final JsonBodyAdapter<T> adapter;

ReaderWriter(JsonBodyAdapter<T> adapter) {
this.adapter = adapter;
}

@Override
public T readBody(String content) {
return adapter.fromJsonString(content);
}

@Override
public T read(BodyContent bodyContent) {
return adapter.fromJsonBytes(bodyContent.content());
}

@Override
public BodyContent write(T bean, String contentType) {
return BodyContent.asJson(adapter.toJsonBytes(bean));
}

@Override
public BodyContent write(T bean) {
return BodyContent.asJson(adapter.toJsonBytes(bean));
}
}

private static final class SimpleJsonAdapter<T> implements JsonBodyAdapter<T> {

private final SimpleMapper.Type<T> jsonType;

public SimpleJsonAdapter(SimpleMapper.Type<T> jsonType) {
this.jsonType = jsonType;
}

@Override
public T fromJsonString(String json) {
return jsonType.fromJson(json);
}

@Override
public T fromJsonBytes(byte[] bytes) {
return jsonType.fromJson(bytes);
}

@Override
public byte[] toJsonBytes(T bean) {
return jsonType.toJsonBytes(bean);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.avaje.http.client;

import io.avaje.json.simple.SimpleMapper;

/**
* A BodyAdapter that supports converting the request/response body to a single type.
* <p>
* Useful for an endpoint that only returns a single JSON response type.
*/
public interface SingleBodyAdapter extends BodyAdapter {

/**
* Create with an json content adapter for a single java type.
*
* @param jsonAdapter The adapter to use to read and write the body content.
* @return The BodyAdapter that the HttpClient can use.
*/
static BodyAdapter create(JsonBodyAdapter<?> jsonAdapter) {
return DSingleAdapter.of(jsonAdapter);
}

/**
* Create using an avaje-json-core simple mapper type.
*
* @param jsonType The only type supported to read or write the body content.
* @return The BodyAdapter that the HttpClient can use.
*/
static BodyAdapter create(SimpleMapper.Type<?> jsonType) {
return DSingleAdapter.of(jsonType);
}

/**
* Json body reading and writing for a single type.
*
* @param <T> The Java type of the request or response body.
*/
interface JsonBodyAdapter<T> {

/**
* Read the raw content String and return the java type.
*/
T fromJsonString(String json);

/**
* Read the raw content bytes and return the java type.
*/
T fromJsonBytes(byte[] bytes);

/**
* Write the java type to bytes.
*/
byte[] toJsonBytes(T bean);
}
}
12 changes: 8 additions & 4 deletions http-client/src/test/java/io/avaje/http/client/BaseWebTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ public static void shutdown() {
webServer.stop();
}

public static HttpClient client() {
public static HttpClient client(BodyAdapter bodyAdapter) {
return HttpClient.builder()
.baseUrl(baseUrl)
.connectionTimeout(Duration.ofSeconds(1))
.requestTimeout(Duration.ofSeconds(1))
.bodyAdapter(new JacksonBodyAdapter(new ObjectMapper()))
.connectionTimeout(Duration.ofSeconds(10))
.requestTimeout(Duration.ofSeconds(10))
.bodyAdapter(bodyAdapter)
.build();
}

public static HttpClient client() {
return client(new JacksonBodyAdapter(new ObjectMapper()));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.avaje.http.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.simple.SimpleMapper;
import org.example.webserver.ErrorResponse;
import org.example.webserver.HelloDto;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -818,6 +819,31 @@ void get_bean_404() {
}
}

@Test
void singleBodyAdapter_returningBean() {
var simpleMapper = SimpleMapper.builder().build();

SimpleMapper.Type<HelloDto> type = simpleMapper.type(new HelloDtoAdapter());
BodyAdapter bodyAdapter = SingleBodyAdapter.create(type);
HttpClient client = client(bodyAdapter);

final var str = client.request()
.path("hello/43/2020-03-05").queryParam("otherParam", "other").queryParam("foo", (Object) null)
.GET()
.asString();

System.out.println(str.body());

final HelloDto dto = client.request()
.path("hello/43/2020-03-05").queryParam("otherParam", "other").queryParam("foo", (Object) null)
.GET()
.bean(HelloDto.class);

assertThat(dto.id).isEqualTo(43L);
assertThat(dto.name).isEqualTo("2020-03-05");
assertThat(dto.otherParam).isEqualTo("other");
}

@Test
void get_withPathParamAndQueryParam_returningBean() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.avaje.http.client;

import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.avaje.json.node.JsonNodeMapper;
import io.avaje.json.node.JsonObject;
import org.example.webserver.HelloDto;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

final class HelloDtoAdapter implements JsonAdapter<HelloDto> {

private final JsonNodeMapper nodeMapper;

HelloDtoAdapter() {
nodeMapper = JsonNodeMapper.builder().build();
}

@Override
public void toJson(JsonWriter writer, HelloDto value) {
throw new UnsupportedOperationException();
}

@Override
public HelloDto fromJson(JsonReader reader) {
JsonObject jsonObject = nodeMapper.objectMapper().fromJson(reader);

int id = jsonObject.extract("id", 0);
String name = jsonObject.extract("name", "");
String otherParam = jsonObject.extract("otherParam", "");

var hello = new HelloDto(id, name, otherParam);
UUID gid = jsonObject.extractOrEmpty("gid")
.map(UUID::fromString)
.orElse(null);
hello.setGid(gid);

String when = jsonObject.extract("whenAction", (String) null);
if (when != null) {
hello.setWhenAction(Instant.parse(when));
}
return hello;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.example.github;

import io.avaje.jsonb.JsonAdapter;
import io.avaje.jsonb.JsonReader;
import io.avaje.jsonb.JsonWriter;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.spi.PropertyNames;
import io.avaje.jsonb.spi.ViewBuilder;
import io.avaje.jsonb.spi.ViewBuilderAware;
import io.avaje.json.PropertyNames;
import io.avaje.json.view.ViewBuilder;
import io.avaje.json.view.ViewBuilderAware;

import java.lang.invoke.MethodHandle;

Expand Down

0 comments on commit 97fe4b6

Please sign in to comment.