Skip to content

[http-client] Add SingleBodyAdapter for client API with only a single body type #533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading