diff --git a/http-client-gson-adapter/src/main/java/module-info.java b/http-client-gson-adapter/src/main/java/module-info.java index 9933af689..0a9b1f4e4 100644 --- a/http-client-gson-adapter/src/main/java/module-info.java +++ b/http-client-gson-adapter/src/main/java/module-info.java @@ -2,6 +2,6 @@ exports io.avaje.http.client.gson; - requires io.avaje.http.client; - requires com.google.gson; + requires transitive io.avaje.http.client; + requires transitive com.google.gson; } diff --git a/http-client-moshi-adapter/pom.xml b/http-client-moshi-adapter/pom.xml new file mode 100644 index 000000000..152e5dcae --- /dev/null +++ b/http-client-moshi-adapter/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + io.avaje + avaje-http-parent + 2.4 + + avaje-http-client-moshi + + + + + com.squareup.moshi + moshi + 1.15.1 + true + + + + io.avaje + avaje-http-client + ${project.version} + provided + + + + + + io.avaje + junit + 1.4 + test + + + + + \ No newline at end of file diff --git a/http-client-moshi-adapter/src/main/java/io/avaje/http/client/moshi/MoshiBodyAdapter.java b/http-client-moshi-adapter/src/main/java/io/avaje/http/client/moshi/MoshiBodyAdapter.java new file mode 100644 index 000000000..80e754bc5 --- /dev/null +++ b/http-client-moshi-adapter/src/main/java/io/avaje/http/client/moshi/MoshiBodyAdapter.java @@ -0,0 +1,139 @@ +package io.avaje.http.client.moshi; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import com.squareup.moshi.Types; + +import io.avaje.http.client.BodyAdapter; +import io.avaje.http.client.BodyContent; +import io.avaje.http.client.BodyReader; +import io.avaje.http.client.BodyWriter; + +/** + * Moshi BodyAdapter to read and write beans as JSON. + * + *
{@code
+ * HttpClient.builder()
+ *     .baseUrl(baseUrl)
+ *     .bodyAdapter(new MoshiBodyAdapter())
+ *     .build();
+ *
+ * }
+ */ +public final class MoshiBodyAdapter implements BodyAdapter { + + private final Moshi moshi; + private final ConcurrentHashMap> beanWriterCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> beanReaderCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> listReaderCache = new ConcurrentHashMap<>(); + + /** + * Create passing the Moshi to use. + */ + public MoshiBodyAdapter(Moshi moshi) { + this.moshi = moshi; + } + + /** + * Create with a default Moshi that allows unknown properties. + */ + public MoshiBodyAdapter() { + this(new Moshi.Builder().build()); + } + + @SuppressWarnings("unchecked") + @Override + public BodyWriter beanWriter(Class cls) { + return (BodyWriter) beanWriterCache.computeIfAbsent(cls, aClass -> new JWriter<>(moshi.adapter(cls))); + } + + @SuppressWarnings("unchecked") + @Override + public BodyWriter beanWriter(Type type) { + return (BodyWriter) beanWriterCache.computeIfAbsent(type, aClass -> new JWriter<>(moshi.adapter(type))); + } + + @SuppressWarnings("unchecked") + @Override + public BodyReader beanReader(Class cls) { + return (BodyReader) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(moshi.adapter(cls))); + } + + @SuppressWarnings("unchecked") + @Override + public BodyReader beanReader(Type type) { + return (BodyReader) beanReaderCache.computeIfAbsent(type, aClass -> new JReader<>(moshi.adapter(type))); + } + + @SuppressWarnings("unchecked") + @Override + public BodyReader> listReader(Type type) { + return (BodyReader>) + listReaderCache.computeIfAbsent( + type, + aClass -> new JReader<>(moshi.adapter(Types.newParameterizedType(List.class, type)))); + } + + @SuppressWarnings("unchecked") + @Override + public BodyReader> listReader(Class cls) { + return (BodyReader>) + listReaderCache.computeIfAbsent( + cls, + aClass -> new JReader<>(moshi.adapter(Types.newParameterizedType(List.class, cls)))); + } + + private static class JReader implements BodyReader { + + private final JsonAdapter reader; + + JReader(JsonAdapter reader) { + this.reader = reader; + } + + @Override + public T readBody(String content) { + try { + return reader.fromJson(content); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T read(BodyContent bodyContent) { + try { + return reader.fromJson(bodyContent.contentAsUtf8()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static class JWriter implements BodyWriter { + + private final JsonAdapter writer; + + public JWriter(JsonAdapter writer) { + this.writer = writer; + } + + @Override + public BodyContent write(T bean, String contentType) { + // ignoring the requested contentType and always + // writing the body as json content + return write(bean); + } + + @Override + public BodyContent write(T bean) { + return BodyContent.of(writer.toJson(bean)); + } + } +} diff --git a/http-client-moshi-adapter/src/main/java/module-info.java b/http-client-moshi-adapter/src/main/java/module-info.java new file mode 100644 index 000000000..4c68c474e --- /dev/null +++ b/http-client-moshi-adapter/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module io.avaje.http.client.gson { + + exports io.avaje.http.client.moshi; + + requires transitive io.avaje.http.client; + requires transitive com.squareup.moshi; +} diff --git a/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/Foo.java b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/Foo.java new file mode 100644 index 000000000..ec78ac267 --- /dev/null +++ b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/Foo.java @@ -0,0 +1,9 @@ +package io.avaje.http.client.moshi; + +public class Foo { + + public long id; + + public String name; + +} diff --git a/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/MoshiBodyAdapterTest.java b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/MoshiBodyAdapterTest.java new file mode 100644 index 000000000..1dcc4649a --- /dev/null +++ b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/MoshiBodyAdapterTest.java @@ -0,0 +1,58 @@ +package io.avaje.http.client.moshi; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.avaje.http.client.BodyContent; +import io.avaje.http.client.BodyReader; +import io.avaje.http.client.BodyWriter; + +class MoshiBodyAdapterTest { + + private final MoshiBodyAdapter adapter = new MoshiBodyAdapter(); + + @Test + void beanWriter() { + + final Foo foo = new Foo(); + foo.id = 42; + foo.name = "bar"; + + final BodyWriter writer = adapter.beanWriter(Foo.class); + final BodyContent content = writer.write(foo); + + final String json = new String(content.content(), StandardCharsets.UTF_8); + assertEquals("{\"id\":42,\"name\":\"bar\"}", json); + } + + @Test + void beanReader() { + + final BodyReader reader = adapter.beanReader(Foo.class); + + final Foo read = reader.read(content("{\"id\":42, \"name\":\"bar\"}")); + assertEquals(42, read.id); + assertEquals("bar", read.name); + } + + @Test + void listReader() { + + final BodyReader> reader = adapter.listReader(Foo.class); + + final List read = + reader.read(content("[{\"id\":42, \"name\":\"bar\"},{\"id\":43, \"name\":\"baz\"}]")); + + assertEquals(2, read.size()); + assertEquals(42, read.get(0).id); + assertEquals(43, read.get(1).id); + } + + BodyContent content(String raw) { + return BodyContent.of(raw.getBytes()); + } +} diff --git a/pom.xml b/pom.xml index 93d149d0f..2f73594d7 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,7 @@ http-api-javalin http-client http-client-gson-adapter + http-client-moshi-adapter http-inject-plugin http-generator-core http-generator-javalin