mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Allow custom generators (#847)
* Extract generatedJsonAdapterName to public API for other generators/consumers * Fix kapt location in tests * Add IDE-generated dependency-reduced-pom.xml to gitignore This always bites me * Add generator property to JsonClass and skip in processor * Opportunistically fix formatting for generateAdapter doc * Extract NullSafeJsonAdapter for delegate testing * Add custom adapter tests * Allow no-moshi constructors for generated adapters * Fix rebase issue * Use something other than nullSafe() for lenient check This no longer propagates lenient * Add missing copyrights * Add top-level class note * Add note about working against Moshi's generated signature * Add missing bit to "requirements for" * Note kotlin requirement relaxed in custom generators * Style
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.NullSafeJsonAdapter;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
@@ -128,29 +129,7 @@ public abstract class JsonAdapter<T> {
|
||||
* nulls.
|
||||
*/
|
||||
@CheckReturnValue public final JsonAdapter<T> nullSafe() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
return reader.nextNull();
|
||||
} else {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
}
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
} else {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".nullSafe()";
|
||||
}
|
||||
};
|
||||
return new NullSafeJsonAdapter<>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -17,6 +17,7 @@ package com.squareup.moshi;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@@ -29,13 +30,52 @@ public @interface JsonClass {
|
||||
/**
|
||||
* True to trigger the annotation processor to generate an adapter for this type.
|
||||
*
|
||||
* There are currently some restrictions on which types that can be used with generated adapters:
|
||||
*
|
||||
* * The class must be implemented in Kotlin.
|
||||
* * The class may not be an abstract class, an inner class, or a local class.
|
||||
* * All superclasses must be implemented in Kotlin.
|
||||
* * All properties must be public, protected, or internal.
|
||||
* * All properties must be either non-transient or have a default value.
|
||||
* <p>There are currently some restrictions on which types that can be used with generated
|
||||
* adapters:
|
||||
* <ul>
|
||||
* <li>
|
||||
* The class must be implemented in Kotlin (unless using a custom generator, see
|
||||
* {@link #generator()}).
|
||||
* </li>
|
||||
* <li>The class may not be an abstract class, an inner class, or a local class.</li>
|
||||
* <li>All superclasses must be implemented in Kotlin.</li>
|
||||
* <li>All properties must be public, protected, or internal.</li>
|
||||
* <li>All properties must be either non-transient or have a default value.</li>
|
||||
* </ul>
|
||||
*/
|
||||
boolean generateAdapter();
|
||||
|
||||
/**
|
||||
* An optional custom generator tag used to indicate which generator should be used. If empty,
|
||||
* Moshi's annotation processor will generate an adapter for the annotated type. If not empty,
|
||||
* Moshi's processor will skip it and defer to a custom generator. This can be used to allow
|
||||
* other custom code generation tools to run and still allow Moshi to read their generated
|
||||
* JsonAdapter outputs.
|
||||
*
|
||||
* <p>Requirements for generated adapter class signatures:
|
||||
* <ul>
|
||||
* <li>
|
||||
* The generated adapter must subclass {@link JsonAdapter} and be parameterized by this type.
|
||||
* </li>
|
||||
* <li>
|
||||
* {@link Types#generatedJsonAdapterName} should be used for the fully qualified class name in
|
||||
* order for Moshi to correctly resolve and load the generated JsonAdapter.
|
||||
* </li>
|
||||
* <li>The first parameter must be a {@link Moshi} instance.</li>
|
||||
* <li>
|
||||
* If generic, a second {@link Type[]} parameter should be declared to accept type arguments.
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example for a class "CustomType":<pre>{@code
|
||||
* class CustomTypeJsonAdapter(moshi: Moshi, types: Array<Type>) : JsonAdapter<CustomType>() {
|
||||
* // ...
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>To help ensure your own generator meets requirements above, you can use Moshi’s built-in
|
||||
* generator to create the API signature to get started, then make your own generator match that
|
||||
* expected signature.
|
||||
*/
|
||||
String generator() default "";
|
||||
}
|
||||
|
@@ -49,6 +49,37 @@ public final class Types {
|
||||
private Types() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
||||
* {@link JsonClass JsonClass-annotated} {@code clazz}. This is the same lookup logic used by
|
||||
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
|
||||
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
|
||||
*
|
||||
* @param clazz the class to calculate a generated JsonAdapter name for.
|
||||
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
|
||||
* Note that this name will always be a top-level class name and not a nested class.
|
||||
*/
|
||||
public static String generatedJsonAdapterName(Class<?> clazz) {
|
||||
if (clazz.getAnnotation(JsonClass.class) == null) {
|
||||
throw new IllegalArgumentException("Class does not have a JsonClass annotation: " + clazz);
|
||||
}
|
||||
return generatedJsonAdapterName(clazz.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
||||
* {@link JsonClass JsonClass-annotated} {@code className}. This is the same lookup logic used by
|
||||
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
|
||||
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
|
||||
*
|
||||
* @param className the fully qualified class to calculate a generated JsonAdapter name for.
|
||||
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
|
||||
* Note that this name will always be a top-level class name and not a nested class.
|
||||
*/
|
||||
public static String generatedJsonAdapterName(String className) {
|
||||
return className.replace("$", "_") + "JsonAdapter";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if {@code annotations} contains {@code jsonQualifier}.
|
||||
* Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code
|
||||
|
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.internal;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class NullSafeJsonAdapter<T> extends JsonAdapter<T> {
|
||||
|
||||
private final JsonAdapter<T> delegate;
|
||||
|
||||
public NullSafeJsonAdapter(JsonAdapter<T> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public JsonAdapter<T> delegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
return reader.nextNull();
|
||||
} else {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
} else {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return delegate + ".nullSafe()";
|
||||
}
|
||||
}
|
@@ -460,23 +460,35 @@ public final class Util {
|
||||
if (jsonClass == null || !jsonClass.generateAdapter()) {
|
||||
return null;
|
||||
}
|
||||
String adapterClassName = rawType.getName().replace("$", "_") + "JsonAdapter";
|
||||
String adapterClassName = Types.generatedJsonAdapterName(rawType.getName());
|
||||
try {
|
||||
@SuppressWarnings("unchecked") // We generate types to match.
|
||||
Class<? extends JsonAdapter<?>> adapterClass = (Class<? extends JsonAdapter<?>>)
|
||||
Class.forName(adapterClassName, true, rawType.getClassLoader());
|
||||
Constructor<? extends JsonAdapter<?>> constructor;
|
||||
Object[] args;
|
||||
if (type instanceof ParameterizedType) {
|
||||
Constructor<? extends JsonAdapter<?>> constructor
|
||||
= adapterClass.getDeclaredConstructor(Moshi.class, Type[].class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(moshi, ((ParameterizedType) type).getActualTypeArguments())
|
||||
.nullSafe();
|
||||
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
|
||||
try {
|
||||
// Common case first
|
||||
constructor = adapterClass.getDeclaredConstructor(Moshi.class, Type[].class);
|
||||
args = new Object[] { moshi, typeArgs };
|
||||
} catch (NoSuchMethodException e) {
|
||||
constructor = adapterClass.getDeclaredConstructor(Type[].class);
|
||||
args = new Object[] { typeArgs };
|
||||
}
|
||||
} else {
|
||||
Constructor<? extends JsonAdapter<?>> constructor
|
||||
= adapterClass.getDeclaredConstructor(Moshi.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(moshi).nullSafe();
|
||||
try {
|
||||
// Common case first
|
||||
constructor = adapterClass.getDeclaredConstructor(Moshi.class);
|
||||
args = new Object[] { moshi };
|
||||
} catch (NoSuchMethodException e) {
|
||||
constructor = adapterClass.getDeclaredConstructor();
|
||||
args = new Object[0];
|
||||
}
|
||||
}
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(args).nullSafe();
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to find the generated JsonAdapter class for " + rawType, e);
|
||||
|
@@ -282,7 +282,7 @@ public final class JsonAdapterTest {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}.lenient().nullSafe();
|
||||
}.lenient().nonNull();
|
||||
assertThat(adapter.fromJson("true true")).isEqualTo(true);
|
||||
}
|
||||
}
|
||||
|
@@ -287,6 +287,33 @@ public final class TypesTest {
|
||||
assertThat(annotations).hasSize(0);
|
||||
}
|
||||
|
||||
@Test public void generatedJsonAdapterName_strings() {
|
||||
assertThat(Types.generatedJsonAdapterName("com.foo.Test")).isEqualTo("com.foo.TestJsonAdapter");
|
||||
assertThat(Types.generatedJsonAdapterName("com.foo.Test$Bar")).isEqualTo("com.foo.Test_BarJsonAdapter");
|
||||
}
|
||||
|
||||
@Test public void generatedJsonAdapterName_class() {
|
||||
assertThat(Types.generatedJsonAdapterName(TestJsonClass.class)).isEqualTo("com.squareup.moshi.TypesTest_TestJsonClassJsonAdapter");
|
||||
}
|
||||
|
||||
@Test public void generatedJsonAdapterName_class_missingJsonClass() {
|
||||
try {
|
||||
Types.generatedJsonAdapterName(TestNonJsonClass.class);
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e).hasMessageContaining("Class does not have a JsonClass annotation");
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
static class TestJsonClass {
|
||||
|
||||
}
|
||||
|
||||
static class TestNonJsonClass {
|
||||
|
||||
}
|
||||
|
||||
@JsonQualifier
|
||||
@Target(FIELD)
|
||||
@Retention(RUNTIME)
|
||||
|
Reference in New Issue
Block a user