diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt index 91a5c94..a894cae 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/JsonClassCodeGenProcessor.kt @@ -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() + for (enclosedElement in element.enclosedElements) { + if (enclosedElement !is ExecutableElement) continue + val property = classData.getPropertyOrNull(enclosedElement) ?: continue + annotatedElements[property] = enclosedElement + } + val propertyGenerators = mutableListOf() 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 { - val fieldJsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations( - field, JsonQualifier::class.java) - - val parameterJsonQualifiers: Set = 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 + 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 + } +} diff --git a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt index 68b61d7..f630110 100644 --- a/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt +++ b/kotlin-codegen/compiler/src/main/java/com/squareup/moshi/PropertyGenerator.kt @@ -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()) diff --git a/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/GeneratedAdaptersTest.kt b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/GeneratedAdaptersTest.kt index e077d80..49e8be0 100644 --- a/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/GeneratedAdaptersTest.kt +++ b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/GeneratedAdaptersTest.kt @@ -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 diff --git a/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt index 151f124..a8c1df3 100644 --- a/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt +++ b/kotlin-codegen/integration-test/src/test/kotlin/com/squareup/moshi/KotlinCodeGenTest.kt @@ -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)