Change the directory structure to match our modules (#1451)

This commit is contained in:
Jesse Wilson
2021-12-08 23:52:51 -05:00
committed by GitHub
parent d5d172c3bb
commit 7578984f25
68 changed files with 24 additions and 265 deletions

37
moshi-adapters/README.md Normal file
View 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/

View 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()))
}

View 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)
}

View File

@@ -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);
}
}

View File

@@ -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() + ")";
}
}

View File

@@ -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;
/**
* Jacksons 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();
}
}

View File

@@ -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 objects 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 + ")";
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}