Support non-data classes for generated JsonAdapters

This is towards making the reflection and codegen adapters work the same.
The process is relatively straightforward: try to promote all of the tests
in KotlinCodeGenTest to be passing tests in GeneratedAdaptersTest or
compile failures in CompilerTest
This commit is contained in:
Jesse Wilson 2018-04-02 00:37:17 -04:00
parent 7750d179be
commit b3d7dfd603
6 changed files with 263 additions and 167 deletions

View file

@ -35,20 +35,17 @@ import javax.lang.model.util.Elements
/** Generates a JSON adapter for a target type. */
internal class AdapterGenerator(
val fqClassName: String,
val packageName: String,
val className: ClassName,
val propertyList: List<PropertyGenerator>,
val originalElement: Element,
name: String = fqClassName.substringAfter(packageName)
.replace('.', '_')
.removePrefix("_"),
val isDataClass: Boolean,
val hasCompanionObject: Boolean,
val visibility: ProtoBuf.Visibility,
val elements: Elements,
val genericTypeNames: List<TypeVariableName>?
) {
val nameAllocator = NameAllocator()
val adapterName = "${name}JsonAdapter"
val adapterName = "${className.simpleNames().joinToString(separator = "_")}JsonAdapter"
val originalTypeName = originalElement.asType().asTypeName()
val moshiParam = ParameterSpec.builder(
@ -92,7 +89,7 @@ internal class AdapterGenerator(
property.allocateNames(nameAllocator)
}
val result = FileSpec.builder(packageName, adapterName)
val result = FileSpec.builder(className.packageName(), adapterName)
if (hasCompanionObject) {
result.addFunction(generateJsonAdapterFun())
}
@ -148,6 +145,8 @@ internal class AdapterGenerator(
}
private fun generateFromJsonFun(): FunSpec {
val resultName = nameAllocator.newName("result")
val result = FunSpec.builder("fromJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(readerParam)
@ -187,36 +186,72 @@ internal class AdapterGenerator(
result.endControlFlow() // while
result.addStatement("%N.endObject()", readerParam)
val propertiesWithoutDefaults = propertyList.filter { !it.hasDefault }
result.addCode("%[return %T(\n", originalTypeName)
propertiesWithoutDefaults.forEachIndexed { index, property ->
// Call the constructor providing only required parameters.
var hasOptionalParameters = false
result.addCode("%[var %N = %T(", resultName, originalTypeName)
var separator = "\n"
for (property in propertyList) {
if (!property.hasConstructorParameter) {
continue
}
if (property.hasDefault) {
hasOptionalParameters = true
continue
}
result.addCode(separator)
result.addCode("%N = %N", property.name, property.localName)
if (property.isRequired) {
result.addCode(" ?: throw %T(\"Required property '%L' missing at \${%N.path}\")",
JsonDataException::class, property.localName, readerParam)
}
result.addCode(if (index + 1 < propertiesWithoutDefaults.size) ",\n" else "\n")
separator = ",\n"
}
result.addCode("%])\n", originalTypeName)
result.addCode(")%]\n", originalTypeName)
val propertiesWithDefaults = propertyList.filter { it.hasDefault }
if (!propertiesWithDefaults.isEmpty()) {
result.addCode(".let {%>\n")
result.addCode("%[it.copy(\n")
propertiesWithDefaults.forEachIndexed { index, property ->
if (property.differentiateAbsentFromNull) {
result.addCode("%1N = if (%2N) %3N else it.%1N",
property.name, property.localIsPresentName, property.localName)
} else {
result.addCode("%1N = %2N ?: it.%1N",
property.name, property.localName)
// Call either the constructor again, or the copy() method, this time providing any optional
// parameters that we have.
if (hasOptionalParameters) {
if (isDataClass) {
result.addCode("%[%1N = %1N.copy(", resultName)
} else {
result.addCode("%[%1N = %2T(", resultName, originalTypeName)
}
separator = "\n"
for (property in propertyList) {
if (!property.hasConstructorParameter) {
continue // No constructor parameter for this property.
}
result.addCode(if (index + 1 < propertiesWithDefaults.size) ",\n" else "\n")
if (isDataClass && !property.hasDefault) {
continue // Property already assigned.
}
result.addCode(separator)
if (property.differentiateAbsentFromNull) {
result.addCode("%2N = if (%3N) %4N else %1N.%2N",
resultName, property.name, property.localIsPresentName, property.localName)
} else {
result.addCode("%2N = %3N ?: %1N.%2N", resultName, property.name, property.localName)
}
separator = ",\n"
}
result.addCode("%])\n")
result.addCode("%<}\n")
}
// Assign properties not present in the constructor.
for (property in propertyList) {
if (property.hasConstructorParameter) {
continue // Property already handled.
}
if (property.differentiateAbsentFromNull) {
result.addStatement("%1N.%2N = if (%3N) %4N else %1N.%2N",
resultName, property.name, property.localIsPresentName, property.localName)
} else {
result.addStatement("%1N.%2N = %3N ?: %1N.%2N",
resultName, property.name, property.localName)
}
}
result.addStatement("return %1N", resultName)
return result.build()
}
@ -244,8 +279,7 @@ internal class AdapterGenerator(
private fun generateJsonAdapterFun(): FunSpec {
val rawType = when (originalTypeName) {
is TypeVariableName -> throw IllegalArgumentException(
"Cannot get raw type of TypeVariable!")
is TypeVariableName -> throw IllegalArgumentException("Cannot get raw type of TypeVariable!")
is ParameterizedTypeName -> originalTypeName.rawType
else -> originalTypeName as ClassName
}

View file

@ -17,13 +17,16 @@ package com.squareup.moshi
import com.google.auto.common.AnnotationMirrors
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier.OUT
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asTypeName
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.extractFullName
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
@ -31,14 +34,17 @@ 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.ValueParameter
import java.io.File
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
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.TypeElement
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic.Kind.ERROR
/**
@ -77,24 +83,24 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
val metadata = element.kotlinMetadata
if (metadata !is KotlinClassMetadata) {
errorMustBeDataClass(element)
errorMustBeKotlinClass(element)
return null
}
val classData = metadata.data
val (nameResolver, classProto) = classData
fun ProtoBuf.Type.extractFullName() = extractFullName(classData)
if (!classProto.isDataClass) {
errorMustBeDataClass(element)
if (classProto.classKind != ProtoBuf.Class.Kind.CLASS) {
errorMustBeKotlinClass(element)
return null
}
val fqClassName = nameResolver.getString(classProto.fqName).replace('/', '.')
val packageName = nameResolver.getString(classProto.fqName).substringBeforeLast('/').replace(
'/', '.')
val typeName = element.asType().asTypeName()
val className = when (typeName) {
is ClassName -> typeName
is ParameterizedTypeName -> typeName.rawType
else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}")
}
val hasCompanionObject = classProto.hasCompanionObjectName()
// todo allow custom constructor
@ -113,34 +119,49 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
.first()
// TODO Temporary until jvm method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = protoConstructor
.valueParameterList
.mapIndexed { index, valueParameter ->
val paramName = nameResolver.getString(valueParameter.name)
val parameters: Map<String, ValueParameter> = protoConstructor.valueParameterList.associateBy {
nameResolver.getString(it.name)
}
val nullable = valueParameter.type.nullable
val paramFqcn = valueParameter.type.extractFullName()
.replace("`", "")
.removeSuffix("?")
val properties = classData.classProto.propertyList.associateBy {
nameResolver.getString(it.name)
}
val actualElement = constructor.parameters[index]
val propertyGenerators = mutableListOf<PropertyGenerator>()
for (enclosedElement in element.enclosedElements) {
if (enclosedElement !is VariableElement) continue
val serializedName = actualElement.getAnnotation(Json::class.java)?.name
?: paramName
val name = enclosedElement.simpleName.toString()
val property = properties[name] ?: continue
val parameter = parameters[name]
val jsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations(actualElement,
JsonQualifier::class.java)
val parameterElement = if (parameter != null) {
val parameterIndex = protoConstructor.valueParameterList.indexOf(parameter)
constructor.parameters[parameterIndex]
} else {
null
}
PropertyGenerator(
name = paramName,
serializedName = serializedName,
hasDefault = valueParameter.declaresDefaultValue,
nullable = nullable,
typeName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter),
unaliasedName = valueParameter.type.asTypeName(nameResolver,
classProto::getTypeParameter, true),
jsonQualifiers = jsonQualifiers)
}
if (property.visibility != ProtoBuf.Visibility.INTERNAL
&& property.visibility != ProtoBuf.Visibility.PROTECTED
&& property.visibility != ProtoBuf.Visibility.PUBLIC) {
messager.printMessage(ERROR, "property $name is not visible", enclosedElement)
return null
}
propertyGenerators += PropertyGenerator(
name,
serializedName(name, enclosedElement, parameterElement),
parameter != null,
parameter?.declaresDefaultValue ?: true,
property.returnType.nullable,
property.returnType.asTypeName(nameResolver, classProto::getTypeParameter),
property.returnType.asTypeName(nameResolver, classProto::getTypeParameter, true),
jsonQualifiers(enclosedElement, parameterElement))
}
// Sort properties so that those with constructor parameters come first.
propertyGenerators.sortBy { if (it.hasConstructorParameter) -1 else 1 }
val genericTypeNames = classProto.typeParameterList
.map {
@ -168,19 +189,56 @@ class JsonClassCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils
}
return AdapterGenerator(
fqClassName = fqClassName,
packageName = packageName,
propertyList = parameters,
className,
propertyList = propertyGenerators,
originalElement = element,
hasCompanionObject = hasCompanionObject,
visibility = classProto.visibility!!,
genericTypeNames = genericTypeNames,
elements = elementUtils)
elements = elementUtils,
isDataClass = classProto.isDataClass)
}
private fun errorMustBeDataClass(element: Element) {
/** Returns the JsonQualifiers on the field and parameter of a property. */
private fun jsonQualifiers(
field: VariableElement,
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()
}
// TODO(jwilson): union the qualifiers somehow?
if (fieldJsonQualifiers.isNotEmpty()) {
return fieldJsonQualifiers
} else {
return parameterJsonQualifiers
}
}
/** Returns the @Json name of a property, or `propertyName` if none is provided. */
private fun serializedName(
propertyName: String,
field: VariableElement,
parameter: VariableElement?
): String {
val fieldAnnotation = field.getAnnotation(Json::class.java)
if (fieldAnnotation != null) return fieldAnnotation.name
val parameterAnnotation = parameter?.getAnnotation(Json::class.java)
if (parameterAnnotation != null) return parameterAnnotation.name
return propertyName
}
private fun errorMustBeKotlinClass(element: Element) {
messager.printMessage(ERROR,
"@${JsonClass::class.java.simpleName} can't be applied to $element: must be a Kotlin data class",
"@${JsonClass::class.java.simpleName} can't be applied to $element: must be a Kotlin class",
element)
}

View file

@ -32,6 +32,7 @@ import javax.lang.model.element.AnnotationMirror
internal class PropertyGenerator(
val name: String,
val serializedName: String,
val hasConstructorParameter: Boolean,
val hasDefault: Boolean,
val nullable: Boolean,
val typeName: TypeName,

View file

@ -35,13 +35,11 @@ class CompilerTest {
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class ConstructorParameters(var a: Int, var b: Int)
|class PrivateConstructorParameter(private var a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"@JsonClass can't be applied to ConstructorParameters: must be a Kotlin data class")
assertThat(result.systemErr).contains("property a is not visible")
}
}

View file

@ -20,7 +20,7 @@ import org.assertj.core.api.Assertions.fail
import org.intellij.lang.annotations.Language
import org.junit.Test
class DataClassesTest {
class GeneratedAdaptersTest {
private val moshi = Moshi.Builder().build()
@ -252,6 +252,107 @@ class DataClassesTest {
@JsonClass(generateAdapter = false)
data class DoNotGenerateAdapter(val foo: String)
@Test fun constructorParameters() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorParameters::class.java)
val encoded = ConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class ConstructorParameters(var a: Int, var b: Int)
@Test fun properties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(Properties::class.java)
val encoded = Properties()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!!
assertThat(decoded.a).isEqualTo(3)
assertThat(decoded.b).isEqualTo(5)
}
@JsonClass(generateAdapter = true)
class Properties {
var a: Int = -1
var b: Int = -1
}
@Test fun constructorParametersAndProperties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorParametersAndProperties::class.java)
val encoded = ConstructorParametersAndProperties(3)
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class ConstructorParametersAndProperties(var a: Int) {
var b: Int = -1
}
@Test fun immutableConstructorParameters() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ImmutableConstructorParameters::class.java)
val encoded = ImmutableConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class ImmutableConstructorParameters(val a: Int, val b: Int)
@Test fun immutableProperties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ImmutableProperties::class.java)
val encoded = ImmutableProperties(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!!
assertThat(decoded.a).isEqualTo(3)
assertThat(decoded.b).isEqualTo(5)
}
@JsonClass(generateAdapter = true)
class ImmutableProperties(a: Int, b: Int) {
val a = a
val b = b
}
@Test fun constructorDefaults() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorDefaultValues::class.java)
val encoded = ConstructorDefaultValues(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
@JsonClass(generateAdapter = true)
class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2)
}
// Has to be outside to avoid Types seeing an owning class

View file

@ -25,102 +25,6 @@ import java.util.SimpleTimeZone
import kotlin.annotation.AnnotationRetention.RUNTIME
class KotlinCodeGenTest {
@Ignore @Test fun constructorParameters() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorParameters::class.java)
val encoded = ConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorParameters(var a: Int, var b: Int)
@Ignore @Test fun properties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(Properties::class.java)
val encoded = Properties()
encoded.a = 3
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!!
assertThat(decoded.a).isEqualTo(3)
assertThat(decoded.b).isEqualTo(5)
}
class Properties {
var a: Int = -1
var b: Int = -1
}
@Ignore @Test fun constructorParametersAndProperties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorParametersAndProperties::class.java)
val encoded = ConstructorParametersAndProperties(3)
encoded.b = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorParametersAndProperties(var a: Int) {
var b: Int = -1
}
@Ignore @Test fun immutableConstructorParameters() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ImmutableConstructorParameters::class.java)
val encoded = ImmutableConstructorParameters(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""")!!
assertThat(decoded.a).isEqualTo(4)
assertThat(decoded.b).isEqualTo(6)
}
class ImmutableConstructorParameters(val a: Int, val b: Int)
@Ignore @Test fun immutableProperties() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ImmutableProperties::class.java)
val encoded = ImmutableProperties(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""")!!
assertThat(decoded.a).isEqualTo(3)
assertThat(decoded.b).isEqualTo(5)
}
class ImmutableProperties(a: Int, b: Int) {
val a = a
val b = b
}
@Ignore @Test fun constructorDefaults() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(ConstructorDefaultValues::class.java)
val encoded = ConstructorDefaultValues(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""")
val decoded = jsonAdapter.fromJson("""{"b":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.b).isEqualTo(6)
}
class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2)
@Ignore @Test fun requiredValueAbsent() {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java)