Handle qualifiers, names, and transient in generated adapters

This commit is contained in:
Jesse Wilson 2018-04-03 03:21:27 -04:00
parent e0d84e1fee
commit d555d24d94
4 changed files with 249 additions and 191 deletions

View file

@ -27,6 +27,7 @@ import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.classKind
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
@ -34,6 +35,7 @@ import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.visibility
import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor
import org.jetbrains.kotlin.serialization.ProtoBuf
import org.jetbrains.kotlin.serialization.ProtoBuf.Property
import org.jetbrains.kotlin.serialization.ProtoBuf.ValueParameter
import java.io.File
import javax.annotation.processing.Processor
@ -43,6 +45,7 @@ import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic.Kind.ERROR
@ -127,6 +130,14 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
nameResolver.getString(it.name)
}
// The compiler might emit methods just so it has a place to put annotations. Find these.
val annotatedElements = mutableMapOf<Property, ExecutableElement>()
for (enclosedElement in element.enclosedElements) {
if (enclosedElement !is ExecutableElement) continue
val property = classData.getPropertyOrNull(enclosedElement) ?: continue
annotatedElements[property] = enclosedElement
}
val propertyGenerators = mutableListOf<PropertyGenerator>()
for (enclosedElement in element.enclosedElements) {
if (enclosedElement !is VariableElement) continue
@ -142,6 +153,8 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
null
}
val annotatedElement = annotatedElements[property]
if (property.visibility != ProtoBuf.Visibility.INTERNAL
&& property.visibility != ProtoBuf.Visibility.PROTECTED
&& property.visibility != ProtoBuf.Visibility.PUBLIC) {
@ -149,15 +162,24 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
return null
}
val hasDefault = parameter?.declaresDefaultValue ?: true
if (enclosedElement.modifiers.contains(Modifier.TRANSIENT)) {
if (!hasDefault) {
throw IllegalArgumentException("No default value for transient property $name")
}
continue
}
propertyGenerators += PropertyGenerator(
name,
serializedName(name, enclosedElement, parameterElement),
jsonName(name, enclosedElement, annotatedElement, parameterElement),
parameter != null,
parameter?.declaresDefaultValue ?: true,
hasDefault,
property.returnType.nullable,
property.returnType.asTypeName(nameResolver, classProto::getTypeParameter),
property.returnType.asTypeName(nameResolver, classProto::getTypeParameter, true),
jsonQualifiers(enclosedElement, parameterElement))
jsonQualifiers(enclosedElement, annotatedElement, parameterElement))
}
// Sort properties so that those with constructor parameters come first.
@ -202,38 +224,39 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
/** Returns the JsonQualifiers on the field and parameter of a property. */
private fun jsonQualifiers(
field: VariableElement,
method: ExecutableElement?,
parameter: VariableElement?
): Set<AnnotationMirror> {
val fieldJsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations(
field, JsonQualifier::class.java)
val parameterJsonQualifiers: Set<AnnotationMirror> = if (parameter != null) {
AnnotationMirrors.getAnnotatedAnnotations(parameter, JsonQualifier::class.java)
} else {
setOf()
}
val fieldQualifiers = field.qualifiers
val methodQualifiers = method.qualifiers
val parameterQualifiers = parameter.qualifiers
// TODO(jwilson): union the qualifiers somehow?
if (fieldJsonQualifiers.isNotEmpty()) {
return fieldJsonQualifiers
} else {
return parameterJsonQualifiers
return when {
fieldQualifiers.isNotEmpty() -> fieldQualifiers
methodQualifiers.isNotEmpty() -> methodQualifiers
parameterQualifiers.isNotEmpty() -> parameterQualifiers
else -> setOf()
}
}
/** Returns the @Json name of a property, or `propertyName` if none is provided. */
private fun serializedName(
private fun jsonName(
propertyName: String,
field: VariableElement,
method: ExecutableElement?,
parameter: VariableElement?
): String {
val fieldAnnotation = field.getAnnotation(Json::class.java)
if (fieldAnnotation != null) return fieldAnnotation.name
val fieldJsonName = field.jsonName
val methodJsonName = method.jsonName
val parameterJsonName = parameter.jsonName
val parameterAnnotation = parameter?.getAnnotation(Json::class.java)
if (parameterAnnotation != null) return parameterAnnotation.name
return propertyName
return when {
fieldJsonName != null -> fieldJsonName
methodJsonName != null -> methodJsonName
parameterJsonName != null -> parameterJsonName
else -> propertyName
}
}
private fun errorMustBeKotlinClass(element: Element) {
@ -255,5 +278,16 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
val file = filer.createSourceFile(adapterName).toUri().let(::File)
return file.parentFile.also { file.delete() }
}
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}
}

View file

@ -52,7 +52,7 @@ internal class PropertyGenerator(
fun reserveDelegateNames(nameAllocator: NameAllocator) {
val qualifierNames = jsonQualifiers.joinToString("") {
"at${it.annotationType.asElement().simpleName.toString().capitalize()}"
"At${it.annotationType.asElement().simpleName.toString().capitalize()}"
}
nameAllocator.newName("${unaliasedName.toVariableName()}${qualifierNames}Adapter",
delegateKey())

View file

@ -16,9 +16,10 @@
package com.squareup.moshi
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.intellij.lang.annotations.Language
import org.junit.Assert.fail
import org.junit.Test
import java.util.Locale
class GeneratedAdaptersTest {
@ -353,6 +354,190 @@ class GeneratedAdaptersTest {
@JsonClass(generateAdapter = true)
class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2)
@Test fun requiredValueAbsent() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java)
try {
jsonAdapter.fromJson("""{"a":4}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Required property 'b' missing at \$")
}
}
@JsonClass(generateAdapter = true)
class RequiredValueAbsent(var a: Int = 3, var b: Int)
@Test fun nonNullConstructorParameterCalledWithNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(HasNonNullConstructorParameter::class.java)
try {
jsonAdapter.fromJson("{\"a\":null}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Required property 'a' missing at \$")
}
}
@JsonClass(generateAdapter = true)
class HasNonNullConstructorParameter(val a: String)
@Test fun explicitNull() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ExplicitNull::class.java)
val encoded = ExplicitNull(null, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":null,"b":6}""")!!
assertThat(decoded.a).isEqualTo(null)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class ExplicitNull(var a: Int?, var b: Int?)
@Test fun absentNull() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(AbsentNull::class.java)
val encoded = AbsentNull(null, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"b":6}""")!!
assertThat(decoded.a).isNull()
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class AbsentNull(var a: Int?, var b: Int?)
@Test fun constructorParameterWithQualifier() {
val moshi = Moshi.Builder()
.add(UppercaseJsonAdapter())
.build()
val jsonAdapter = moshi.adapter(ConstructorParameterWithQualifier::class.java)
val encoded = ConstructorParameterWithQualifier("Android", "Banana")
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""")
val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!!
assertThat(decoded.a).isEqualTo("android")
assertThat(decoded.b).isEqualTo("Banana")
}
@JsonClass(generateAdapter = true)
class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String)
@Test fun propertyWithQualifier() {
val moshi = Moshi.Builder()
.add(UppercaseJsonAdapter())
.build()
val jsonAdapter = moshi.adapter(PropertyWithQualifier::class.java)
val encoded = PropertyWithQualifier()
encoded.a = "Android"
encoded.b = "Banana"
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""")
val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!!
assertThat(decoded.a).isEqualTo("android")
assertThat(decoded.b).isEqualTo("Banana")
}
@JsonClass(generateAdapter = true)
class PropertyWithQualifier {
@Uppercase var a: String = ""
var b: String = ""
}
@Test fun constructorParameterWithJsonName() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorParameterWithJsonName::class.java)
val encoded = ConstructorParameterWithJsonName(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int)
@Test fun propertyWithJsonName() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(PropertyWithJsonName::class.java)
val encoded = PropertyWithJsonName()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class PropertyWithJsonName {
@Json(name = "key a") var a: Int = -1
var b: Int = -1
}
@Test fun transientConstructorParameter() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(TransientConstructorParameter::class.java)
val encoded = TransientConstructorParameter(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1)
@Test fun transientProperty() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(TransientProperty::class.java)
val encoded = TransientProperty()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class TransientProperty {
@Transient var a: Int = -1
var b: Int = -1
}
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class Uppercase
class UppercaseJsonAdapter {
@ToJson fun toJson(@Uppercase s: String) : String {
return s.toUpperCase(Locale.US)
}
@FromJson @Uppercase fun fromJson(s: String) : String {
return s.toLowerCase(Locale.US)
}
}
}
// Has to be outside to avoid Types seeing an owning class

View file

@ -25,33 +25,19 @@ import java.util.SimpleTimeZone
import kotlin.annotation.AnnotationRetention.RUNTIME
class KotlinCodeGenTest {
@Ignore @Test fun requiredValueAbsent() {
@Ignore @Test fun duplicatedValue() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java)
val jsonAdapter = moshi.adapter(DuplicateValue::class.java)
try {
jsonAdapter.fromJson("""{"a":4}""")
jsonAdapter.fromJson("""{"a":4,"a":4}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Required value b missing at $")
assertThat(expected).hasMessage("Multiple values for a at $.a")
}
}
class RequiredValueAbsent(var a: Int = 3, var b: Int)
@Ignore @Test fun nonNullConstructorParameterCalledWithNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(HasNonNullConstructorParameter::class.java)
try {
jsonAdapter.fromJson("{\"a\":null}")
fail()
} catch (expected: JsonDataException) {
assertThat(expected).hasMessage("Non-null value a was null at \$")
}
}
class HasNonNullConstructorParameter(val a: String)
class DuplicateValue(var a: Int = -1, var b: Int = -2)
@Ignore @Test fun nonNullPropertySetToNullFailsWithJsonDataException() {
val moshi = Moshi.Builder().build()
@ -69,50 +55,6 @@ class KotlinCodeGenTest {
var a: String = ""
}
@Ignore @Test fun duplicatedValue() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(DuplicateValue::class.java)
try {
jsonAdapter.fromJson("""{"a":4,"a":4}""")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Multiple values for a at $.a")
}
}
class DuplicateValue(var a: Int = -1, var b: Int = -2)
@Ignore @Test fun explicitNull() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ExplicitNull::class.java)
val encoded = ExplicitNull(null, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":null,"b":6}""")!!
assertThat(decoded.a).isEqualTo(null)
assertThat(decoded.b).isEqualTo(6)
}
class ExplicitNull(var a: Int?, var b: Int?)
@Ignore @Test fun absentNull() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(AbsentNull::class.java)
val encoded = AbsentNull(null, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"b":6}""")!!
assertThat(decoded.a).isNull()
assertThat(decoded.b).isEqualTo(6)
}
class AbsentNull(var a: Int?, var b: Int?)
@Ignore @Test fun repeatedValue() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(RepeatedValue::class.java)
@ -127,90 +69,6 @@ class KotlinCodeGenTest {
class RepeatedValue(var a: Int, var b: Int?)
@Ignore @Test fun constructorParameterWithQualifier() {
val moshi = Moshi.Builder()
.add(UppercaseJsonAdapter())
.build()
val jsonAdapter = moshi.adapter(ConstructorParameterWithQualifier::class.java)
val encoded = ConstructorParameterWithQualifier("Android", "Banana")
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""")
val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!!
assertThat(decoded.a).isEqualTo("android")
assertThat(decoded.b).isEqualTo("Banana")
}
class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String)
@Ignore @Test fun propertyWithQualifier() {
val moshi = Moshi.Builder()
.add(UppercaseJsonAdapter())
.build()
val jsonAdapter = moshi.adapter(PropertyWithQualifier::class.java)
val encoded = PropertyWithQualifier()
encoded.a = "Android"
encoded.b = "Banana"
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""")
val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""")!!
assertThat(decoded.a).isEqualTo("android")
assertThat(decoded.b).isEqualTo("Banana")
}
class PropertyWithQualifier {
@Uppercase var a: String = ""
var b: String = ""
}
@Ignore @Test fun constructorParameterWithJsonName() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorParameterWithJsonName::class.java)
val encoded = ConstructorParameterWithJsonName(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int)
@Ignore @Test fun propertyWithJsonName() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(PropertyWithJsonName::class.java)
val encoded = PropertyWithJsonName()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class PropertyWithJsonName {
@Json(name = "key a") var a: Int = -1
var b: Int = -1
}
@Ignore @Test fun transientConstructorParameter() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(TransientConstructorParameter::class.java)
val encoded = TransientConstructorParameter(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1)
@Ignore @Test fun requiredTransientConstructorParameterFails() {
val moshi = Moshi.Builder().build()
try {
@ -225,25 +83,6 @@ class KotlinCodeGenTest {
class RequiredTransientConstructorParameter(@Transient var a: Int)
@Ignore @Test fun transientProperty() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(TransientProperty::class.java)
val encoded = TransientProperty()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class TransientProperty {
@Transient var a: Int = -1
var b: Int = -1
}
@Ignore @Test fun supertypeConstructorParameters() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(SubtypeConstructorParameters::class.java)