mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
Full JsonQualifier support in kotlin codegen.
This commit is contained in:
@@ -34,6 +34,7 @@ import me.eugeniomarletti.kotlin.metadata.isDataClass
|
|||||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
|
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
|
||||||
import me.eugeniomarletti.kotlin.metadata.visibility
|
import me.eugeniomarletti.kotlin.metadata.visibility
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
import javax.annotation.processing.Messager
|
||||||
import javax.lang.model.element.TypeElement
|
import javax.lang.model.element.TypeElement
|
||||||
|
|
||||||
/** Generates a JSON adapter for a target type. */
|
/** Generates a JSON adapter for a target type. */
|
||||||
@@ -81,7 +82,7 @@ internal class AdapterGenerator(
|
|||||||
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
|
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun generateFile(generatedOption: TypeElement?): FileSpec {
|
fun generateFile(messager: Messager, generatedOption: TypeElement?): FileSpec {
|
||||||
for (property in propertyList) {
|
for (property in propertyList) {
|
||||||
property.allocateNames(nameAllocator)
|
property.allocateNames(nameAllocator)
|
||||||
}
|
}
|
||||||
@@ -91,11 +92,11 @@ internal class AdapterGenerator(
|
|||||||
if (hasCompanionObject) {
|
if (hasCompanionObject) {
|
||||||
result.addFunction(generateJsonAdapterFun())
|
result.addFunction(generateJsonAdapterFun())
|
||||||
}
|
}
|
||||||
result.addType(generateType(generatedOption))
|
result.addType(generateType(messager, generatedOption))
|
||||||
return result.build()
|
return result.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateType(generatedOption: TypeElement?): TypeSpec {
|
private fun generateType(messager: Messager, generatedOption: TypeElement?): TypeSpec {
|
||||||
val result = TypeSpec.classBuilder(adapterName)
|
val result = TypeSpec.classBuilder(adapterName)
|
||||||
|
|
||||||
generatedOption?.let {
|
generatedOption?.let {
|
||||||
@@ -129,7 +130,7 @@ internal class AdapterGenerator(
|
|||||||
result.addProperty(optionsProperty)
|
result.addProperty(optionsProperty)
|
||||||
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
|
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
|
||||||
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
|
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
|
||||||
nameAllocator, typeRenderer, moshiParam))
|
nameAllocator, typeRenderer, moshiParam, messager))
|
||||||
}
|
}
|
||||||
|
|
||||||
result.addFunction(generateToStringFun())
|
result.addFunction(generateToStringFun())
|
||||||
|
@@ -15,6 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.squareup.moshi
|
package com.squareup.moshi
|
||||||
|
|
||||||
|
import com.google.auto.common.MoreTypes
|
||||||
|
import com.squareup.kotlinpoet.AnnotationSpec
|
||||||
|
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD
|
||||||
import com.squareup.kotlinpoet.ClassName
|
import com.squareup.kotlinpoet.ClassName
|
||||||
import com.squareup.kotlinpoet.CodeBlock
|
import com.squareup.kotlinpoet.CodeBlock
|
||||||
import com.squareup.kotlinpoet.KModifier
|
import com.squareup.kotlinpoet.KModifier
|
||||||
@@ -26,7 +29,11 @@ import com.squareup.kotlinpoet.TypeName
|
|||||||
import com.squareup.kotlinpoet.TypeVariableName
|
import com.squareup.kotlinpoet.TypeVariableName
|
||||||
import com.squareup.kotlinpoet.WildcardTypeName
|
import com.squareup.kotlinpoet.WildcardTypeName
|
||||||
import com.squareup.kotlinpoet.asTypeName
|
import com.squareup.kotlinpoet.asTypeName
|
||||||
|
import java.lang.annotation.ElementType
|
||||||
|
import java.lang.annotation.RetentionPolicy
|
||||||
|
import javax.annotation.processing.Messager
|
||||||
import javax.lang.model.element.AnnotationMirror
|
import javax.lang.model.element.AnnotationMirror
|
||||||
|
import javax.tools.Diagnostic.Kind.ERROR
|
||||||
|
|
||||||
/** A JsonAdapter that can be used to encode and decode a particular field. */
|
/** A JsonAdapter that can be used to encode and decode a particular field. */
|
||||||
internal data class DelegateKey(
|
internal data class DelegateKey(
|
||||||
@@ -39,7 +46,24 @@ internal data class DelegateKey(
|
|||||||
fun generateProperty(
|
fun generateProperty(
|
||||||
nameAllocator: NameAllocator,
|
nameAllocator: NameAllocator,
|
||||||
typeRenderer: TypeRenderer,
|
typeRenderer: TypeRenderer,
|
||||||
moshiParameter: ParameterSpec): PropertySpec {
|
moshiParameter: ParameterSpec,
|
||||||
|
messager: Messager): PropertySpec {
|
||||||
|
fun AnnotationMirror.validate(): AnnotationMirror {
|
||||||
|
// Check java types since that covers both java and kotlin annotations
|
||||||
|
val annotationElement = MoreTypes.asTypeElement(annotationType)
|
||||||
|
annotationElement.getAnnotation(java.lang.annotation.Retention::class.java)?.let {
|
||||||
|
if (it.value != RetentionPolicy.RUNTIME) {
|
||||||
|
messager.printMessage(ERROR, "JsonQualifier @${MoreTypes.asTypeElement(annotationType).simpleName} must have RUNTIME retention")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
annotationElement.getAnnotation(java.lang.annotation.Target::class.java)?.let {
|
||||||
|
if (ElementType.FIELD !in it.value) {
|
||||||
|
messager.printMessage(ERROR, "JsonQualifier @${MoreTypes.asTypeElement(annotationType).simpleName} must support FIELD target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
jsonQualifiers.forEach { it.validate() }
|
||||||
val qualifierNames = jsonQualifiers.joinToString("") {
|
val qualifierNames = jsonQualifiers.joinToString("") {
|
||||||
"At${it.annotationType.asElement().simpleName}"
|
"At${it.annotationType.asElement().simpleName}"
|
||||||
}
|
}
|
||||||
@@ -59,21 +83,10 @@ internal data class DelegateKey(
|
|||||||
val standardArgsSize = standardArgs.size + 1
|
val standardArgsSize = standardArgs.size + 1
|
||||||
val (initializerString, args) = when {
|
val (initializerString, args) = when {
|
||||||
qualifiers.isEmpty() -> "" to emptyArray()
|
qualifiers.isEmpty() -> "" to emptyArray()
|
||||||
qualifiers.size == 1 -> {
|
|
||||||
", %${standardArgsSize}T::class.java" to arrayOf(
|
|
||||||
qualifiers.first().annotationType.asTypeName())
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
val initString = qualifiers
|
", %${standardArgsSize}T.getFieldJsonQualifierAnnotations(javaClass, %${standardArgsSize + 1}S)" to arrayOf(
|
||||||
.mapIndexed { index, _ ->
|
Types::class.asTypeName(),
|
||||||
val annoClassIndex = standardArgsSize + index
|
adapterName)
|
||||||
return@mapIndexed "%${annoClassIndex}T::class.java"
|
|
||||||
}
|
|
||||||
.joinToString()
|
|
||||||
val initArgs = qualifiers
|
|
||||||
.map { it.annotationType.asTypeName() }
|
|
||||||
.toTypedArray()
|
|
||||||
", $initString" to initArgs
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val finalArgs = arrayOf(*standardArgs, *args)
|
val finalArgs = arrayOf(*standardArgs, *args)
|
||||||
@@ -81,6 +94,7 @@ internal data class DelegateKey(
|
|||||||
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
|
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
|
||||||
|
|
||||||
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
|
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
|
||||||
|
.addAnnotations(qualifiers.map { AnnotationSpec.get(it).toBuilder().useSiteTarget(FIELD).build() })
|
||||||
.initializer("%1N.adapter%2L(%3L$initializerString)$nullModifier", *finalArgs)
|
.initializer("%1N.adapter%2L(%3L$initializerString)$nullModifier", *finalArgs)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@@ -124,7 +124,7 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
|
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
|
||||||
val fileSpec = generateFile(generatedOption)
|
val fileSpec = generateFile(messager, generatedOption)
|
||||||
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
|
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
|
||||||
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
|
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
|
||||||
fileSpec.writeTo(outputDir)
|
fileSpec.writeTo(outputDir)
|
||||||
|
@@ -244,4 +244,60 @@ class CompilerTest {
|
|||||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||||
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
|
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nonFieldApplicableQualifier() {
|
||||||
|
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||||
|
call.inheritClasspath = true
|
||||||
|
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
|
||||||
|
call.addKt("source.kt", """
|
||||||
|
|import com.squareup.moshi.JsonClass
|
||||||
|
|import com.squareup.moshi.JsonQualifier
|
||||||
|
|import kotlin.annotation.AnnotationRetention.RUNTIME
|
||||||
|
|import kotlin.annotation.AnnotationTarget.PROPERTY
|
||||||
|
|import kotlin.annotation.Retention
|
||||||
|
|import kotlin.annotation.Target
|
||||||
|
|
|
||||||
|
|@Retention(RUNTIME)
|
||||||
|
|@Target(PROPERTY)
|
||||||
|
|@JsonQualifier
|
||||||
|
|annotation class UpperCase
|
||||||
|
|
|
||||||
|
|@JsonClass(generateAdapter = true)
|
||||||
|
|class ClassWithQualifier(@UpperCase val a: Int)
|
||||||
|
|""".trimMargin())
|
||||||
|
|
||||||
|
val result = call.execute()
|
||||||
|
println(result.systemErr)
|
||||||
|
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||||
|
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must support FIELD target")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nonRuntimeQualifier() {
|
||||||
|
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||||
|
call.inheritClasspath = true
|
||||||
|
call.addService(Processor::class, JsonClassCodeGenProcessor::class)
|
||||||
|
call.addKt("source.kt", """
|
||||||
|
|import com.squareup.moshi.JsonClass
|
||||||
|
|import com.squareup.moshi.JsonQualifier
|
||||||
|
|import kotlin.annotation.AnnotationRetention.BINARY
|
||||||
|
|import kotlin.annotation.AnnotationTarget.FIELD
|
||||||
|
|import kotlin.annotation.AnnotationTarget.PROPERTY
|
||||||
|
|import kotlin.annotation.Retention
|
||||||
|
|import kotlin.annotation.Target
|
||||||
|
|
|
||||||
|
|@Retention(BINARY)
|
||||||
|
|@Target(PROPERTY, FIELD)
|
||||||
|
|@JsonQualifier
|
||||||
|
|annotation class UpperCase
|
||||||
|
|
|
||||||
|
|@JsonClass(generateAdapter = true)
|
||||||
|
|class ClassWithQualifier(@UpperCase val a: Int)
|
||||||
|
|""".trimMargin())
|
||||||
|
|
||||||
|
val result = call.execute()
|
||||||
|
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||||
|
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must have RUNTIME retention")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -447,7 +447,7 @@ class GeneratedAdaptersTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String)
|
class ConstructorParameterWithQualifier(@Uppercase(inFrench = true) var a: String, var b: String)
|
||||||
|
|
||||||
@Test fun propertyWithQualifier() {
|
@Test fun propertyWithQualifier() {
|
||||||
val moshi = Moshi.Builder()
|
val moshi = Moshi.Builder()
|
||||||
@@ -467,7 +467,7 @@ class GeneratedAdaptersTest {
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
class PropertyWithQualifier {
|
class PropertyWithQualifier {
|
||||||
@Uppercase var a: String = ""
|
@Uppercase(inFrench = true) var a: String = ""
|
||||||
var b: String = ""
|
var b: String = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,15 +762,14 @@ class GeneratedAdaptersTest {
|
|||||||
var b: Int = -1
|
var b: Int = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@JsonQualifier
|
@JsonQualifier
|
||||||
annotation class Uppercase
|
annotation class Uppercase(val inFrench: Boolean, val onSundays: Boolean = false)
|
||||||
|
|
||||||
class UppercaseJsonAdapter {
|
class UppercaseJsonAdapter {
|
||||||
@ToJson fun toJson(@Uppercase s: String) : String {
|
@ToJson fun toJson(@Uppercase(inFrench = true) s: String) : String {
|
||||||
return s.toUpperCase(Locale.US)
|
return s.toUpperCase(Locale.US)
|
||||||
}
|
}
|
||||||
@FromJson @Uppercase fun fromJson(s: String) : String {
|
@FromJson @Uppercase(inFrench = true) fun fromJson(s: String) : String {
|
||||||
return s.toLowerCase(Locale.US)
|
return s.toLowerCase(Locale.US)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
|
|||||||
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
|
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Array;
|
import java.lang.reflect.Array;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.GenericArrayType;
|
import java.lang.reflect.GenericArrayType;
|
||||||
import java.lang.reflect.InvocationHandler;
|
import java.lang.reflect.InvocationHandler;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -216,6 +217,36 @@ public final class Types {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clazz the target class to read the {@code fieldName} field annotations from.
|
||||||
|
* @param fieldName the target field name on {@code clazz}.
|
||||||
|
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from
|
||||||
|
* the targeted field. Can be empty if none are found.
|
||||||
|
*/
|
||||||
|
public static Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz,
|
||||||
|
String fieldName) {
|
||||||
|
try {
|
||||||
|
Field field = clazz.getDeclaredField(fieldName);
|
||||||
|
if (!field.isAccessible()) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
}
|
||||||
|
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
|
||||||
|
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length);
|
||||||
|
for (Annotation annotation : fieldAnnotations) {
|
||||||
|
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
|
||||||
|
annotations.add(annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableSet(annotations);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
throw new IllegalArgumentException("Could not access field "
|
||||||
|
+ fieldName
|
||||||
|
+ " on class "
|
||||||
|
+ clazz.getCanonicalName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
|
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
|
||||||
if (!annotationType.isAnnotation()) {
|
if (!annotationType.isAnnotation()) {
|
||||||
|
@@ -17,6 +17,7 @@ package com.squareup.moshi;
|
|||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -29,6 +30,7 @@ import java.util.Set;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static com.squareup.moshi.internal.Util.canonicalize;
|
import static com.squareup.moshi.internal.Util.canonicalize;
|
||||||
|
import static java.lang.annotation.ElementType.FIELD;
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
@@ -261,4 +263,52 @@ public final class TypesTest {
|
|||||||
assertThat(expected).hasMessage("Unexpected primitive boolean. Use the boxed type.");
|
assertThat(expected).hasMessage("Unexpected primitive boolean. Use the boxed type.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void getFieldJsonQualifierAnnotations_privateFieldTest() {
|
||||||
|
Set<? extends Annotation> annotations = Types.getFieldJsonQualifierAnnotations(ClassWithAnnotatedFields.class,
|
||||||
|
"privateField");
|
||||||
|
|
||||||
|
assertThat(annotations).hasSize(1);
|
||||||
|
assertThat(annotations.iterator().next()).isInstanceOf(FieldAnnotation.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void getFieldJsonQualifierAnnotations_publicFieldTest() {
|
||||||
|
Set<? extends Annotation> annotations = Types.getFieldJsonQualifierAnnotations(ClassWithAnnotatedFields.class,
|
||||||
|
"publicField");
|
||||||
|
|
||||||
|
assertThat(annotations).hasSize(1);
|
||||||
|
assertThat(annotations.iterator().next()).isInstanceOf(FieldAnnotation.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void getFieldJsonQualifierAnnotations_unannotatedTest() {
|
||||||
|
Set<? extends Annotation> annotations = Types.getFieldJsonQualifierAnnotations(ClassWithAnnotatedFields.class,
|
||||||
|
"unannotatedField");
|
||||||
|
|
||||||
|
assertThat(annotations).hasSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonQualifier
|
||||||
|
@Target(FIELD)
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@interface FieldAnnotation {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Target(FIELD)
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@interface NoQualifierAnnotation {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ClassWithAnnotatedFields {
|
||||||
|
@FieldAnnotation
|
||||||
|
@NoQualifierAnnotation
|
||||||
|
private final int privateField = 0;
|
||||||
|
|
||||||
|
@FieldAnnotation
|
||||||
|
@NoQualifierAnnotation
|
||||||
|
public final int publicField = 0;
|
||||||
|
|
||||||
|
private final int unannotatedField = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user