mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Adapter methods.
This is a fun new API that could make writing JSON adapters much easier.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// TODO: support qualifier annotations.
|
||||
// TODO: support @Nullable
|
||||
// TODO: path in JsonWriter.
|
||||
|
||||
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||
private final Map<Type, ToAdapter> toAdapters;
|
||||
private final Map<Type, FromAdapter> fromAdapters;
|
||||
|
||||
AdapterMethodsFactory(Map<Type, ToAdapter> toAdapters, Map<Type, FromAdapter> fromAdapters) {
|
||||
this.toAdapters = toAdapters;
|
||||
this.fromAdapters = fromAdapters;
|
||||
}
|
||||
|
||||
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, final Moshi moshi) {
|
||||
final ToAdapter toAdapter = toAdapters.get(type);
|
||||
final FromAdapter fromAdapter = fromAdapters.get(type);
|
||||
if (toAdapter == null && fromAdapter == null) return null;
|
||||
|
||||
final JsonAdapter<Object> delegate = toAdapter == null || fromAdapter == null
|
||||
? moshi.nextAdapter(this, type, annotations)
|
||||
: null;
|
||||
|
||||
return new JsonAdapter<Object>() {
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
if (toAdapter == null) {
|
||||
delegate.toJson(writer, value);
|
||||
} else {
|
||||
try {
|
||||
toAdapter.toJson(moshi, writer, value);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||
throw new JsonDataException(e.getCause().getMessage()); // TODO: more context?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
if (fromAdapter == null) {
|
||||
return delegate.fromJson(reader);
|
||||
} else {
|
||||
try {
|
||||
return fromAdapter.fromJson(moshi, reader);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||
throw new JsonDataException(e.getCause().getMessage()); // TODO: more context?
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static AdapterMethodsFactory get(Object adapter) {
|
||||
Map<Type, ToAdapter> toAdapters = new LinkedHashMap<>();
|
||||
Map<Type, FromAdapter> fromAdapters = new LinkedHashMap<>();
|
||||
|
||||
for (Class<?> c = adapter.getClass(); c != Object.class; c = c.getSuperclass()) {
|
||||
for (Method m : c.getDeclaredMethods()) {
|
||||
if (m.isAnnotationPresent(ToJson.class)) {
|
||||
ToAdapter toAdapter = toAdapter(adapter, m);
|
||||
ToAdapter replaced = toAdapters.put(toAdapter.type, toAdapter);
|
||||
if (replaced != null) {
|
||||
throw new IllegalArgumentException("Conflicting @ToJson methods:\n"
|
||||
+ " " + replaced.method + "\n"
|
||||
+ " " + toAdapter.method);
|
||||
}
|
||||
}
|
||||
|
||||
if (m.isAnnotationPresent(FromJson.class)) {
|
||||
FromAdapter fromAdapter = fromAdapter(adapter, m);
|
||||
FromAdapter replaced = fromAdapters.put(fromAdapter.type, fromAdapter);
|
||||
if (replaced != null) {
|
||||
throw new IllegalArgumentException("Conflicting @FromJson methods:\n"
|
||||
+ " " + replaced.method + "\n"
|
||||
+ " " + fromAdapter.method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toAdapters.isEmpty() && fromAdapters.isEmpty()) {
|
||||
throw new IllegalArgumentException("Expected at least one @ToJson or @FromJson method on "
|
||||
+ adapter.getClass().getName());
|
||||
}
|
||||
|
||||
return new AdapterMethodsFactory(toAdapters, fromAdapters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that calls a {@code method} method on {@code adapter} in service of
|
||||
* converting an object to JSON.
|
||||
*/
|
||||
static ToAdapter toAdapter(Object adapter, Method method) {
|
||||
method.setAccessible(true);
|
||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
final Type returnType = method.getGenericReturnType();
|
||||
|
||||
if (parameterTypes.length == 2
|
||||
&& parameterTypes[0] == JsonWriter.class
|
||||
&& returnType == void.class) {
|
||||
return new ToAdapter(parameterTypes[1], adapter, method) {
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
throws IOException, InvocationTargetException, IllegalAccessException {
|
||||
method.invoke(adapter, writer, value);
|
||||
}
|
||||
};
|
||||
|
||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||
return new ToAdapter(parameterTypes[0], adapter, method) {
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
throws IOException, InvocationTargetException, IllegalAccessException {
|
||||
JsonAdapter<Object> delegate = moshi.adapter(returnType, method);
|
||||
Object intermediate = method.invoke(adapter, value);
|
||||
delegate.toJson(writer, intermediate);
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||
+ "@ToJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
|
||||
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class ToAdapter {
|
||||
final Type type;
|
||||
final Object adapter;
|
||||
final Method method;
|
||||
|
||||
public ToAdapter(Type type, Object adapter, Method method) {
|
||||
this.type = type;
|
||||
this.adapter = adapter;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public abstract void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
throws IOException, IllegalAccessException, InvocationTargetException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that calls a {@code method} method on {@code adapter} in service of
|
||||
* converting an object from JSON.
|
||||
*/
|
||||
static FromAdapter fromAdapter(Object adapter, Method method) {
|
||||
method.setAccessible(true);
|
||||
final Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
final Type returnType = method.getGenericReturnType();
|
||||
|
||||
if (parameterTypes.length == 1
|
||||
&& parameterTypes[0] == JsonReader.class
|
||||
&& returnType != void.class) {
|
||||
// public Point pointFromJson(JsonReader jsonReader) throws Exception {
|
||||
return new FromAdapter(returnType, adapter, method) {
|
||||
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||
return method.invoke(adapter, reader);
|
||||
}
|
||||
};
|
||||
|
||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||
// public Point pointFromJson(List<Integer> o) throws Exception {
|
||||
return new FromAdapter(returnType, adapter, method) {
|
||||
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0]);
|
||||
Object intermediate = delegate.fromJson(reader);
|
||||
return method.invoke(adapter, intermediate);
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||
+ "@ToJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
|
||||
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class FromAdapter {
|
||||
final Type type;
|
||||
final Object adapter;
|
||||
final Method method;
|
||||
|
||||
public FromAdapter(Type type, Object adapter, Method method) {
|
||||
this.type = type;
|
||||
this.adapter = adapter;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public abstract Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, IllegalAccessException, InvocationTargetException;
|
||||
}
|
||||
}
|
26
moshi/src/main/java/com/squareup/moshi/FromJson.java
Normal file
26
moshi/src/main/java/com/squareup/moshi/FromJson.java
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface FromJson {
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
/** Thrown when a JSON document doesn't match the expected format. */
|
||||
public final class JsonDataException extends RuntimeException {
|
||||
public JsonDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JsonDataException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public JsonDataException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@@ -131,6 +131,10 @@ public final class Moshi {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder add(Object adapter) {
|
||||
return add(AdapterMethodsFactory.get(adapter));
|
||||
}
|
||||
|
||||
public Moshi build() {
|
||||
return new Moshi(this);
|
||||
}
|
||||
|
26
moshi/src/main/java/com/squareup/moshi/ToJson.java
Normal file
26
moshi/src/main/java/com/squareup/moshi/ToJson.java
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ToJson {
|
||||
}
|
201
moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java
Normal file
201
moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public final class AdapterMethodsTest {
|
||||
@Test public void toAndFromJsonViaListOfIntegers() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointAsListOfIntegersJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class);
|
||||
assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("[5,8]");
|
||||
assertThat(pointAdapter.fromJson("[5,8]")).isEqualTo(new Point(5, 8));
|
||||
}
|
||||
|
||||
static class PointAsListOfIntegersJsonAdapter {
|
||||
@ToJson List<Integer> pointToJson(Point point) {
|
||||
return Arrays.asList(point.x, point.y);
|
||||
}
|
||||
|
||||
@FromJson Point pointFromJson(List<Integer> o) throws Exception {
|
||||
if (o.size() != 2) throw new Exception("Expected 2 elements but was " + o);
|
||||
return new Point(o.get(0), o.get(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void toAndFromJsonWithWriterAndReader() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointWriterAndReaderJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class);
|
||||
assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("[5,8]");
|
||||
assertThat(pointAdapter.fromJson("[5,8]")).isEqualTo(new Point(5, 8));
|
||||
}
|
||||
|
||||
static class PointWriterAndReaderJsonAdapter {
|
||||
@ToJson void pointToJson(JsonWriter writer, Point point) throws IOException {
|
||||
writer.beginArray();
|
||||
writer.value(point.x);
|
||||
writer.value(point.y);
|
||||
writer.endArray();
|
||||
}
|
||||
|
||||
@FromJson Point pointFromJson(JsonReader reader) throws Exception {
|
||||
reader.beginArray();
|
||||
int x = reader.nextInt();
|
||||
int y = reader.nextInt();
|
||||
reader.endArray();
|
||||
return new Point(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void toJsonOnly() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointAsListOfIntegersToAdapter())
|
||||
.build();
|
||||
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class);
|
||||
assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("[5,8]");
|
||||
assertThat(pointAdapter.fromJson("{\"x\":5,\"y\":8}")).isEqualTo(new Point(5, 8));
|
||||
}
|
||||
|
||||
static class PointAsListOfIntegersToAdapter {
|
||||
@ToJson List<Integer> pointToJson(Point point) {
|
||||
return Arrays.asList(point.x, point.y);
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void fromJsonOnly() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointAsListOfIntegersFromAdapter())
|
||||
.build();
|
||||
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class);
|
||||
assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("{\"x\":5,\"y\":8}");
|
||||
assertThat(pointAdapter.fromJson("[5,8]")).isEqualTo(new Point(5, 8));
|
||||
}
|
||||
|
||||
static class PointAsListOfIntegersFromAdapter {
|
||||
@FromJson Point pointFromJson(List<Integer> o) throws Exception {
|
||||
if (o.size() != 2) throw new Exception("Expected 2 elements but was " + o);
|
||||
return new Point(o.get(0), o.get(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void multipleLayersOfAdapters() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new MultipleLayersJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class).lenient();
|
||||
assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("\"5 8\"");
|
||||
assertThat(pointAdapter.fromJson("\"5 8\"")).isEqualTo(new Point(5, 8));
|
||||
}
|
||||
|
||||
static class MultipleLayersJsonAdapter {
|
||||
@ToJson List<Integer> pointToJson(Point point) {
|
||||
return Arrays.asList(point.x, point.y);
|
||||
}
|
||||
|
||||
@ToJson String integerListToJson(List<Integer> list) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Integer i : list) {
|
||||
if (result.length() != 0) result.append(" ");
|
||||
result.append(i.intValue());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@FromJson Point pointFromJson(List<Integer> o) throws Exception {
|
||||
if (o.size() != 2) throw new Exception("Expected 2 elements but was " + o);
|
||||
return new Point(o.get(0), o.get(1));
|
||||
}
|
||||
|
||||
@FromJson List<Integer> listOfIntegersFromJson(String list) throws Exception {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
for (String part : list.split(" ")) {
|
||||
result.add(Integer.parseInt(part));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void conflictingToAdapters() throws Exception {
|
||||
Moshi.Builder builder = new Moshi.Builder();
|
||||
try {
|
||||
builder.add(new ConflictingsToJsonAdapter());
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected.getMessage()).contains(
|
||||
"Conflicting @ToJson methods:", "pointToJson1", "pointToJson2");
|
||||
}
|
||||
}
|
||||
|
||||
static class ConflictingsToJsonAdapter {
|
||||
@ToJson List<Integer> pointToJson1(Point point) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@ToJson String pointToJson2(Point point) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void conflictingFromAdapters() throws Exception {
|
||||
Moshi.Builder builder = new Moshi.Builder();
|
||||
try {
|
||||
builder.add(new ConflictingsFromJsonAdapter());
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected.getMessage()).contains(
|
||||
"Conflicting @FromJson methods:", "pointFromJson1", "pointFromJson2");
|
||||
}
|
||||
}
|
||||
|
||||
static class ConflictingsFromJsonAdapter {
|
||||
@FromJson Point pointFromJson1(List<Integer> point) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@FromJson Point pointFromJson2(String point) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
static class Point {
|
||||
final int x;
|
||||
final int y;
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
return o instanceof Point && ((Point) o).x == x && ((Point) o).y == y;
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return x * 37 + y;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user