Skip to content

Commit cf07649

Browse files
committed
add retrofit-converter source & test source
1 parent 3ce9ccf commit cf07649

File tree

5 files changed

+367
-1
lines changed

5 files changed

+367
-1
lines changed

retrofit-converter/build.gradle

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
plugins {
2+
id 'java'
3+
id 'idea'
4+
id 'maven'
5+
}
6+
7+
apply plugin: 'com.novoda.bintray-release'
8+
9+
sourceCompatibility = '1.7'
10+
targetCompatibility = '1.7'
11+
12+
sourceSets {
13+
test {
14+
resources {
15+
srcDir "resources"
16+
includes["**/*.json"]
17+
}
18+
}
19+
}
20+
21+
dependencies {
22+
compileOnly project(':core')
23+
compileOnly 'com.squareup.moshi:moshi:1.4.0'
24+
compileOnly 'com.squareup.retrofit2:retrofit:2.4.0'
25+
testCompile project(':core')
26+
testCompile project(':core').sourceSets.test.output
27+
testCompile 'com.squareup.moshi:moshi:1.4.0'
28+
testCompile 'com.squareup.retrofit2:retrofit:2.4.0'
29+
testCompile "org.mock-server:mockserver-netty:5.3.0"
30+
testCompile 'junit:junit:4.12'
31+
}
32+
33+
test {
34+
testLogging {
35+
events "passed", "skipped", "failed", "standardOut", "standardError"
36+
}
37+
}
38+
39+
task sourcesJar(type: Jar, dependsOn: classes) {
40+
classifier 'sources'
41+
from sourceSets.main.allSource
42+
}
43+
44+
task javadocJar(type: Jar, dependsOn: javadoc) {
45+
classifier 'javadoc'
46+
from javadoc.destinationDir
47+
}
48+
49+
artifacts {
50+
archives sourcesJar
51+
archives javadocJar
52+
}
53+
54+
publish {
55+
groupId = 'moe.banana'
56+
artifactId = 'moshi-jsonapi-retrofit-converter'
57+
publishVersion = describeVersion()
58+
desc = 'Retrofit converter extension for moshi-jsonapi library.'
59+
website = 'https://github.com/kamikat/moshi-jsonapi'
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package moe.banana.jsonapi2;
2+
3+
import com.squareup.moshi.JsonAdapter;
4+
import com.squareup.moshi.Moshi;
5+
import com.squareup.moshi.Types;
6+
7+
import java.io.IOException;
8+
import java.lang.annotation.Annotation;
9+
import java.lang.reflect.Array;
10+
import java.lang.reflect.ParameterizedType;
11+
import java.lang.reflect.Type;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
15+
import moe.banana.jsonapi2.ArrayDocument;
16+
import moe.banana.jsonapi2.Document;
17+
import moe.banana.jsonapi2.ObjectDocument;
18+
import moe.banana.jsonapi2.Resource;
19+
import moe.banana.jsonapi2.ResourceIdentifier;
20+
import okhttp3.MediaType;
21+
import okhttp3.RequestBody;
22+
import okhttp3.ResponseBody;
23+
import okio.Buffer;
24+
import retrofit2.Converter;
25+
import retrofit2.Retrofit;
26+
27+
@SuppressWarnings("unchecked")
28+
public final class JsonApiConverterFactory extends Converter.Factory {
29+
30+
public static JsonApiConverterFactory create() {
31+
return create(new Moshi.Builder().build());
32+
}
33+
34+
public static JsonApiConverterFactory create(Moshi moshi) {
35+
return new JsonApiConverterFactory(moshi, false);
36+
}
37+
38+
private final Moshi moshi;
39+
private final boolean lenient;
40+
41+
private JsonApiConverterFactory(Moshi moshi, boolean lenient) {
42+
if (moshi == null) throw new NullPointerException("moshi == null");
43+
this.moshi = moshi;
44+
this.lenient = lenient;
45+
}
46+
47+
public JsonApiConverterFactory asLenient() {
48+
return new JsonApiConverterFactory(moshi, true);
49+
}
50+
51+
private JsonAdapter<?> getAdapterFromType(Type type) {
52+
Class<?> rawType = Types.getRawType(type);
53+
JsonAdapter<?> adapter;
54+
if (rawType.isArray() && ResourceIdentifier.class.isAssignableFrom(rawType.getComponentType())) {
55+
adapter = moshi.adapter(Types.newParameterizedType(Document.class, rawType.getComponentType()));
56+
} else if (List.class.isAssignableFrom(rawType) && type instanceof ParameterizedType) {
57+
Type typeParameter = ((ParameterizedType) type).getActualTypeArguments()[0];
58+
if (typeParameter instanceof Class<?> && ResourceIdentifier.class.isAssignableFrom((Class<?>) typeParameter)) {
59+
adapter = moshi.adapter(Types.newParameterizedType(Document.class, typeParameter));
60+
} else {
61+
return null;
62+
}
63+
} else if (ResourceIdentifier.class.isAssignableFrom(rawType)) {
64+
adapter = moshi.adapter(Types.newParameterizedType(Document.class, rawType));
65+
} else if (Document.class.isAssignableFrom(rawType)) {
66+
adapter = moshi.adapter(Types.newParameterizedType(Document.class, Resource.class));
67+
} else {
68+
return null;
69+
}
70+
return adapter;
71+
}
72+
73+
@Override
74+
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
75+
JsonAdapter<?> adapter = getAdapterFromType(type);
76+
if (adapter == null) {
77+
return null;
78+
}
79+
if (lenient) {
80+
adapter = adapter.lenient();
81+
}
82+
return new MoshiResponseBodyConverter<>((JsonAdapter<Document>) adapter, type);
83+
}
84+
85+
@Override
86+
public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
87+
JsonAdapter<?> adapter = getAdapterFromType(type);
88+
if (adapter == null) {
89+
return null;
90+
}
91+
if (lenient) {
92+
adapter = adapter.lenient();
93+
}
94+
return new MoshiRequestBodyConverter<>((JsonAdapter<Document>) adapter, type);
95+
}
96+
97+
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
98+
99+
private static class MoshiResponseBodyConverter<R> implements Converter<ResponseBody, R> {
100+
private final JsonAdapter<Document> adapter;
101+
private final Class<R> rawType;
102+
103+
MoshiResponseBodyConverter(JsonAdapter<Document> adapter, Type type) {
104+
this.adapter = adapter;
105+
this.rawType = (Class<R>) Types.getRawType(type);
106+
}
107+
108+
@Override
109+
public R convert(ResponseBody value) throws IOException {
110+
try {
111+
Document document = adapter.fromJson(value.source());
112+
if (Document.class.isAssignableFrom(rawType)) {
113+
return (R) document;
114+
} else if (List.class.isAssignableFrom(rawType)) {
115+
ArrayDocument arrayDocument = document.asArrayDocument();
116+
List a;
117+
if (rawType.isAssignableFrom(ArrayList.class)) {
118+
a = new ArrayList();
119+
} else {
120+
a = (List) rawType.newInstance();
121+
}
122+
a.addAll(arrayDocument);
123+
return (R) a;
124+
} else if (rawType.isArray()) {
125+
ArrayDocument<?> arrayDocument = document.asArrayDocument();
126+
Object a = Array.newInstance(rawType.getComponentType(), arrayDocument.size());
127+
for (int i = 0; i != Array.getLength(a); i++) {
128+
Array.set(a, i, arrayDocument.get(i));
129+
}
130+
return (R) a;
131+
} else {
132+
return (R) document.asObjectDocument().get();
133+
}
134+
} catch (InstantiationException e) {
135+
throw new RuntimeException("Cannot find default constructor of [" + rawType.getCanonicalName() + "].", e);
136+
} catch (IllegalAccessException e) {
137+
throw new RuntimeException("Cannot access default constructor of [" + rawType.getCanonicalName() + "].", e);
138+
} finally {
139+
value.close();
140+
}
141+
}
142+
}
143+
144+
private static class MoshiRequestBodyConverter<T> implements Converter<T, RequestBody> {
145+
146+
private final JsonAdapter<Document> adapter;
147+
private final Class<T> rawType;
148+
149+
MoshiRequestBodyConverter(JsonAdapter<Document> adapter, Type type) {
150+
this.adapter = adapter;
151+
this.rawType = (Class<T>) Types.getRawType(type);
152+
}
153+
154+
@Override
155+
public RequestBody convert(T value) throws IOException {
156+
Document document;
157+
if (Document.class.isAssignableFrom(rawType)) {
158+
document = (Document) value;
159+
} else if (List.class.isAssignableFrom(rawType)) {
160+
ArrayDocument arrayDocument = new ArrayDocument();
161+
List a = ((List) value);
162+
if (!a.isEmpty() && a.get(0) != null && ((ResourceIdentifier) a.get(0)).getContext() != null) {
163+
arrayDocument = ((ResourceIdentifier) a.get(0)).getContext().asArrayDocument();
164+
}
165+
arrayDocument.addAll(a);
166+
document = arrayDocument;
167+
} else if (rawType.isArray()) {
168+
ArrayDocument arrayDocument = new ArrayDocument();
169+
if (Array.getLength(value) > 0 && ((ResourceIdentifier) Array.get(value, 0)).getContext() != null) {
170+
arrayDocument = ((ResourceIdentifier) Array.get(value, 0)).getContext().asArrayDocument();
171+
}
172+
for (int i = 0; i != Array.getLength(value); i++) {
173+
arrayDocument.add((ResourceIdentifier) Array.get(value, i));
174+
}
175+
document = arrayDocument;
176+
} else {
177+
ResourceIdentifier data = ((ResourceIdentifier) value);
178+
ObjectDocument objectDocument = new ObjectDocument();
179+
if (data.getDocument() != null) {
180+
objectDocument = data.getDocument().asObjectDocument();
181+
}
182+
objectDocument.set(data);
183+
document = objectDocument;
184+
}
185+
Buffer buffer = new Buffer();
186+
adapter.toJson(buffer, document);
187+
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
188+
}
189+
}
190+
191+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package moe.banana.jsonapi2;
2+
3+
import com.squareup.moshi.Moshi;
4+
import moe.banana.jsonapi2.model.Article;
5+
import moe.banana.jsonapi2.model.Comment;
6+
import org.hamcrest.CoreMatchers;
7+
import org.hamcrest.Matcher;
8+
import org.junit.Rule;
9+
import org.junit.Test;
10+
import org.mockserver.client.server.MockServerClient;
11+
import org.mockserver.junit.MockServerRule;
12+
import retrofit2.Retrofit;
13+
14+
import static org.hamcrest.CoreMatchers.*;
15+
import static org.hamcrest.MatcherAssert.assertThat;
16+
import static org.mockserver.model.HttpRequest.request;
17+
import static org.mockserver.model.HttpResponse.response;
18+
19+
@SuppressWarnings("all")
20+
public class ConverterTest {
21+
22+
@Rule
23+
public MockServerRule mockServerRule = new MockServerRule(this);
24+
25+
private TestApi api() throws Exception {
26+
Moshi moshi = TestUtil.moshi(Article.class, Comment.class);
27+
Retrofit retrofit = new Retrofit.Builder()
28+
.baseUrl("http://localhost:" + this.mockServerRule.getPort())
29+
.addConverterFactory(JsonApiConverterFactory.create(moshi))
30+
.build();
31+
return retrofit.create(TestApi.class);
32+
}
33+
34+
private MockServerClient server() {
35+
return mockServerRule.getClient();
36+
}
37+
38+
@Test
39+
public void deserialize_resource_list() throws Exception {
40+
server().when(request("/articles").withMethod("GET"))
41+
.respond(response(TestUtil.fromResource("/multiple_compound.json")));
42+
Article[] articles = api().listArticles().execute().body();
43+
assertThat(articles, notNullValue());
44+
assertThat(articles, instanceOf(Article[].class));
45+
assertThat(articles[0], notNullValue());
46+
assertThat(articles[0].getDocument(), notNullValue());
47+
}
48+
49+
@Test
50+
public void deserialize_resource_object() throws Exception {
51+
server().when(request("/articles/1").withMethod("GET"))
52+
.respond(response(TestUtil.fromResource("/single.json")));
53+
Article article = api().getArticle("1").execute().body();
54+
assertThat(article, notNullValue());
55+
assertThat(article.getId(), equalTo("1"));
56+
assertThat(article.getDocument(), notNullValue());
57+
}
58+
59+
@Test
60+
public void serialize_resource_object() throws Exception {
61+
server().when(request("/articles/1/comments").withMethod("POST").withBody("{\"data\":{\"type\":\"comments\",\"attributes\":{\"body\":\"Awesome!\"}}}"))
62+
.respond(response("{}"));
63+
Comment comment = new Comment();
64+
comment.setBody("Awesome!");
65+
Document response = api().addComment("1", comment).execute().body();
66+
assertThat(response.asObjectDocument().hasData(), equalTo(false));
67+
}
68+
69+
@Test
70+
public void deserialize_relationship_object() throws Exception {
71+
server().when(request("/articles/1/relationships/tags").withMethod("GET"))
72+
.respond(response(TestUtil.fromResource("/relationship_multi.json")));
73+
ResourceIdentifier[] relData = api().getRelTags("1").execute().body();
74+
assertThat(relData.length, equalTo(2));
75+
assertThat(relData[0], instanceOf(ResourceIdentifier.class));
76+
assertThat(relData[1].getType(), equalTo("tags"));
77+
}
78+
79+
@Test
80+
public void serialize_relationship_object() throws Exception {
81+
server().when(request("/articles/1/relationships/author").withMethod("PUT").withBody("{\"data\":{\"type\":\"people\",\"id\":\"1\"}}"))
82+
.respond(response("{}"));
83+
Document response = api().updateAuthor("1", new ResourceIdentifier("people", "1")).execute().body();
84+
assertThat(response.asObjectDocument().hasData(), equalTo(false));
85+
}
86+
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package moe.banana.jsonapi2;
2+
3+
import moe.banana.jsonapi2.model.Article;
4+
import moe.banana.jsonapi2.model.Comment;
5+
import retrofit2.Call;
6+
import retrofit2.http.*;
7+
8+
public interface TestApi {
9+
10+
@GET("articles")
11+
Call<Article[]> listArticles();
12+
13+
@GET("articles/{id}")
14+
Call<Article> getArticle(@Path("id") String id);
15+
16+
@GET("articles/{id}/comments")
17+
Call<Comment[]> getComments(@Path("id") String id);
18+
19+
@POST("articles/{id}/comments")
20+
Call<Document> addComment(@Path("id") String id, @Body Comment comment);
21+
22+
@PUT("articles/{id}/relationships/author")
23+
Call<Document> updateAuthor(@Path("id") String id, @Body ResourceIdentifier authorLinkage);
24+
25+
@GET("articles/{id}/relationships/tags")
26+
Call<ResourceIdentifier[]> getRelTags(@Path("id") String id);
27+
28+
}

settings.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
include 'core'
1+
include 'core', 'retrofit-converter'

0 commit comments

Comments
 (0)