mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Change the directory structure to match our modules (#1451)
This commit is contained in:
37
moshi-adapters/README.md
Normal file
37
moshi-adapters/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
Adapters
|
||||
===================
|
||||
|
||||
Prebuilt Moshi `JsonAdapter`s for various things, such as `Rfc3339DateJsonAdapter` for parsing `java.util.Date`s
|
||||
|
||||
To use, supply an instance of your desired converter when building your `Moshi` instance.
|
||||
|
||||
```java
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Date.class, new Rfc3339DateJsonAdapter())
|
||||
//etc
|
||||
.build();
|
||||
```
|
||||
|
||||
Download
|
||||
--------
|
||||
|
||||
Download [the latest JAR][1] or grab via [Maven][2]:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-adapters</artifactId>
|
||||
<version>latest.version</version>
|
||||
</dependency>
|
||||
```
|
||||
or [Gradle][2]:
|
||||
```groovy
|
||||
implementation 'com.squareup.moshi:moshi-adapters:latest.version'
|
||||
```
|
||||
|
||||
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
|
||||
|
||||
|
||||
|
||||
[1]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi-adapters&v=LATEST
|
||||
[2]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.moshi%22%20a%3A%22moshi-adapters%22
|
||||
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/moshi-adapters/
|
29
moshi-adapters/build.gradle.kts
Normal file
29
moshi-adapters/build.gradle.kts
Normal file
@@ -0,0 +1,29 @@
|
||||
import com.vanniktech.maven.publish.JavadocJar.Javadoc
|
||||
import com.vanniktech.maven.publish.KotlinJvm
|
||||
import com.vanniktech.maven.publish.MavenPublishBaseExtension
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("com.vanniktech.maven.publish.base")
|
||||
id("org.jetbrains.dokka")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.jsr305)
|
||||
api(project(":moshi"))
|
||||
|
||||
testCompileOnly(libs.jsr305)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.truth)
|
||||
}
|
||||
|
||||
tasks.withType<Jar>().configureEach {
|
||||
manifest {
|
||||
attributes("Automatic-Module-Name" to "com.squareup.moshi.adapters")
|
||||
}
|
||||
}
|
||||
|
||||
configure<MavenPublishBaseExtension> {
|
||||
configure(KotlinJvm(javadocJar = Javadoc()))
|
||||
}
|
32
moshi-adapters/japicmp/build.gradle.kts
Normal file
32
moshi-adapters/japicmp/build.gradle.kts
Normal file
@@ -0,0 +1,32 @@
|
||||
import me.champeau.gradle.japicmp.JapicmpTask
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
id("me.champeau.gradle.japicmp")
|
||||
}
|
||||
|
||||
val baseline = configurations.create("baseline")
|
||||
val latest = configurations.create("latest")
|
||||
|
||||
dependencies {
|
||||
baseline("com.squareup.moshi:moshi-adapters:1.12.0") {
|
||||
isTransitive = false
|
||||
isForce = true
|
||||
}
|
||||
latest(project(":moshi-adapters"))
|
||||
}
|
||||
|
||||
val japicmp = tasks.register<JapicmpTask>("japicmp") {
|
||||
dependsOn("jar")
|
||||
oldClasspath = baseline
|
||||
newClasspath = latest
|
||||
isOnlyBinaryIncompatibleModified = true
|
||||
isFailOnModification = true
|
||||
txtOutputFile = file("$buildDir/reports/japi.txt")
|
||||
isIgnoreMissingClasses = true
|
||||
isIncludeSynthetic = true
|
||||
}
|
||||
|
||||
tasks.named("check").configure {
|
||||
dependsOn(japicmp)
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.Date;
|
||||
|
||||
/**
|
||||
* @deprecated this class moved to avoid a package name conflict in the Java Platform Module System.
|
||||
* The new class is {@code com.squareup.moshi.adapters.Rfc3339DateJsonAdapter}.
|
||||
*/
|
||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||
private final com.squareup.moshi.adapters.Rfc3339DateJsonAdapter delegate =
|
||||
new com.squareup.moshi.adapters.Rfc3339DateJsonAdapter();
|
||||
|
||||
@Override
|
||||
public Date fromJson(JsonReader reader) throws IOException {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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
|
||||
*
|
||||
* https://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.adapters;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.jsonName;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A JsonAdapter for enums that allows having a fallback enum value when a deserialized string does
|
||||
* not match any enum value. To use, add this as an adapter for your enum type on your {@link
|
||||
* com.squareup.moshi.Moshi.Builder Moshi.Builder}:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class)
|
||||
* .withUnknownFallback(CurrencyCode.USD))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
|
||||
final Class<T> enumType;
|
||||
final String[] nameStrings;
|
||||
final T[] constants;
|
||||
final JsonReader.Options options;
|
||||
final boolean useFallbackValue;
|
||||
final @Nullable T fallbackValue;
|
||||
|
||||
public static <T extends Enum<T>> EnumJsonAdapter<T> create(Class<T> enumType) {
|
||||
return new EnumJsonAdapter<>(enumType, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new adapter for this enum with a fallback value to use when the JSON string does not
|
||||
* match any of the enum's constants. Note that this value will not be used when the JSON value is
|
||||
* null, absent, or not a string. Also, the string values are case-sensitive, and this fallback
|
||||
* value will be used even on case mismatches.
|
||||
*/
|
||||
public EnumJsonAdapter<T> withUnknownFallback(@Nullable T fallbackValue) {
|
||||
return new EnumJsonAdapter<>(enumType, fallbackValue, true);
|
||||
}
|
||||
|
||||
EnumJsonAdapter(Class<T> enumType, @Nullable T fallbackValue, boolean useFallbackValue) {
|
||||
this.enumType = enumType;
|
||||
this.fallbackValue = fallbackValue;
|
||||
this.useFallbackValue = useFallbackValue;
|
||||
try {
|
||||
constants = enumType.getEnumConstants();
|
||||
nameStrings = new String[constants.length];
|
||||
for (int i = 0; i < constants.length; i++) {
|
||||
String constantName = constants[i].name();
|
||||
nameStrings[i] = jsonName(constantName, enumType.getField(constantName));
|
||||
}
|
||||
options = JsonReader.Options.of(nameStrings);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new AssertionError("Missing field in " + enumType.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
int index = reader.selectString(options);
|
||||
if (index != -1) return constants[index];
|
||||
|
||||
String path = reader.getPath();
|
||||
if (!useFallbackValue) {
|
||||
String name = reader.nextString();
|
||||
throw new JsonDataException(
|
||||
"Expected one of "
|
||||
+ Arrays.asList(nameStrings)
|
||||
+ " but was "
|
||||
+ name
|
||||
+ " at path "
|
||||
+ path);
|
||||
}
|
||||
if (reader.peek() != JsonReader.Token.STRING) {
|
||||
throw new JsonDataException(
|
||||
"Expected a string but was " + reader.peek() + " at path " + path);
|
||||
}
|
||||
reader.skipValue();
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
if (value == null) {
|
||||
throw new NullPointerException(
|
||||
"value was null! Wrap in .nullSafe() to write nullable values.");
|
||||
}
|
||||
writer.value(nameStrings[value.ordinal()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EnumJsonAdapter(" + enumType.getName() + ")";
|
||||
}
|
||||
}
|
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (C) 2011 FasterXML, LLC
|
||||
*
|
||||
* 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.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||
*
|
||||
* <p>Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC
|
||||
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
|
||||
* objects.
|
||||
*
|
||||
* <p>Supported parse format:
|
||||
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]
|
||||
*
|
||||
* @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a>
|
||||
*/
|
||||
final class Iso8601Utils {
|
||||
/** ID to represent the 'GMT' string */
|
||||
static final String GMT_ID = "GMT";
|
||||
|
||||
/** The GMT timezone, prefetched to avoid more lookups. */
|
||||
static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone(GMT_ID);
|
||||
|
||||
/** Returns {@code date} formatted as yyyy-MM-ddThh:mm:ss.sssZ */
|
||||
public static String format(Date date) {
|
||||
Calendar calendar = new GregorianCalendar(TIMEZONE_Z, Locale.US);
|
||||
calendar.setTime(date);
|
||||
|
||||
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
|
||||
int capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length();
|
||||
StringBuilder formatted = new StringBuilder(capacity);
|
||||
padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
|
||||
formatted.append('-');
|
||||
padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
|
||||
formatted.append('-');
|
||||
padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
|
||||
formatted.append('T');
|
||||
padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
|
||||
formatted.append(':');
|
||||
padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
|
||||
formatted.append(':');
|
||||
padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
|
||||
formatted.append('.');
|
||||
padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
|
||||
formatted.append('Z');
|
||||
return formatted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date from ISO-8601 formatted string. It expects a format
|
||||
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
|
||||
*
|
||||
* @param date ISO string to parse in the appropriate format.
|
||||
* @return the parsed date
|
||||
*/
|
||||
public static Date parse(String date) {
|
||||
try {
|
||||
int offset = 0;
|
||||
|
||||
// extract year
|
||||
int year = parseInt(date, offset, offset += 4);
|
||||
if (checkOffset(date, offset, '-')) {
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// extract month
|
||||
int month = parseInt(date, offset, offset += 2);
|
||||
if (checkOffset(date, offset, '-')) {
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// extract day
|
||||
int day = parseInt(date, offset, offset += 2);
|
||||
// default time value
|
||||
int hour = 0;
|
||||
int minutes = 0;
|
||||
int seconds = 0;
|
||||
int milliseconds =
|
||||
0; // always use 0 otherwise returned date will include millis of current time
|
||||
|
||||
// if the value has no time component (and no time zone), we are done
|
||||
boolean hasT = checkOffset(date, offset, 'T');
|
||||
|
||||
if (!hasT && (date.length() <= offset)) {
|
||||
Calendar calendar = new GregorianCalendar(year, month - 1, day);
|
||||
|
||||
return calendar.getTime();
|
||||
}
|
||||
|
||||
if (hasT) {
|
||||
|
||||
// extract hours, minutes, seconds and milliseconds
|
||||
hour = parseInt(date, offset += 1, offset += 2);
|
||||
if (checkOffset(date, offset, ':')) {
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
minutes = parseInt(date, offset, offset += 2);
|
||||
if (checkOffset(date, offset, ':')) {
|
||||
offset += 1;
|
||||
}
|
||||
// second and milliseconds can be optional
|
||||
if (date.length() > offset) {
|
||||
char c = date.charAt(offset);
|
||||
if (c != 'Z' && c != '+' && c != '-') {
|
||||
seconds = parseInt(date, offset, offset += 2);
|
||||
if (seconds > 59 && seconds < 63) seconds = 59; // truncate up to 3 leap seconds
|
||||
// milliseconds can be optional in the format
|
||||
if (checkOffset(date, offset, '.')) {
|
||||
offset += 1;
|
||||
int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit
|
||||
int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits
|
||||
int fraction = parseInt(date, offset, parseEndOffset);
|
||||
milliseconds = (int) (Math.pow(10, 3 - (parseEndOffset - offset)) * fraction);
|
||||
offset = endOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract timezone
|
||||
if (date.length() <= offset) {
|
||||
throw new IllegalArgumentException("No time zone indicator");
|
||||
}
|
||||
|
||||
TimeZone timezone;
|
||||
char timezoneIndicator = date.charAt(offset);
|
||||
|
||||
if (timezoneIndicator == 'Z') {
|
||||
timezone = TIMEZONE_Z;
|
||||
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
|
||||
String timezoneOffset = date.substring(offset);
|
||||
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
|
||||
if ("+0000".equals(timezoneOffset) || "+00:00".equals(timezoneOffset)) {
|
||||
timezone = TIMEZONE_Z;
|
||||
} else {
|
||||
// 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
|
||||
// not sure why, but it is what it is.
|
||||
String timezoneId = GMT_ID + timezoneOffset;
|
||||
timezone = TimeZone.getTimeZone(timezoneId);
|
||||
String act = timezone.getID();
|
||||
if (!act.equals(timezoneId)) {
|
||||
/* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
|
||||
* one without. If so, don't sweat.
|
||||
* Yes, very inefficient. Hopefully not hit often.
|
||||
* If it becomes a perf problem, add 'loose' comparison instead.
|
||||
*/
|
||||
String cleaned = act.replace(":", "");
|
||||
if (!cleaned.equals(timezoneId)) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"Mismatching time zone indicator: "
|
||||
+ timezoneId
|
||||
+ " given, resolves to "
|
||||
+ timezone.getID());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"Invalid time zone indicator '" + timezoneIndicator + "'");
|
||||
}
|
||||
|
||||
Calendar calendar = new GregorianCalendar(timezone);
|
||||
calendar.setLenient(false);
|
||||
calendar.set(Calendar.YEAR, year);
|
||||
calendar.set(Calendar.MONTH, month - 1);
|
||||
calendar.set(Calendar.DAY_OF_MONTH, day);
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
||||
calendar.set(Calendar.MINUTE, minutes);
|
||||
calendar.set(Calendar.SECOND, seconds);
|
||||
calendar.set(Calendar.MILLISECOND, milliseconds);
|
||||
|
||||
return calendar.getTime();
|
||||
// If we get a ParseException it'll already have the right message/offset.
|
||||
// Other exception types can convert here.
|
||||
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
|
||||
throw new JsonDataException("Not an RFC 3339 date: " + date, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the expected character exist at the given offset in the value.
|
||||
*
|
||||
* @param value the string to check at the specified offset
|
||||
* @param offset the offset to look for the expected character
|
||||
* @param expected the expected character
|
||||
* @return true if the expected character exist at the given offset
|
||||
*/
|
||||
private static boolean checkOffset(String value, int offset, char expected) {
|
||||
return (offset < value.length()) && (value.charAt(offset) == expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an integer located between 2 given offsets in a string
|
||||
*
|
||||
* @param value the string to parse
|
||||
* @param beginIndex the start index for the integer in the string
|
||||
* @param endIndex the end index for the integer in the string
|
||||
* @return the int
|
||||
* @throws NumberFormatException if the value is not a number
|
||||
*/
|
||||
private static int parseInt(String value, int beginIndex, int endIndex)
|
||||
throws NumberFormatException {
|
||||
if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
|
||||
throw new NumberFormatException(value);
|
||||
}
|
||||
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values
|
||||
int i = beginIndex;
|
||||
int result = 0;
|
||||
int digit;
|
||||
if (i < endIndex) {
|
||||
digit = Character.digit(value.charAt(i++), 10);
|
||||
if (digit < 0) {
|
||||
throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
|
||||
}
|
||||
result = -digit;
|
||||
}
|
||||
while (i < endIndex) {
|
||||
digit = Character.digit(value.charAt(i++), 10);
|
||||
if (digit < 0) {
|
||||
throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
|
||||
}
|
||||
result *= 10;
|
||||
result -= digit;
|
||||
}
|
||||
return -result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero pad a number to a specified length
|
||||
*
|
||||
* @param buffer buffer to use for padding
|
||||
* @param value the integer value to pad if necessary.
|
||||
* @param length the length of the string we should zero pad
|
||||
*/
|
||||
private static void padInt(StringBuilder buffer, int value, int length) {
|
||||
String strValue = Integer.toString(value);
|
||||
for (int i = length - strValue.length(); i > 0; i--) {
|
||||
buffer.append('0');
|
||||
}
|
||||
buffer.append(strValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first character in the string that is not a digit, starting at offset.
|
||||
*/
|
||||
private static int indexOfNonDigit(String string, int offset) {
|
||||
for (int i = offset; i < string.length(); i++) {
|
||||
char c = string.charAt(i);
|
||||
if (c < '0' || c > '9') return i;
|
||||
}
|
||||
return string.length();
|
||||
}
|
||||
}
|
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Google 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
|
||||
*
|
||||
* https://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.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
|
||||
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
|
||||
* the object’s class to determine what type information to include.
|
||||
*
|
||||
* <p>Suppose we have an interface, its implementations, and a class that uses them:
|
||||
*
|
||||
* <pre>{@code
|
||||
* interface HandOfCards {
|
||||
* }
|
||||
*
|
||||
* class BlackjackHand implements HandOfCards {
|
||||
* Card hidden_card;
|
||||
* List<Card> visible_cards;
|
||||
* }
|
||||
*
|
||||
* class HoldemHand implements HandOfCards {
|
||||
* Set<Card> hidden_cards;
|
||||
* }
|
||||
*
|
||||
* class Player {
|
||||
* String name;
|
||||
* HandOfCards hand;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>We want to decode the following JSON into the player model above:
|
||||
*
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "name": "Jesse",
|
||||
* "hand": {
|
||||
* "hand_type": "blackjack",
|
||||
* "hidden_card": "9D",
|
||||
* "visible_cards": ["8H", "4C"]
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
|
||||
* {@code HandOfCards} interface. We configure it to use the appropriate subtype instead:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
|
||||
* .withSubtype(BlackjackHand.class, "blackjack")
|
||||
* .withSubtype(HoldemHand.class, "holdem"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This class imposes strict requirements on its use:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Base types may be classes or interfaces.
|
||||
* <li>Subtypes must encode as JSON objects.
|
||||
* <li>Type information must be in the encoded object. Each message must have a type label like
|
||||
* {@code hand_type} whose value is a string like {@code blackjack} that identifies which type
|
||||
* to use.
|
||||
* <li>Each type identifier must be unique.
|
||||
* </ul>
|
||||
*
|
||||
* <p>For best performance type information should be the first field in the object. Otherwise Moshi
|
||||
* must reprocess the JSON stream once it knows the object's type.
|
||||
*
|
||||
* <p>If an unknown subtype is encountered when decoding:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If {@link #withDefaultValue(Object)} is used, then {@code defaultValue} will be returned.
|
||||
* <li>If {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code
|
||||
* fallbackJsonAdapter.fromJson(reader)} result will be returned.
|
||||
* <li>Otherwise a {@link JsonDataException} will be thrown.
|
||||
* </ul>
|
||||
*
|
||||
* <p>If an unknown type is encountered when encoding:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code
|
||||
* fallbackJsonAdapter.toJson(writer, value)} result will be returned.
|
||||
* <li>Otherwise a {@link IllegalArgumentException} will be thrown.
|
||||
* </ul>
|
||||
*
|
||||
* <p>If the same subtype has multiple labels the first one is used when encoding.
|
||||
*/
|
||||
public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||
final Class<T> baseType;
|
||||
final String labelKey;
|
||||
final List<String> labels;
|
||||
final List<Type> subtypes;
|
||||
@Nullable final JsonAdapter<Object> fallbackJsonAdapter;
|
||||
|
||||
PolymorphicJsonAdapterFactory(
|
||||
Class<T> baseType,
|
||||
String labelKey,
|
||||
List<String> labels,
|
||||
List<Type> subtypes,
|
||||
@Nullable JsonAdapter<Object> fallbackJsonAdapter) {
|
||||
this.baseType = baseType;
|
||||
this.labelKey = labelKey;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.fallbackJsonAdapter = fallbackJsonAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
|
||||
* @param labelKey The key in the JSON object whose value determines the type to which to map the
|
||||
* JSON object.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public static <T> PolymorphicJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) {
|
||||
if (baseType == null) throw new NullPointerException("baseType == null");
|
||||
if (labelKey == null) throw new NullPointerException("labelKey == null");
|
||||
return new PolymorphicJsonAdapterFactory<>(
|
||||
baseType, labelKey, Collections.<String>emptyList(), Collections.<Type>emptyList(), null);
|
||||
}
|
||||
|
||||
/** Returns a new factory that decodes instances of {@code subtype}. */
|
||||
public PolymorphicJsonAdapterFactory<T> withSubtype(Class<? extends T> subtype, String label) {
|
||||
if (subtype == null) throw new NullPointerException("subtype == null");
|
||||
if (label == null) throw new NullPointerException("label == null");
|
||||
if (labels.contains(label)) {
|
||||
throw new IllegalArgumentException("Labels must be unique.");
|
||||
}
|
||||
List<String> newLabels = new ArrayList<>(labels);
|
||||
newLabels.add(label);
|
||||
List<Type> newSubtypes = new ArrayList<>(subtypes);
|
||||
newSubtypes.add(subtype);
|
||||
return new PolymorphicJsonAdapterFactory<>(
|
||||
baseType, labelKey, newLabels, newSubtypes, fallbackJsonAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that with default to {@code fallbackJsonAdapter.fromJson(reader)} upon
|
||||
* decoding of unrecognized labels.
|
||||
*
|
||||
* <p>The {@link JsonReader} instance will not be automatically consumed, so make sure to consume
|
||||
* it within your implementation of {@link JsonAdapter#fromJson(JsonReader)}
|
||||
*/
|
||||
public PolymorphicJsonAdapterFactory<T> withFallbackJsonAdapter(
|
||||
@Nullable JsonAdapter<Object> fallbackJsonAdapter) {
|
||||
return new PolymorphicJsonAdapterFactory<>(
|
||||
baseType, labelKey, labels, subtypes, fallbackJsonAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that will default to {@code defaultValue} upon decoding of unrecognized
|
||||
* labels. The default value should be immutable.
|
||||
*/
|
||||
public PolymorphicJsonAdapterFactory<T> withDefaultValue(@Nullable T defaultValue) {
|
||||
return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue));
|
||||
}
|
||||
|
||||
private JsonAdapter<Object> buildFallbackJsonAdapter(final T defaultValue) {
|
||||
return new JsonAdapter<Object>() {
|
||||
@Override
|
||||
public @Nullable Object fromJson(JsonReader reader) throws IOException {
|
||||
reader.skipValue();
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected one of "
|
||||
+ subtypes
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". Register this subtype.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>(subtypes.size());
|
||||
for (int i = 0, size = subtypes.size(); i < size; i++) {
|
||||
jsonAdapters.add(moshi.adapter(subtypes.get(i)));
|
||||
}
|
||||
|
||||
return new PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter)
|
||||
.nullSafe();
|
||||
}
|
||||
|
||||
static final class PolymorphicJsonAdapter extends JsonAdapter<Object> {
|
||||
final String labelKey;
|
||||
final List<String> labels;
|
||||
final List<Type> subtypes;
|
||||
final List<JsonAdapter<Object>> jsonAdapters;
|
||||
@Nullable final JsonAdapter<Object> fallbackJsonAdapter;
|
||||
|
||||
/** Single-element options containing the label's key only. */
|
||||
final JsonReader.Options labelKeyOptions;
|
||||
/** Corresponds to subtypes. */
|
||||
final JsonReader.Options labelOptions;
|
||||
|
||||
PolymorphicJsonAdapter(
|
||||
String labelKey,
|
||||
List<String> labels,
|
||||
List<Type> subtypes,
|
||||
List<JsonAdapter<Object>> jsonAdapters,
|
||||
@Nullable JsonAdapter<Object> fallbackJsonAdapter) {
|
||||
this.labelKey = labelKey;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.jsonAdapters = jsonAdapters;
|
||||
this.fallbackJsonAdapter = fallbackJsonAdapter;
|
||||
|
||||
this.labelKeyOptions = JsonReader.Options.of(labelKey);
|
||||
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromJson(JsonReader reader) throws IOException {
|
||||
JsonReader peeked = reader.peekJson();
|
||||
peeked.setFailOnUnknown(false);
|
||||
int labelIndex;
|
||||
try {
|
||||
labelIndex = labelIndex(peeked);
|
||||
} finally {
|
||||
peeked.close();
|
||||
}
|
||||
if (labelIndex == -1) {
|
||||
return this.fallbackJsonAdapter.fromJson(reader);
|
||||
} else {
|
||||
return jsonAdapters.get(labelIndex).fromJson(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private int labelIndex(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
if (reader.selectName(labelKeyOptions) == -1) {
|
||||
reader.skipName();
|
||||
reader.skipValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
int labelIndex = reader.selectString(labelOptions);
|
||||
if (labelIndex == -1 && this.fallbackJsonAdapter == null) {
|
||||
throw new JsonDataException(
|
||||
"Expected one of "
|
||||
+ labels
|
||||
+ " for key '"
|
||||
+ labelKey
|
||||
+ "' but found '"
|
||||
+ reader.nextString()
|
||||
+ "'. Register a subtype for this label.");
|
||||
}
|
||||
return labelIndex;
|
||||
}
|
||||
|
||||
throw new JsonDataException("Missing label for " + labelKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
Class<?> type = value.getClass();
|
||||
int labelIndex = subtypes.indexOf(type);
|
||||
final JsonAdapter<Object> adapter;
|
||||
if (labelIndex == -1) {
|
||||
if (fallbackJsonAdapter == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected one of "
|
||||
+ subtypes
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". Register this subtype.");
|
||||
}
|
||||
adapter = fallbackJsonAdapter;
|
||||
} else {
|
||||
adapter = jsonAdapters.get(labelIndex);
|
||||
}
|
||||
|
||||
writer.beginObject();
|
||||
if (adapter != fallbackJsonAdapter) {
|
||||
writer.name(labelKey).value(labels.get(labelIndex));
|
||||
}
|
||||
int flattenToken = writer.beginFlatten();
|
||||
adapter.toJson(writer, value);
|
||||
writer.endFlatten(flattenToken);
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PolymorphicJsonAdapter(" + labelKey + ")";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
|
||||
* formatted like {@code 2015-09-26T18:23:50.250Z}. This adapter is null-safe. To use, add this as
|
||||
* an adapter for {@code Date.class} on your {@link com.squareup.moshi.Moshi.Builder Moshi.Builder}:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(Date.class, new Rfc3339DateJsonAdapter())
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||
@Override
|
||||
public synchronized Date fromJson(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
return reader.nextNull();
|
||||
}
|
||||
String string = reader.nextString();
|
||||
return Iso8601Utils.parse(string);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
} else {
|
||||
String string = Iso8601Utils.format(value);
|
||||
writer.value(string);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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
|
||||
*
|
||||
* https://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.adapters;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.squareup.moshi.Json;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import okio.Buffer;
|
||||
import org.junit.Test;
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public final class EnumJsonAdapterTest {
|
||||
@Test
|
||||
public void toAndFromJson() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
|
||||
assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK);
|
||||
assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJsonName() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
|
||||
assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS);
|
||||
assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withoutFallbackValue() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
|
||||
try {
|
||||
adapter.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Expected one of [ROCK, PAPER, scr] but was SPOCK at path $");
|
||||
}
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withFallbackValue() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter =
|
||||
EnumJsonAdapter.create(Roshambo.class).withUnknownFallback(Roshambo.ROCK);
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
|
||||
assertThat(adapter.fromJson(reader)).isEqualTo(Roshambo.ROCK);
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withNullFallbackValue() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter =
|
||||
EnumJsonAdapter.create(Roshambo.class).withUnknownFallback(null);
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
|
||||
assertThat(adapter.fromJson(reader)).isNull();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
enum Roshambo {
|
||||
ROCK,
|
||||
PAPER,
|
||||
@Json(name = "scr")
|
||||
SCISSORS
|
||||
}
|
||||
}
|
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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
|
||||
*
|
||||
* https://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.adapters;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import org.junit.Test;
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public final class PolymorphicJsonAdapterFactoryTest {
|
||||
@Test
|
||||
public void fromJson() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}"))
|
||||
.isEqualTo(new Success("Okay!"));
|
||||
assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"))
|
||||
.isEqualTo(new Error(Collections.<String, Object>singletonMap("order", 66d)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toJson() {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.toJson(new Success("Okay!")))
|
||||
.isEqualTo("{\"type\":\"success\",\"value\":\"Okay!\"}");
|
||||
assertThat(adapter.toJson(new Error(Collections.<String, Object>singletonMap("order", 66))))
|
||||
.isEqualTo("{\"type\":\"error\",\"error_logs\":{\"order\":66}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisteredLabelValue() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader =
|
||||
JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}"));
|
||||
try {
|
||||
adapter.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Expected one of [success, error] for key 'type' but found"
|
||||
+ " 'data'. Register a subtype for this label.");
|
||||
}
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void specifiedFallbackSubtype() throws IOException {
|
||||
Error fallbackError = new Error(Collections.<String, Object>emptyMap());
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withDefaultValue(fallbackError))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
|
||||
assertThat(message).isSameInstanceAs(fallbackError);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void specifiedNullFallbackSubtype() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withDefaultValue(null))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
|
||||
assertThat(message).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void specifiedFallbackJsonAdapter() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withFallbackJsonAdapter(
|
||||
new JsonAdapter<Object>() {
|
||||
@Override
|
||||
public Object fromJson(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
assertThat(reader.nextName()).isEqualTo("type");
|
||||
assertThat(reader.nextString()).isEqualTo("data");
|
||||
assertThat(reader.nextName()).isEqualTo("value");
|
||||
assertThat(reader.nextString()).isEqualTo("Okay!");
|
||||
reader.endObject();
|
||||
return new EmptyMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, @Nullable Object value) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader =
|
||||
JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}"));
|
||||
|
||||
Message message = adapter.fromJson(reader);
|
||||
assertThat(message).isInstanceOf(EmptyMessage.class);
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisteredSubtype() {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.toJson(new EmptyMessage());
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Expected one of [class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
|
||||
+ " EmptyMessage, a class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
|
||||
+ " this subtype.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisteredSubtypeWithDefaultValue() {
|
||||
Error fallbackError = new Error(Collections.<String, Object>emptyMap());
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withDefaultValue(fallbackError))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.toJson(new EmptyMessage());
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Expected one of [class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
|
||||
+ " EmptyMessage, a class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
|
||||
+ " this subtype.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisteredSubtypeWithFallbackJsonAdapter() {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withFallbackJsonAdapter(
|
||||
new JsonAdapter<Object>() {
|
||||
@Override
|
||||
public Object fromJson(JsonReader reader) {
|
||||
throw new RuntimeException(
|
||||
"Not implemented as not needed for the test");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
writer.name("type").value("injected by fallbackJsonAdapter");
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
String json = adapter.toJson(new EmptyMessage());
|
||||
assertThat(json).isEqualTo("{\"type\":\"injected by fallbackJsonAdapter\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonStringLabelValue() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Expected a string but was BEGIN_OBJECT at path $.type");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonObjectDoesNotConsume() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"Failure\""));
|
||||
try {
|
||||
adapter.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Expected BEGIN_OBJECT but was STRING at path $");
|
||||
}
|
||||
assertThat(reader.nextString()).isEqualTo("Failure");
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonUniqueSubtypes() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Success.class, "data")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}"))
|
||||
.isEqualTo(new Success("Okay!"));
|
||||
assertThat(adapter.fromJson("{\"type\":\"data\",\"value\":\"Data!\"}"))
|
||||
.isEqualTo(new Success("Data!"));
|
||||
assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"))
|
||||
.isEqualTo(new Error(Collections.<String, Object>singletonMap("order", 66d)));
|
||||
assertThat(adapter.toJson(new Success("Data!")))
|
||||
.isEqualTo("{\"type\":\"success\",\"value\":\"Data!\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uniqueLabels() {
|
||||
PolymorphicJsonAdapterFactory<Message> factory =
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type").withSubtype(Success.class, "data");
|
||||
try {
|
||||
factory.withSubtype(Error.class, "data");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessageThat().isEqualTo("Labels must be unique.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullSafe() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("null"));
|
||||
assertThat(adapter.fromJson(reader)).isNull();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Longs that do not have an exact double representation are problematic for JSON. It is a bad
|
||||
* idea to use JSON for these values! But Moshi tries to retain long precision where possible.
|
||||
*/
|
||||
@Test
|
||||
public void unportableTypes() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(MessageWithUnportableTypes.class, "unportable"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.toJson(new MessageWithUnportableTypes(9007199254740993L)))
|
||||
.isEqualTo("{\"type\":\"unportable\",\"long_value\":9007199254740993}");
|
||||
MessageWithUnportableTypes decoded =
|
||||
(MessageWithUnportableTypes)
|
||||
adapter.fromJson("{\"type\":\"unportable\",\"long_value\":9007199254740993}");
|
||||
assertThat(decoded.long_value).isEqualTo(9007199254740993L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failOnUnknownMissingTypeLabel() throws IOException {
|
||||
Moshi moshi =
|
||||
new Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(MessageWithType.class, "success"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class).failOnUnknown();
|
||||
|
||||
MessageWithType decoded =
|
||||
(MessageWithType) adapter.fromJson("{\"value\":\"Okay!\",\"type\":\"success\"}");
|
||||
assertThat(decoded.value).isEqualTo("Okay!");
|
||||
}
|
||||
|
||||
interface Message {}
|
||||
|
||||
static final class Success implements Message {
|
||||
final String value;
|
||||
|
||||
Success(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Success)) return false;
|
||||
Success success = (Success) o;
|
||||
return value.equals(success.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
static final class Error implements Message {
|
||||
final Map<String, Object> error_logs;
|
||||
|
||||
Error(Map<String, Object> error_logs) {
|
||||
this.error_logs = error_logs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Error)) return false;
|
||||
Error error = (Error) o;
|
||||
return error_logs.equals(error.error_logs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return error_logs.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
static final class EmptyMessage implements Message {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EmptyMessage";
|
||||
}
|
||||
}
|
||||
|
||||
static final class MessageWithUnportableTypes implements Message {
|
||||
final long long_value;
|
||||
|
||||
MessageWithUnportableTypes(long long_value) {
|
||||
this.long_value = long_value;
|
||||
}
|
||||
}
|
||||
|
||||
static final class MessageWithType implements Message {
|
||||
final String type;
|
||||
final String value;
|
||||
|
||||
MessageWithType(String type, String value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.adapters;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.Test;
|
||||
|
||||
public final class Rfc3339DateJsonAdapterTest {
|
||||
private final JsonAdapter<Date> adapter = new Rfc3339DateJsonAdapter().lenient();
|
||||
|
||||
@Test
|
||||
public void fromJsonWithTwoDigitMillis() throws Exception {
|
||||
assertThat(adapter.fromJson("\"1985-04-12T23:20:50.52Z\""))
|
||||
.isEqualTo(newDate(1985, 4, 12, 23, 20, 50, 520, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromJson() throws Exception {
|
||||
assertThat(adapter.fromJson("\"1970-01-01T00:00:00.000Z\""))
|
||||
.isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, 0));
|
||||
assertThat(adapter.fromJson("\"1985-04-12T23:20:50.520Z\""))
|
||||
.isEqualTo(newDate(1985, 4, 12, 23, 20, 50, 520, 0));
|
||||
assertThat(adapter.fromJson("\"1996-12-19T16:39:57-08:00\""))
|
||||
.isEqualTo(newDate(1996, 12, 19, 16, 39, 57, 0, -8 * 60));
|
||||
assertThat(adapter.fromJson("\"1990-12-31T23:59:60Z\""))
|
||||
.isEqualTo(newDate(1990, 12, 31, 23, 59, 59, 0, 0));
|
||||
assertThat(adapter.fromJson("\"1990-12-31T15:59:60-08:00\""))
|
||||
.isEqualTo(newDate(1990, 12, 31, 15, 59, 59, 0, -8 * 60));
|
||||
assertThat(adapter.fromJson("\"1937-01-01T12:00:27.870+00:20\""))
|
||||
.isEqualTo(newDate(1937, 1, 1, 12, 0, 27, 870, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toJson() throws Exception {
|
||||
assertThat(adapter.toJson(newDate(1970, 1, 1, 0, 0, 0, 0, 0)))
|
||||
.isEqualTo("\"1970-01-01T00:00:00.000Z\"");
|
||||
assertThat(adapter.toJson(newDate(1985, 4, 12, 23, 20, 50, 520, 0)))
|
||||
.isEqualTo("\"1985-04-12T23:20:50.520Z\"");
|
||||
assertThat(adapter.toJson(newDate(1996, 12, 19, 16, 39, 57, 0, -8 * 60)))
|
||||
.isEqualTo("\"1996-12-20T00:39:57.000Z\"");
|
||||
assertThat(adapter.toJson(newDate(1990, 12, 31, 23, 59, 59, 0, 0)))
|
||||
.isEqualTo("\"1990-12-31T23:59:59.000Z\"");
|
||||
assertThat(adapter.toJson(newDate(1990, 12, 31, 15, 59, 59, 0, -8 * 60)))
|
||||
.isEqualTo("\"1990-12-31T23:59:59.000Z\"");
|
||||
assertThat(adapter.toJson(newDate(1937, 1, 1, 12, 0, 27, 870, 20)))
|
||||
.isEqualTo("\"1937-01-01T11:40:27.870Z\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullSafety() throws Exception {
|
||||
assertThat(adapter.toJson(null)).isEqualTo("null");
|
||||
assertThat(adapter.fromJson("null")).isNull();
|
||||
}
|
||||
|
||||
private Date newDate(
|
||||
int year, int month, int day, int hour, int minute, int second, int millis, int offset) {
|
||||
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||
calendar.set(year, month - 1, day, hour, minute, second);
|
||||
calendar.set(Calendar.MILLISECOND, millis);
|
||||
return new Date(calendar.getTimeInMillis() - TimeUnit.MINUTES.toMillis(offset));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user