Full JsonQualifier support in kotlin codegen.

This commit is contained in:
Zac Sweers
2018-04-29 15:54:45 -07:00
committed by Jesse Wilson
parent 10a5dc827b
commit 4b610329bd
7 changed files with 177 additions and 26 deletions

View File

@@ -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())

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()) {

View File

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