├── settings.gradle ├── core ├── src │ ├── test │ │ ├── resources │ │ │ ├── errors_empty.json │ │ │ ├── multiple_empty.json │ │ │ ├── single_null.json │ │ │ ├── relationship_multi_empty.json │ │ │ ├── relationship_single_null.json │ │ │ ├── relationship_single.json │ │ │ ├── meta.json │ │ │ ├── relationship_multi.json │ │ │ ├── errors.json │ │ │ ├── single.json │ │ │ ├── multiple_comments.json │ │ │ ├── multiple_polymorphic.json │ │ │ ├── multiple_compound.json │ │ │ └── sample.json │ │ └── java │ │ │ └── moe │ │ │ └── banana │ │ │ └── jsonapi2 │ │ │ ├── MiscellaneousTest.java │ │ │ ├── model │ │ │ ├── Color.java │ │ │ ├── PlainObject.java │ │ │ ├── Comment.java │ │ │ ├── Meta.java │ │ │ ├── Person.java │ │ │ ├── Article.java │ │ │ └── Photo.java │ │ │ ├── MoshiHelperTest.java │ │ │ ├── ErrorTest.java │ │ │ ├── ResourceIdentifierTest.java │ │ │ ├── TestUtil.java │ │ │ ├── HasOneTest.java │ │ │ ├── JsonBufferTest.java │ │ │ ├── HasManyTest.java │ │ │ ├── ResourceTest.java │ │ │ └── DocumentTest.java │ └── main │ │ └── java │ │ └── moe │ │ └── banana │ │ └── jsonapi2 │ │ ├── JsonNameMapping.java │ │ ├── MoshiJsonNameMapping.java │ │ ├── Resource.java │ │ ├── Relationship.java │ │ ├── Policy.java │ │ ├── JsonApi.java │ │ ├── AnnotationUtils.java │ │ ├── ObjectDocument.java │ │ ├── JsonBuffer.java │ │ ├── HasOne.java │ │ ├── ResourceIdentifier.java │ │ ├── ArrayDocument.java │ │ ├── MoshiHelper.java │ │ ├── Error.java │ │ ├── HasMany.java │ │ ├── ResourceAdapter.java │ │ ├── Document.java │ │ └── ResourceAdapterFactory.java └── build.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .travis.yml ├── LICENSE ├── retrofit-converter ├── src │ ├── test │ │ └── java │ │ │ └── moe │ │ │ └── banana │ │ │ └── jsonapi2 │ │ │ ├── TestApi.java │ │ │ └── ConverterTest.java │ └── main │ │ └── java │ │ └── moe │ │ └── banana │ │ └── jsonapi2 │ │ └── JsonApiConverterFactory.java └── build.gradle ├── gradlew.bat ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'core', 'retrofit-converter' 2 | -------------------------------------------------------------------------------- /core/src/test/resources/errors_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | *.iml 4 | build/ 5 | gradle.properties 6 | build.properties 7 | local.properties 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamikat/moshi-jsonapi/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /core/src/test/resources/multiple_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "http://example.com/articles" 4 | }, 5 | "data": [] 6 | } -------------------------------------------------------------------------------- /core/src/test/resources/single_null.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "http://example.com/articles/1/author" 4 | }, 5 | "data": null 6 | } -------------------------------------------------------------------------------- /core/src/test/resources/relationship_multi_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "/articles/1/relationships/tags", 4 | "related": "/articles/1/tags" 5 | }, 6 | "data": [] 7 | } -------------------------------------------------------------------------------- /core/src/test/resources/relationship_single_null.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "/articles/1/relationships/author", 4 | "related": "/articles/1/author" 5 | }, 6 | "data": null 7 | } -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/JsonNameMapping.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | public interface JsonNameMapping { 6 | String getJsonName(Field field); 7 | } 8 | -------------------------------------------------------------------------------- /core/src/test/resources/relationship_single.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "/articles/1/relationships/author", 4 | "related": "/articles/1/author" 5 | }, 6 | "data": { 7 | "type": "people", 8 | "id": "12" 9 | } 10 | } -------------------------------------------------------------------------------- /core/src/test/resources/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "copyright": "Copyright 20XX Example Corp.", 4 | "authors": [ 5 | "Yehuda Katz", 6 | "Steve Klabnik", 7 | "Dan Gebhardt", 8 | "Tyler Kellen" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /core/src/test/resources/relationship_multi.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "/articles/1/relationships/tags", 4 | "related": "/articles/1/tags" 5 | }, 6 | "data": [ 7 | { "type": "tags", "id": "2" }, 8 | { "type": "tags", "id": "3" } 9 | ] 10 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 05 14:00:04 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip 7 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/MiscellaneousTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import org.junit.Test; 4 | 5 | public class MiscellaneousTest { 6 | 7 | @Test 8 | public void exercise_helpers() throws Exception { 9 | new MoshiHelper(); 10 | new AnnotationUtils(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/Color.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import com.squareup.moshi.JsonQualifier; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | 8 | @JsonQualifier 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Color { 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: required 3 | jdk: 4 | - oraclejdk8 5 | - openjdk8 6 | after_success: 7 | - ./gradlew jacocoTestReport coveralls 8 | deploy: 9 | provider: script 10 | script: ./gradlew clean build bintrayUpload -PbintrayUser=$BINTRAY_USER -PbintrayKey=$BINTRAY_KEY -PdryRun=false 11 | on: 12 | tags: true 13 | jdk: openjdk8 14 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/PlainObject.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import moe.banana.jsonapi2.HasMany; 4 | import moe.banana.jsonapi2.HasOne; 5 | import moe.banana.jsonapi2.JsonApi; 6 | import moe.banana.jsonapi2.Resource; 7 | 8 | @JsonApi(type = "articles") 9 | public class PlainObject extends Resource { 10 | public String title; 11 | public HasOne author; 12 | public HasMany comments; 13 | public transient String ignored; 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/MoshiJsonNameMapping.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.Json; 4 | 5 | import java.lang.reflect.Field; 6 | 7 | public class MoshiJsonNameMapping implements JsonNameMapping { 8 | @Override 9 | public String getJsonName(Field field) { 10 | String name = field.getName(); 11 | Json json = field.getAnnotation(Json.class); 12 | if (json != null) { 13 | name = json.name(); 14 | } 15 | return name; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/Resource.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import java.io.Serializable; 4 | 5 | public abstract class Resource extends ResourceIdentifier implements Serializable { 6 | 7 | public Resource() { 8 | setType(AnnotationUtils.typeNameOf(getClass())); 9 | } 10 | 11 | private JsonBuffer links; 12 | 13 | public JsonBuffer getLinks() { 14 | return links; 15 | } 16 | 17 | public void setLinks(JsonBuffer links) { 18 | this.links = links; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/Relationship.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | abstract class Relationship { 4 | 5 | private JsonBuffer meta; 6 | private JsonBuffer links; 7 | 8 | public JsonBuffer getMeta() { 9 | return meta; 10 | } 11 | 12 | public JsonBuffer getLinks() { 13 | return links; 14 | } 15 | 16 | public void setMeta(JsonBuffer meta) { 17 | this.meta = meta; 18 | } 19 | 20 | public void setLinks(JsonBuffer links) { 21 | this.links = links; 22 | } 23 | 24 | public abstract RESULT get(Document document); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/Comment.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import moe.banana.jsonapi2.HasOne; 4 | import moe.banana.jsonapi2.JsonApi; 5 | import moe.banana.jsonapi2.Resource; 6 | 7 | @JsonApi(type = "comments") 8 | public class Comment extends Resource { 9 | private String body; 10 | private HasOne author; 11 | 12 | public String getBody() { 13 | return body; 14 | } 15 | 16 | public void setBody(String body) { 17 | this.body = body; 18 | } 19 | 20 | public HasOne getAuthor() { 21 | return author; 22 | } 23 | 24 | public void setAuthor(HasOne author) { 25 | this.author = author; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/Policy.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | public enum Policy { 4 | 5 | /** 6 | * Registered class is intended to be used in both serialization and deserialization process. 7 | * Other class with same type name can only be registered with {@link #SERIALIZATION_ONLY} policy. 8 | */ 9 | SERIALIZATION_AND_DESERIALIZATION, 10 | 11 | /** 12 | * Registered class is available only when doing serialization. 13 | */ 14 | SERIALIZATION_ONLY, 15 | 16 | /** 17 | * Registered class is available only when doing de-serialization. 18 | * Other class with same type name can only be registered with {@link #SERIALIZATION_ONLY} policy. 19 | */ 20 | DESERIALIZATION_ONLY 21 | 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/Meta.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import java.util.List; 4 | 5 | @SuppressWarnings("all") 6 | public class Meta { 7 | public String copyright; 8 | public List authors; 9 | 10 | @Override 11 | public boolean equals(Object o) { 12 | if (this == o) return true; 13 | if (o == null || getClass() != o.getClass()) return false; 14 | 15 | Meta meta = (Meta) o; 16 | 17 | if (copyright != null ? !copyright.equals(meta.copyright) : meta.copyright != null) return false; 18 | return authors != null ? authors.equals(meta.authors) : meta.authors == null; 19 | 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | int result = copyright != null ? copyright.hashCode() : 0; 25 | result = 31 * result + (authors != null ? authors.hashCode() : 0); 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/resources/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "id": "1", 5 | "status": "400", 6 | "code": "400030", 7 | "title": "Bad request", 8 | "detail": "Unexpected character '<' in string value", 9 | "source": { 10 | "pointer": "/data/attributes/title" 11 | }, 12 | "meta": { 13 | "query_time": 12 14 | }, 15 | "links": { 16 | "about": "http://example.com/support/T19821001" 17 | } 18 | }, 19 | { 20 | "id": "2", 21 | "status": "400", 22 | "code": "400030", 23 | "title": "Bad request", 24 | "detail": "Unexpected character '>' in string value", 25 | "source": { 26 | "pointer": "/data/attributes/title" 27 | }, 28 | "meta": { 29 | "query_time": 12 30 | }, 31 | "links": { 32 | "about": "http://example.com/support/T19821001" 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/resources/single.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "http://example.com/articles/1" 4 | }, 5 | "data": { 6 | "type": "articles", 7 | "id": "1", 8 | "attributes": { 9 | "title": "JSON API paints my bikeshed!" 10 | }, 11 | "links": { 12 | "self": "http://example.com/articles/1" 13 | }, 14 | "relationships": { 15 | "author": { 16 | "links": { 17 | "self": "http://example.com/articles/1/relationships/author", 18 | "related": "http://example.com/articles/1/author" 19 | }, 20 | "data": { "type": "people", "id": "9" } 21 | }, 22 | "comments": { 23 | "links": { 24 | "self": "http://example.com/articles/1/relationships/comments", 25 | "related": "http://example.com/articles/1/comments" 26 | }, 27 | "data": [ 28 | { "type": "comments", "id": "5" }, 29 | { "type": "comments", "id": "12" } 30 | ] 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /core/src/test/resources/multiple_comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "comments", 5 | "id": "5", 6 | "attributes": { 7 | "body": "First!" 8 | }, 9 | "relationships": { 10 | "author": { 11 | "data": { 12 | "type": "people", 13 | "id": "2" 14 | } 15 | } 16 | }, 17 | "links": { 18 | "self": "http://example.com/comments/5" 19 | }, 20 | "meta": { 21 | "cache_hit": true 22 | } 23 | }, 24 | { 25 | "type": "comments", 26 | "id": "12", 27 | "attributes": { 28 | "body": "I like XML better" 29 | }, 30 | "relationships": { 31 | "author": { 32 | "data": { 33 | "type": "people", 34 | "id": "9" 35 | } 36 | } 37 | }, 38 | "links": { 39 | "self": "http://example.com/comments/12" 40 | }, 41 | "meta": { 42 | "cache_hit": true 43 | } 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/JsonApi.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Documented 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Target(ElementType.TYPE) 8 | public @interface JsonApi { 9 | 10 | /** 11 | * Specify type name of resource (typically, a kebab-cased plural noun) 12 | */ 13 | String type(); 14 | 15 | /** 16 | * Priority to determine the class to de-serializing a generic type of 17 | * resource across classes of same resource type name. 18 | * Class with smaller priority value should be chosen by the conflict 19 | * resolving function. 20 | * @deprecated You can find {@link #policy()} more useful in most cases. 21 | */ 22 | @Deprecated 23 | int priority() default 100; 24 | 25 | /** 26 | * Select policy applied to the resource class. 27 | * A type can have multiple classes to serialize but only one class to deserialize with. 28 | */ 29 | Policy policy() default Policy.SERIALIZATION_AND_DESERIALIZATION; 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 kamikat 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/AnnotationUtils.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonQualifier; 4 | 5 | import java.lang.annotation.Annotation; 6 | import java.util.Collections; 7 | import java.util.LinkedHashSet; 8 | import java.util.Set; 9 | 10 | final class AnnotationUtils { 11 | 12 | public static final Set NO_ANNOTATIONS = Collections.emptySet(); 13 | 14 | public static Set jsonAnnotations(Annotation[] annotations) { 15 | Set result = null; 16 | for (Annotation annotation : annotations) { 17 | if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) { 18 | if (result == null) result = new LinkedHashSet<>(); 19 | result.add(annotation); 20 | } 21 | } 22 | return result != null ? Collections.unmodifiableSet(result) : NO_ANNOTATIONS; 23 | } 24 | 25 | public static String typeNameOf(Class type) { 26 | return type.getAnnotation(JsonApi.class).type(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/MoshiHelperTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonReader; 4 | import com.squareup.moshi.Moshi; 5 | import okio.Buffer; 6 | import org.junit.Test; 7 | 8 | import java.io.EOFException; 9 | import java.nio.charset.Charset; 10 | 11 | import static org.hamcrest.CoreMatchers.equalTo; 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.Assert.assertThat; 14 | 15 | public class MoshiHelperTest { 16 | 17 | @Test 18 | public void equality() throws Exception { 19 | String json = TestUtil.fromResource("/sample.json"); 20 | Buffer a = new Buffer(); 21 | Buffer b = new Buffer(); 22 | a.writeString(json, Charset.forName("UTF-8")); 23 | MoshiHelper.dump(JsonReader.of(a), b); 24 | assertEquals(b.readString(Charset.forName("UTF-8")), json); 25 | } 26 | 27 | @Test(expected = EOFException.class) 28 | public void eof_exception() throws Exception { 29 | Buffer a = new Buffer(); 30 | Buffer b = new Buffer(); 31 | MoshiHelper.dump(JsonReader.of(a), b); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/Person.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import com.squareup.moshi.Json; 4 | import moe.banana.jsonapi2.JsonApi; 5 | import moe.banana.jsonapi2.Resource; 6 | 7 | @JsonApi(type = "people") 8 | public class Person extends Resource { 9 | 10 | @Json(name="first-name") 11 | private String firstName; 12 | 13 | @Json(name="last-name") 14 | private String lastName; 15 | 16 | private String twitter; 17 | private Integer age; 18 | 19 | public String getFirstName() { 20 | return firstName; 21 | } 22 | 23 | public void setFirstName(String firstName) { 24 | this.firstName = firstName; 25 | } 26 | 27 | public String getLastName() { 28 | return lastName; 29 | } 30 | 31 | public void setLastName(String lastName) { 32 | this.lastName = lastName; 33 | } 34 | 35 | public String getTwitter() { 36 | return twitter; 37 | } 38 | 39 | public void setTwitter(String twitter) { 40 | this.twitter = twitter; 41 | } 42 | 43 | public Integer getAge() { 44 | return age; 45 | } 46 | 47 | public void setAge(Integer age) { 48 | this.age = age; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/test/resources/multiple_polymorphic.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": { 3 | "self": "http://example.com/articles" 4 | }, 5 | "data": [{ 6 | "type": "articles", 7 | "id": "1", 8 | "attributes": { 9 | "title": "JSON API paints my bikeshed!" 10 | }, 11 | "relationships": { 12 | "author": { 13 | "links": { 14 | "self": "http://example.com/articles/1/relationships/author", 15 | "related": "http://example.com/articles/1/author" 16 | }, 17 | "data": { "type": "people", "id": "9" } 18 | }, 19 | "comments": { 20 | "links": { 21 | "self": "http://example.com/articles/1/relationships/comments", 22 | "related": "http://example.com/articles/1/comments" 23 | }, 24 | "data": [ 25 | { "type": "comments", "id": "5" }, 26 | { "type": "comments", "id": "12" } 27 | ] 28 | } 29 | } 30 | }, { 31 | "type": "photos", 32 | "id": "2", 33 | "attributes": { 34 | "url": "http://...", 35 | "title": "Rails is Omakase", 36 | "location": { 37 | "longitude": 116.4074, 38 | "latitude": 39.9042 39 | }, 40 | "color": "#EF5350", 41 | "created_at": 1484022733658 42 | } 43 | }] 44 | } -------------------------------------------------------------------------------- /retrofit-converter/src/test/java/moe/banana/jsonapi2/TestApi.java: -------------------------------------------------------------------------------- 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 | import java.util.List; 9 | 10 | public interface TestApi { 11 | 12 | @GET("articles") 13 | Call listArticles(); 14 | 15 | @GET("articles/{id}") 16 | Call
getArticle(@Path("id") String id); 17 | 18 | @GET("articles/{id}/comments") 19 | Call> getComments(@Path("id") String id); 20 | 21 | @PUT("articles/{id}/comments") 22 | Call addComments(@Path("id") String id, @Body List comments); 23 | 24 | @POST("articles/{id}/comments") 25 | Call addComment(@Path("id") String id, @Body Comment comment); 26 | 27 | @PUT("articles/{id}/relationships/author") 28 | Call updateAuthor(@Path("id") String id, @Body ResourceIdentifier authorLinkage); 29 | 30 | @GET("articles/{id}/relationships/tags") 31 | Call getRelTags(@Path("id") String id); 32 | 33 | @PUT("articles/{id}/relationships/tags") 34 | Call updateTags(@Path("id") String id, @Body ResourceIdentifier[] tagLinkages); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/Article.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import moe.banana.jsonapi2.HasMany; 4 | import moe.banana.jsonapi2.HasOne; 5 | import moe.banana.jsonapi2.JsonApi; 6 | import moe.banana.jsonapi2.Resource; 7 | 8 | @JsonApi(type = "articles") 9 | public class Article extends Resource { 10 | 11 | private String title; 12 | private HasOne author; 13 | private HasMany comments; 14 | private transient String ignored; 15 | private String nullable; 16 | 17 | public String getTitle() { 18 | return title; 19 | } 20 | 21 | public void setTitle(String title) { 22 | this.title = title; 23 | } 24 | 25 | public HasOne getAuthor() { 26 | return author; 27 | } 28 | 29 | public void setAuthor(HasOne author) { 30 | this.author = author; 31 | } 32 | 33 | public HasMany getComments() { 34 | return comments; 35 | } 36 | 37 | public void setComments(HasMany comments) { 38 | this.comments = comments; 39 | } 40 | 41 | public String getIgnored() { 42 | return ignored; 43 | } 44 | 45 | public void setIgnored(String ignored) { 46 | this.ignored = ignored; 47 | } 48 | 49 | public String getNullable() { 50 | return nullable; 51 | } 52 | 53 | public void setNullable(String nullable) { 54 | this.nullable = nullable; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | id 'maven' 5 | id 'jacoco' 6 | id 'com.github.kt3k.coveralls' version '2.6.3' 7 | } 8 | 9 | apply plugin: 'com.novoda.bintray-release' 10 | 11 | sourceCompatibility = '1.7' 12 | targetCompatibility = '1.7' 13 | 14 | sourceSets { 15 | test { 16 | resources { 17 | srcDir "resources" 18 | includes["**/*.json"] 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compileOnly 'com.squareup.moshi:moshi:1.4.0' 25 | testCompile 'com.squareup.moshi:moshi:1.4.0' 26 | testCompile 'junit:junit:4.12' 27 | } 28 | 29 | test { 30 | testLogging { 31 | events "passed", "skipped", "failed", "standardOut", "standardError" 32 | } 33 | } 34 | 35 | task sourcesJar(type: Jar, dependsOn: classes) { 36 | classifier 'sources' 37 | from sourceSets.main.allSource 38 | } 39 | 40 | task javadocJar(type: Jar, dependsOn: javadoc) { 41 | classifier 'javadoc' 42 | from javadoc.destinationDir 43 | } 44 | 45 | artifacts { 46 | archives sourcesJar 47 | archives javadocJar 48 | } 49 | 50 | jacocoTestReport { 51 | reports { 52 | xml.enabled = true // coveralls plugin depends on xml format report 53 | html.enabled = true 54 | } 55 | } 56 | 57 | publish { 58 | groupId = 'moe.banana' 59 | artifactId = 'moshi-jsonapi' 60 | publishVersion = describeVersion() 61 | desc = 'Implement JSON API v1.0 Specification in Moshi.' 62 | website = 'https://github.com/kamikat/moshi-jsonapi' 63 | } 64 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/ErrorTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.hamcrest.CoreMatchers.equalTo; 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertThat; 8 | 9 | public class ErrorTest { 10 | 11 | private Error createError() { 12 | Error error = new Error(); 13 | error.setId("1"); 14 | error.setStatus("409"); 15 | error.setCode("E409"); 16 | error.setTitle("Conflict"); 17 | error.setDetail("Detail error description"); 18 | return error; 19 | } 20 | 21 | @Test 22 | public void equality() throws Exception { 23 | assertEquals(createError(), createError()); 24 | } 25 | 26 | @Test 27 | public void hashcode() throws Exception { 28 | assertEquals(createError().hashCode(), createError().hashCode()); 29 | } 30 | 31 | @Test 32 | public void serialization() throws Exception { 33 | assertThat(TestUtil.moshi().adapter(Error.class).toJson(createError()), 34 | equalTo("{\"id\":\"1\",\"status\":\"409\",\"code\":\"E409\",\"title\":\"Conflict\",\"detail\":\"Detail error description\"}")); 35 | } 36 | 37 | @Test 38 | public void deserialization() throws Exception { 39 | assertThat(TestUtil.moshi().adapter(Error.class).fromJson("{\"id\":\"1\",\"status\":\"409\",\"code\":\"E409\",\"title\":\"Conflict\",\"detail\":\"Detail error description\"}"), 40 | equalTo(createError())); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/ResourceIdentifierTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import org.junit.Test; 4 | 5 | import moe.banana.jsonapi2.model.Person; 6 | 7 | import static org.hamcrest.CoreMatchers.equalTo; 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertNotEquals; 10 | import static org.junit.Assert.assertThat; 11 | 12 | public class ResourceIdentifierTest { 13 | 14 | private ResourceIdentifier createResourceIdentifier() { 15 | return new ResourceIdentifier("people", "11"); 16 | } 17 | 18 | @Test 19 | public void equality_of_identifier_vs_identifier() throws Exception { 20 | ResourceIdentifier identifier = createResourceIdentifier(); 21 | assertEquals(identifier, new ResourceIdentifier(identifier)); 22 | assertEquals(identifier, createResourceIdentifier()); 23 | } 24 | 25 | @Test 26 | public void equality_of_hashcode() throws Exception { 27 | assertEquals(createResourceIdentifier().hashCode(), createResourceIdentifier().hashCode()); 28 | } 29 | 30 | @Test 31 | public void serialization() throws Exception { 32 | assertThat(TestUtil.moshi().adapter(ResourceIdentifier.class).toJson(createResourceIdentifier()), 33 | equalTo("{\"type\":\"people\",\"id\":\"11\"}")); 34 | } 35 | 36 | @Test 37 | public void deserialization() throws Exception { 38 | assertThat(TestUtil.moshi().adapter(ResourceIdentifier.class).fromJson("{\"type\":\"people\",\"id\":\"11\"}"), 39 | equalTo(createResourceIdentifier())); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /retrofit-converter/build.gradle: -------------------------------------------------------------------------------- 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 | } 61 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/TestUtil.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.FromJson; 4 | import com.squareup.moshi.Moshi; 5 | import com.squareup.moshi.ToJson; 6 | import moe.banana.jsonapi2.model.Color; 7 | import okio.Buffer; 8 | 9 | import java.io.FileNotFoundException; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.nio.charset.Charset; 13 | 14 | public class TestUtil { 15 | 16 | @JsonApi(type = "default") 17 | public static class Default extends Resource { 18 | 19 | } 20 | 21 | private static class ColorAdapter { 22 | @ToJson String toJson(@Color int rgb) { 23 | return String.format("#%06x", rgb); 24 | } 25 | 26 | @FromJson @Color int fromJson(String rgb) { 27 | return Integer.parseInt(rgb.substring(1), 16); 28 | } 29 | } 30 | 31 | @SafeVarargs 32 | public static Moshi moshi(Class... types) { 33 | return moshi(false, types); 34 | } 35 | 36 | @SafeVarargs 37 | public static Moshi moshi(boolean strict, Class... types) { 38 | ResourceAdapterFactory.Builder factoryBuilder = ResourceAdapterFactory.builder(); 39 | factoryBuilder.add(types); 40 | if (!strict) { 41 | factoryBuilder.add(Default.class); 42 | } 43 | return new Moshi.Builder().add(factoryBuilder.build()).add(new ColorAdapter()).build(); 44 | } 45 | 46 | public static String fromResource(String resourceName) throws IOException { 47 | InputStream in = TestUtil.class.getResourceAsStream(resourceName); 48 | if (in == null) { 49 | throw new FileNotFoundException(resourceName); 50 | } 51 | Buffer buffer = new Buffer(); 52 | buffer.readFrom(in); 53 | return buffer.readString(Charset.forName("UTF-8")); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/ObjectDocument.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | public class ObjectDocument extends Document { 4 | 5 | /** 6 | * NOTE In JSON API Spec version 1.0, a Document may contains no data or "null" data. 7 | * The field `hasData` denotes whether the document contains a nullable data. 8 | */ 9 | private boolean hasData = false; 10 | 11 | private DATA data = null; 12 | 13 | public ObjectDocument() { 14 | } 15 | 16 | public ObjectDocument(DATA data) { 17 | set(data); 18 | } 19 | 20 | public ObjectDocument(Document document) { 21 | super(document); 22 | } 23 | 24 | public void set(DATA data) { 25 | this.hasData = true; 26 | bindDocument(null, this.data); 27 | bindDocument(this, data); 28 | this.data = data; 29 | } 30 | 31 | public DATA get() { 32 | return data; 33 | } 34 | 35 | @Deprecated 36 | public void setNull(boolean isNull) { 37 | hasData = !isNull; 38 | } 39 | 40 | @Deprecated 41 | public boolean isNull() { 42 | return hasData && data == null; 43 | } 44 | 45 | public boolean hasData() { 46 | return hasData; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | if (!super.equals(o)) return false; 54 | 55 | ObjectDocument that = (ObjectDocument) o; 56 | 57 | if (hasData != that.hasData) return false; 58 | return data.equals(that.data); 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | int result = super.hashCode(); 64 | result = 31 * result + data.hashCode(); 65 | result = 31 * result + (hasData ? 1 : 0); 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/HasOneTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import moe.banana.jsonapi2.model.Person; 4 | import org.junit.Test; 5 | 6 | import static org.hamcrest.CoreMatchers.equalTo; 7 | import static org.junit.Assert.*; 8 | 9 | public class HasOneTest { 10 | 11 | private HasOne createHasOne() { 12 | return new HasOne<>("people", "5"); 13 | } 14 | 15 | @Test 16 | public void equality() throws Exception { 17 | assertEquals(createHasOne(), createHasOne()); 18 | } 19 | 20 | @Test 21 | public void hashcode_equality() throws Exception { 22 | assertEquals(createHasOne().hashCode(), createHasOne().hashCode()); 23 | } 24 | 25 | @Test 26 | public void resolution() throws Exception { 27 | Document document = new ObjectDocument(); 28 | assertNull(createHasOne().get(document)); 29 | Person holder = new Person(); 30 | assertEquals(createHasOne().get(document, holder), holder); 31 | Person person = new Person(); 32 | person.setId("5"); 33 | document.addInclude(person); 34 | assertEquals(createHasOne().get(document), person); 35 | } 36 | 37 | @Test 38 | public void serialization() throws Exception { 39 | assertThat(TestUtil.moshi().adapter(HasOne.class).toJson(createHasOne()), 40 | equalTo("{\"data\":{\"type\":\"people\",\"id\":\"5\"}}")); 41 | } 42 | 43 | @Test 44 | public void serialization_null() throws Exception { 45 | assertThat(TestUtil.moshi().adapter(HasOne.class).toJson(new HasOne<>(null)), 46 | equalTo("{\"data\":null}")); 47 | } 48 | 49 | @Test 50 | public void deserialization() throws Exception { 51 | assertThat(TestUtil.moshi().adapter(HasOne.class).fromJson("{\"data\":{\"type\":\"people\",\"id\":\"5\"}}"), 52 | equalTo((HasOne) createHasOne())); 53 | assertNull(TestUtil.moshi().adapter(HasOne.class).fromJson("{\"data\":null}").get()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/JsonBufferTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import moe.banana.jsonapi2.model.Meta; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | 8 | import static org.hamcrest.CoreMatchers.equalTo; 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertNotEquals; 11 | import static org.junit.Assert.assertThat; 12 | 13 | public class JsonBufferTest { 14 | 15 | private Meta createMeta() { 16 | Meta meta = new Meta(); 17 | meta.copyright = "Copyright 20XX Example Corp."; 18 | meta.authors = Arrays.asList( 19 | "Yehuda Katz", 20 | "Steve Klabnik", 21 | "Dan Gebhardt", 22 | "Tyler Kellen"); 23 | return meta; 24 | } 25 | 26 | private JsonBuffer createJsonBuffer() { 27 | return JsonBuffer.create(TestUtil.moshi().adapter(Meta.class), createMeta()); 28 | } 29 | 30 | @Test 31 | public void equality() throws Exception { 32 | JsonBuffer buffer1 = createJsonBuffer(); 33 | JsonBuffer buffer2 = createJsonBuffer(); 34 | assertEquals(buffer1, buffer2); 35 | assertNotEquals(buffer1, JsonBuffer.create(TestUtil.moshi().adapter(Meta.class), null)); 36 | assertEquals(buffer1.hashCode(), buffer2.hashCode()); 37 | } 38 | 39 | @Test 40 | public void serialization() throws Exception { 41 | assertThat(TestUtil.moshi().adapter(JsonBuffer.class).toJson(createJsonBuffer()), 42 | equalTo("{\"authors\":[\"Yehuda Katz\",\"Steve Klabnik\",\"Dan Gebhardt\",\"Tyler Kellen\"],\"copyright\":\"Copyright 20XX Example Corp.\"}")); 43 | } 44 | 45 | @Test 46 | public void deserialization() throws Exception { 47 | assertThat(TestUtil.moshi().adapter(JsonBuffer.class).fromJson("{\"authors\":[\"Yehuda Katz\",\"Steve Klabnik\",\"Dan Gebhardt\",\"Tyler Kellen\"],\"copyright\":\"Copyright 20XX Example Corp.\"}"), 48 | equalTo((JsonBuffer) createJsonBuffer())); 49 | } 50 | 51 | @Test 52 | public void resolve() { 53 | assertThat(createJsonBuffer().get(TestUtil.moshi().adapter(Meta.class)), 54 | equalTo(createMeta())); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/model/Photo.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2.model; 2 | 3 | import moe.banana.jsonapi2.HasOne; 4 | import moe.banana.jsonapi2.JsonApi; 5 | import moe.banana.jsonapi2.Policy; 6 | import moe.banana.jsonapi2.Resource; 7 | 8 | @JsonApi(type = "photos") 9 | public class Photo extends Resource { 10 | 11 | private String url; 12 | private Boolean visible; 13 | private Double shutter; 14 | private Location location; 15 | private HasOne author; 16 | 17 | private @Color 18 | int color; 19 | 20 | public static class Location { 21 | public Double longitude; 22 | public Double latitude; 23 | } 24 | 25 | public String getUrl() { 26 | return url; 27 | } 28 | 29 | public void setUrl(String url) { 30 | this.url = url; 31 | } 32 | 33 | public Boolean getVisible() { 34 | return visible; 35 | } 36 | 37 | public void setVisible(Boolean visible) { 38 | this.visible = visible; 39 | } 40 | 41 | public Double getShutter() { 42 | return shutter; 43 | } 44 | 45 | public void setShutter(Double shutter) { 46 | this.shutter = shutter; 47 | } 48 | 49 | public Location getLocation() { 50 | return location; 51 | } 52 | 53 | public void setLocation(Location location) { 54 | this.location = location; 55 | } 56 | 57 | public HasOne getAuthor() { 58 | return author; 59 | } 60 | 61 | public void setAuthor(HasOne author) { 62 | this.author = author; 63 | } 64 | 65 | public int getColor() { 66 | return color; 67 | } 68 | 69 | public void setColor(int color) { 70 | this.color = color; 71 | } 72 | 73 | @JsonApi(type = "photos", priority = -1) 74 | public static class Photo2 extends Photo { 75 | 76 | } 77 | 78 | @JsonApi(type = "photos") 79 | public static class Photo3 extends Photo { 80 | 81 | } 82 | 83 | @JsonApi(type = "photos", policy = Policy.SERIALIZATION_ONLY) 84 | public static class Photo4 extends Photo { 85 | 86 | } 87 | 88 | @JsonApi(type = "photos", policy = Policy.DESERIALIZATION_ONLY) 89 | public static class Photo5 extends Photo { 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/JsonBuffer.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonReader; 5 | import com.squareup.moshi.JsonWriter; 6 | import okio.Buffer; 7 | 8 | import java.io.IOException; 9 | import java.io.Serializable; 10 | import java.util.Arrays; 11 | 12 | /** 13 | * Buffer JSON result as byte[] for lazy bind 14 | */ 15 | public class JsonBuffer implements Serializable { 16 | 17 | private byte[] buffer; 18 | 19 | private JsonBuffer(byte[] buffer) { 20 | this.buffer = buffer; 21 | } 22 | 23 | public R get(JsonAdapter adapter) { 24 | try { 25 | Buffer buffer = new Buffer(); 26 | buffer.write(this.buffer); 27 | return adapter.fromJson(buffer); 28 | } catch (IOException e) { 29 | throw new RuntimeException("JsonBuffer failed to deserialize value with [" + adapter.getClass() + "]", e); 30 | } 31 | } 32 | 33 | public static JsonBuffer create(JsonAdapter adapter, T value) { 34 | try { 35 | Buffer buffer = new Buffer(); 36 | adapter.toJson(buffer, value); 37 | return new JsonBuffer<>(buffer.readByteArray()); 38 | } catch (IOException e) { 39 | throw new RuntimeException("JsonBuffer failed to serialize value with [" + adapter.getClass() + "]", e); 40 | } 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | 48 | JsonBuffer that = (JsonBuffer) o; 49 | 50 | return Arrays.equals(buffer, that.buffer); 51 | 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Arrays.hashCode(buffer); 57 | } 58 | 59 | public static class Adapter extends JsonAdapter> { 60 | 61 | @Override 62 | public JsonBuffer fromJson(JsonReader reader) throws IOException { 63 | Buffer buffer = new Buffer(); 64 | MoshiHelper.dump(reader, buffer); 65 | return new JsonBuffer<>(buffer.readByteArray()); 66 | } 67 | 68 | @Override 69 | public void toJson(JsonWriter writer, JsonBuffer value) throws IOException { 70 | Buffer buffer = new Buffer(); 71 | buffer.write(value.buffer); 72 | MoshiHelper.dump(buffer, writer); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/src/test/resources/multiple_compound.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonapi": { 3 | "version": "1.0" 4 | }, 5 | "meta": { 6 | "cluster_id": "jp-lb03", 7 | "server_id": "hx01c", 8 | "response_time": 313 9 | }, 10 | "links": { 11 | "self": "http://example.com/articles/" 12 | }, 13 | "data": [{ 14 | "type": "articles", 15 | "id": "1", 16 | "attributes": { 17 | "title": "JSON API paints my bikeshed!" 18 | }, 19 | "links": { 20 | "self": "http://example.com/articles/1" 21 | }, 22 | "meta": { 23 | "cache_hit": true 24 | }, 25 | "relationships": { 26 | "author": { 27 | "links": { 28 | "self": "http://example.com/articles/1/relationships/author", 29 | "related": "http://example.com/articles/1/author" 30 | }, 31 | "data": { "type": "people", "id": "9" }, 32 | "meta": { 33 | "updated_at": 1484022733658 34 | } 35 | }, 36 | "comments": { 37 | "links": { 38 | "self": "http://example.com/articles/1/relationships/comments", 39 | "related": "http://example.com/articles/1/comments" 40 | }, 41 | "data": [ 42 | { "type": "comments", "id": "5", "meta": { "created_at": 1484022733658 } }, 43 | { "type": "comments", "id": "12", "meta": { "created_at": 1484022733658 } } 44 | ], 45 | "meta": { 46 | "updated_at": 1484022733658 47 | } 48 | } 49 | } 50 | }], 51 | "included": [{ 52 | "type": "people", 53 | "id": "9", 54 | "attributes": { 55 | "first-name": "Dan", 56 | "last-name": "Gebhardt", 57 | "twitter": "dgeb" 58 | }, 59 | "links": { 60 | "self": "http://example.com/people/9" 61 | }, 62 | "meta": { 63 | "cache_hit": true 64 | } 65 | }, { 66 | "type": "comments", 67 | "id": "5", 68 | "attributes": { 69 | "body": "First!" 70 | }, 71 | "relationships": { 72 | "author": { 73 | "data": { "type": "people", "id": "2" } 74 | } 75 | }, 76 | "links": { 77 | "self": "http://example.com/comments/5" 78 | }, 79 | "meta": { 80 | "cache_hit": true 81 | } 82 | }, { 83 | "type": "comments", 84 | "id": "12", 85 | "attributes": { 86 | "body": "I like XML better" 87 | }, 88 | "relationships": { 89 | "author": { 90 | "data": { "type": "people", "id": "9" } 91 | } 92 | }, 93 | "links": { 94 | "self": "http://example.com/comments/12" 95 | }, 96 | "meta": { 97 | "cache_hit": true 98 | } 99 | }] 100 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/HasManyTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import moe.banana.jsonapi2.model.Comment; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | 8 | import static org.hamcrest.CoreMatchers.*; 9 | import static org.junit.Assert.*; 10 | 11 | @SuppressWarnings("all") 12 | public class HasManyTest { 13 | 14 | private HasMany comments(int size) { 15 | ArrayList comments = new ArrayList<>(size); 16 | for (int i = 0; i < size; i++) { 17 | Comment comment = new Comment(); 18 | comment.setId("" + i); 19 | comments.add(comment); 20 | } 21 | return new HasMany(comments.toArray(new Comment[size])); 22 | } 23 | 24 | @Test 25 | public void equality() throws Exception { 26 | assertEquals(comments(0), comments(0)); 27 | assertEquals(comments(1), comments(1)); 28 | assertEquals(comments(2), comments(2)); 29 | } 30 | 31 | @Test 32 | public void hashcode_equality() throws Exception { 33 | assertEquals(comments(0).hashCode(), comments(0).hashCode()); 34 | assertEquals(comments(1).hashCode(), comments(1).hashCode()); 35 | assertEquals(comments(2).hashCode(), comments(2).hashCode()); 36 | } 37 | 38 | @Test 39 | public void resolution() { 40 | ObjectDocument document = new ObjectDocument(); 41 | Comment holder = new Comment(); 42 | assertThat(comments(2).get(document), hasItems(nullValue(Comment.class), nullValue(Comment.class))); 43 | assertThat(comments(2).get(document, holder), hasItems(holder, holder)); 44 | Comment[] comments = new Comment[] { new Comment(), new Comment() }; 45 | comments[0].setId("0"); 46 | comments[1].setId("1"); 47 | document.addInclude(comments[0]); 48 | assertThat(comments(2).get(document), hasItems(equalTo(comments[0]), nullValue(Comment.class))); 49 | document.addInclude(comments[1]); 50 | assertThat(comments(2).get(document), hasItems(comments[0], comments[1])); 51 | } 52 | 53 | @Test 54 | public void serialization() throws Exception { 55 | assertThat(TestUtil.moshi().adapter(HasMany.class).toJson(comments(2)), 56 | equalTo("{\"data\":[{\"type\":\"comments\",\"id\":\"0\"},{\"type\":\"comments\",\"id\":\"1\"}]}")); 57 | } 58 | 59 | @Test 60 | public void serialization_null() throws Exception { 61 | assertThat(TestUtil.moshi().adapter(HasMany.class).toJson(new HasMany(null)), 62 | equalTo("{\"data\":null}")); 63 | } 64 | 65 | @Test 66 | public void deserialization() throws Exception { 67 | HasMany hasMany = TestUtil.moshi().adapter(HasMany.class) 68 | .fromJson("{\"data\":[{\"type\":\"comments\",\"id\":\"0\"},{\"type\":\"comments\",\"id\":\"1\"}]}"); 69 | assertThat(hasMany, equalTo((HasMany) comments(2))); 70 | assertTrue(hasMany.hasData()); 71 | assertFalse(TestUtil.moshi().adapter(HasMany.class).fromJson("{\"data\":null}").hasData()); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/HasOne.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonReader; 5 | import com.squareup.moshi.JsonWriter; 6 | import com.squareup.moshi.Moshi; 7 | 8 | import java.io.IOException; 9 | import java.io.Serializable; 10 | 11 | import static moe.banana.jsonapi2.MoshiHelper.nextNullableObject; 12 | import static moe.banana.jsonapi2.MoshiHelper.writeNullable; 13 | 14 | public final class HasOne extends Relationship implements Serializable { 15 | 16 | private ResourceIdentifier linkedResource; 17 | 18 | public HasOne() { } 19 | 20 | public HasOne(String type, String id) { 21 | this(new ResourceIdentifier(type, id)); 22 | } 23 | 24 | public HasOne(ResourceIdentifier linkedResource) { 25 | set(linkedResource); 26 | } 27 | 28 | @Override 29 | public T get(Document document) { 30 | return get(document, null); 31 | } 32 | 33 | public T get(Document document, T defaultValue) { 34 | T obj = document.find(linkedResource); 35 | if (obj == null) { 36 | return defaultValue; 37 | } else { 38 | return obj; 39 | } 40 | } 41 | 42 | public ResourceIdentifier get() { 43 | return linkedResource; 44 | } 45 | 46 | public void set(ResourceIdentifier identifier) { 47 | if (identifier == null) { 48 | linkedResource = null; 49 | } else if (ResourceIdentifier.class == identifier.getClass()) { 50 | linkedResource = identifier; 51 | } else { 52 | set(identifier.getType(), identifier.getId()); 53 | } 54 | } 55 | 56 | public void set(String type, String id) { 57 | set(new ResourceIdentifier(type, id)); 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "HasOne{" + 63 | "linkedResource=" + linkedResource + 64 | '}'; 65 | } 66 | 67 | @Override 68 | public boolean equals(Object o) { 69 | if (this == o) return true; 70 | if (o == null || getClass() != o.getClass()) return false; 71 | 72 | HasOne hasOne = (HasOne) o; 73 | 74 | return linkedResource != null ? linkedResource.equals(hasOne.linkedResource) : hasOne.linkedResource == null; 75 | 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | return linkedResource != null ? linkedResource.hashCode() : 0; 81 | } 82 | 83 | 84 | static class Adapter extends JsonAdapter> { 85 | 86 | JsonAdapter resourceIdentifierJsonAdapter; 87 | JsonAdapter jsonBufferJsonAdapter; 88 | 89 | public Adapter(Moshi moshi) { 90 | resourceIdentifierJsonAdapter = moshi.adapter(ResourceIdentifier.class); 91 | jsonBufferJsonAdapter = moshi.adapter(JsonBuffer.class); 92 | } 93 | 94 | @Override 95 | public HasOne fromJson(JsonReader reader) throws IOException { 96 | HasOne relationship = new HasOne<>(); 97 | reader.beginObject(); 98 | while (reader.hasNext()) { 99 | switch (reader.nextName()) { 100 | case "data": 101 | relationship.set(nextNullableObject(reader, resourceIdentifierJsonAdapter)); 102 | break; 103 | case "meta": 104 | relationship.setMeta(nextNullableObject(reader, jsonBufferJsonAdapter)); 105 | break; 106 | case "links": 107 | relationship.setLinks(nextNullableObject(reader, jsonBufferJsonAdapter)); 108 | break; 109 | default: 110 | reader.skipValue(); 111 | break; 112 | } 113 | } 114 | reader.endObject(); 115 | return relationship; 116 | } 117 | 118 | @Override 119 | public void toJson(JsonWriter writer, HasOne value) throws IOException { 120 | writer.beginObject(); 121 | writeNullable(writer, resourceIdentifierJsonAdapter, "data", value.linkedResource, true); 122 | writeNullable(writer, jsonBufferJsonAdapter, "meta", value.getMeta()); 123 | writeNullable(writer, jsonBufferJsonAdapter, "links", value.getLinks()); 124 | writer.endObject(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/ResourceIdentifier.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonReader; 5 | import com.squareup.moshi.JsonWriter; 6 | import com.squareup.moshi.Moshi; 7 | 8 | import java.io.IOException; 9 | import java.io.Serializable; 10 | 11 | import static moe.banana.jsonapi2.MoshiHelper.*; 12 | 13 | public class ResourceIdentifier implements Serializable { 14 | 15 | private Document document; 16 | private String type; 17 | private String id; 18 | private JsonBuffer meta; 19 | 20 | public ResourceIdentifier() { 21 | this(null, null); 22 | } 23 | 24 | public ResourceIdentifier(ResourceIdentifier identifier) { 25 | this(identifier.getType(), identifier.getId()); 26 | } 27 | 28 | public ResourceIdentifier(String type, String id) { 29 | this.type = type; 30 | this.id = id; 31 | } 32 | 33 | @Deprecated 34 | public Document getContext() { 35 | return getDocument(); 36 | } 37 | 38 | @Deprecated 39 | public void setContext(Document document) { 40 | setDocument(document); 41 | } 42 | 43 | public Document getDocument() { 44 | return document; 45 | } 46 | 47 | public void setDocument(Document document) { 48 | this.document = document; 49 | } 50 | 51 | public String getType() { 52 | return type; 53 | } 54 | 55 | public void setType(String type) { 56 | this.type = type; 57 | } 58 | 59 | public String getId() { 60 | return id; 61 | } 62 | 63 | public void setId(String id) { 64 | this.id = id; 65 | } 66 | 67 | public JsonBuffer getMeta() { 68 | return meta; 69 | } 70 | 71 | public void setMeta(JsonBuffer meta) { 72 | this.meta = meta; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return getClass().getSimpleName() + "{" + 78 | "type='" + type + '\'' + 79 | ", id='" + id + '\'' + 80 | '}'; 81 | } 82 | 83 | @SuppressWarnings("SimplifiableIfStatement") 84 | @Override 85 | public boolean equals(Object o) { 86 | if (this == o) return true; 87 | if (o == null || !getClass().equals(o.getClass())) return false; 88 | 89 | ResourceIdentifier that = (ResourceIdentifier) o; 90 | 91 | if (type != null ? !type.equals(that.type) : that.type != null) return false; 92 | return id != null ? id.equals(that.id) : that.id == null; 93 | 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | int result = type != null ? type.hashCode() : 0; 99 | result = 31 * result + (id != null ? id.hashCode() : 0); 100 | return result; 101 | } 102 | 103 | public static class Adapter extends JsonAdapter { 104 | 105 | JsonAdapter jsonBufferJsonAdapter; 106 | 107 | public Adapter(Moshi moshi) { 108 | jsonBufferJsonAdapter = moshi.adapter(JsonBuffer.class); 109 | } 110 | 111 | @Override 112 | public ResourceIdentifier fromJson(JsonReader reader) throws IOException { 113 | ResourceIdentifier object = new ResourceIdentifier(); 114 | reader.beginObject(); 115 | while (reader.hasNext()) { 116 | switch (reader.nextName()) { 117 | case "id": 118 | object.setId(nextNullableString(reader)); 119 | break; 120 | case "type": 121 | object.setType(nextNullableString(reader)); 122 | break; 123 | case "meta": 124 | object.setMeta(nextNullableObject(reader, jsonBufferJsonAdapter)); 125 | break; 126 | default: 127 | reader.skipValue(); 128 | break; 129 | } 130 | } 131 | reader.endObject(); 132 | return object; 133 | } 134 | 135 | @Override 136 | public void toJson(JsonWriter writer, ResourceIdentifier value) throws IOException { 137 | writer.beginObject(); 138 | writer.name("type").value(value.getType()); 139 | writer.name("id").value(value.getId()); 140 | writeNullable(writer, jsonBufferJsonAdapter, "meta", value.getMeta()); 141 | writer.endObject(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/ArrayDocument.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import java.io.Serializable; 4 | import java.util.*; 5 | 6 | public class ArrayDocument extends Document implements Serializable, List { 7 | 8 | List data = new ArrayList<>(); 9 | 10 | public ArrayDocument() { 11 | } 12 | 13 | public ArrayDocument(Document document) { 14 | super(document); 15 | } 16 | 17 | public boolean add(DATA element) { 18 | if (data.add(element)) { 19 | bindDocument(this, element); 20 | return true; 21 | } 22 | return false; 23 | } 24 | 25 | @Override 26 | public boolean remove(Object o) { 27 | if (data.remove(o)) { 28 | bindDocument(null, o); 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | @Override 35 | public boolean containsAll(Collection c) { 36 | return data.containsAll(c); 37 | } 38 | 39 | @Override 40 | public boolean addAll(Collection c) { 41 | if (data.addAll(c)) { 42 | bindDocument(this, c); 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | @Override 49 | public boolean addAll(int index, Collection c) { 50 | if (data.addAll(index, c)) { 51 | bindDocument(this, c); 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | @Override 58 | public boolean removeAll(Collection c) { 59 | if (data.removeAll(c)) { 60 | bindDocument(null, c); 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | @Override 67 | public boolean retainAll(Collection c) { 68 | bindDocument(null, data); 69 | boolean result = data.retainAll(c); 70 | bindDocument(this, data); 71 | return result; 72 | } 73 | 74 | @Override 75 | public void clear() { 76 | bindDocument(null, data); 77 | data.clear(); 78 | } 79 | 80 | public DATA get(int position) { 81 | return data.get(position); 82 | } 83 | 84 | @Override 85 | public DATA set(int index, DATA element) { 86 | DATA oldElement = data.set(index, element); 87 | bindDocument(null, oldElement); 88 | bindDocument(this, element); 89 | return oldElement; 90 | } 91 | 92 | @Override 93 | public void add(int index, DATA element) { 94 | data.add(index, element); 95 | bindDocument(this, data); 96 | } 97 | 98 | public DATA remove(int position) { 99 | DATA element = data.remove(position); 100 | bindDocument(null, element); 101 | return element; 102 | } 103 | 104 | @Override 105 | public int indexOf(Object o) { 106 | return data.indexOf(o); 107 | } 108 | 109 | @Override 110 | public int lastIndexOf(Object o) { 111 | return data.lastIndexOf(o); 112 | } 113 | 114 | @Override 115 | public ListIterator listIterator() { 116 | return data.listIterator(); 117 | } 118 | 119 | @Override 120 | public ListIterator listIterator(int index) { 121 | return data.listIterator(index); 122 | } 123 | 124 | @Override 125 | public ArrayDocument subList(int fromIndex, int toIndex) { 126 | ArrayDocument copy = new ArrayDocument<>(this); 127 | copy.addAll(data.subList(fromIndex, toIndex)); 128 | return copy; 129 | } 130 | 131 | public int size() { 132 | return data.size(); 133 | } 134 | 135 | @Override 136 | public boolean isEmpty() { 137 | return data.isEmpty(); 138 | } 139 | 140 | @Override 141 | public boolean contains(Object o) { 142 | return data.contains(o); 143 | } 144 | 145 | @Override 146 | public Iterator iterator() { 147 | return data.iterator(); 148 | } 149 | 150 | @Override 151 | public Object[] toArray() { 152 | return data.toArray(); 153 | } 154 | 155 | @Override 156 | public T[] toArray(T[] a) { 157 | return data.toArray(a); 158 | } 159 | 160 | @Override 161 | public boolean equals(Object o) { 162 | if (this == o) return true; 163 | if (o == null || getClass() != o.getClass()) return false; 164 | if (!super.equals(o)) return false; 165 | return data.equals(o); 166 | } 167 | 168 | @Override 169 | public int hashCode() { 170 | int result = super.hashCode(); 171 | result = 31 * result + data.hashCode(); 172 | return result; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /core/src/test/resources/sample.json: -------------------------------------------------------------------------------- 1 | [{"_id":"5874bd9ab3bf81c115c53da5","index":0,"guid":"a0d975b0-b802-41e4-9e03-7cc2a6206337","isActive":false,"balance":"$3,804.67","deleted_at":null,"picture":"http://placehold.it/32x32","age":26,"eyeColor":"brown","name":"TateLawson","gender":"male","company":"GOLOGY","email":"tatelawson@gology.com","phone":"+1(928)433-2888","address":"957EmmonsAvenue,Statenville,Georgia,6956","about":"CupidatatexercitationmollitutveniamnonvoluptateLoremproidentLoremestaliquip.Aliquipanimmollitpariaturpariaturexcepteurveniamauteetsuntaliquip.IncididuntoccaecatestduiscillumpariaturnullaetconsequataliquafugiatestLoremproident.\r\n","registered":"2016-12-18T02:32:53-08:00","latitude":79.660412,"longitude":-134.352973,"tags":["exercitation","Lorem","Lorem","id","cupidatat","esse","qui"],"friends":[{"id":0,"name":"JimenezKirkland"},{"id":1,"name":"RyanGriffin"},{"id":2,"name":"WarnerSosa"}],"greeting":"Hello,TateLawson!Youhave4unreadmessages.","favoriteFruit":"banana"},{"_id":"5874bd9a7690fa650a524bbf","index":1,"guid":"667c052c-2f68-413d-8fbd-b861959633b2","isActive":false,"balance":"$1,393.95","picture":"http://placehold.it/32x32","age":30,"eyeColor":"green","name":"NataliaDelacruz","gender":"female","company":"SURETECH","email":"nataliadelacruz@suretech.com","phone":"+1(910)569-3977","address":"888DinsmorePlace,Clinton,Iowa,3751","about":"Suntmollitexexercitationlaborumlabore.Inaliquaoccaecatdoloreidconsequatdeseruntexdoloreoccaecatdoadipisicingvelitquiesse.Occaecatenimdodoexipsumesseveniamdeseruntquiconsectetur.\r\n","registered":"2014-01-21T03:39:55-08:00","latitude":-45.840506,"longitude":-0.506736,"tags":["sunt","eu","ullamco","labore","id","consequat","qui"],"friends":[{"id":0,"name":"AlvaradoSolomon"},{"id":1,"name":"LindseyMay"},{"id":2,"name":"MorrisHarvey"}],"greeting":"Hello,NataliaDelacruz!Youhave7unreadmessages.","favoriteFruit":"apple"},{"_id":"5874bd9a035e0497bcfa3206","index":2,"guid":"22838813-1487-456f-b357-bc9a386f9444","isActive":true,"balance":"$1,711.05","picture":"http://placehold.it/32x32","age":38,"eyeColor":"blue","name":"StarkNavarro","gender":"male","company":"ECRATIC","email":"starknavarro@ecratic.com","phone":"+1(873)498-3615","address":"132ArkansasDrive,Wanamie,Palau,3470","about":"Autenullaquiseudoloredolorduisreprehenderitpariaturcupidatatofficiamollitqui.Essedoessesuntdolorepariatursuntautemollitveniamqui.Dolorproidentproidentadaliquipauteauteauteofficialaboreoccaecatsuntenim.\r\n","registered":"2016-10-14T11:56:52-08:00","latitude":-82.219156,"longitude":68.788123,"tags":["aute","et","aliquip","commodo","exercitation","fugiat","sint"],"friends":[{"id":0,"name":"WoodsDowns"},{"id":1,"name":"ChangWalters"},{"id":2,"name":"EatonRay"}],"greeting":"Hello,StarkNavarro!Youhave6unreadmessages.","favoriteFruit":"banana"},{"_id":"5874bd9af90b0cb5e66c55e4","index":3,"guid":"bbd16a4b-2165-4c21-b07b-4e8832cbcbb3","isActive":false,"balance":"$3,260.95","picture":"http://placehold.it/32x32","age":39,"eyeColor":"blue","name":"RobertsonAlbert","gender":"male","company":"POSHOME","email":"robertsonalbert@poshome.com","phone":"+1(907)410-2529","address":"197DrewStreet,Fairacres,Utah,8742","about":"Sintidmollitincididuntaliquacommodominim.Culpadolorereprehenderitquilaboreirureametenimproidentpariatur.Doloreduisvoluptateenimnostrudsitmollitinduis.Nostrudquimollitadipisicingeiusmodlaborumaliquipsintexoccaecatdoincididuntconsectetur.Animinreprehenderitanimeadeseruntadlaborumoccaecateavelitaliquip.\r\n","registered":"2015-05-23T08:03:50-08:00","latitude":30.242567,"longitude":-33.036611,"tags":["ipsum","cupidatat","nulla","laboris","aliqua","velit","excepteur"],"friends":[{"id":0,"name":"MorenoRice"},{"id":1,"name":"NolanHayes"},{"id":2,"name":"ScottVaughan"}],"greeting":"Hello,RobertsonAlbert!Youhave5unreadmessages.","favoriteFruit":"strawberry"},{"_id":"5874bd9a7112c7b2299025cd","index":4,"guid":"26d21232-9c56-4e60-bdf2-83f201ed5d20","isActive":true,"balance":"$2,984.40","picture":"http://placehold.it/32x32","age":28,"eyeColor":"green","name":"WilmaDelaney","gender":"female","company":"RODEOMAD","email":"wilmadelaney@rodeomad.com","phone":"+1(851)520-2822","address":"458DahlCourt,Strykersville,Colorado,4690","about":"Elitquinoneaeumagnaexercitationaliquip.Doetadtemporvoluptatefugiatlaborumexercitationincididunt.Essedoloreesseinnostrudconsecteturullamconostrudminimsunt.Cupidatatmolliteaofficiaproidentculpacommodocommododoloressecommodoirurequiquilaboris.Proidentquidolorenisimollitcillumutsuntidoccaecattemporcillumeiusmodin.Exnisidoloreveniamproidenteiusmodcillumvoluptatedoeuculpalaborissuntad.Mollitofficiaduiscillumduisincididunteunon.\r\n","registered":"2015-12-20T08:15:51-08:00","latitude":70.063463,"longitude":170.323399,"tags":["non","labore","cupidatat","et","tempor","non","ullamco"],"friends":[{"id":0,"name":"DebraJames"},{"id":1,"name":"DavenportReid"},{"id":2,"name":"OrrHorne"}],"greeting":"Hello,WilmaDelaney!Youhave4unreadmessages.","favoriteFruit":"apple"}] -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/MoshiHelper.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonReader; 5 | import com.squareup.moshi.JsonWriter; 6 | import okio.BufferedSink; 7 | import okio.BufferedSource; 8 | 9 | import java.io.IOException; 10 | 11 | public final class MoshiHelper { 12 | 13 | public static void dump(BufferedSource source, JsonWriter writer) throws IOException { 14 | dump(JsonReader.of(source), writer); 15 | } 16 | 17 | public static void dump(JsonReader reader, BufferedSink sink) throws IOException { 18 | dump(reader, JsonWriter.of(sink)); 19 | } 20 | 21 | public static void dump(JsonReader reader, JsonWriter writer) throws IOException { 22 | int nested = 0; 23 | boolean nullFlag = writer.getSerializeNulls(); 24 | writer.setSerializeNulls(true); 25 | try { 26 | while (reader.peek() != JsonReader.Token.END_DOCUMENT) { 27 | switch (reader.peek()) { 28 | case BEGIN_ARRAY: 29 | nested++; 30 | reader.beginArray(); 31 | writer.beginArray(); 32 | break; 33 | case END_ARRAY: 34 | reader.endArray(); 35 | writer.endArray(); 36 | if (0 == --nested) return; 37 | break; 38 | case BEGIN_OBJECT: 39 | nested++; 40 | reader.beginObject(); 41 | writer.beginObject(); 42 | break; 43 | case END_OBJECT: 44 | reader.endObject(); 45 | writer.endObject(); 46 | if (0 == --nested) return; 47 | break; 48 | case NAME: 49 | writer.name(reader.nextName()); 50 | break; 51 | case NUMBER: 52 | Double doubleValue = reader.nextDouble(); 53 | if (Math.floor(doubleValue) == doubleValue) { 54 | writer.value(doubleValue.longValue()); 55 | } else { 56 | writer.value(doubleValue); 57 | } 58 | break; 59 | case BOOLEAN: 60 | writer.value(reader.nextBoolean()); 61 | break; 62 | case STRING: 63 | writer.value(reader.nextString()); 64 | break; 65 | case NULL: 66 | reader.nextNull(); 67 | writer.nullValue(); 68 | break; 69 | } 70 | } 71 | } finally { 72 | writer.setSerializeNulls(nullFlag); 73 | } 74 | } 75 | 76 | public static String nextNullableString(JsonReader reader) throws IOException { 77 | if (reader.peek() == JsonReader.Token.NULL) { 78 | reader.skipValue(); 79 | return null; 80 | } else { 81 | return reader.nextString(); 82 | } 83 | } 84 | 85 | public static T nextNullableObject(JsonReader reader, JsonAdapter adapter) throws IOException { 86 | if (reader.peek() == JsonReader.Token.NULL) { 87 | reader.skipValue(); 88 | return null; 89 | } else { 90 | return adapter.fromJson(reader); 91 | } 92 | } 93 | 94 | public static void writeNullable(JsonWriter writer, JsonAdapter valueAdapter, String name, T value) throws IOException { 95 | writeNullable(writer, valueAdapter, name, value, false); 96 | } 97 | 98 | public static void writeNullable(JsonWriter writer, JsonAdapter valueAdapter, String name, T value, boolean enforced) throws IOException { 99 | writer.name(name); 100 | writeNullableValue(writer, valueAdapter, value, enforced); 101 | } 102 | 103 | public static void writeNullableValue(JsonWriter writer, JsonAdapter adapter, T value, boolean enforced) throws IOException { 104 | if (value != null) { 105 | adapter.toJson(writer, value); 106 | } else { 107 | writeNull(writer, enforced); 108 | } 109 | } 110 | 111 | public static void writeNull(JsonWriter writer, boolean enforced) throws IOException { 112 | if (enforced) { 113 | boolean serializeFlag = writer.getSerializeNulls(); 114 | try { 115 | writer.setSerializeNulls(true); 116 | writer.nullValue(); 117 | } finally { 118 | writer.setSerializeNulls(serializeFlag); 119 | } 120 | } else { 121 | writer.nullValue(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/ResourceTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import moe.banana.jsonapi2.model.Article; 4 | import moe.banana.jsonapi2.model.Person; 5 | import moe.banana.jsonapi2.model.PlainObject; 6 | import org.junit.FixMethodOrder; 7 | import org.junit.Test; 8 | import org.junit.runners.MethodSorters; 9 | 10 | import static org.hamcrest.CoreMatchers.equalTo; 11 | import static org.hamcrest.CoreMatchers.hasItems; 12 | import static org.hamcrest.CoreMatchers.nullValue; 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertNotEquals; 15 | import static org.junit.Assert.assertThat; 16 | 17 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 18 | @SuppressWarnings("all") 19 | public class ResourceTest { 20 | 21 | private static final String JSON = "{" + 22 | " \"type\": \"articles\"," + 23 | " \"id\": \"1\"," + 24 | " \"attributes\": {" + 25 | " \"title\": \"JSON API paints my bikeshed!\"," + 26 | " \"ignored\": \"JSON API paints my bikeshed!\"," + 27 | " \"nullable\": null" + 28 | " }," + 29 | " \"relationships\": {" + 30 | " \"author\": {" + 31 | " \"links\": {" + 32 | " \"self\": \"http://example.com/articles/1/relationships/author\"," + 33 | " \"related\": \"http://example.com/articles/1/author\"" + 34 | " }," + 35 | " \"data\": { \"type\": \"people\", \"id\": \"9\" }" + 36 | " }," + 37 | " \"comments\": {" + 38 | " \"links\": {" + 39 | " \"self\": \"http://example.com/articles/1/relationships/comments\"," + 40 | " \"related\": \"http://example.com/articles/1/comments\"" + 41 | " }," + 42 | " \"data\": [" + 43 | " { \"type\": \"comments\", \"id\": \"5\" }," + 44 | " { \"type\": \"comments\", \"id\": \"12\" }" + 45 | " ]" + 46 | " }" + 47 | " }," + 48 | " \"links\": {" + 49 | " \"self\": \"http://example.com/articles/1\"" + 50 | " }" + 51 | "}"; 52 | 53 | @Test 54 | public void equality() throws Exception { 55 | Article a = TestUtil.moshi(Article.class).adapter(Article.class).fromJson(JSON); 56 | Article b = new Article(); 57 | assertNotEquals(b, a); 58 | assertEquals(b, new Article()); 59 | assertNotEquals(b, null); 60 | b.setId(a.getId()); 61 | assertEquals(b, a); 62 | assertEquals(b.hashCode(), a.hashCode()); 63 | } 64 | 65 | @Test 66 | public void deserialization() throws Exception { 67 | Article article = TestUtil.moshi(Article.class).adapter(Article.class).fromJson(JSON); 68 | assertThat(article.getId(), equalTo("1")); 69 | assertThat(article.getType(), equalTo("articles")); 70 | assertThat(article.getTitle(), equalTo("JSON API paints my bikeshed!")); 71 | assertThat(article.getIgnored(), nullValue()); 72 | assertThat(article.getNullable(), nullValue()); 73 | assertThat(article.getAuthor().get(), equalTo(new ResourceIdentifier("people", "9"))); 74 | assertThat(article.getComments().get(), hasItems( 75 | new ResourceIdentifier("comments", "5"), 76 | new ResourceIdentifier("comments", "12"))); 77 | } 78 | 79 | @Test 80 | public void serialization_empty() throws Exception { 81 | assertThat(TestUtil.moshi(Article.class).adapter(Article.class).toJson(new Article()), equalTo("{\"type\":\"articles\"}")); 82 | } 83 | 84 | @Test 85 | public void serialization_attributes() throws Exception { 86 | Article article = new Article(); 87 | article.setTitle("It sucks!"); 88 | article.setIgnored("should be ok to set"); 89 | assertThat(TestUtil.moshi(Article.class).adapter(Article.class).toJson(article), equalTo( 90 | "{\"type\":\"articles\",\"attributes\":{\"title\":\"It sucks!\"}}")); 91 | } 92 | 93 | @Test 94 | public void serialization_relationships() throws Exception { 95 | Article article = new Article(); 96 | article.setAuthor(new HasOne(new ResourceIdentifier("people", "2"))); 97 | assertThat(TestUtil.moshi(Article.class).adapter(Article.class).toJson(article), equalTo( 98 | "{\"type\":\"articles\",\"relationships\":{\"author\":{\"data\":{\"type\":\"people\",\"id\":\"2\"}}}}")); 99 | } 100 | 101 | @Test 102 | public void deserialization_pojo() throws Exception { 103 | PlainObject article = TestUtil.moshi(PlainObject.class).adapter(PlainObject.class).fromJson(JSON); 104 | assertThat(article.getId(), equalTo("1")); 105 | assertThat(article.getType(), equalTo("articles")); 106 | assertThat(article.title, equalTo("JSON API paints my bikeshed!")); 107 | assertThat(article.ignored, nullValue()); 108 | assertThat(article.author.get(), equalTo(new ResourceIdentifier("people", "9"))); 109 | assertThat(article.comments.get(), hasItems( 110 | new ResourceIdentifier("comments", "5"), 111 | new ResourceIdentifier("comments", "12"))); 112 | } 113 | 114 | @Test 115 | public void serialization_pojo() throws Exception { 116 | PlainObject article = new PlainObject(); 117 | article.title = "It sucks!"; 118 | article.author = new HasOne(new ResourceIdentifier("people", "2")); 119 | assertThat(TestUtil.moshi(PlainObject.class).adapter(PlainObject.class).toJson(article), equalTo( 120 | "{\"type\":\"articles\",\"attributes\":{\"title\":\"It sucks!\"},\"relationships\":{\"author\":{\"data\":{\"type\":\"people\",\"id\":\"2\"}}}}")); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/Error.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonReader; 5 | import com.squareup.moshi.JsonWriter; 6 | import com.squareup.moshi.Moshi; 7 | 8 | import java.io.IOException; 9 | import java.io.Serializable; 10 | 11 | import static moe.banana.jsonapi2.MoshiHelper.*; 12 | 13 | public class Error implements Serializable { 14 | 15 | private String id; 16 | private String status; 17 | private String code; 18 | private String title; 19 | private String detail; 20 | 21 | private JsonBuffer source; 22 | private JsonBuffer meta; 23 | private JsonBuffer links; 24 | 25 | public String getId() { 26 | return id; 27 | } 28 | 29 | public void setId(String id) { 30 | this.id = id; 31 | } 32 | 33 | public String getStatus() { 34 | return status; 35 | } 36 | 37 | public void setStatus(String status) { 38 | this.status = status; 39 | } 40 | 41 | public String getCode() { 42 | return code; 43 | } 44 | 45 | public void setCode(String code) { 46 | this.code = code; 47 | } 48 | 49 | public String getTitle() { 50 | return title; 51 | } 52 | 53 | public void setTitle(String title) { 54 | this.title = title; 55 | } 56 | 57 | public String getDetail() { 58 | return detail; 59 | } 60 | 61 | public void setDetail(String detail) { 62 | this.detail = detail; 63 | } 64 | 65 | public JsonBuffer getMeta() { 66 | return meta; 67 | } 68 | 69 | public JsonBuffer getLinks() { 70 | return links; 71 | } 72 | 73 | public void setMeta(JsonBuffer meta) { 74 | this.meta = meta; 75 | } 76 | 77 | public void setLinks(JsonBuffer links) { 78 | this.links = links; 79 | } 80 | 81 | public JsonBuffer getSource() { 82 | return source; 83 | } 84 | 85 | public void setSource(JsonBuffer source) { 86 | this.source = source; 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "Error{" + 92 | "id='" + id + '\'' + 93 | ", status='" + status + '\'' + 94 | ", code='" + code + '\'' + 95 | ", title='" + title + '\'' + 96 | ", detail='" + detail + '\'' + 97 | '}'; 98 | } 99 | 100 | @SuppressWarnings("SimplifiableIfStatement") 101 | @Override 102 | public boolean equals(Object o) { 103 | if (this == o) return true; 104 | if (o == null || getClass() != o.getClass()) return false; 105 | 106 | Error error = (Error) o; 107 | 108 | if (id != null ? !id.equals(error.id) : error.id != null) return false; 109 | if (status != null ? !status.equals(error.status) : error.status != null) return false; 110 | if (code != null ? !code.equals(error.code) : error.code != null) return false; 111 | if (title != null ? !title.equals(error.title) : error.title != null) return false; 112 | return detail != null ? detail.equals(error.detail) : error.detail == null; 113 | 114 | } 115 | 116 | @Override 117 | public int hashCode() { 118 | int result = id != null ? id.hashCode() : 0; 119 | result = 31 * result + (status != null ? status.hashCode() : 0); 120 | result = 31 * result + (code != null ? code.hashCode() : 0); 121 | result = 31 * result + (title != null ? title.hashCode() : 0); 122 | result = 31 * result + (detail != null ? detail.hashCode() : 0); 123 | return result; 124 | } 125 | 126 | static class Adapter extends JsonAdapter { 127 | 128 | JsonAdapter jsonBufferJsonAdapter; 129 | 130 | public Adapter(Moshi moshi) { 131 | jsonBufferJsonAdapter = moshi.adapter(JsonBuffer.class); 132 | } 133 | 134 | @Override 135 | public Error fromJson(JsonReader reader) throws IOException { 136 | Error err = new Error(); 137 | reader.beginObject(); 138 | while (reader.hasNext()) { 139 | switch (reader.nextName()) { 140 | case "id": 141 | err.setId(nextNullableString(reader)); 142 | break; 143 | case "status": 144 | err.setStatus(nextNullableString(reader)); 145 | break; 146 | case "code": 147 | err.setCode(nextNullableString(reader)); 148 | break; 149 | case "title": 150 | err.setTitle(nextNullableString(reader)); 151 | break; 152 | case "detail": 153 | err.setDetail(nextNullableString(reader)); 154 | break; 155 | case "source": 156 | err.setSource(nextNullableObject(reader, jsonBufferJsonAdapter)); 157 | break; 158 | case "meta": 159 | err.setMeta(nextNullableObject(reader, jsonBufferJsonAdapter)); 160 | break; 161 | case "links": 162 | err.setLinks(nextNullableObject(reader, jsonBufferJsonAdapter)); 163 | break; 164 | default: 165 | reader.skipValue(); 166 | break; 167 | } 168 | } 169 | reader.endObject(); 170 | return err; 171 | } 172 | 173 | @Override 174 | public void toJson(JsonWriter writer, Error value) throws IOException { 175 | writer.beginObject(); 176 | writer.name("id").value(value.getId()); 177 | writer.name("status").value(value.getStatus()); 178 | writer.name("code").value(value.getCode()); 179 | writer.name("title").value(value.getTitle()); 180 | writer.name("detail").value(value.getDetail()); 181 | writeNullable(writer, jsonBufferJsonAdapter, "source", value.getSource()); 182 | writeNullable(writer, jsonBufferJsonAdapter, "meta", value.getMeta()); 183 | writeNullable(writer, jsonBufferJsonAdapter, "links", value.getLinks()); 184 | writer.endObject(); 185 | } 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /retrofit-converter/src/test/java/moe/banana/jsonapi2/ConverterTest.java: -------------------------------------------------------------------------------- 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.junit.Rule; 7 | import org.junit.Test; 8 | import org.mockserver.client.server.MockServerClient; 9 | import org.mockserver.junit.MockServerRule; 10 | import retrofit2.Retrofit; 11 | 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import static org.hamcrest.CoreMatchers.*; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.mockserver.model.HttpRequest.request; 18 | import static org.mockserver.model.HttpResponse.response; 19 | 20 | @SuppressWarnings("all") 21 | public class ConverterTest { 22 | 23 | @Rule 24 | public MockServerRule mockServerRule = new MockServerRule(this); 25 | 26 | private TestApi api() throws Exception { 27 | Moshi moshi = TestUtil.moshi(Article.class, Comment.class); 28 | Retrofit retrofit = new Retrofit.Builder() 29 | .baseUrl("http://localhost:" + this.mockServerRule.getPort()) 30 | .addConverterFactory(JsonApiConverterFactory.create(moshi)) 31 | .build(); 32 | return retrofit.create(TestApi.class); 33 | } 34 | 35 | private MockServerClient server() { 36 | return mockServerRule.getClient(); 37 | } 38 | 39 | @Test 40 | public void deserialize_resources_to_list() throws Exception { 41 | server().when(request("/articles/1/comments").withMethod("GET")) 42 | .respond(response(TestUtil.fromResource("/multiple_comments.json"))); 43 | List comments = api().getComments("1").execute().body(); 44 | assertThat(comments, notNullValue()); 45 | assertThat(comments.get(0), instanceOf(Comment.class)); 46 | assertThat(comments.get(1).getId(), equalTo("12")); 47 | } 48 | 49 | @Test 50 | public void deserialize_resources_to_array() throws Exception { 51 | server().when(request("/articles").withMethod("GET")) 52 | .respond(response(TestUtil.fromResource("/multiple_compound.json"))); 53 | Article[] articles = api().listArticles().execute().body(); 54 | assertThat(articles, notNullValue()); 55 | assertThat(articles, instanceOf(Article[].class)); 56 | assertThat(articles[0], notNullValue()); 57 | assertThat(articles[0].getDocument(), notNullValue()); 58 | } 59 | 60 | @Test 61 | public void deserialize_resource_object() throws Exception { 62 | server().when(request("/articles/1").withMethod("GET")) 63 | .respond(response(TestUtil.fromResource("/single.json"))); 64 | Article article = api().getArticle("1").execute().body(); 65 | assertThat(article, notNullValue()); 66 | assertThat(article.getId(), equalTo("1")); 67 | assertThat(article.getDocument(), notNullValue()); 68 | } 69 | 70 | @Test 71 | public void serialize_resource_object() throws Exception { 72 | server().when( 73 | request("/articles/1/comments") 74 | .withMethod("POST") 75 | .withHeader("Content-Type", "application/vnd.api+json") 76 | .withBody("{\"data\":{\"type\":\"comments\",\"attributes\":{\"body\":\"Awesome!\"}}}")) 77 | .respond(response("{}")); 78 | Comment comment = new Comment(); 79 | comment.setBody("Awesome!"); 80 | Document response = api().addComment("1", comment).execute().body(); 81 | assertThat(response, notNullValue()); 82 | assertThat(response.asObjectDocument().hasData(), equalTo(false)); 83 | } 84 | 85 | @Test 86 | public void serialize_resource_list() throws Exception { 87 | server().when( 88 | request("/articles/1/comments") 89 | .withMethod("PUT") 90 | .withHeader("Content-Type", "application/vnd.api+json") 91 | .withBody("{\"data\":[{\"type\":\"comments\",\"attributes\":{\"body\":\"Awesome!\"}}]}")) 92 | .respond(response("{}")); 93 | Comment comment = new Comment(); 94 | comment.setBody("Awesome!"); 95 | Document response = api().addComments("1", Collections.singletonList(comment)).execute().body(); 96 | assertThat(response, notNullValue()); 97 | assertThat(response.asObjectDocument().hasData(), equalTo(false)); 98 | } 99 | 100 | @Test 101 | public void serialize_relationship_array() throws Exception { 102 | server().when( 103 | request("/articles/1/relationships/tags") 104 | .withMethod("PUT") 105 | .withHeader("Content-Type", "application/vnd.api+json") 106 | .withBody("{\"data\":[{\"type\":\"tags\",\"id\":\"1\"},{\"type\":\"tags\",\"id\":\"2\"}]}")) 107 | .respond(response("{}")); 108 | Document response = api().updateTags("1", new ResourceIdentifier[] { 109 | new ResourceIdentifier("tags", "1"), 110 | new ResourceIdentifier("tags", "2") 111 | }).execute().body(); 112 | assertThat(response, notNullValue()); 113 | assertThat(response.asObjectDocument().hasData(), equalTo(false)); 114 | } 115 | 116 | @Test 117 | public void deserialize_relationship_object() throws Exception { 118 | server().when(request("/articles/1/relationships/tags").withMethod("GET")) 119 | .respond(response(TestUtil.fromResource("/relationship_multi.json"))); 120 | ResourceIdentifier[] relData = api().getRelTags("1").execute().body(); 121 | assertThat(relData.length, equalTo(2)); 122 | assertThat(relData[0], instanceOf(ResourceIdentifier.class)); 123 | assertThat(relData[1].getType(), equalTo("tags")); 124 | } 125 | 126 | @Test 127 | public void serialize_relationship_object() throws Exception { 128 | server().when( 129 | request("/articles/1/relationships/author") 130 | .withMethod("PUT") 131 | .withHeader("Content-Type", "application/vnd.api+json") 132 | .withBody("{\"data\":{\"type\":\"people\",\"id\":\"1\"}}")) 133 | .respond(response("{}")); 134 | Document response = api().updateAuthor("1", new ResourceIdentifier("people", "1")).execute().body(); 135 | assertThat(response, notNullValue()); 136 | assertThat(response.asObjectDocument().hasData(), equalTo(false)); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/HasMany.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonReader; 5 | import com.squareup.moshi.JsonWriter; 6 | import com.squareup.moshi.Moshi; 7 | 8 | import java.io.IOException; 9 | import java.io.Serializable; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | 15 | import static moe.banana.jsonapi2.MoshiHelper.nextNullableObject; 16 | import static moe.banana.jsonapi2.MoshiHelper.writeNull; 17 | import static moe.banana.jsonapi2.MoshiHelper.writeNullable; 18 | 19 | public final class HasMany extends Relationship> implements Iterable, Serializable { 20 | 21 | private final List linkedResources = new ArrayList<>(); 22 | 23 | /** 24 | * null value is acceptable as data for a hasMany linkage. 25 | */ 26 | private boolean hasData = true; 27 | 28 | public HasMany() { 29 | } 30 | 31 | public HasMany(ResourceIdentifier... resources) { 32 | if (resources == null) { 33 | this.hasData = false; 34 | } else { 35 | for (ResourceIdentifier resource : resources) { 36 | add(resource); 37 | } 38 | } 39 | } 40 | 41 | @Override 42 | public List get(Document document) { 43 | return get(document, null); 44 | } 45 | 46 | public List get(Document document, T defaultValue) { 47 | List collector = new ArrayList<>(linkedResources.size()); 48 | for (ResourceIdentifier resourceId : linkedResources) { 49 | T obj = document.find(resourceId); 50 | collector.add(obj == null ? defaultValue : obj); 51 | } 52 | return collector; 53 | } 54 | 55 | public ResourceIdentifier get(int position) { 56 | return linkedResources.get(position); 57 | } 58 | 59 | public List get() { 60 | return Arrays.asList(linkedResources.toArray(new ResourceIdentifier[linkedResources.size()])); 61 | } 62 | 63 | @Override 64 | public Iterator iterator() { 65 | return linkedResources.iterator(); 66 | } 67 | 68 | public boolean add(ResourceIdentifier identifier) { 69 | if (identifier == null) { 70 | return false; 71 | } else if (identifier.getClass() == ResourceIdentifier.class) { 72 | this.hasData = true; 73 | return linkedResources.add(identifier); 74 | } else { 75 | return add(identifier.getType(), identifier.getId()); 76 | } 77 | } 78 | 79 | public boolean add(String type, String id) { 80 | return add(new ResourceIdentifier(type, id)); 81 | } 82 | 83 | public boolean remove(ResourceIdentifier identifier) { 84 | return remove(identifier.getType(), identifier.getId()); 85 | } 86 | 87 | public boolean remove(String type, String id) { 88 | return linkedResources.remove(new ResourceIdentifier(type, id)); 89 | } 90 | 91 | public int size() { 92 | return linkedResources.size(); 93 | } 94 | 95 | public boolean hasData() { 96 | return this.hasData; 97 | } 98 | 99 | @Override 100 | public String toString() { 101 | return "HasMany{" + 102 | "linkedResources=" + linkedResources + 103 | '}'; 104 | } 105 | 106 | @Override 107 | public boolean equals(Object o) { 108 | if (this == o) return true; 109 | if (o == null || getClass() != o.getClass()) return false; 110 | 111 | HasMany hasMany = (HasMany) o; 112 | 113 | return linkedResources.equals(hasMany.linkedResources); 114 | 115 | } 116 | 117 | @Override 118 | public int hashCode() { 119 | return linkedResources.hashCode(); 120 | } 121 | 122 | static class Adapter extends JsonAdapter> { 123 | 124 | JsonAdapter resourceIdentifierJsonAdapter; 125 | JsonAdapter jsonBufferJsonAdapter; 126 | 127 | public Adapter(Moshi moshi) { 128 | resourceIdentifierJsonAdapter = moshi.adapter(ResourceIdentifier.class); 129 | jsonBufferJsonAdapter = moshi.adapter(JsonBuffer.class); 130 | } 131 | 132 | @Override 133 | public HasMany fromJson(JsonReader reader) throws IOException { 134 | HasMany relationship = new HasMany<>(); 135 | reader.beginObject(); 136 | while (reader.hasNext()) { 137 | switch (reader.nextName()) { 138 | case "data": 139 | if (reader.peek() == JsonReader.Token.NULL) { 140 | relationship.hasData = false; 141 | reader.nextNull(); 142 | } else { 143 | reader.beginArray(); 144 | while (reader.hasNext()) { 145 | relationship.add(resourceIdentifierJsonAdapter.fromJson(reader)); 146 | } 147 | reader.endArray(); 148 | } 149 | break; 150 | case "meta": 151 | relationship.setMeta(nextNullableObject(reader, jsonBufferJsonAdapter)); 152 | break; 153 | case "links": 154 | relationship.setLinks(nextNullableObject(reader, jsonBufferJsonAdapter)); 155 | break; 156 | default: 157 | reader.skipValue(); 158 | break; 159 | } 160 | } 161 | reader.endObject(); 162 | return relationship; 163 | } 164 | 165 | @Override 166 | public void toJson(JsonWriter writer, HasMany value) throws IOException { 167 | writer.beginObject(); 168 | writer.name("data"); 169 | if (!value.hasData) { 170 | writeNull(writer, true); 171 | } else { 172 | writer.beginArray(); 173 | for (ResourceIdentifier resource : value.linkedResources) { 174 | resourceIdentifierJsonAdapter.toJson(writer, resource); 175 | } 176 | writer.endArray(); 177 | } 178 | writeNullable(writer, jsonBufferJsonAdapter, "meta", value.getMeta()); 179 | writeNullable(writer, jsonBufferJsonAdapter, "links", value.getLinks()); 180 | writer.endObject(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/ResourceAdapter.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.*; 4 | 5 | import java.io.IOException; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Modifier; 9 | import java.util.*; 10 | 11 | import static moe.banana.jsonapi2.MoshiHelper.*; 12 | 13 | class ResourceAdapter extends JsonAdapter { 14 | 15 | private final Constructor constructor; 16 | 17 | private static final int TYPE_ATTRIBUTE = 0x01; 18 | private static final int TYPE_RELATIONSHIP = 0x03; 19 | 20 | private final Map bindings = new LinkedHashMap<>(); 21 | private final JsonAdapter jsonBufferJsonAdapter; 22 | 23 | ResourceAdapter(Class type, JsonNameMapping jsonNameMapping, Moshi moshi) { 24 | this.jsonBufferJsonAdapter = moshi.adapter(JsonBuffer.class); 25 | 26 | try { 27 | constructor = type.getDeclaredConstructor(); 28 | constructor.setAccessible(true); 29 | } catch (NoSuchMethodException e) { 30 | throw new IllegalArgumentException("No default constructor on [" + type + "]", e); 31 | } 32 | 33 | for (Field field : listFields(type, Resource.class)) { 34 | int modifiers = field.getModifiers(); 35 | if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers)) { 36 | // skip transient fields and static fields 37 | continue; 38 | } 39 | if (!Modifier.isPublic(modifiers) || Modifier.isFinal(modifiers)) { 40 | // make private or final fields accessible 41 | field.setAccessible(true); 42 | } 43 | String name = jsonNameMapping.getJsonName(field); 44 | if (bindings.containsKey(name)) { 45 | throw new IllegalArgumentException("Duplicated field '" + name + "' in [" + type + "]."); 46 | } 47 | bindings.put(name, new FieldAdapter<>(field, 48 | Relationship.class.isAssignableFrom(Types.getRawType(field.getGenericType())) ? TYPE_RELATIONSHIP: TYPE_ATTRIBUTE, 49 | moshi.adapter(field.getGenericType(), AnnotationUtils.jsonAnnotations(field.getAnnotations())))); 50 | } 51 | } 52 | 53 | @Override 54 | public T fromJson(JsonReader reader) throws IOException { 55 | T resource; 56 | try { 57 | resource = constructor.newInstance(); 58 | } catch (Exception e) { 59 | throw new RuntimeException(e); 60 | } 61 | reader.beginObject(); 62 | while (reader.hasNext()) { 63 | switch (reader.nextName()) { 64 | case "id": 65 | resource.setId(nextNullableString(reader)); 66 | break; 67 | case "type": 68 | resource.setType(nextNullableString(reader)); 69 | break; 70 | case "attributes": 71 | case "relationships": 72 | readFields(reader, resource); 73 | break; 74 | case "meta": 75 | resource.setMeta(nextNullableObject(reader, jsonBufferJsonAdapter)); 76 | break; 77 | case "links": 78 | resource.setLinks(nextNullableObject(reader, jsonBufferJsonAdapter)); 79 | break; 80 | default: 81 | reader.skipValue(); 82 | break; 83 | } 84 | } 85 | reader.endObject(); 86 | return resource; 87 | } 88 | 89 | 90 | @Override 91 | public void toJson(JsonWriter writer, T value) throws IOException { 92 | writer.beginObject(); 93 | writer.name("type").value(value.getType()); 94 | writer.name("id").value(value.getId()); 95 | writeFields(writer, TYPE_ATTRIBUTE, "attributes", value); 96 | writeFields(writer, TYPE_RELATIONSHIP, "relationships", value); 97 | writeNullable(writer, jsonBufferJsonAdapter, "meta", value.getMeta()); 98 | writeNullable(writer, jsonBufferJsonAdapter, "links", value.getLinks()); 99 | writer.endObject(); 100 | } 101 | 102 | private void readFields(JsonReader reader, Object resource) throws IOException { 103 | reader.beginObject(); 104 | while (reader.hasNext()) { 105 | FieldAdapter fieldAdapter = bindings.get(reader.nextName()); 106 | if (fieldAdapter != null) { 107 | fieldAdapter.readFrom(reader, resource); 108 | } else { 109 | reader.skipValue(); 110 | } 111 | } 112 | reader.endObject(); 113 | } 114 | 115 | private void writeFields(JsonWriter writer, int fieldType, String name, Object value) throws IOException { 116 | boolean skipFlag = true; 117 | for (Map.Entry entry : bindings.entrySet()) { 118 | FieldAdapter adapter = entry.getValue(); 119 | if (adapter.fieldType != fieldType) { 120 | continue; 121 | } 122 | if (adapter.get(value) == null && !writer.getSerializeNulls()) { 123 | // skip write of null values 124 | continue; 125 | } 126 | if (skipFlag) { 127 | writer.name(name).beginObject(); 128 | skipFlag = false; 129 | } 130 | writer.name(entry.getKey()); 131 | adapter.writeTo(writer, value); 132 | } 133 | if (!skipFlag) { 134 | writer.endObject(); 135 | } 136 | } 137 | 138 | private static List listFields(Class type, Class baseType) { 139 | List fields = new ArrayList<>(); 140 | Class clazz = type; 141 | while (clazz != baseType) { 142 | Collections.addAll(fields, clazz.getDeclaredFields()); 143 | clazz = clazz.getSuperclass(); 144 | } 145 | return fields; 146 | } 147 | 148 | private static class FieldAdapter { 149 | 150 | final Field field; 151 | final JsonAdapter adapter; 152 | final int fieldType; 153 | 154 | FieldAdapter(Field field, int fieldType, JsonAdapter adapter) { 155 | this.field = field; 156 | this.fieldType = fieldType; 157 | this.adapter = adapter; 158 | } 159 | 160 | void set(Object target, T value){ 161 | try { 162 | field.set(target, value); 163 | } catch (IllegalAccessException e) { 164 | throw new RuntimeException(e); 165 | } 166 | } 167 | 168 | @SuppressWarnings("unchecked") 169 | T get(Object object) { 170 | try { 171 | return (T) field.get(object); 172 | } catch (IllegalAccessException e) { 173 | throw new RuntimeException(e); 174 | } 175 | } 176 | 177 | void readFrom(JsonReader reader, Object object) throws IOException { 178 | set(object, nextNullableObject(reader, adapter)); 179 | } 180 | 181 | void writeTo(JsonWriter writer, Object object) throws IOException { 182 | writeNullableValue(writer, adapter, get(object), false); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /retrofit-converter/src/main/java/moe/banana/jsonapi2/JsonApiConverterFactory.java: -------------------------------------------------------------------------------- 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 | import okhttp3.MediaType; 7 | import okhttp3.RequestBody; 8 | import okhttp3.ResponseBody; 9 | import okio.Buffer; 10 | import retrofit2.Converter; 11 | import retrofit2.Retrofit; 12 | 13 | import java.io.IOException; 14 | import java.lang.annotation.Annotation; 15 | import java.lang.reflect.Array; 16 | import java.lang.reflect.ParameterizedType; 17 | import java.lang.reflect.Type; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | @SuppressWarnings("unchecked") 22 | public final class JsonApiConverterFactory extends Converter.Factory { 23 | 24 | public static JsonApiConverterFactory create() { 25 | return create(new Moshi.Builder().build()); 26 | } 27 | 28 | public static JsonApiConverterFactory create(Moshi moshi) { 29 | return new JsonApiConverterFactory(moshi, false); 30 | } 31 | 32 | private final Moshi moshi; 33 | private final boolean lenient; 34 | 35 | private JsonApiConverterFactory(Moshi moshi, boolean lenient) { 36 | if (moshi == null) throw new NullPointerException("moshi == null"); 37 | this.moshi = moshi; 38 | this.lenient = lenient; 39 | } 40 | 41 | public JsonApiConverterFactory asLenient() { 42 | return new JsonApiConverterFactory(moshi, true); 43 | } 44 | 45 | private JsonAdapter getAdapterFromType(Type type) { 46 | Class rawType = Types.getRawType(type); 47 | JsonAdapter adapter; 48 | if (rawType.isArray() && ResourceIdentifier.class.isAssignableFrom(rawType.getComponentType())) { 49 | adapter = moshi.adapter(Types.newParameterizedType(Document.class, rawType.getComponentType())); 50 | } else if (List.class.isAssignableFrom(rawType) && type instanceof ParameterizedType) { 51 | Type typeParameter = ((ParameterizedType) type).getActualTypeArguments()[0]; 52 | if (typeParameter instanceof Class && ResourceIdentifier.class.isAssignableFrom((Class) typeParameter)) { 53 | adapter = moshi.adapter(Types.newParameterizedType(Document.class, typeParameter)); 54 | } else { 55 | return null; 56 | } 57 | } else if (ResourceIdentifier.class.isAssignableFrom(rawType)) { 58 | adapter = moshi.adapter(Types.newParameterizedType(Document.class, rawType)); 59 | } else if (Document.class.isAssignableFrom(rawType)) { 60 | adapter = moshi.adapter(Types.newParameterizedType(Document.class, Resource.class)); 61 | } else { 62 | return null; 63 | } 64 | return adapter; 65 | } 66 | 67 | @Override 68 | public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { 69 | JsonAdapter adapter = getAdapterFromType(type); 70 | if (adapter == null) { 71 | return null; 72 | } 73 | if (lenient) { 74 | adapter = adapter.lenient(); 75 | } 76 | return new MoshiResponseBodyConverter<>((JsonAdapter) adapter, type); 77 | } 78 | 79 | @Override 80 | public Converter requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { 81 | JsonAdapter adapter = getAdapterFromType(type); 82 | if (adapter == null) { 83 | return null; 84 | } 85 | if (lenient) { 86 | adapter = adapter.lenient(); 87 | } 88 | return new MoshiRequestBodyConverter<>((JsonAdapter) adapter, type); 89 | } 90 | 91 | private static final MediaType MEDIA_TYPE = MediaType.parse("application/vnd.api+json"); 92 | 93 | private static class MoshiResponseBodyConverter implements Converter { 94 | private final JsonAdapter adapter; 95 | private final Class rawType; 96 | 97 | MoshiResponseBodyConverter(JsonAdapter adapter, Type type) { 98 | this.adapter = adapter; 99 | this.rawType = (Class) Types.getRawType(type); 100 | } 101 | 102 | @Override 103 | public R convert(ResponseBody value) throws IOException { 104 | try { 105 | Document document = adapter.fromJson(value.source()); 106 | if (Document.class.isAssignableFrom(rawType)) { 107 | return (R) document; 108 | } else if (List.class.isAssignableFrom(rawType)) { 109 | ArrayDocument arrayDocument = document.asArrayDocument(); 110 | List a; 111 | if (rawType.isAssignableFrom(ArrayList.class)) { 112 | a = new ArrayList(); 113 | } else { 114 | a = (List) rawType.newInstance(); 115 | } 116 | a.addAll(arrayDocument); 117 | return (R) a; 118 | } else if (rawType.isArray()) { 119 | ArrayDocument arrayDocument = document.asArrayDocument(); 120 | Object a = Array.newInstance(rawType.getComponentType(), arrayDocument.size()); 121 | for (int i = 0; i != Array.getLength(a); i++) { 122 | Array.set(a, i, arrayDocument.get(i)); 123 | } 124 | return (R) a; 125 | } else { 126 | return (R) document.asObjectDocument().get(); 127 | } 128 | } catch (InstantiationException e) { 129 | throw new RuntimeException("Cannot find default constructor of [" + rawType.getCanonicalName() + "].", e); 130 | } catch (IllegalAccessException e) { 131 | throw new RuntimeException("Cannot access default constructor of [" + rawType.getCanonicalName() + "].", e); 132 | } finally { 133 | value.close(); 134 | } 135 | } 136 | } 137 | 138 | private static class MoshiRequestBodyConverter implements Converter { 139 | 140 | private final JsonAdapter adapter; 141 | private final Class rawType; 142 | 143 | MoshiRequestBodyConverter(JsonAdapter adapter, Type type) { 144 | this.adapter = adapter; 145 | this.rawType = (Class) Types.getRawType(type); 146 | } 147 | 148 | @Override 149 | public RequestBody convert(T value) throws IOException { 150 | Document document; 151 | if (Document.class.isAssignableFrom(rawType)) { 152 | document = (Document) value; 153 | } else if (List.class.isAssignableFrom(rawType)) { 154 | ArrayDocument arrayDocument = new ArrayDocument(); 155 | List a = ((List) value); 156 | if (!a.isEmpty() && a.get(0) != null && ((ResourceIdentifier) a.get(0)).getContext() != null) { 157 | arrayDocument = ((ResourceIdentifier) a.get(0)).getContext().asArrayDocument(); 158 | } 159 | arrayDocument.addAll(a); 160 | document = arrayDocument; 161 | } else if (rawType.isArray()) { 162 | ArrayDocument arrayDocument = new ArrayDocument(); 163 | if (Array.getLength(value) > 0 && ((ResourceIdentifier) Array.get(value, 0)).getContext() != null) { 164 | arrayDocument = ((ResourceIdentifier) Array.get(value, 0)).getContext().asArrayDocument(); 165 | } 166 | for (int i = 0; i != Array.getLength(value); i++) { 167 | arrayDocument.add((ResourceIdentifier) Array.get(value, i)); 168 | } 169 | document = arrayDocument; 170 | } else { 171 | ResourceIdentifier data = ((ResourceIdentifier) value); 172 | ObjectDocument objectDocument = new ObjectDocument(); 173 | if (data.getDocument() != null) { 174 | objectDocument = data.getDocument().asObjectDocument(); 175 | } 176 | objectDocument.set(data); 177 | document = objectDocument; 178 | } 179 | Buffer buffer = new Buffer(); 180 | adapter.toJson(buffer, document); 181 | return RequestBody.create(MEDIA_TYPE, buffer.readByteString()); 182 | } 183 | } 184 | 185 | } -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/Document.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import java.io.Serializable; 4 | import java.util.*; 5 | 6 | public abstract class Document implements Serializable { 7 | 8 | List errors = new ArrayList<>(0); 9 | Map included = new HashMap<>(0); 10 | 11 | private JsonBuffer meta; 12 | private JsonBuffer links; 13 | private JsonBuffer jsonApi; 14 | 15 | public Document() { 16 | } 17 | 18 | public Document(Document document) { 19 | this.meta = document.meta; 20 | this.links = document.links; 21 | this.jsonApi = document.jsonApi; 22 | this.included.putAll(document.included); 23 | this.errors.addAll(document.errors); 24 | } 25 | 26 | @Deprecated 27 | public boolean include(Resource resource) { 28 | return addInclude(resource); 29 | } 30 | 31 | @Deprecated 32 | public boolean exclude(Resource resource) { 33 | return getIncluded().remove(resource); 34 | } 35 | 36 | public boolean addInclude(Resource resource) { 37 | return getIncluded().add(resource); 38 | } 39 | 40 | public Collection getIncluded() { 41 | return new Collection() { 42 | @Override 43 | public int size() { 44 | return included.size(); 45 | } 46 | 47 | @Override 48 | public boolean isEmpty() { 49 | return included.isEmpty(); 50 | } 51 | 52 | @Override 53 | public boolean contains(Object o) { 54 | return included.containsValue(o); 55 | } 56 | 57 | @Override 58 | public Iterator iterator() { 59 | return included.values().iterator(); 60 | } 61 | 62 | @Override 63 | public Object[] toArray() { 64 | return included.values().toArray(); 65 | } 66 | 67 | @Override 68 | public T[] toArray(T[] a) { 69 | return included.values().toArray(a); 70 | } 71 | 72 | @Override 73 | public boolean add(Resource resource) { 74 | bindDocument(Document.this, resource); 75 | included.put(new ResourceIdentifier(resource), resource); 76 | return true; 77 | } 78 | 79 | @Override 80 | public boolean remove(Object o) { 81 | if (o instanceof ResourceIdentifier) { 82 | Resource resource = included.remove(new ResourceIdentifier(((ResourceIdentifier) o))); 83 | bindDocument(null, resource); 84 | return resource != null; 85 | } 86 | return false; 87 | } 88 | 89 | @Override 90 | public boolean containsAll(Collection c) { 91 | return included.values().containsAll(c); 92 | } 93 | 94 | @Override 95 | public boolean addAll(Collection c) { 96 | for (Resource resource : c) { 97 | add(resource); 98 | } 99 | return true; 100 | } 101 | 102 | @Override 103 | public boolean removeAll(Collection c) { 104 | for (Object o : c) { 105 | remove(o); 106 | } 107 | return true; 108 | } 109 | 110 | @Override 111 | public boolean retainAll(Collection c) { 112 | return false; 113 | } 114 | 115 | @Override 116 | public void clear() { 117 | bindDocument(null, included.values()); 118 | included.clear(); 119 | } 120 | }; 121 | } 122 | 123 | @SuppressWarnings({"SuspiciousMethodCalls", "unchecked"}) 124 | public T find(ResourceIdentifier resourceIdentifier) { 125 | return (T) included.get(resourceIdentifier); 126 | } 127 | 128 | public T find(String type, String id) { 129 | return find(new ResourceIdentifier(type, id)); 130 | } 131 | 132 | @Deprecated 133 | public boolean errors(List errors) { 134 | return setErrors(errors); 135 | } 136 | 137 | @Deprecated 138 | public List errors() { 139 | return getErrors(); 140 | } 141 | 142 | public boolean addError(Error error) { 143 | return errors.add(error); 144 | } 145 | 146 | public boolean setErrors(Collection errors) { 147 | this.errors.clear(); 148 | if (errors != null) { 149 | this.errors.addAll(errors); 150 | } 151 | return true; 152 | } 153 | 154 | public List getErrors() { 155 | return errors; 156 | } 157 | 158 | public boolean hasError() { 159 | return errors.size() != 0; 160 | } 161 | 162 | public JsonBuffer getMeta() { 163 | return meta; 164 | } 165 | 166 | public void setMeta(JsonBuffer meta) { 167 | this.meta = meta; 168 | } 169 | 170 | public JsonBuffer getLinks() { 171 | return links; 172 | } 173 | 174 | public void setLinks(JsonBuffer links) { 175 | this.links = links; 176 | } 177 | 178 | public JsonBuffer getJsonApi() { 179 | return jsonApi; 180 | } 181 | 182 | public void setJsonApi(JsonBuffer jsonApi) { 183 | this.jsonApi = jsonApi; 184 | } 185 | 186 | @SuppressWarnings("unchecked") 187 | public ArrayDocument asArrayDocument() { 188 | if (this instanceof ArrayDocument) { 189 | return ((ArrayDocument) this); 190 | } else if (this instanceof ObjectDocument) { 191 | ArrayDocument document = new ArrayDocument<>(this); 192 | DATA data = ((ObjectDocument) this).get(); 193 | if (data != null) { 194 | document.add(data); 195 | } 196 | return document; 197 | } 198 | throw new AssertionError("unexpected document type"); 199 | } 200 | 201 | public ObjectDocument asObjectDocument() { 202 | return asObjectDocument(0); 203 | } 204 | 205 | @SuppressWarnings("unchecked") 206 | public ObjectDocument asObjectDocument(int position) { 207 | if (this instanceof ObjectDocument) { 208 | return ((ObjectDocument) this); 209 | } else if (this instanceof ArrayDocument) { 210 | ObjectDocument document = new ObjectDocument<>(this); 211 | if (((ArrayDocument) this).size() > position) { 212 | document.set(((ArrayDocument) this).get(position)); 213 | } 214 | return document; 215 | } 216 | throw new AssertionError("unexpected document type"); 217 | } 218 | 219 | @Override 220 | public boolean equals(Object o) { 221 | if (this == o) return true; 222 | if (o == null || getClass() != o.getClass()) return false; 223 | 224 | Document document = (Document) o; 225 | 226 | if (!included.equals(document.included)) return false; 227 | if (!errors.equals(document.errors)) return false; 228 | if (meta != null ? !meta.equals(document.meta) : document.meta != null) return false; 229 | if (links != null ? !links.equals(document.links) : document.links != null) return false; 230 | return jsonApi != null ? jsonApi.equals(document.jsonApi) : document.jsonApi == null; 231 | } 232 | 233 | @Override 234 | public int hashCode() { 235 | int result = included.hashCode(); 236 | result = 31 * result + errors.hashCode(); 237 | result = 31 * result + (meta != null ? meta.hashCode() : 0); 238 | result = 31 * result + (links != null ? links.hashCode() : 0); 239 | result = 31 * result + (jsonApi != null ? jsonApi.hashCode() : 0); 240 | return result; 241 | } 242 | 243 | static void bindDocument(Document document, Object resource) { 244 | if (resource instanceof ResourceIdentifier) { 245 | ((ResourceIdentifier) resource).setDocument(document); 246 | } 247 | } 248 | 249 | static void bindDocument(Document document, Collection resources) { 250 | for (Object i : resources) { 251 | bindDocument(document, i); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moshi-jsonapi 2 | 3 | [![Build Status](https://travis-ci.org/kamikat/moshi-jsonapi.svg?branch=master)](https://travis-ci.org/kamikat/moshi-jsonapi) 4 | [![Coverage Status](https://coveralls.io/repos/github/kamikat/moshi-jsonapi/badge.svg?branch=master)](https://coveralls.io/github/kamikat/moshi-jsonapi?branch=master) 5 | [![Download](https://api.bintray.com/packages/kamikat/maven/moshi-jsonapi/images/download.svg)](https://bintray.com/kamikat/maven/moshi-jsonapi/_latestVersion) 6 | 7 | Java implementation of [JSON API](http://jsonapi.org/) specification v1.0 for [moshi](https://github.com/square/moshi). 8 | 9 | ## Getting Started 10 | 11 | ```java 12 | JsonAdapter.Factory jsonApiAdapterFactory = ResourceAdapterFactory.builder() 13 | .add(Article.class) 14 | .add(Person.class) 15 | .add(Comment.class) 16 | // ... 17 | .build(); 18 | Moshi moshi = new Moshi.Builder() 19 | .add(jsonApiAdapterFactory) 20 | // ... 21 | .build(); 22 | ``` 23 | 24 | You're now ready to serialize/deserialize JSON API objects with cool Moshi interface! 25 | 26 | ```java 27 | String json = "..."; 28 | ArrayDocument
articles = moshi.adapter(Document.class).fromJson(json).asArrayDocument(); 29 | for (Article article : articles) { 30 | System.out.println(article.title); 31 | } 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Resource Object 37 | 38 | Extend a `Resource` class to create a model for resource object. 39 | 40 | ```java 41 | @JsonApi(type = "people") 42 | class Person extends Resource { 43 | @Json(name="first-name") String firstName; 44 | @Json(name="last-name") String lastName; 45 | String twitter; 46 | } 47 | ``` 48 | 49 | `@JsonApi(type = ...)` annotation identifies each model by `type` as is mentioned in specification. 50 | 51 | ### Relationships 52 | 53 | There are two kinds of relationship defined in JSON API specification. 54 | Defining these relationship in resource object is quite simple: 55 | 56 | ```java 57 | @JsonApi(type = "articles") 58 | public class Article extends Resource { 59 | public String title; 60 | public HasOne author; 61 | public HasMany comments; 62 | } 63 | ``` 64 | 65 | Relationships can be resolved to resource object in a `Document`: 66 | 67 | ```java 68 | Person author = article.author.get(article.getDocument()); 69 | ``` 70 | 71 | You can use `Resource.getDocument()` to access the `Document` object the `Resource` be added/included in. 72 | Further more, with a little bit encapsulation: 73 | 74 | ```java 75 | @JsonApi(type = "articles") 76 | public class Article extends Resource { 77 | private String title; 78 | private HasOne author; 79 | private HasMany comments; 80 | 81 | public String getTitle() { 82 | return title; 83 | } 84 | 85 | public void setTitle(String title) { 86 | this.title = title; 87 | } 88 | 89 | public Person getAuthor() { 90 | return author.get(getDocument()); 91 | } 92 | 93 | public List getComments() { 94 | return comments.get(getDocument()); 95 | } 96 | } 97 | ``` 98 | 99 | ### Document 100 | 101 | `Document` interfaces denotes a JSON API document, document object contains one of the following attributes: 102 | 103 | - `data` the primary data, can be null, resource object or array of resource object 104 | - `error` error object 105 | - `meta` 106 | 107 | To keep consistency with the specification, moshi-jsonapi implements `ArrayDocument` and `ObjectDocument`. 108 | `Document` object can be converted with `Document.asXDocument()` function. 109 | 110 | ```java 111 | ObjectDocument
document = new ObjectDocument<>(); 112 | document.set(article); 113 | document.addInclude(author); 114 | 115 | // Serialize 116 | System.out.println(moshi.adapter(Document.class).toJson(document)); 117 | // => { 118 | // data: { "type": "articles", "relationships": { "author": { "data": "type": "people", id: "1" } } }, 119 | // included: [ 120 | // { "type": "people", "attributes": { "first-name": "Yuki", "last-name": "Kiriyama", "twitter": "kamikat_bot" } } 121 | // ] 122 | // } 123 | 124 | // Deserialize 125 | Document document2 = adapter.fromJson(...); 126 | ObjectDocument
document3 = document2.asObjectDocument(); 127 | assert document3.get() instanceof Article 128 | assert document3.get().getDocument() == document3 129 | ``` 130 | 131 | The linkage (relationship) of a resource object is resolved in document of the resource object (check `Resource.getDocument()`). 132 | 133 | ### Default Resource Type 134 | 135 | Create a `default` typed class to have all unknown type parsed in the class to avoid deserialization error processing unknown type of resource. 136 | 137 | ```java 138 | @JsonApi(type = "default") 139 | class Unknown extends Resource { 140 | // nothing... 141 | } 142 | ``` 143 | 144 | ### meta/links/jsonapi Properties 145 | 146 | You'd like to access `meta`/`links`/`jsonapi` value on `Document` for example. 147 | 148 | ```java 149 | Document document = ...; 150 | document.getMeta() // => JsonBuffer 151 | ``` 152 | 153 | As `meta` and `links` can contain a variant of objects, they are not been parsed when access with `getMeta` and `getLinks`. 154 | You will get a `JsonBuffer` and you're expected to implement your `JsonAdapter` to read/write these objects. 155 | 156 | ### Retrofit 157 | 158 | Retrofit extension library (see following section) provides `JsonApiConverterFactory` to get integrate with Retrofit 2. 159 | Here's an example: 160 | 161 | ```java 162 | Retrofit retrofit = new Retrofit.Builder() 163 | // ... 164 | .addConverterFactory(JsonApiConverterFactory.create(moshi)) 165 | .build() 166 | retrofit.create(MyAPI.class); 167 | ``` 168 | 169 | And `MyAPI` interface: 170 | 171 | ```java 172 | public interface MyAPI { 173 | 174 | @GET("posts") 175 | Call listPosts(); 176 | 177 | @GET("posts/{id}") 178 | Call getPost(@Path("id") String id); 179 | 180 | @GET("posts/{id}/comments") 181 | Call> getComments(@Path("id") String id); 182 | 183 | @POST("posts/{id}/comments") 184 | Call addComment(@Path("id") String id, @Body Comment comment); 185 | 186 | @GET("posts/{id}/relationships/comments") 187 | Call getCommentRels(@Path("id") String id); 188 | } 189 | ``` 190 | 191 | Note that the body can either be serialized/deserialized to resource object or document object with additional information. 192 | 193 | ## Download 194 | 195 | In gradle build script: 196 | 197 | ```groovy 198 | repositories { 199 | jcenter() 200 | } 201 | 202 | dependencies { 203 | implementation 'com.squareup.moshi:moshi:1.4.0' // required, peer dependency to moshi 204 | implementation 'moe.banana:moshi-jsonapi:' // required, core library 205 | implementation 'moe.banana:moshi-jsonapi-retrofit-converter:' // optional, for retrofit 206 | } 207 | ``` 208 | 209 | For library version >= 3.5, moshi is removed from runtime dependencies of the library to become a peer dependency. 210 | 211 | Use snapshot version: 212 | 213 | ```groovy 214 | repositories { 215 | maven { url "https://jitpack.io" } 216 | } 217 | 218 | dependencies { 219 | implementation 'com.squareup.moshi:moshi:1.4.0' 220 | implementation 'moe.banana:moshi-jsonapi:master-SNAPSHOT' 221 | } 222 | ``` 223 | 224 | NOTE: It's necessary clean gradle library cache to access the latest snapshot version. 225 | 226 | ## Proguard Guide 227 | 228 | For moshi-jsonapi: 229 | 230 | ``` 231 | -keepattributes Signature 232 | -keepclassmembers public abstract class moe.banana.jsonapi2.** { 233 | *; 234 | } 235 | ``` 236 | 237 | For moshi, if you use a custom JSON adapter (e.g. for Enum types): 238 | 239 | ``` 240 | -keepclassmembers class ** { 241 | @com.squareup.moshi.FromJson *; 242 | @com.squareup.moshi.ToJson *; 243 | } 244 | ``` 245 | 246 | ## Supported Features 247 | 248 | | Feature | Supported | Note | 249 | | ------------------------------ | --------- | ----------------------------------------------- | 250 | | Serialization | Yes | | 251 | | Deserialization | Yes | | 252 | | Custom-named fields | Yes | With `@Json` | 253 | | Top level errors | Yes | | 254 | | Top level metadata | Yes | | 255 | | Top level links | Yes | | 256 | | Top level JSON API Object | Yes | | 257 | | Resource metadata | Yes | | 258 | | Resource links | Yes | | 259 | | Relationships | Yes | `HasOne` and `HasMany` | 260 | | Inclusion of related resources | Yes | | 261 | | Resource IDs | Yes | | 262 | 263 | ## Migration Note for 3.4 and 3.5 264 | 265 | Release 3.4 removed type parameter from `Document` object which can break your code. Please replace the type declaration with 266 | `ObjectDocument` or `ArrayDocument` if you insist that. 267 | 268 | Release 3.5 changes the dependency to moshi from runtime dependency to compile-only dependency, which means moshi-jsonapi does no longer 269 | includes moshi as a dependency for your project. And you need to add moshi to the dependencies of the project manually. 270 | 271 | ## Migration from 2.x to 3.x 272 | 273 | 3.x supports all features supported by JSON API specification. And the interface changed a lot especially in serialization/deserialization. 274 | More object oriented features are added to new API. If you're using the library with Retrofit, migration should be a lot easier by using a 275 | special `Converter` adapts `Document
` to `Article[]` and backward as well (see [retrofit section](#retrofit)). Migration should be 276 | easy if you use latest 2.x API with some OO features already available. Otherwise, it can take hours to migrate to new API. 277 | 278 | ## Migration from 1.x to 2.x 279 | 280 | 2.x abandoned much of seldomly used features of JSON API specification and re-implement the core of JSON API without 281 | AutoValue since AutoValue is considered too verbose to implement a clean model. 282 | 283 | And the new API no longer requires a verbose null check since you should take all control over the POJO model's nullability check. 284 | 285 | Another major change is that the new API is not compatible with AutoValue any more. Means that one have to choose 1.x implementation 286 | if AutoValue is vital to bussiness logic. 287 | 288 | ## License 289 | 290 | (The MIT License) 291 | -------------------------------------------------------------------------------- /core/src/main/java/moe/banana/jsonapi2/ResourceAdapterFactory.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.*; 4 | import okio.Buffer; 5 | 6 | import java.io.IOException; 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | import java.util.*; 11 | 12 | import static moe.banana.jsonapi2.MoshiHelper.nextNullableObject; 13 | import static moe.banana.jsonapi2.MoshiHelper.writeNullable; 14 | 15 | public final class ResourceAdapterFactory implements JsonAdapter.Factory { 16 | 17 | private Map> typeMap = new HashMap<>(); 18 | private JsonNameMapping jsonNameMapping; 19 | 20 | private ResourceAdapterFactory(List> types, JsonNameMapping jsonNameMapping) { 21 | this.jsonNameMapping = jsonNameMapping; 22 | for (Class type : types) { 23 | JsonApi annotation = type.getAnnotation(JsonApi.class); 24 | String typeName = annotation.type(); 25 | if (annotation.policy() == Policy.SERIALIZATION_ONLY) { 26 | continue; 27 | } 28 | if (typeMap.containsKey(typeName)) { 29 | JsonApi annotationOld = typeMap.get(typeName).getAnnotation(JsonApi.class); 30 | switch (annotationOld.policy()) { 31 | case SERIALIZATION_AND_DESERIALIZATION: 32 | if (annotation.policy() == Policy.SERIALIZATION_AND_DESERIALIZATION) { 33 | // TODO deprecate priority here! 34 | if (annotationOld.priority() < annotation.priority()) { 35 | continue; 36 | } 37 | if (annotationOld.priority() > annotation.priority()) { 38 | break; 39 | } 40 | } 41 | case DESERIALIZATION_ONLY: 42 | throw new IllegalArgumentException( 43 | "@JsonApi(type = \"" + typeName + "\") declaration of [" + type.getCanonicalName() + "] conflicts with [" + typeMap.get(typeName).getCanonicalName() + "]." ); 44 | } 45 | } 46 | typeMap.put(typeName, type); 47 | } 48 | } 49 | 50 | @Override 51 | @SuppressWarnings("unchecked") 52 | public JsonAdapter create(Type type, Set annotations, Moshi moshi) { 53 | Class rawType = Types.getRawType(type); 54 | if (rawType.equals(JsonBuffer.class)) return new JsonBuffer.Adapter(); 55 | if (rawType.equals(HasMany.class)) return new HasMany.Adapter(moshi); 56 | if (rawType.equals(HasOne.class)) return new HasOne.Adapter(moshi); 57 | if (rawType.equals(Error.class)) return new Error.Adapter(moshi); 58 | if (rawType.equals(ResourceIdentifier.class)) return new ResourceIdentifier.Adapter(moshi); 59 | if (rawType.equals(Resource.class)) return new GenericAdapter(typeMap, moshi); 60 | if (Document.class.isAssignableFrom(rawType)) { 61 | if (type instanceof ParameterizedType) { 62 | Type typeParameter = ((ParameterizedType) type).getActualTypeArguments()[0]; 63 | if (typeParameter instanceof Class) { 64 | return new DocumentAdapter((Class) typeParameter, moshi); 65 | } 66 | } 67 | return new DocumentAdapter<>(Resource.class, moshi); 68 | } 69 | if (Resource.class.isAssignableFrom(rawType)) return new ResourceAdapter(rawType, jsonNameMapping, moshi); 70 | return null; 71 | } 72 | 73 | static class DocumentAdapter extends JsonAdapter { 74 | 75 | JsonAdapter jsonBufferJsonAdapter; 76 | JsonAdapter errorJsonAdapter; 77 | JsonAdapter dataJsonAdapter; 78 | JsonAdapter resourceJsonAdapter; 79 | 80 | public DocumentAdapter(Class type, Moshi moshi) { 81 | jsonBufferJsonAdapter = moshi.adapter(JsonBuffer.class); 82 | resourceJsonAdapter = moshi.adapter(Resource.class); 83 | errorJsonAdapter = moshi.adapter(Error.class); 84 | dataJsonAdapter = moshi.adapter(type); 85 | } 86 | 87 | @Override 88 | @SuppressWarnings("unchecked") 89 | public Document fromJson(JsonReader reader) throws IOException { 90 | if (reader.peek() == JsonReader.Token.NULL) { 91 | return null; 92 | } 93 | Document document = new ObjectDocument(); 94 | reader.beginObject(); 95 | while (reader.hasNext()) { 96 | switch (reader.nextName()) { 97 | case "data": 98 | if (reader.peek() == JsonReader.Token.BEGIN_ARRAY) { 99 | document = document.asArrayDocument(); 100 | reader.beginArray(); 101 | while (reader.hasNext()) { 102 | ((ArrayDocument) document).add(dataJsonAdapter.fromJson(reader)); 103 | } 104 | reader.endArray(); 105 | } else if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) { 106 | document = document.asObjectDocument(); 107 | ((ObjectDocument) document).set(dataJsonAdapter.fromJson(reader)); 108 | } else if (reader.peek() == JsonReader.Token.NULL) { 109 | reader.nextNull(); 110 | document = document.asObjectDocument(); 111 | ((ObjectDocument) document).set(null); 112 | } else { 113 | reader.skipValue(); 114 | } 115 | break; 116 | case "included": 117 | reader.beginArray(); 118 | Collection includes = document.getIncluded(); 119 | while (reader.hasNext()) { 120 | includes.add(resourceJsonAdapter.fromJson(reader)); 121 | } 122 | reader.endArray(); 123 | break; 124 | case "errors": 125 | reader.beginArray(); 126 | List errors = document.getErrors(); 127 | while (reader.hasNext()) { 128 | errors.add(errorJsonAdapter.fromJson(reader)); 129 | } 130 | reader.endArray(); 131 | break; 132 | case "links": 133 | document.setLinks(nextNullableObject(reader, jsonBufferJsonAdapter)); 134 | break; 135 | case "meta": 136 | document.setMeta(nextNullableObject(reader, jsonBufferJsonAdapter)); 137 | break; 138 | case "jsonapi": 139 | document.setJsonApi(nextNullableObject(reader, jsonBufferJsonAdapter)); 140 | break; 141 | default: 142 | reader.skipValue(); 143 | break; 144 | } 145 | } 146 | reader.endObject(); 147 | return document; 148 | } 149 | 150 | @Override 151 | @SuppressWarnings("unchecked") 152 | public void toJson(JsonWriter writer, Document value) throws IOException { 153 | writer.beginObject(); 154 | if (value instanceof ArrayDocument) { 155 | writer.name("data"); 156 | writer.beginArray(); 157 | for (DATA resource : ((ArrayDocument) value)) { 158 | dataJsonAdapter.toJson(writer, resource); 159 | } 160 | writer.endArray(); 161 | } else if (value instanceof ObjectDocument) { 162 | writeNullable(writer, dataJsonAdapter, 163 | "data", 164 | ((ObjectDocument) value).get(), 165 | ((ObjectDocument) value).hasData()); 166 | } 167 | if (value.included.size() > 0) { 168 | writer.name("included"); 169 | writer.beginArray(); 170 | for (Resource resource : value.included.values()) { 171 | resourceJsonAdapter.toJson(writer, resource); 172 | } 173 | writer.endArray(); 174 | } 175 | if (value.errors.size() > 0) { 176 | writer.name("error"); 177 | writer.beginArray(); 178 | for (Error err : value.errors) { 179 | errorJsonAdapter.toJson(writer, err); 180 | } 181 | writer.endArray(); 182 | } 183 | writeNullable(writer, jsonBufferJsonAdapter, "meta", value.getMeta()); 184 | writeNullable(writer, jsonBufferJsonAdapter, "links", value.getLinks()); 185 | writeNullable(writer, jsonBufferJsonAdapter, "jsonapi", value.getJsonApi()); 186 | writer.endObject(); 187 | } 188 | 189 | } 190 | 191 | private static class GenericAdapter extends JsonAdapter { 192 | 193 | Map> typeMap; 194 | Moshi moshi; 195 | 196 | GenericAdapter(Map> typeMap, Moshi moshi) { 197 | this.typeMap = typeMap; 198 | this.moshi = moshi; 199 | } 200 | 201 | @Override 202 | public Resource fromJson(JsonReader reader) throws IOException { 203 | Buffer buffer = new Buffer(); 204 | MoshiHelper.dump(reader, buffer); 205 | String typeName = findTypeOf(buffer); 206 | JsonAdapter adapter; 207 | if (typeMap.containsKey(typeName)) { 208 | adapter = moshi.adapter(typeMap.get(typeName)); 209 | } else if (typeMap.containsKey("default")) { 210 | adapter = moshi.adapter(typeMap.get("default")); 211 | } else { 212 | throw new JsonDataException("Unknown type of resource: " + typeName); 213 | } 214 | return (Resource) adapter.fromJson(buffer); 215 | } 216 | 217 | @Override 218 | @SuppressWarnings("unchecked") 219 | public void toJson(JsonWriter writer, Resource value) throws IOException { 220 | moshi.adapter((Class) value.getClass()).toJson(writer, value); 221 | } 222 | 223 | private static String findTypeOf(Buffer buffer) throws IOException { 224 | Buffer forked = new Buffer(); 225 | buffer.copyTo(forked, 0, buffer.size()); 226 | JsonReader reader = JsonReader.of(forked); 227 | reader.beginObject(); 228 | while (reader.hasNext()) { 229 | String name = reader.nextName(); 230 | switch (name) { 231 | case "type": 232 | return reader.nextString(); 233 | default: 234 | reader.skipValue(); 235 | } 236 | } 237 | return null; 238 | } 239 | } 240 | 241 | public static class Builder { 242 | 243 | List> types = new ArrayList<>(); 244 | JsonNameMapping jsonNameMapping = new MoshiJsonNameMapping(); 245 | 246 | private Builder() { } 247 | 248 | @SafeVarargs 249 | public final Builder add(Class... type) { 250 | types.addAll(Arrays.asList(type)); 251 | return this; 252 | } 253 | 254 | public final Builder setJsonNameMapping(JsonNameMapping mapping) { 255 | if (mapping == null) { 256 | throw new IllegalArgumentException(); 257 | } 258 | this.jsonNameMapping = mapping; 259 | return this; 260 | } 261 | 262 | public final ResourceAdapterFactory build() { 263 | return new ResourceAdapterFactory(types, jsonNameMapping); 264 | } 265 | } 266 | 267 | public static Builder builder() { 268 | return new Builder(); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /core/src/test/java/moe/banana/jsonapi2/DocumentTest.java: -------------------------------------------------------------------------------- 1 | package moe.banana.jsonapi2; 2 | 3 | import com.squareup.moshi.JsonAdapter; 4 | import com.squareup.moshi.JsonDataException; 5 | import com.squareup.moshi.Moshi; 6 | import com.squareup.moshi.Types; 7 | import moe.banana.jsonapi2.model.*; 8 | import org.junit.FixMethodOrder; 9 | import org.junit.Test; 10 | import org.junit.runners.MethodSorters; 11 | 12 | import java.io.EOFException; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static org.hamcrest.CoreMatchers.*; 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.junit.Assert.*; 20 | 21 | @SuppressWarnings("all") 22 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 23 | public class DocumentTest { 24 | 25 | @Test(expected = EOFException.class) 26 | public void deserialize_empty() throws Exception { 27 | getDocumentAdapter(Article.class).fromJson(""); 28 | } 29 | 30 | @Test 31 | public void deserialize_null() throws Exception { 32 | assertNull(getDocumentAdapter(null).fromJson("null")); 33 | } 34 | 35 | @Test 36 | public void deserialize_object() throws Exception { 37 | Document document = getDocumentAdapter(Article.class) 38 | .fromJson(TestUtil.fromResource("/single.json")); 39 | assertThat(document, instanceOf(ObjectDocument.class)); 40 | assertOnArticle1(document.
asObjectDocument().get()); 41 | } 42 | 43 | @Test 44 | public void deserialize_object_null() throws Exception { 45 | Document document = getDocumentAdapter(Article.class) 46 | .fromJson(TestUtil.fromResource("/single_null.json")); 47 | assertNull(document.asObjectDocument().get()); 48 | } 49 | 50 | @Test 51 | public void deserialize_private_type() throws Exception { 52 | Document document = getDocumentAdapter(Article2.class) 53 | .fromJson(TestUtil.fromResource("/single.json")); 54 | assertOnArticle1(document.
asObjectDocument().get()); 55 | } 56 | 57 | @Test(expected = JsonDataException.class) 58 | public void deserialize_no_default() throws Exception { 59 | TestUtil.moshi(true, Article.class) 60 | .adapter(Types.newParameterizedType(Document.class, Article.class)) 61 | .fromJson(TestUtil.fromResource("/multiple_compound.json")); 62 | } 63 | 64 | @Test 65 | public void deserialize_polymorphic_type() throws Exception { 66 | Resource resource = getDocumentAdapter(Resource.class, Article.class) 67 | .fromJson(TestUtil.fromResource("/single.json")).
asObjectDocument() 68 | .get(); 69 | assertThat(resource, instanceOf(Article.class)); 70 | assertOnArticle1(((Article) resource)); 71 | } 72 | 73 | @Test 74 | public void deserialize_polymorphic_fallback() throws Exception { 75 | Resource resource = getDocumentAdapter(Resource.class) 76 | .fromJson(TestUtil.fromResource("/single.json")) 77 | .asObjectDocument().get(); 78 | assertThat(resource.getId(), equalTo("1")); 79 | assertThat(resource, instanceOf(TestUtil.Default.class)); 80 | } 81 | 82 | @Test 83 | public void deserialize_multiple_objects() throws Exception { 84 | Document document = getDocumentAdapter(Article.class) 85 | .fromJson(TestUtil.fromResource("/multiple_compound.json")); 86 | assertThat(document, instanceOf(ArrayDocument.class)); 87 | ArrayDocument
arrayDocument = document.asArrayDocument(); 88 | assertThat(arrayDocument.size(), equalTo(1)); 89 | assertOnArticle1(arrayDocument.get(0)); 90 | } 91 | 92 | @Test 93 | public void deserialize_multiple_empty() throws Exception { 94 | Document document = getDocumentAdapter(Article.class) 95 | .fromJson(TestUtil.fromResource("/multiple_empty.json")); 96 | assertThat(document, instanceOf(ArrayDocument.class)); 97 | ArrayDocument
arrayDocument = document.asArrayDocument(); 98 | assertTrue(arrayDocument.isEmpty()); 99 | } 100 | 101 | @Test 102 | public void deserialize_multiple_polymorphic() throws Exception { 103 | Document document = getDocumentAdapter(Resource.class, Article.class, Photo.class) 104 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 105 | assertThat(document, instanceOf(ArrayDocument.class)); 106 | ArrayDocument arrayDocument = document.asArrayDocument(); 107 | assertThat(arrayDocument.get(0), instanceOf(Article.class)); 108 | assertThat(arrayDocument.get(1), instanceOf(Photo.class)); 109 | assertOnArticle1((Article) arrayDocument.get(0)); 110 | } 111 | 112 | @Test 113 | public void deserialize_multiple_polymorphic_type_priority() throws Exception { 114 | Document document = getDocumentAdapter(Resource.class, Photo.Photo2.class, Photo.class) 115 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 116 | assertThat(document.asArrayDocument().get(1), instanceOf(Photo.Photo2.class)); 117 | Document document2 = getDocumentAdapter(Resource.class, Photo.class, Photo.Photo2.class) 118 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 119 | assertThat(document.asArrayDocument().get(1), instanceOf(Photo.Photo2.class)); 120 | } 121 | 122 | @Test 123 | public void deserialize_multiple_polymorphic_type_policy() throws Exception { 124 | Document document = getDocumentAdapter(Resource.class, Photo.Photo4.class, Photo.class) 125 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 126 | assertThat(document.asArrayDocument().get(1), instanceOf(Photo.class)); 127 | Document document2 = getDocumentAdapter(Resource.class, Photo.Photo4.class, Photo.Photo2.class) 128 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 129 | assertThat(document2.asArrayDocument().get(1), instanceOf(Photo.Photo2.class)); 130 | Document document3 = getDocumentAdapter(Resource.class, Photo.Photo4.class, Photo.Photo3.class) 131 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 132 | assertThat(document3.asArrayDocument().get(1), instanceOf(Photo.Photo3.class)); 133 | Document document4 = getDocumentAdapter(Resource.class, Photo.Photo4.class, Photo.Photo5.class) 134 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 135 | assertThat(document4.asArrayDocument().get(1), instanceOf(Photo.Photo5.class)); 136 | Document document5 = getDocumentAdapter(Resource.class, Photo.class, Photo.Photo4.class) 137 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 138 | assertThat(document5.asArrayDocument().get(1), instanceOf(Photo.class)); 139 | Document document6 = getDocumentAdapter(Resource.class, Photo.Photo2.class, Photo.Photo4.class) 140 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 141 | assertThat(document6.asArrayDocument().get(1), instanceOf(Photo.Photo2.class)); 142 | Document document7 = getDocumentAdapter(Resource.class, Photo.Photo3.class, Photo.Photo4.class) 143 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 144 | assertThat(document7.asArrayDocument().get(1), instanceOf(Photo.Photo3.class)); 145 | Document document8 = getDocumentAdapter(Resource.class, Photo.Photo5.class, Photo.Photo4.class) 146 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 147 | assertThat(document8.asArrayDocument().get(1), instanceOf(Photo.Photo5.class)); 148 | } 149 | 150 | @Test(expected = IllegalArgumentException.class) 151 | public void deserialize_multiple_polymorphic_type_policy_ex1() throws Exception { 152 | getDocumentAdapter(Resource.class, Photo.class, Photo.Photo3.class); 153 | } 154 | 155 | @Test(expected = IllegalArgumentException.class) 156 | public void deserialize_multiple_polymorphic_type_policy_ex1_sym() throws Exception { 157 | getDocumentAdapter(Resource.class, Photo.Photo3.class, Photo.class); 158 | } 159 | 160 | @Test(expected = IllegalArgumentException.class) 161 | public void deserialize_multiple_polymorphic_type_policy_ex2() throws Exception { 162 | getDocumentAdapter(Resource.class, Photo.Photo2.class, Photo.Photo5.class); 163 | } 164 | 165 | @Test(expected = IllegalArgumentException.class) 166 | public void deserialize_multiple_polymorphic_type_policy_ex2_sym() throws Exception { 167 | getDocumentAdapter(Resource.class, Photo.Photo5.class, Photo.Photo2.class); 168 | } 169 | 170 | @Test(expected = JsonDataException.class) 171 | public void deserialize_multiple_polymorphic_no_default() throws Exception { 172 | TestUtil.moshi(true, Article.class) 173 | .adapter(Types.newParameterizedType(Document.class, Resource.class)) 174 | .fromJson(TestUtil.fromResource("/multiple_polymorphic.json")); 175 | } 176 | 177 | @Test 178 | public void deserialize_unparameterized() throws Exception { 179 | Document document = getDocumentAdapter(null, Person.class) 180 | .fromJson("{\"data\":{\"type\":\"people\",\"id\":\"5\"}}"); 181 | assertThat(document, instanceOf(ObjectDocument.class)); 182 | assertThat(document.asObjectDocument().get().getType(), equalTo("people")); 183 | assertThat(document.asObjectDocument().get(), instanceOf(Person.class)); 184 | } 185 | 186 | @Test 187 | public void deserialize_object_to_object_typed_document() throws Exception { 188 | Moshi moshi = TestUtil.moshi(Article.class); 189 | JsonAdapter adapter = moshi.adapter(Types.newParameterizedType(ObjectDocument.class, Article.class)); 190 | assertThat(adapter, instanceOf(ResourceAdapterFactory.DocumentAdapter.class)); 191 | ObjectDocument
objectDocument = ((ObjectDocument
) adapter.fromJson(TestUtil.fromResource("/single.json"))); 192 | assertThat(objectDocument, instanceOf(ObjectDocument.class)); 193 | assertOnArticle1(objectDocument.
asObjectDocument().get()); 194 | } 195 | 196 | @Test 197 | public void deserialize_array_to_array_typed_document() throws Exception { 198 | Moshi moshi = TestUtil.moshi(Article.class); 199 | JsonAdapter adapter = moshi.adapter(Types.newParameterizedType(ArrayDocument.class, Article.class)); 200 | assertThat(adapter, instanceOf(ResourceAdapterFactory.DocumentAdapter.class)); 201 | ArrayDocument
arrayDocument = ((ArrayDocument
) adapter.fromJson(TestUtil.fromResource("/multiple_compound.json"))); 202 | assertThat(arrayDocument.size(), equalTo(1)); 203 | assertOnArticle1(arrayDocument.get(0)); 204 | } 205 | 206 | @Test 207 | public void serialize_null() { 208 | ObjectDocument document = new ObjectDocument(); 209 | assertThat(getDocumentAdapter(ResourceIdentifier.class).toJson(document), equalTo("{}")); 210 | document.set(null); 211 | assertThat(getDocumentAdapter(ResourceIdentifier.class).toJson(document), equalTo("{\"data\":null}")); 212 | } 213 | 214 | @Test 215 | public void serialize_empty() throws Exception { 216 | Document document = new ArrayDocument(); 217 | assertThat(getDocumentAdapter(ResourceIdentifier.class).toJson(document), equalTo("{\"data\":[]}")); 218 | } 219 | 220 | @Test 221 | public void serialize_object() throws Exception { 222 | Article article = new Article(); 223 | article.setTitle("Nineteen Eighty-Four"); 224 | article.setAuthor(new HasOne("people", "5")); 225 | article.setComments(new HasMany( 226 | new ResourceIdentifier("comments", "1"))); 227 | ObjectDocument document = new ObjectDocument(); 228 | document.set(article); 229 | assertThat(getDocumentAdapter(Article.class).toJson(document), equalTo( 230 | "{\"data\":{\"type\":\"articles\",\"attributes\":{\"title\":\"Nineteen Eighty-Four\"},\"relationships\":{\"author\":{\"data\":{\"type\":\"people\",\"id\":\"5\"}},\"comments\":{\"data\":[{\"type\":\"comments\",\"id\":\"1\"}]}}}}")); 231 | } 232 | 233 | @Test 234 | public void serialize_polymorphic() throws Exception { 235 | Article article = new Article(); 236 | article.setTitle("Nineteen Eighty-Four"); 237 | ObjectDocument document = new ObjectDocument(); 238 | document.set(article); 239 | assertThat(getDocumentAdapter(Resource.class).toJson(document), 240 | equalTo("{\"data\":{\"type\":\"articles\",\"attributes\":{\"title\":\"Nineteen Eighty-Four\"}}}")); 241 | } 242 | 243 | @Test 244 | public void serialize_multiple_polymorphic_compound() throws Exception { 245 | ArrayDocument document = new ArrayDocument(); 246 | Comment comment1 = new Comment(); 247 | comment1.setId("1"); 248 | comment1.setBody("Awesome!"); 249 | Person author = new Person(); 250 | author.setId("5"); 251 | author.setFirstName("George"); 252 | author.setLastName("Orwell"); 253 | Article article = new Article(); 254 | article.setTitle("Nineteen Eighty-Four"); 255 | article.setAuthor(new HasOne(author)); 256 | article.setComments(new HasMany(comment1)); 257 | document.add(article); 258 | document.add(author); 259 | document.addInclude(comment1); 260 | assertThat(getDocumentAdapter(Resource.class).toJson(document), 261 | equalTo("{\"data\":[" + 262 | "{\"type\":\"articles\",\"attributes\":{\"title\":\"Nineteen Eighty-Four\"},\"relationships\":{\"author\":{\"data\":{\"type\":\"people\",\"id\":\"5\"}},\"comments\":{\"data\":[{\"type\":\"comments\",\"id\":\"1\"}]}}}," + 263 | "{\"type\":\"people\",\"id\":\"5\",\"attributes\":{\"first-name\":\"George\",\"last-name\":\"Orwell\"}}" + 264 | "],\"included\":[{\"type\":\"comments\",\"id\":\"1\",\"attributes\":{\"body\":\"Awesome!\"}}]}")); 265 | } 266 | 267 | @Test 268 | public void deserialize_resource_identifier() throws Exception { 269 | ObjectDocument document = getDocumentAdapter(ResourceIdentifier.class) 270 | .fromJson(TestUtil.fromResource("/relationship_single.json")).asObjectDocument(); 271 | assertThat(document.get(), instanceOf(ResourceIdentifier.class)); 272 | assertThat(document.get().getId(), equalTo("12")); 273 | assertFalse(document.isNull()); 274 | } 275 | 276 | @Test 277 | public void deserialize_with_null_data() throws Exception { 278 | ObjectDocument document = getDocumentAdapter(ResourceIdentifier.class) 279 | .fromJson(TestUtil.fromResource("/relationship_single_null.json")) 280 | .asObjectDocument(); 281 | assertTrue(document.hasData()); 282 | assertNull(document.get()); 283 | } 284 | 285 | @Test 286 | public void deserialize_without_data() throws Exception { 287 | assertFalse(getDocumentAdapter(ResourceIdentifier.class) 288 | .fromJson(TestUtil.fromResource("/meta.json")).asObjectDocument().hasData()); 289 | } 290 | 291 | @Test 292 | public void deserialize_multiple_resource_identifiers() throws Exception { 293 | ArrayDocument document = getDocumentAdapter(ResourceIdentifier.class) 294 | .fromJson(TestUtil.fromResource("/relationship_multi.json")).asArrayDocument(); 295 | assertThat(document.size(), equalTo(2)); 296 | assertThat(document.get(0), instanceOf(ResourceIdentifier.class)); 297 | assertThat(document.get(1).getType(), equalTo("tags")); 298 | assertThat(getDocumentAdapter(ResourceIdentifier.class) 299 | .fromJson(TestUtil.fromResource("/relationship_multi_empty.json")), instanceOf(ArrayDocument.class)); 300 | } 301 | 302 | @Test 303 | public void serialize_resource_identifier() throws Exception { 304 | ObjectDocument document = new ObjectDocument(); 305 | document.set(new ResourceIdentifier("people", "5")); 306 | assertThat(getDocumentAdapter(ResourceIdentifier.class).toJson(document), 307 | equalTo("{\"data\":{\"type\":\"people\",\"id\":\"5\"}}")); 308 | } 309 | 310 | @Test 311 | public void serialize_multiple_resource_identifiers() throws Exception { 312 | ArrayDocument document = new ArrayDocument(); 313 | document.add(new ResourceIdentifier("people", "5")); 314 | document.add(new ResourceIdentifier("people", "11")); 315 | assertThat(getDocumentAdapter(ResourceIdentifier.class).toJson(document), 316 | equalTo("{\"data\":[{\"type\":\"people\",\"id\":\"5\"},{\"type\":\"people\",\"id\":\"11\"}]}")); 317 | } 318 | 319 | @Test 320 | public void serialize_errors() throws Exception { 321 | Error error = new Error(); 322 | error.setId("4"); 323 | error.setStatus("502"); 324 | error.setTitle("Internal error"); 325 | error.setCode("502000"); 326 | error.setDetail("Ouch! There's some trouble with our server."); 327 | ObjectDocument document = new ObjectDocument(); 328 | document.setErrors(Collections.singletonList(error)); 329 | assertThat(getDocumentAdapter(null).toJson(document), 330 | equalTo("{\"error\":[{\"id\":\"4\",\"status\":\"502\",\"code\":\"502000\",\"title\":\"Internal error\",\"detail\":\"Ouch! There's some trouble with our server.\"}]}")); 331 | } 332 | 333 | @Test 334 | public void deserialize_errors() throws Exception { 335 | Document document1 = getDocumentAdapter(null) 336 | .fromJson(TestUtil.fromResource("/errors.json")); 337 | assertTrue(document1.hasError()); 338 | assertEquals(document1.getErrors().size(), 2); 339 | Document document2 = getDocumentAdapter(null) 340 | .fromJson(TestUtil.fromResource("/errors_empty.json")); 341 | assertFalse(document2.hasError()); 342 | } 343 | 344 | @Test 345 | public void deserialize_meta() throws Exception { 346 | Document document = getDocumentAdapter(null) 347 | .fromJson(TestUtil.fromResource("/meta.json")); 348 | assertThat(document.getMeta().get(TestUtil.moshi().adapter(Meta.class)), instanceOf(Meta.class)); 349 | } 350 | 351 | @Test 352 | public void deserialize_photo_from_value() { 353 | Map photoMap = new HashMap(); 354 | 355 | Map linksNode = new HashMap(); 356 | linksNode.put("self", "http://example.com/photos/1"); 357 | photoMap.put("links", linksNode); 358 | 359 | Map dataNode = new HashMap(); 360 | dataNode.put("type", "photos"); 361 | dataNode.put("id", "1"); 362 | 363 | Map attributesNode = new HashMap(); 364 | attributesNode.put("url", "http://photo.com/photo.jpg"); 365 | attributesNode.put("title", "My Photo"); 366 | attributesNode.put("color", "#EF5350"); 367 | attributesNode.put("shutter", 23.641); 368 | 369 | Map locationNode = new HashMap(); 370 | locationNode.put("latitude", 39.9042); 371 | locationNode.put("longitude", 116.4074); 372 | attributesNode.put("location", locationNode); 373 | dataNode.put("attributes", attributesNode); 374 | 375 | photoMap.put("data", dataNode); 376 | 377 | Document document = getDocumentAdapter(null, Photo.class).fromJsonValue(photoMap); 378 | Photo photo = (Photo) document.asObjectDocument().get(); 379 | assertEquals(new Double(23.641), photo.getShutter()); 380 | assertEquals(new Double(39.9042), photo.getLocation().latitude); 381 | } 382 | 383 | @Test 384 | public void equality() throws Exception { 385 | Document document1 = getDocumentAdapter(Article.class) 386 | .fromJson(TestUtil.fromResource("/multiple_compound.json")); 387 | Document document2 = getDocumentAdapter(Resource.class, Article.class) 388 | .fromJson(TestUtil.fromResource("/multiple_compound.json")); 389 | assertEquals(document1, document2); 390 | assertEquals(document1.hashCode(), document2.hashCode()); 391 | } 392 | 393 | @JsonApi(type = "articles") 394 | private static class Article2 extends Article { 395 | 396 | } 397 | 398 | public JsonAdapter getDocumentAdapter(Class typeParameter, 399 | Class... knownTypes) { 400 | Moshi moshi; 401 | if (typeParameter == null) { 402 | return (JsonAdapter) TestUtil.moshi(knownTypes).adapter(Document.class); 403 | } else if (typeParameter.getAnnotation(JsonApi.class) != null) { 404 | Class[] types = new Class[knownTypes.length + 1]; 405 | types[0] = (Class) typeParameter; 406 | for (int i = 0; i != knownTypes.length; i++) { 407 | types[i + 1] = knownTypes[i]; 408 | } 409 | moshi = TestUtil.moshi(types); 410 | } else { 411 | moshi = TestUtil.moshi(knownTypes); 412 | } 413 | return moshi.adapter(Types.newParameterizedType(Document.class, typeParameter)); 414 | } 415 | 416 | private void assertOnArticle1(Article article) { 417 | assertThat(article.getId(), equalTo("1")); 418 | assertThat(article.getType(), equalTo("articles")); 419 | assertThat(article.getTitle(), equalTo("JSON API paints my bikeshed!")); 420 | assertThat(article.getAuthor().get(), equalTo( 421 | new ResourceIdentifier("people", "9"))); 422 | assertThat(article.getComments().get(), hasItems( 423 | new ResourceIdentifier("comments", "5"), 424 | new ResourceIdentifier("comments", "12"))); 425 | } 426 | 427 | } 428 | --------------------------------------------------------------------------------