mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 07:59:21 +08:00
Add RuntimeJsonAdapterFactory to adapters module. (#606)
* Add RuntimeJsonAdapterFactory to adapters module. * Make RuntimeJsonAdapterFactory create null-safe adapters. * Add copyright headers.
This commit is contained in:
committed by
Jesse Wilson
parent
9251309c3f
commit
0f1fa3d385
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* 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.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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
* A JsonAdapter factory for polymorphic types. This is useful when the type is not known before
|
||||
* deserializing the JSON. This factory's adapters expect JSON in the format of a JSON object with a
|
||||
* key whose value is a label that determines the type to which to map the JSON object.
|
||||
*/
|
||||
public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||
final Class<T> baseType;
|
||||
final String labelKey;
|
||||
final Map<String, Type> labelToType = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* @param baseType The base type for which this factory will create adapters.
|
||||
* @param labelKey The key in the JSON object whose value determines the type to which to map the
|
||||
* JSON object.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public static <T> RuntimeJsonAdapterFactory<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 RuntimeJsonAdapterFactory<>(baseType, labelKey);
|
||||
}
|
||||
|
||||
RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey) {
|
||||
this.baseType = baseType;
|
||||
this.labelKey = labelKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the subtype that can be created based on the label. When deserializing, if a label
|
||||
* that was not registered is found, a JsonDataException will be thrown. When serializing, if a
|
||||
* type that was not registered is used, an IllegalArgumentException will be thrown.
|
||||
*/
|
||||
public RuntimeJsonAdapterFactory<T> registerSubtype(Class<? extends T> subtype, String label) {
|
||||
if (subtype == null) throw new NullPointerException("subtype == null");
|
||||
if (label == null) throw new NullPointerException("label == null");
|
||||
if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) {
|
||||
throw new IllegalArgumentException("Subtypes and labels must be unique.");
|
||||
}
|
||||
labelToType.put(label, subtype);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int size = labelToType.size();
|
||||
Map<Type, JsonAdapter<Object>> typeToAdapter = new LinkedHashMap<>(size);
|
||||
Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size);
|
||||
for (Map.Entry<String, Type> entry : labelToType.entrySet()) {
|
||||
String label = entry.getKey();
|
||||
Type typeValue = entry.getValue();
|
||||
JsonAdapter<Object> adapter = moshi.adapter(typeValue);
|
||||
labelToAdapter.put(label, adapter);
|
||||
typeToAdapter.put(typeValue, adapter);
|
||||
}
|
||||
return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToAdapter).nullSafe();
|
||||
}
|
||||
|
||||
static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
|
||||
final String labelKey;
|
||||
final Map<String, JsonAdapter<Object>> labelToAdapter;
|
||||
final Map<Type, JsonAdapter<Object>> typeToAdapter;
|
||||
|
||||
RuntimeJsonAdapter(
|
||||
String labelKey,
|
||||
Map<String, JsonAdapter<Object>> labelToAdapter,
|
||||
Map<Type, JsonAdapter<Object>> typeToAdapter) {
|
||||
this.labelKey = labelKey;
|
||||
this.labelToAdapter = labelToAdapter;
|
||||
this.typeToAdapter = typeToAdapter;
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
JsonReader.Token peekedToken = reader.peek();
|
||||
if (peekedToken != JsonReader.Token.BEGIN_OBJECT) {
|
||||
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken
|
||||
+ " at path " + reader.getPath());
|
||||
}
|
||||
Object jsonValue = reader.readJsonValue();
|
||||
Map<String, Object> jsonObject = (Map<String, Object>) jsonValue;
|
||||
Object label = jsonObject.get(labelKey);
|
||||
if (label == null) {
|
||||
throw new JsonDataException("Missing label for " + labelKey);
|
||||
}
|
||||
if (!(label instanceof String)) {
|
||||
throw new JsonDataException("Label for '"
|
||||
+ labelKey
|
||||
+ "' must be a string but was "
|
||||
+ label
|
||||
+ ", a "
|
||||
+ label.getClass());
|
||||
}
|
||||
JsonAdapter<Object> adapter = labelToAdapter.get(label);
|
||||
if (adapter == null) {
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ labelToAdapter.keySet()
|
||||
+ " for key '"
|
||||
+ labelKey
|
||||
+ "' but found '"
|
||||
+ label
|
||||
+ "'. Register a subtype for this label.");
|
||||
}
|
||||
return adapter.fromJsonValue(jsonValue);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
Class<?> type = value.getClass();
|
||||
JsonAdapter<Object> adapter = typeToAdapter.get(type);
|
||||
if (adapter == null) {
|
||||
throw new IllegalArgumentException("Expected one of "
|
||||
+ typeToAdapter.keySet()
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". Register this subtype.");
|
||||
}
|
||||
adapter.toJson(writer, value);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "RuntimeJsonAdapter(" + labelKey + ")";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* 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.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import okio.Buffer;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public final class RuntimeJsonAdapterFactoryTest {
|
||||
@Test public void fromJson() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(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(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.toJson(new Success("Okay!")))
|
||||
.isEqualTo("{\"value\":\"Okay!\"}");
|
||||
assertThat(adapter.toJson(new Error(Collections.<String, Object>singletonMap("order", 66))))
|
||||
.isEqualTo("{\"error_logs\":{\"order\":66}}");
|
||||
}
|
||||
|
||||
@Test public void unregisteredLabelValue() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(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).hasMessage("Expected one of [success, error] for key 'type' but found"
|
||||
+ " 'data'. Register a subtype for this label.");
|
||||
}
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void unregisteredSubtype() {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.toJson(new EmptyMessage());
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Expected one of [class"
|
||||
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$Success, class"
|
||||
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$Error] but found"
|
||||
+ " EmptyMessage, a class"
|
||||
+ " com.squareup.moshi.adapters.RuntimeJsonAdapterFactoryTest$EmptyMessage. Register"
|
||||
+ " this subtype.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nonStringLabelValue() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Label for 'type' must be a string but was {},"
|
||||
+ " a class com.squareup.moshi.LinkedHashTreeMap");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nonObjectDoesNotConsume() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(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).hasMessage("Expected BEGIN_OBJECT but was STRING at path $");
|
||||
}
|
||||
assertThat(reader.nextString()).isEqualTo("Failure");
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void uniqueSubtypes() {
|
||||
RuntimeJsonAdapterFactory<Message> factory =
|
||||
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success");
|
||||
try {
|
||||
factory.registerSubtype(Success.class, "data");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void uniqueLabels() {
|
||||
RuntimeJsonAdapterFactory<Message> factory =
|
||||
RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "data");
|
||||
try {
|
||||
factory.registerSubtype(Error.class, "data");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Subtypes and labels must be unique.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nullSafe() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(Message.class, "type")
|
||||
.registerSubtype(Success.class, "success")
|
||||
.registerSubtype(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);
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user