Merge pull request #4 from square/jwilson_0810_moshi

Initial JsonAdapter structure.
This commit is contained in:
Jesse Wilson
2014-08-11 08:37:30 -04:00
8 changed files with 544 additions and 3 deletions

View File

@@ -22,5 +22,10 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2014 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.io.StringWriter;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
/**
* Converts Java values to JSON, and JSON values to Java.
*/
public abstract class JsonAdapter<T> {
public abstract T fromJson(JsonReader reader) throws IOException;
public final T fromJson(String string) throws IOException {
return fromJson(new JsonReader(string));
}
public abstract void toJson(JsonWriter writer, T value) throws IOException;
public final String toJson(T value) throws IOException {
StringWriter stringWriter = new StringWriter();
toJson(new JsonWriter(stringWriter), value);
return stringWriter.toString();
}
/**
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls.
*/
public final JsonAdapter<T> nullSafe() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
return reader.nextNull();
} else {
return delegate.fromJson(reader);
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
if (value == null) {
writer.nullValue();
} else {
delegate.toJson(writer, value);
}
}
};
}
/** Returns a JSON adapter equal to this JSON adapter, but is lenient when reading and writing. */
public final JsonAdapter<T> lenient() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
boolean lenient = reader.isLenient();
reader.setLenient(true);
try {
return delegate.fromJson(reader);
} finally {
reader.setLenient(lenient);
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
boolean lenient = writer.isLenient();
writer.setLenient(true);
try {
delegate.toJson(writer, value);
} finally {
writer.setLenient(lenient);
}
}
};
}
public interface Factory {
/**
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
* returns the adapter if one was created, or null if this factory isn't capable of creating
* such an adapter.
*
* <p>Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
*/
JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi);
}
}

View File

@@ -214,7 +214,6 @@ public class JsonReader implements Closeable {
private static final int NUMBER_CHAR_EXP_SIGN = 6;
private static final int NUMBER_CHAR_EXP_DIGIT = 7;
/** True to accept non-spec compliant JSON */
private boolean lenient = false;
@@ -843,12 +842,12 @@ public class JsonReader implements Closeable {
/**
* Consumes the next token from the JSON stream and asserts that it is a
* literal null.
* literal null. Returns null.
*
* @throws IllegalStateException if the next token is not null or if this
* reader is closed.
*/
public void nextNull() throws IOException {
public <T> T nextNull() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
@@ -856,6 +855,7 @@ public class JsonReader implements Closeable {
if (p == PEEKED_NULL) {
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
return null;
} else {
throw new IllegalStateException("Expected null but was " + peek()
+ " at path " + getPath());

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2014 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.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Coordinates binding between JSON values and Java objects.
*/
public final class Moshi {
private final List<JsonAdapter.Factory> factories;
private Moshi(Builder builder) {
List<JsonAdapter.Factory> factories = new ArrayList<JsonAdapter.Factory>();
factories.addAll(builder.factories);
factories.add(new StandardJsonAdapterFactory());
this.factories = Collections.unmodifiableList(factories);
}
public <T> JsonAdapter<T> adapter(Type type) {
return adapter(type, Util.NO_ANNOTATIONS);
}
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
public <T> JsonAdapter<T> adapter(Class<T> type) {
// TODO: cache created JSON adapters.
return adapter(type, Util.NO_ANNOTATIONS);
}
public <T> JsonAdapter<T> adapter(Type type, AnnotatedElement annotations) {
// TODO: support re-entrant calls.
return createAdapter(0, type, annotations);
}
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
AnnotatedElement annotations) {
return createAdapter(factories.indexOf(skipPast) + 1, type, annotations);
}
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
private <T> JsonAdapter<T> createAdapter(
int firstIndex, Type type, AnnotatedElement annotations) {
for (int i = firstIndex, size = factories.size(); i < size; i++) {
JsonAdapter<?> result = factories.get(i).create(type, annotations, this);
if (result != null) return (JsonAdapter<T>) result;
}
throw new IllegalArgumentException("no JsonAdapter for " + type);
}
public static final class Builder {
private final List<JsonAdapter.Factory> factories = new ArrayList<JsonAdapter.Factory>();
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
Type targetType, AnnotatedElement annotations, Moshi moshi) {
return Util.typesMatch(type, targetType) ? jsonAdapter : null;
}
});
}
public Builder add(JsonAdapter.Factory jsonAdapter) {
// TODO: define precedence order. Last added wins? First added wins?
factories.add(jsonAdapter);
return this;
}
public Moshi build() {
return new Moshi(this);
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2014 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.Type;
final class StandardJsonAdapterFactory implements JsonAdapter.Factory {
static final JsonAdapter<Boolean> BOOLEAN_JSON_ADAPTER = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
writer.value(value);
}
};
static final JsonAdapter<Double> DOUBLE_JSON_ADAPTER = new JsonAdapter<Double>() {
@Override public Double fromJson(JsonReader reader) throws IOException {
return reader.nextDouble();
}
@Override public void toJson(JsonWriter writer, Double value) throws IOException {
writer.value(value);
}
};
static final JsonAdapter<Integer> INTEGER_JSON_ADAPTER = new JsonAdapter<Integer>() {
@Override public Integer fromJson(JsonReader reader) throws IOException {
return reader.nextInt();
}
@Override public void toJson(JsonWriter writer, Integer value) throws IOException {
writer.value(value.intValue());
}
};
static final JsonAdapter<String> STRING_JSON_ADAPTER = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
return reader.nextString();
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value);
}
};
@Override public JsonAdapter<?> create(
Type type, AnnotatedElement annotations, Moshi moshi) {
// TODO: support all 8 primitive types.
if (type == boolean.class) return BOOLEAN_JSON_ADAPTER;
if (type == double.class) return DOUBLE_JSON_ADAPTER;
if (type == int.class) return INTEGER_JSON_ADAPTER;
if (type == Boolean.class) return BOOLEAN_JSON_ADAPTER.nullSafe();
if (type == Double.class) return DOUBLE_JSON_ADAPTER.nullSafe();
if (type == Integer.class) return INTEGER_JSON_ADAPTER.nullSafe();
if (type == String.class) return STRING_JSON_ADAPTER.nullSafe();
return null;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2014 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.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
final class Util {
public static final Annotation[] EMPTY_ANNOTATIONS_ARRAY = new Annotation[0];
public static final AnnotatedElement NO_ANNOTATIONS = new AnnotatedElement() {
@Override public boolean isAnnotationPresent(Class<? extends Annotation> aClass) {
return false;
}
@Override public <T extends Annotation> T getAnnotation(Class<T> tClass) {
return null;
}
@Override public Annotation[] getAnnotations() {
return EMPTY_ANNOTATIONS_ARRAY;
}
@Override public Annotation[] getDeclaredAnnotations() {
return EMPTY_ANNOTATIONS_ARRAY;
}
};
public static boolean typesMatch(Type pattern, Type candidate) {
// TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>).
return pattern.equals(candidate);
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright (C) 2014 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.annotation.Retention;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.Locale;
import org.junit.Test;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
public final class MoshiTest {
/** No nulls for int.class. */
@Test public void intAdapter() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Integer> adapter = moshi.adapter(int.class).lenient();
assertThat(adapter.fromJson("1")).isEqualTo(1);
assertThat(adapter.toJson(2)).isEqualTo("2");
try {
adapter.fromJson("null");
fail();
} catch (IllegalStateException expected) {
assertThat(expected.getMessage()).isEqualTo("Expected an int but was NULL at path $");
}
try {
moshi.adapter(int.class).toJson(null);
fail();
} catch (NullPointerException expected) {
}
}
/** Moshi supports nulls for Integer.class. */
@Test public void integerAdapter() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Integer> adapter = moshi.adapter(Integer.class).lenient();
assertThat(adapter.fromJson("1")).isEqualTo(1);
assertThat(adapter.toJson(2)).isEqualTo("2");
assertThat(adapter.fromJson("null")).isEqualTo(null);
assertThat(adapter.toJson(null)).isEqualTo("null");
}
@Test public void stringAdapter() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<String> adapter = moshi.adapter(String.class).lenient();
assertThat(adapter.fromJson("\"a\"")).isEqualTo("a");
assertThat(adapter.toJson("b")).isEqualTo("\"b\"");
assertThat(adapter.fromJson("null")).isEqualTo(null);
assertThat(adapter.toJson(null)).isEqualTo("null");
}
@Test public void customJsonAdapter() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(Pizza.class, new PizzaAdapter())
.build();
JsonAdapter<Pizza> jsonAdapter = moshi.adapter(Pizza.class);
assertThat(jsonAdapter.toJson(new Pizza(15, true)))
.isEqualTo("{\"size\":15,\"extra cheese\":true}");
assertThat(jsonAdapter.fromJson("{\"extra cheese\":true,\"size\":18}"))
.isEqualTo(new Pizza(18, true));
}
@Test public void composingJsonAdapterFactory() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new MealDealAdapterFactory())
.add(Pizza.class, new PizzaAdapter())
.build();
JsonAdapter<MealDeal> jsonAdapter = moshi.adapter(MealDeal.class);
assertThat(jsonAdapter.toJson(new MealDeal(new Pizza(15, true), "Pepsi")))
.isEqualTo("[{\"size\":15,\"extra cheese\":true},\"Pepsi\"]");
assertThat(jsonAdapter.fromJson("[{\"extra cheese\":true,\"size\":18},\"Coke\"]"))
.isEqualTo(new MealDeal(new Pizza(18, true), "Coke"));
}
@Uppercase
static String uppercaseString;
@Test public void delegatingJsonAdapterFactory() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new UppercaseAdapterFactory())
.build();
AnnotatedElement annotations = MoshiTest.class.getDeclaredField("uppercaseString");
JsonAdapter<String> adapter = moshi.<String>adapter(String.class, annotations).lenient();
assertThat(adapter.toJson("a")).isEqualTo("\"A\"");
assertThat(adapter.fromJson("\"b\"")).isEqualTo("B");
}
static class Pizza {
final int diameter;
final boolean extraCheese;
Pizza(int diameter, boolean extraCheese) {
this.diameter = diameter;
this.extraCheese = extraCheese;
}
@Override public boolean equals(Object o) {
return o instanceof Pizza
&& ((Pizza) o).diameter == diameter
&& ((Pizza) o).extraCheese == extraCheese;
}
@Override public int hashCode() {
return diameter * (extraCheese ? 31 : 1);
}
}
static class MealDeal {
final Pizza pizza;
final String drink;
MealDeal(Pizza pizza, String drink) {
this.pizza = pizza;
this.drink = drink;
}
@Override public boolean equals(Object o) {
return o instanceof MealDeal
&& ((MealDeal) o).pizza.equals(pizza)
&& ((MealDeal) o).drink.equals(drink);
}
@Override public int hashCode() {
return pizza.hashCode() + (31 * drink.hashCode());
}
}
static class PizzaAdapter extends JsonAdapter<Pizza> {
@Override public Pizza fromJson(JsonReader reader) throws IOException {
int diameter = 13;
boolean extraCheese = false;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("size")) {
diameter = reader.nextInt();
} else if (name.equals("extra cheese")) {
extraCheese = reader.nextBoolean();
} else {
reader.skipValue();
}
}
reader.endObject();
return new Pizza(diameter, extraCheese);
}
@Override public void toJson(JsonWriter writer, Pizza value) throws IOException {
writer.beginObject();
writer.name("size").value(value.diameter);
writer.name("extra cheese").value(value.extraCheese);
writer.endObject();
}
}
static class MealDealAdapterFactory implements JsonAdapter.Factory {
@Override public JsonAdapter<?> create(
Type type, AnnotatedElement annotations, Moshi moshi) {
if (!type.equals(MealDeal.class)) return null;
final JsonAdapter<Pizza> pizzaAdapter = moshi.adapter(Pizza.class);
final JsonAdapter<String> drinkAdapter = moshi.adapter(String.class);
return new JsonAdapter<MealDeal>() {
@Override public MealDeal fromJson(JsonReader reader) throws IOException {
reader.beginArray();
Pizza pizza = pizzaAdapter.fromJson(reader);
String drink = drinkAdapter.fromJson(reader);
reader.endArray();
return new MealDeal(pizza, drink);
}
@Override public void toJson(JsonWriter writer, MealDeal value) throws IOException {
writer.beginArray();
pizzaAdapter.toJson(writer, value.pizza);
drinkAdapter.toJson(writer, value.drink);
writer.endArray();
}
};
}
}
@Retention(RUNTIME)
public @interface Uppercase {
}
static class UppercaseAdapterFactory implements JsonAdapter.Factory {
@Override public JsonAdapter<?> create(
Type type, AnnotatedElement annotations, Moshi moshi) {
if (!type.equals(String.class)) return null;
if (!annotations.isAnnotationPresent(Uppercase.class)) return null;
final JsonAdapter<String> stringAdapter = moshi.nextAdapter(this, String.class, annotations);
return new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
String s = stringAdapter.fromJson(reader);
return s.toUpperCase(Locale.US);
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
stringAdapter.toJson(writer, value.toUpperCase());
}
};
}
}
}

View File

@@ -30,6 +30,7 @@
<!-- Test Dependencies -->
<junit.version>4.11</junit.version>
<assertj.version>1.6.1</assertj.version>
</properties>
<scm>
@@ -63,6 +64,11 @@
<artifactId>okio</artifactId>
<version>${okio.version}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>
</dependencies>
</dependencyManagement>