Skip to content

Commit 7ebbc5b

Browse files
SentryManrbygrave
andauthored
[http-client] Adds Moshi Body Adapter (#424)
* adds moshi adapter * Move moshi module to be consistent with gson module * Use UncheckedIOException and format --------- Co-authored-by: Rob Bygrave <[email protected]>
1 parent bf57b5c commit 7ebbc5b

File tree

7 files changed

+253
-2
lines changed

7 files changed

+253
-2
lines changed

http-client-gson-adapter/src/main/java/module-info.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
exports io.avaje.http.client.gson;
44

5-
requires io.avaje.http.client;
6-
requires com.google.gson;
5+
requires transitive io.avaje.http.client;
6+
requires transitive com.google.gson;
77
}

http-client-moshi-adapter/pom.xml

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<modelVersion>4.0.0</modelVersion>
3+
<parent>
4+
<groupId>io.avaje</groupId>
5+
<artifactId>avaje-http-parent</artifactId>
6+
<version>2.4</version>
7+
</parent>
8+
<artifactId>avaje-http-client-moshi</artifactId>
9+
10+
<dependencies>
11+
12+
<dependency>
13+
<groupId>com.squareup.moshi</groupId>
14+
<artifactId>moshi</artifactId>
15+
<version>1.15.1</version>
16+
<optional>true</optional>
17+
</dependency>
18+
19+
<dependency>
20+
<groupId>io.avaje</groupId>
21+
<artifactId>avaje-http-client</artifactId>
22+
<version>${project.version}</version>
23+
<scope>provided</scope>
24+
</dependency>
25+
26+
<!-- test dependencies -->
27+
28+
<dependency>
29+
<groupId>io.avaje</groupId>
30+
<artifactId>junit</artifactId>
31+
<version>1.4</version>
32+
<scope>test</scope>
33+
</dependency>
34+
35+
</dependencies>
36+
37+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package io.avaje.http.client.moshi;
2+
3+
import java.io.IOException;
4+
import java.io.UncheckedIOException;
5+
import java.lang.reflect.Type;
6+
import java.util.List;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
9+
import com.squareup.moshi.JsonAdapter;
10+
import com.squareup.moshi.Moshi;
11+
import com.squareup.moshi.Types;
12+
13+
import io.avaje.http.client.BodyAdapter;
14+
import io.avaje.http.client.BodyContent;
15+
import io.avaje.http.client.BodyReader;
16+
import io.avaje.http.client.BodyWriter;
17+
18+
/**
19+
* Moshi BodyAdapter to read and write beans as JSON.
20+
*
21+
* <pre>{@code
22+
* HttpClient.builder()
23+
* .baseUrl(baseUrl)
24+
* .bodyAdapter(new MoshiBodyAdapter())
25+
* .build();
26+
*
27+
* }</pre>
28+
*/
29+
public final class MoshiBodyAdapter implements BodyAdapter {
30+
31+
private final Moshi moshi;
32+
private final ConcurrentHashMap<Type, BodyWriter<?>> beanWriterCache = new ConcurrentHashMap<>();
33+
private final ConcurrentHashMap<Type, BodyReader<?>> beanReaderCache = new ConcurrentHashMap<>();
34+
private final ConcurrentHashMap<Type, BodyReader<?>> listReaderCache = new ConcurrentHashMap<>();
35+
36+
/**
37+
* Create passing the Moshi to use.
38+
*/
39+
public MoshiBodyAdapter(Moshi moshi) {
40+
this.moshi = moshi;
41+
}
42+
43+
/**
44+
* Create with a default Moshi that allows unknown properties.
45+
*/
46+
public MoshiBodyAdapter() {
47+
this(new Moshi.Builder().build());
48+
}
49+
50+
@SuppressWarnings("unchecked")
51+
@Override
52+
public <T> BodyWriter<T> beanWriter(Class<?> cls) {
53+
return (BodyWriter<T>) beanWriterCache.computeIfAbsent(cls, aClass -> new JWriter<>(moshi.adapter(cls)));
54+
}
55+
56+
@SuppressWarnings("unchecked")
57+
@Override
58+
public <T> BodyWriter<T> beanWriter(Type type) {
59+
return (BodyWriter<T>) beanWriterCache.computeIfAbsent(type, aClass -> new JWriter<>(moshi.adapter(type)));
60+
}
61+
62+
@SuppressWarnings("unchecked")
63+
@Override
64+
public <T> BodyReader<T> beanReader(Class<T> cls) {
65+
return (BodyReader<T>) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(moshi.adapter(cls)));
66+
}
67+
68+
@SuppressWarnings("unchecked")
69+
@Override
70+
public <T> BodyReader<T> beanReader(Type type) {
71+
return (BodyReader<T>) beanReaderCache.computeIfAbsent(type, aClass -> new JReader<>(moshi.adapter(type)));
72+
}
73+
74+
@SuppressWarnings("unchecked")
75+
@Override
76+
public <T> BodyReader<List<T>> listReader(Type type) {
77+
return (BodyReader<List<T>>)
78+
listReaderCache.computeIfAbsent(
79+
type,
80+
aClass -> new JReader<>(moshi.adapter(Types.newParameterizedType(List.class, type))));
81+
}
82+
83+
@SuppressWarnings("unchecked")
84+
@Override
85+
public <T> BodyReader<List<T>> listReader(Class<T> cls) {
86+
return (BodyReader<List<T>>)
87+
listReaderCache.computeIfAbsent(
88+
cls,
89+
aClass -> new JReader<>(moshi.adapter(Types.newParameterizedType(List.class, cls))));
90+
}
91+
92+
private static class JReader<T> implements BodyReader<T> {
93+
94+
private final JsonAdapter<T> reader;
95+
96+
JReader(JsonAdapter<T> reader) {
97+
this.reader = reader;
98+
}
99+
100+
@Override
101+
public T readBody(String content) {
102+
try {
103+
return reader.fromJson(content);
104+
} catch (IOException e) {
105+
throw new UncheckedIOException(e);
106+
}
107+
}
108+
109+
@Override
110+
public T read(BodyContent bodyContent) {
111+
try {
112+
return reader.fromJson(bodyContent.contentAsUtf8());
113+
} catch (IOException e) {
114+
throw new UncheckedIOException(e);
115+
}
116+
}
117+
}
118+
119+
private static class JWriter<T> implements BodyWriter<T> {
120+
121+
private final JsonAdapter<T> writer;
122+
123+
public JWriter(JsonAdapter<T> writer) {
124+
this.writer = writer;
125+
}
126+
127+
@Override
128+
public BodyContent write(T bean, String contentType) {
129+
// ignoring the requested contentType and always
130+
// writing the body as json content
131+
return write(bean);
132+
}
133+
134+
@Override
135+
public BodyContent write(T bean) {
136+
return BodyContent.of(writer.toJson(bean));
137+
}
138+
}
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module io.avaje.http.client.gson {
2+
3+
exports io.avaje.http.client.moshi;
4+
5+
requires transitive io.avaje.http.client;
6+
requires transitive com.squareup.moshi;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.avaje.http.client.moshi;
2+
3+
public class Foo {
4+
5+
public long id;
6+
7+
public String name;
8+
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.avaje.http.client.moshi;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.nio.charset.StandardCharsets;
6+
import java.util.List;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import io.avaje.http.client.BodyContent;
11+
import io.avaje.http.client.BodyReader;
12+
import io.avaje.http.client.BodyWriter;
13+
14+
class MoshiBodyAdapterTest {
15+
16+
private final MoshiBodyAdapter adapter = new MoshiBodyAdapter();
17+
18+
@Test
19+
void beanWriter() {
20+
21+
final Foo foo = new Foo();
22+
foo.id = 42;
23+
foo.name = "bar";
24+
25+
final BodyWriter writer = adapter.beanWriter(Foo.class);
26+
final BodyContent content = writer.write(foo);
27+
28+
final String json = new String(content.content(), StandardCharsets.UTF_8);
29+
assertEquals("{\"id\":42,\"name\":\"bar\"}", json);
30+
}
31+
32+
@Test
33+
void beanReader() {
34+
35+
final BodyReader<Foo> reader = adapter.beanReader(Foo.class);
36+
37+
final Foo read = reader.read(content("{\"id\":42, \"name\":\"bar\"}"));
38+
assertEquals(42, read.id);
39+
assertEquals("bar", read.name);
40+
}
41+
42+
@Test
43+
void listReader() {
44+
45+
final BodyReader<List<Foo>> reader = adapter.listReader(Foo.class);
46+
47+
final List<Foo> read =
48+
reader.read(content("[{\"id\":42, \"name\":\"bar\"},{\"id\":43, \"name\":\"baz\"}]"));
49+
50+
assertEquals(2, read.size());
51+
assertEquals(42, read.get(0).id);
52+
assertEquals(43, read.get(1).id);
53+
}
54+
55+
BodyContent content(String raw) {
56+
return BodyContent.of(raw.getBytes());
57+
}
58+
}

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<module>http-api-javalin</module>
4040
<module>http-client</module>
4141
<module>http-client-gson-adapter</module>
42+
<module>http-client-moshi-adapter</module>
4243
<module>http-inject-plugin</module>
4344
<module>http-generator-core</module>
4445
<module>http-generator-javalin</module>

0 commit comments

Comments
 (0)