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.visibility
import java.lang.reflect.Type
import javax.annotation.processing.Messager
import javax.lang.model.element.TypeElement
/** Generates a JSON adapter for a target type. */
@@ -81,7 +82,7 @@ internal class AdapterGenerator(
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
.build()
fun generateFile(generatedOption: TypeElement?): FileSpec {
fun generateFile(messager: Messager, generatedOption: TypeElement?): FileSpec {
for (property in propertyList) {
property.allocateNames(nameAllocator)
}
@@ -91,11 +92,11 @@ internal class AdapterGenerator(
if (hasCompanionObject) {
result.addFunction(generateJsonAdapterFun())
}
result.addType(generateType(generatedOption))
result.addType(generateType(messager, generatedOption))
return result.build()
}
private fun generateType(generatedOption: TypeElement?): TypeSpec {
private fun generateType(messager: Messager, generatedOption: TypeElement?): TypeSpec {
val result = TypeSpec.classBuilder(adapterName)
generatedOption?.let {
@@ -129,7 +130,7 @@ internal class AdapterGenerator(
result.addProperty(optionsProperty)
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
nameAllocator, typeRenderer, moshiParam))
nameAllocator, typeRenderer, moshiParam, messager))
}
result.addFunction(generateToStringFun())

View File

@@ -15,6 +15,9 @@
*/
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.CodeBlock
import com.squareup.kotlinpoet.KModifier
@@ -26,7 +29,11 @@ import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
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.tools.Diagnostic.Kind.ERROR
/** A JsonAdapter that can be used to encode and decode a particular field. */
internal data class DelegateKey(
@@ -39,7 +46,24 @@ internal data class DelegateKey(
fun generateProperty(
nameAllocator: NameAllocator,
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("") {
"At${it.annotationType.asElement().simpleName}"
}
@@ -59,21 +83,10 @@ internal data class DelegateKey(
val standardArgsSize = standardArgs.size + 1
val (initializerString, args) = when {
qualifiers.isEmpty() -> "" to emptyArray()
qualifiers.size == 1 -> {
", %${standardArgsSize}T::class.java" to arrayOf(
qualifiers.first().annotationType.asTypeName())
}
else -> {
val initString = qualifiers
.mapIndexed { index, _ ->
val annoClassIndex = standardArgsSize + index
return@mapIndexed "%${annoClassIndex}T::class.java"
}
.joinToString()
val initArgs = qualifiers
.map { it.annotationType.asTypeName() }
.toTypedArray()
", $initString" to initArgs
", %${standardArgsSize}T.getFieldJsonQualifierAnnotations(javaClass, %${standardArgsSize + 1}S)" to arrayOf(
Types::class.asTypeName(),
adapterName)
}
}
val finalArgs = arrayOf(*standardArgs, *args)
@@ -81,6 +94,7 @@ internal data class DelegateKey(
val nullModifier = if (nullable) ".nullSafe()" else ".nonNull()"
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)
.build()
}

View File

@@ -124,7 +124,7 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
}
private fun AdapterGenerator.generateAndWrite(generatedOption: TypeElement?) {
val fileSpec = generateFile(generatedOption)
val fileSpec = generateFile(messager, generatedOption)
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
fileSpec.writeTo(outputDir)

View File

@@ -244,4 +244,60 @@ class CompilerTest {
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
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)
class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String)
class ConstructorParameterWithQualifier(@Uppercase(inFrench = true) var a: String, var b: String)
@Test fun propertyWithQualifier() {
val moshi = Moshi.Builder()
@@ -467,7 +467,7 @@ class GeneratedAdaptersTest {
@JsonClass(generateAdapter = true)
class PropertyWithQualifier {
@Uppercase var a: String = ""
@Uppercase(inFrench = true) var a: String = ""
var b: String = ""
}
@@ -762,15 +762,14 @@ class GeneratedAdaptersTest {
var b: Int = -1
}
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class Uppercase
annotation class Uppercase(val inFrench: Boolean, val onSundays: Boolean = false)
class UppercaseJsonAdapter {
@ToJson fun toJson(@Uppercase s: String) : String {
@ToJson fun toJson(@Uppercase(inFrench = true) s: String) : String {
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)
}
}

View File

@@ -20,6 +20,7 @@ import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
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")
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
if (!annotationType.isAnnotation()) {

View File

@@ -17,6 +17,7 @@ package com.squareup.moshi;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
@@ -29,6 +30,7 @@ import java.util.Set;
import org.junit.Test;
import static com.squareup.moshi.internal.Util.canonicalize;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@@ -261,4 +263,52 @@ public final class TypesTest {
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;
}
}