Split the MoshiKotlinCodeGenProcessor into multiple types. (#462)

Move more behavior into the types: Adapter and Property now have many
instance methods and are now mutable value classes rather than immutable
data classes.

This changes how some of the bookkeeping code works. Previously there were
some pretty tricky types: maps with pairs as keys, indexed and non-indexed
lists of properties. With this change more of the logic operates directly
on the properties.
This commit is contained in:
Jesse Wilson 2018-03-24 20:53:08 -04:00 committed by GitHub
parent 96e074d030
commit d045947ea7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 708 additions and 580 deletions

View file

@ -0,0 +1,276 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import org.jetbrains.kotlin.serialization.ProtoBuf
import java.lang.reflect.Type
import javax.lang.model.element.Element
import javax.lang.model.util.Elements
/** Generates a JSON adapter for a target type. */
internal class AdapterGenerator(
val fqClassName: String,
val packageName: String,
val propertyList: List<PropertyGenerator>,
val originalElement: Element,
name: String = fqClassName.substringAfter(packageName)
.replace('.', '_')
.removePrefix("_"),
val hasCompanionObject: Boolean,
val visibility: ProtoBuf.Visibility,
val elements: Elements,
val genericTypeNames: List<TypeVariableName>?
) {
val nameAllocator = NameAllocator()
val adapterName = "${name}JsonAdapter"
val originalTypeName = originalElement.asType().asTypeName()
val moshiParam = ParameterSpec.builder(
nameAllocator.newName("moshi"),
Moshi::class).build()
val typesParam = ParameterSpec.builder(
nameAllocator.newName("types"),
ParameterizedTypeName.get(ARRAY,
Type::class.asTypeName()))
.build()
val readerParam = ParameterSpec.builder(
nameAllocator.newName("reader"),
JsonReader::class)
.build()
val writerParam = ParameterSpec.builder(
nameAllocator.newName("writer"),
JsonWriter::class)
.build()
val valueParam = ParameterSpec.builder(
nameAllocator.newName("value"),
originalTypeName.asNullable())
.build()
val jsonAdapterTypeName = ParameterizedTypeName.get(
JsonAdapter::class.asClassName(), originalTypeName)
// selectName() API setup
val optionsProperty = PropertySpec.builder(
nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(),
KModifier.PRIVATE)
.initializer("%T.of(${propertyList.map { it.serializedName }
.joinToString(", ") { "\"$it\"" }})", JsonReader.Options::class.asTypeName())
.build()
val delegateAdapters = propertyList.distinctBy { it.delegateKey() }
fun generateFile(): FileSpec {
for (property in delegateAdapters) {
property.reserveDelegateNames(nameAllocator)
}
for (property in propertyList) {
property.allocateNames(nameAllocator)
}
val result = FileSpec.builder(packageName, adapterName)
if (hasCompanionObject) {
result.addFunction(generateJsonAdapterFun())
}
result.addType(generateType())
return result.build()
}
private fun generateType(): TypeSpec {
val result = TypeSpec.classBuilder(adapterName)
result.superclass(jsonAdapterTypeName)
genericTypeNames?.let {
result.addTypeVariables(genericTypeNames)
}
// TODO make this configurable. Right now it just matches the source model
if (visibility == ProtoBuf.Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL)
}
result.primaryConstructor(generateConstructor())
result.addProperty(optionsProperty)
for (uniqueAdapter in delegateAdapters) {
result.addProperty(uniqueAdapter.generateDelegateProperty(this))
}
result.addFunction(generateToStringFun())
result.addFunction(generateFromJsonFun())
result.addFunction(generateToJsonFun())
return result.build()
}
private fun generateConstructor(): FunSpec {
val result = FunSpec.constructorBuilder()
result.addParameter(moshiParam)
genericTypeNames?.let {
result.addParameter(typesParam)
}
return result.build()
}
private fun generateToStringFun(): FunSpec {
return FunSpec.builder("toString")
.addModifiers(KModifier.OVERRIDE)
.returns(String::class)
.addStatement("return %S",
"GeneratedJsonAdapter(${originalTypeName.rawType().simpleNames().joinToString(".")})")
.build()
}
private fun generateFromJsonFun(): FunSpec {
val result = FunSpec.builder("fromJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(readerParam)
.returns(originalTypeName)
for (property in propertyList) {
result.addCode("%L", property.generateLocalProperty())
if (property.differentiateAbsentFromNull) {
result.addCode("%L", property.generateLocalIsPresentProperty())
}
}
result.addStatement("%N.beginObject()", readerParam)
result.beginControlFlow("while (%N.hasNext())", readerParam)
result.beginControlFlow("when (%N.selectName(%N))", readerParam, optionsProperty)
propertyList.forEachIndexed { index, property ->
if (property.differentiateAbsentFromNull) {
result.beginControlFlow("%L -> ", index)
result.addStatement("%N = %N.fromJson(%N)",
property.localName, property.delegateName, readerParam);
result.addStatement("%N = true", property.localIsPresentName)
result.endControlFlow()
} else {
result.addStatement("%L -> %N = %N.fromJson(%N)",
index, property.localName, property.delegateName, readerParam)
}
}
result.beginControlFlow("-1 ->")
result.addComment("Unknown name, skip it.")
result.addStatement("%N.nextName()", readerParam)
result.addStatement("%N.skipValue()", readerParam)
result.endControlFlow()
result.endControlFlow() // when
result.endControlFlow() // while
result.addStatement("%N.endObject()", readerParam)
val propertiesWithoutDefaults = propertyList.filter { !it.hasDefault }
result.addCode("%[return %T(\n", originalTypeName)
propertiesWithoutDefaults.forEachIndexed { index, property ->
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")
}
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)
}
result.addCode(if (index + 1 < propertiesWithDefaults.size) ",\n" else "\n")
}
result.addCode("%])\n")
result.addCode("%<}\n")
}
return result.build()
}
private fun generateToJsonFun(): FunSpec {
val result = FunSpec.builder("toJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(writerParam)
.addParameter(valueParam)
result.beginControlFlow("if (%N == null)", valueParam)
result.addStatement("throw %T(%S)", NullPointerException::class,
"${valueParam.name} was null! Wrap in .nullSafe() to write nullable values.")
result.endControlFlow()
result.addStatement("%N.beginObject()", writerParam)
propertyList.forEach { property ->
result.addStatement("%N.name(%S)", writerParam, property.serializedName)
result.addStatement("%N.toJson(%N, %N.%L)",
property.delegateName, writerParam, valueParam, property.name)
}
result.addStatement("%N.endObject()", writerParam)
return result.build()
}
private fun generateJsonAdapterFun(): FunSpec {
val rawType = when (originalTypeName) {
is TypeVariableName -> throw IllegalArgumentException(
"Cannot get raw type of TypeVariable!")
is ParameterizedTypeName -> originalTypeName.rawType
else -> originalTypeName as ClassName
}
val result = FunSpec.builder("jsonAdapter")
.receiver(rawType.nestedClass("Companion"))
.returns(jsonAdapterTypeName)
.addParameter(moshiParam)
// TODO make this configurable. Right now it just matches the source model
if (visibility == ProtoBuf.Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL)
}
genericTypeNames?.let {
result.addParameter(typesParam)
result.addTypeVariables(it)
}
if (genericTypeNames != null) {
result.addStatement("return %N(%N, %N)", adapterName, moshiParam, typesParam)
} else {
result.addStatement("return %N(%N)", adapterName, moshiParam)
}
return result.build()
}
}

View file

@ -17,35 +17,9 @@ package com.squareup.moshi
import com.google.auto.common.AnnotationMirrors
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.BYTE
import com.squareup.kotlinpoet.CHAR
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.KModifier.IN
import com.squareup.kotlinpoet.KModifier.OUT
import com.squareup.kotlinpoet.KModifier.OVERRIDE
import com.squareup.kotlinpoet.KModifier.PRIVATE
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.SHORT
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
@ -57,22 +31,14 @@ 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.Type.Argument.Projection
import org.jetbrains.kotlin.serialization.ProtoBuf.TypeParameter.Variance
import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility
import org.jetbrains.kotlin.serialization.ProtoBuf.Visibility.INTERNAL
import org.jetbrains.kotlin.serialization.deserialization.NameResolver
import java.io.File
import java.lang.reflect.Type
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.util.Elements
import javax.tools.Diagnostic.Kind.ERROR
/**
@ -105,7 +71,7 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
return true
}
private fun processElement(element: Element): Adapter? {
private fun processElement(element: Element): AdapterGenerator? {
val metadata = element.kotlinMetadata
if (metadata !is KotlinClassMetadata) {
@ -139,10 +105,12 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
.replace('/', '.')
.let(elementUtils::getTypeElement)
.enclosedElements
.mapNotNull { it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement } }
.mapNotNull {
it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement }
}
.first()
// TODO Temporary until jvm method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = protoConstructor
.valueParameterList
.mapIndexed { index, valueParameter ->
@ -161,14 +129,14 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
val jsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations(actualElement,
JsonQualifier::class.java)
Property(
PropertyGenerator(
name = paramName,
fqClassName = paramFqcn,
serializedName = serializedName,
hasDefault = valueParameter.declaresDefaultValue,
nullable = nullable,
typeName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter),
unaliasedName = valueParameter.type.asTypeName(nameResolver, classProto::getTypeParameter, true),
unaliasedName = valueParameter.type.asTypeName(nameResolver,
classProto::getTypeParameter, true),
jsonQualifiers = jsonQualifiers)
}
@ -182,7 +150,7 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
it
}
}
TypeVariableName.invoke(
TypeVariableName(
name = nameResolver.getString(it.name),
bounds = *(it.upperBoundList
.map { it.asTypeName(nameResolver, classProto::getTypeParameter) }
@ -197,7 +165,7 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
}
}
return Adapter(
return AdapterGenerator(
fqClassName = fqClassName,
packageName = packageName,
propertyList = parameters,
@ -205,7 +173,7 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
hasCompanionObject = hasCompanionObject,
visibility = classProto.visibility!!,
genericTypeNames = genericTypeNames,
elementUtils = elementUtils)
elements = elementUtils)
}
private fun errorMustBeDataClass(element: Element) {
@ -214,14 +182,11 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
element)
}
private fun Adapter.generateAndWrite() {
val adapterName = "${name}JsonAdapter"
private fun AdapterGenerator.generateAndWrite() {
val fileSpec = generateFile()
val adapterName = fileSpec.members.filterIsInstance<TypeSpec>().first().name!!
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
val fileBuilder = FileSpec.builder(packageName, adapterName)
generate(adapterName, fileBuilder)
fileBuilder
.build()
.writeTo(outputDir)
fileSpec.writeTo(outputDir)
}
private fun mavenGeneratedDir(adapterName: String): File {
@ -232,533 +197,3 @@ class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUti
}
}
/**
* Creates a joined string representation of simplified typename names.
*/
private fun List<TypeName>.simplifiedNames(): String {
return joinToString("_") { it.simplifiedName() }
}
private fun TypeName.resolveRawType(): ClassName {
return when (this) {
is ClassName -> this
is ParameterizedTypeName -> rawType
else -> throw IllegalArgumentException("Cannot get raw type from $this")
}
}
/**
* Creates a simplified string representation of a TypeName's name
*/
private fun TypeName.simplifiedName(): String {
return when (this) {
is ClassName -> simpleName().decapitalize()
is ParameterizedTypeName -> {
rawType.simpleName().decapitalize() + if (typeArguments.isEmpty()) "" else "__" + typeArguments.simplifiedNames()
}
is WildcardTypeName -> "wildcard__" + (lowerBounds + upperBounds).simplifiedNames()
is TypeVariableName -> name.decapitalize() + if (bounds.isEmpty()) "" else "__" + bounds.simplifiedNames()
else -> throw IllegalArgumentException("Unrecognized type! $this")
}.let { if (nullable) "${it}_nullable" else it }
}
private fun ClassName.isClass(elementUtils: Elements): Boolean {
val fqcn = toString()
if (fqcn.startsWith("kotlin.collections.")) {
// These are special kotlin interfaces are only visible in kotlin, because they're replaced by
// the compiler with concrete java classes
return false
} else if (this == ARRAY) {
// This is a "fake" class and not visible to Elements
return true
}
return elementUtils.getTypeElement(fqcn).kind == ElementKind.INTERFACE
}
private fun TypeName.objectType(): TypeName {
return when (this) {
BOOLEAN -> Boolean::class.javaObjectType.asTypeName()
BYTE -> Byte::class.javaObjectType.asTypeName()
SHORT -> Short::class.javaObjectType.asTypeName()
INT -> Integer::class.javaObjectType.asTypeName()
LONG -> Long::class.javaObjectType.asTypeName()
CHAR -> Character::class.javaObjectType.asTypeName()
FLOAT -> Float::class.javaObjectType.asTypeName()
DOUBLE -> Double::class.javaObjectType.asTypeName()
else -> this
}
}
private fun TypeName.makeType(
elementUtils: Elements,
typesArray: ParameterSpec,
genericTypeNames: List<TypeVariableName>): CodeBlock {
if (nullable) {
return asNonNullable().makeType(elementUtils, typesArray, genericTypeNames)
}
return when (this) {
is ClassName -> CodeBlock.of("%T::class.java", this)
is ParameterizedTypeName -> {
// If it's an Array type, we shortcut this to return Types.arrayOf()
if (rawType == ARRAY) {
return CodeBlock.of("%T.arrayOf(%L)",
Types::class,
typeArguments[0].objectType().makeType(elementUtils, typesArray, genericTypeNames))
}
// If it's a Class type, we have to specify the generics.
val rawTypeParameters = if (rawType.isClass(elementUtils)) {
CodeBlock.of(
typeArguments.joinTo(
buffer = StringBuilder(),
separator = ", ",
prefix = "<",
postfix = ">") { "%T" }
.toString(),
*(typeArguments.map { objectType() }.toTypedArray())
)
} else {
CodeBlock.of("")
}
CodeBlock.of(
"%T.newParameterizedType(%T%L::class.java, ${typeArguments
.joinToString(", ") { "%L" }})",
Types::class,
rawType.objectType(),
rawTypeParameters,
*(typeArguments.map {
it.objectType().makeType(elementUtils, typesArray, genericTypeNames)
}.toTypedArray()))
}
is WildcardTypeName -> {
val target: TypeName
val method: String
when {
lowerBounds.size == 1 -> {
target = lowerBounds[0]
method = "supertypeOf"
}
upperBounds.size == 1 -> {
target = upperBounds[0]
method = "subtypeOf"
}
else -> throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: " + this)
}
CodeBlock.of("%T.%L(%T::class.java)", Types::class, method, target)
}
is TypeVariableName -> {
CodeBlock.of("%N[%L]", typesArray, genericTypeNames.indexOfFirst { it == this })
}
else -> throw IllegalArgumentException("Unrepresentable type: " + this)
}
}
private data class Property(
val name: String,
val fqClassName: String,
val serializedName: String,
val hasDefault: Boolean,
val nullable: Boolean,
val typeName: TypeName,
val unaliasedName: TypeName,
val jsonQualifiers: Set<AnnotationMirror>) {
val isRequired = !nullable && !hasDefault
}
private data class Adapter(
val fqClassName: String,
val packageName: String,
val propertyList: List<Property>,
val originalElement: Element,
val name: String = fqClassName.substringAfter(packageName)
.replace('.', '_')
.removePrefix("_"),
val hasCompanionObject: Boolean,
val visibility: Visibility,
val elementUtils: Elements,
val genericTypeNames: List<TypeVariableName>?) {
fun generate(adapterName: String, fileSpecBuilder: FileSpec.Builder) {
val nameAllocator = NameAllocator()
fun String.allocate() = nameAllocator.newName(this)
val originalTypeName = originalElement.asType().asTypeName()
val moshiName = "moshi".allocate()
val moshiParam = ParameterSpec.builder(moshiName, Moshi::class).build()
val typesParam = ParameterSpec.builder("types".allocate(),
ParameterizedTypeName.get(ARRAY, Type::class.asTypeName())).build()
val reader = ParameterSpec.builder("reader".allocate(),
JsonReader::class).build()
val writer = ParameterSpec.builder("writer".allocate(),
JsonWriter::class).build()
val value = ParameterSpec.builder("value".allocate(),
originalTypeName.asNullable()).build()
val jsonAdapterTypeName = ParameterizedTypeName.get(JsonAdapter::class.asClassName(),
originalTypeName)
// Create fields
val adapterProperties = propertyList
.distinctBy { it.unaliasedName to it.jsonQualifiers }
.associate { prop ->
val typeName = prop.unaliasedName
val qualifierNames = prop.jsonQualifiers.joinToString("") {
"at${it.annotationType.asElement().simpleName.toString().capitalize()}"
}
val propertyName = typeName.simplifiedName().allocate().let {
if (qualifierNames.isBlank()) {
it
} else {
"$it$qualifierNames"
}
}.let { "${it}Adapter" }
val adapterTypeName = ParameterizedTypeName.get(JsonAdapter::class.asTypeName(), typeName)
val key = typeName to prop.jsonQualifiers
return@associate key to PropertySpec.builder(propertyName, adapterTypeName, PRIVATE)
.apply {
val qualifiers = prop.jsonQualifiers.toList()
val standardArgs = arrayOf(moshiParam,
if (typeName is ClassName && qualifiers.isEmpty()) {
""
} else {
CodeBlock.of("<%T>",
typeName)
},
typeName.makeType(elementUtils, typesParam, genericTypeNames ?: emptyList()))
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
}
}
val finalArgs = arrayOf(*standardArgs, *args)
initializer(
"%1N.adapter%2L(%3L$initializerString)${if (prop.nullable) ".nullSafe()" else ""}",
*finalArgs)
}
.build()
}
val localProperties =
propertyList.associate { prop ->
val propertySpec = PropertySpec.builder(prop.name.allocate(), prop.typeName.asNullable())
.mutable(true)
.initializer("null")
.build()
val propertySetSpec = if (prop.hasDefault && prop.nullable) {
PropertySpec.builder("${propertySpec.name}Set".allocate(), BOOLEAN)
.mutable(true)
.initializer("false")
.build()
} else {
null
}
val specs = propertySpec to propertySetSpec
prop to specs
}
val optionsByIndex = propertyList
.associateBy { it.serializedName }.entries.withIndex()
// selectName() API setup
val optionsCN = JsonReader.Options::class.asTypeName()
val optionsProperty = PropertySpec.builder(
"options".allocate(),
optionsCN,
PRIVATE)
.initializer("%T.of(${optionsByIndex.map { it.value.key }
.joinToString(", ") { "\"$it\"" }})",
optionsCN)
.build()
val adapter = TypeSpec.classBuilder(adapterName)
.superclass(jsonAdapterTypeName)
.apply {
genericTypeNames?.let {
addTypeVariables(genericTypeNames)
}
}
.apply {
// TODO make this configurable. Right now it just matches the source model
if (visibility == INTERNAL) {
addModifiers(KModifier.INTERNAL)
}
}
.primaryConstructor(FunSpec.constructorBuilder()
.addParameter(moshiParam)
.apply {
genericTypeNames?.let {
addParameter(typesParam)
}
}
.build())
.addProperty(optionsProperty)
.addProperties(adapterProperties.values)
.addFunction(FunSpec.builder("toString")
.addModifiers(OVERRIDE)
.returns(String::class)
.addStatement("return %S",
"GeneratedJsonAdapter(${originalTypeName.resolveRawType()
.simpleNames()
.joinToString(".")})")
.build())
.addFunction(FunSpec.builder("fromJson")
.addModifiers(OVERRIDE)
.addParameter(reader)
.returns(originalTypeName)
.apply {
localProperties.values.forEach {
addCode("%L", it.first)
it.second?.let {
addCode("%L", it)
}
}
}
.addStatement("%N.beginObject()", reader)
.beginControlFlow("while (%N.hasNext())", reader)
.beginControlFlow("when (%N.selectName(%N))", reader, optionsProperty)
.apply {
optionsByIndex.map { (index, entry) -> index to entry.value }
.forEach { (index, prop) ->
val specs = localProperties[prop]!!
val spec = specs.first
val setterSpec = specs.second
if (setterSpec != null) {
beginControlFlow("%L -> ", index)
addStatement("%N = %N.fromJson(%N)",
spec,
adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!,
reader)
addStatement("%N = true", setterSpec)
endControlFlow()
} else {
addStatement("%L -> %N = %N.fromJson(%N)",
index,
spec,
adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!,
reader)
}
}
}
.beginControlFlow("-1 ->")
.addComment("Unknown name, skip it.")
.addStatement("%N.nextName()", reader)
.addStatement("%N.skipValue()", reader)
.endControlFlow()
.endControlFlow()
.endControlFlow()
.addStatement("%N.endObject()", reader)
.apply {
val propertiesWithDefaults = localProperties.entries.filter { it.key.hasDefault }
val propertiesWithoutDefaults = localProperties.entries.filter { !it.key.hasDefault }
val requiredPropertiesCodeBlock = CodeBlock.of(
propertiesWithoutDefaults.joinToString(",\n") { (property, specs) ->
val spec = specs.first
"${property.name} = ${spec.name}%L"
},
*(propertiesWithoutDefaults
.map { (property, _) ->
if (property.isRequired) {
@Suppress("IMPLICIT_CAST_TO_ANY")
CodeBlock.of(
" ?: throw %T(\"Required property '%L' missing at \${%N.path}\")",
JsonDataException::class,
property.name,
reader
)
} else {
@Suppress("IMPLICIT_CAST_TO_ANY")
""
}
}
.toTypedArray()))
if (propertiesWithDefaults.isEmpty()) {
addStatement("return %T(%L)",
originalTypeName,
requiredPropertiesCodeBlock)
} else {
addStatement("return %T(%L)\n.let {\n it.copy(%L)\n}",
originalTypeName,
requiredPropertiesCodeBlock,
propertiesWithDefaults
.joinToString(",\n ") { (property, specs) ->
val spec = specs.first
val setSpec = specs.second
if (setSpec != null) {
"${property.name} = if (${setSpec.name}) ${spec.name} else it.${property.name}"
} else {
"${property.name} = ${spec.name} ?: it.${property.name}"
}
})
}
}
.build())
.addFunction(FunSpec.builder("toJson")
.addModifiers(OVERRIDE)
.addParameter(writer)
.addParameter(value)
.beginControlFlow("if (%N == null)", value)
.addStatement("throw %T(%S)", NullPointerException::class, "${value.name} was null! Wrap in .nullSafe() to write nullable values.")
.endControlFlow()
.addStatement("%N.beginObject()", writer)
.apply {
propertyList.forEach { prop ->
addStatement("%N.name(%S)",
writer,
prop.serializedName)
addStatement("%N.toJson(%N, %N.%L)",
adapterProperties[prop.unaliasedName to prop.jsonQualifiers]!!,
writer,
value,
prop.name)
}
}
.addStatement("%N.endObject()", writer)
.build())
.build()
if (hasCompanionObject) {
val rawType = when (originalTypeName) {
is TypeVariableName -> throw IllegalArgumentException(
"Cannot get raw type of TypeVariable!")
is ParameterizedTypeName -> originalTypeName.rawType
else -> originalTypeName as ClassName
}
fileSpecBuilder.addFunction(FunSpec.builder("jsonAdapter")
.apply {
// TODO make this configurable. Right now it just matches the source model
if (visibility == INTERNAL) {
addModifiers(KModifier.INTERNAL)
}
}
.receiver(rawType.nestedClass("Companion"))
.returns(jsonAdapterTypeName)
.addParameter(moshiParam)
.apply {
genericTypeNames?.let {
addParameter(typesParam)
addTypeVariables(it)
}
}
.apply {
if (genericTypeNames != null) {
addStatement("return %N(%N, %N)", adapter, moshiParam, typesParam)
} else {
addStatement("return %N(%N)", adapter, moshiParam)
}
}
.build())
}
fileSpecBuilder.addType(adapter)
}
}
private fun ProtoBuf.TypeParameter.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> ProtoBuf.TypeParameter,
resolveAliases: Boolean = false): TypeName {
return TypeVariableName(
name = nameResolver.getString(name),
bounds = *(upperBoundList.map { it.asTypeName(nameResolver, getTypeParameter, resolveAliases) }
.toTypedArray()),
variance = variance.asKModifier()
)
}
private fun ProtoBuf.TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) {
Variance.IN -> IN
Variance.OUT -> OUT
Variance.INV -> null
}
}
/**
* Returns the TypeName of this type as it would be seen in the source code,
* including nullability and generic type parameters.
*
* @param [nameResolver] a [NameResolver] instance from the source proto
* @param [getTypeParameter]
* A function that returns the type parameter for the given index.
* **Only called if [ProtoBuf.Type.hasTypeParameter] is `true`!**
*/
private fun ProtoBuf.Type.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> ProtoBuf.TypeParameter,
resolveAliases: Boolean = false
): TypeName {
val argumentList = when {
hasAbbreviatedType() -> abbreviatedType.argumentList
else -> argumentList
}
if (hasFlexibleUpperBound()) {
return WildcardTypeName.subtypeOf(
flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, resolveAliases))
} else if (hasOuterType()) {
return WildcardTypeName.supertypeOf(outerType.asTypeName(nameResolver, getTypeParameter, resolveAliases))
}
val realType = when {
hasTypeParameter() -> return getTypeParameter(typeParameter)
.asTypeName(nameResolver, getTypeParameter, resolveAliases)
hasTypeParameterName() -> typeParameterName
hasAbbreviatedType() && !resolveAliases -> abbreviatedType.typeAliasName
else -> className
}
var typeName: TypeName = ClassName.bestGuess(nameResolver.getString(realType)
.replace("/", "."))
if (argumentList.isNotEmpty()) {
val remappedArgs: Array<TypeName> = argumentList.map {
val projection = if (it.hasProjection()) {
it.projection
} else null
if (it.hasType()) {
it.type.asTypeName(nameResolver, getTypeParameter, resolveAliases)
.let { typeName ->
projection?.let {
when (it) {
Projection.IN -> WildcardTypeName.supertypeOf(typeName)
Projection.OUT -> {
if (typeName == ANY) {
// This becomes a *, which we actually don't want here.
// List<Any> works with List<*>, but List<*> doesn't work with List<Any>
typeName
} else {
WildcardTypeName.subtypeOf(typeName)
}
}
Projection.STAR -> WildcardTypeName.subtypeOf(ANY)
Projection.INV -> TODO("INV projection is unsupported")
}
} ?: typeName
}
} else {
WildcardTypeName.subtypeOf(ANY)
}
}.toTypedArray()
typeName = ParameterizedTypeName.get(typeName as ClassName, *remappedArgs)
}
if (nullable) {
typeName = typeName.asNullable()
}
return typeName
}

View file

@ -0,0 +1,146 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asTypeName
import javax.lang.model.element.AnnotationMirror
/** Generates functions to encode and decode a property as JSON. */
internal class PropertyGenerator(
val name: String,
val serializedName: String,
val hasDefault: Boolean,
val nullable: Boolean,
val typeName: TypeName,
val unaliasedName: TypeName,
val jsonQualifiers: Set<AnnotationMirror>
) {
lateinit var delegateName: String
lateinit var localName: String
lateinit var localIsPresentName: String
val isRequired
get() = !nullable && !hasDefault
/** We prefer to use 'null' to mean absent, but for some properties those are distinct. */
val differentiateAbsentFromNull
get() = hasDefault && nullable
fun reserveDelegateNames(nameAllocator: NameAllocator) {
val qualifierNames = jsonQualifiers.joinToString("") {
"at${it.annotationType.asElement().simpleName.toString().capitalize()}"
}
nameAllocator.newName("${unaliasedName.toVariableName()}${qualifierNames}Adapter",
delegateKey())
}
fun allocateNames(nameAllocator: NameAllocator) {
localName = nameAllocator.newName(name)
localIsPresentName = nameAllocator.newName("${name}Set")
delegateName = nameAllocator.get(delegateKey())
}
/** Returns a key that matches keys of properties that can share an adapter. */
fun delegateKey() = unaliasedName to jsonQualifiers
/** Returns an adapter to use when encoding and decoding this property. */
fun generateDelegateProperty(enclosing: AdapterGenerator): PropertySpec {
val adapterTypeName = ParameterizedTypeName.get(
JsonAdapter::class.asTypeName(), unaliasedName)
val qualifiers = jsonQualifiers.toList()
val standardArgs = arrayOf(enclosing.moshiParam,
if (unaliasedName is ClassName && qualifiers.isEmpty()) {
""
} else {
CodeBlock.of("<%T>", unaliasedName)
},
unaliasedName.makeType(
enclosing.elements, enclosing.typesParam, enclosing.genericTypeNames ?: emptyList()))
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
}
}
val finalArgs = arrayOf(*standardArgs, *args)
return PropertySpec.builder(delegateName, adapterTypeName,
KModifier.PRIVATE)
.initializer("%1N.adapter%2L(%3L$initializerString)${if (nullable) ".nullSafe()" else ""}",
*finalArgs)
.build()
}
fun generateLocalProperty(): PropertySpec {
return PropertySpec.builder(localName, typeName.asNullable())
.mutable(true)
.initializer("null")
.build()
}
fun generateLocalIsPresentProperty(): PropertySpec {
return PropertySpec.builder(localIsPresentName, BOOLEAN)
.mutable(true)
.initializer("false")
.build()
}
}
/**
* Returns a suggested variable name derived from a list of type names.
*/
private fun List<TypeName>.toVariableNames(): String {
return joinToString("_") { it.toVariableName() }
}
/**
* Returns a suggested variable name derived from a type name.
*/
private fun TypeName.toVariableName(): String {
return when (this) {
is ClassName -> simpleName().decapitalize()
is ParameterizedTypeName -> {
rawType.simpleName().decapitalize() + if (typeArguments.isEmpty()) "" else "__" + typeArguments.toVariableNames()
}
is WildcardTypeName -> "wildcard__" + (lowerBounds + upperBounds).toVariableNames()
is TypeVariableName -> name.decapitalize() + if (bounds.isEmpty()) "" else "__" + bounds.toVariableNames()
else -> throw IllegalArgumentException("Unrecognized type! $this")
}.let { if (nullable) "${it}_nullable" else it }
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.BYTE
import com.squareup.kotlinpoet.CHAR
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.SHORT
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asTypeName
import javax.lang.model.element.ElementKind
import javax.lang.model.util.Elements
internal fun TypeName.rawType(): ClassName {
return when (this) {
is ClassName -> this
is ParameterizedTypeName -> rawType
else -> throw IllegalArgumentException("Cannot get raw type from $this")
}
}
private fun ClassName.isClass(elements: Elements): Boolean {
val fqcn = toString()
if (fqcn.startsWith("kotlin.collections.")) {
// These are special kotlin interfaces are only visible in kotlin, because they're replaced by
// the compiler with concrete java classes/
return false
} else if (this == ARRAY) {
// This is a "fake" class and not visible to Elements.
return true
}
return elements.getTypeElement(fqcn).kind == ElementKind.INTERFACE
}
private fun TypeName.objectType(): TypeName {
return when (this) {
BOOLEAN -> Boolean::class.javaObjectType.asTypeName()
BYTE -> Byte::class.javaObjectType.asTypeName()
SHORT -> Short::class.javaObjectType.asTypeName()
INT -> Integer::class.javaObjectType.asTypeName()
LONG -> Long::class.javaObjectType.asTypeName()
CHAR -> Character::class.javaObjectType.asTypeName()
FLOAT -> Float::class.javaObjectType.asTypeName()
DOUBLE -> Double::class.javaObjectType.asTypeName()
else -> this
}
}
internal fun TypeName.makeType(
elementUtils: Elements,
typesArray: ParameterSpec,
genericTypeNames: List<TypeVariableName>
): CodeBlock {
if (nullable) {
return asNonNullable().makeType(elementUtils, typesArray, genericTypeNames)
}
return when (this) {
is ClassName -> CodeBlock.of(
"%T::class.java", this)
is ParameterizedTypeName -> {
// If it's an Array type, we shortcut this to return Types.arrayOf()
if (rawType == ARRAY) {
return CodeBlock.of("%T.arrayOf(%L)",
Types::class,
typeArguments[0].objectType().makeType(elementUtils, typesArray, genericTypeNames))
}
// If it's a Class type, we have to specify the generics.
val rawTypeParameters = if (rawType.isClass(elementUtils)) {
CodeBlock.of(
typeArguments.joinTo(
buffer = StringBuilder(),
separator = ", ",
prefix = "<",
postfix = ">") { "%T" }
.toString(),
*(typeArguments.map { objectType() }.toTypedArray())
)
} else {
CodeBlock.of("")
}
CodeBlock.of(
"%T.newParameterizedType(%T%L::class.java, ${typeArguments
.joinToString(", ") { "%L" }})",
Types::class,
rawType.objectType(),
rawTypeParameters,
*(typeArguments.map {
it.objectType().makeType(elementUtils, typesArray, genericTypeNames)
}.toTypedArray()))
}
is WildcardTypeName -> {
val target: TypeName
val method: String
when {
lowerBounds.size == 1 -> {
target = lowerBounds[0]
method = "supertypeOf"
}
upperBounds.size == 1 -> {
target = upperBounds[0]
method = "subtypeOf"
}
else -> throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: " + this)
}
CodeBlock.of("%T.%L(%T::class.java)",
Types::class, method, target)
}
is TypeVariableName -> {
CodeBlock.of("%N[%L]", typesArray,
genericTypeNames.indexOfFirst { it == this })
}
else -> throw IllegalArgumentException("Unrepresentable type: " + this)
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import org.jetbrains.kotlin.serialization.ProtoBuf.Type
import org.jetbrains.kotlin.serialization.ProtoBuf.TypeParameter
import org.jetbrains.kotlin.serialization.ProtoBuf.TypeParameter.Variance
import org.jetbrains.kotlin.serialization.deserialization.NameResolver
internal fun TypeParameter.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false
): TypeName {
return TypeVariableName(
name = nameResolver.getString(name),
bounds = *(upperBoundList.map {
it.asTypeName(nameResolver, getTypeParameter, resolveAliases)
}
.toTypedArray()),
variance = variance.asKModifier()
)
}
internal fun TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) {
Variance.IN -> KModifier.IN
Variance.OUT -> KModifier.OUT
Variance.INV -> null
}
}
/**
* Returns the TypeName of this type as it would be seen in the source code, including nullability
* and generic type parameters.
*
* @param [nameResolver] a [NameResolver] instance from the source proto
* @param [getTypeParameter] a function that returns the type parameter for the given index. **Only
* called if [ProtoBuf.Type.hasTypeParameter] is true!**
*/
internal fun Type.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false
): TypeName {
val argumentList = when {
hasAbbreviatedType() -> abbreviatedType.argumentList
else -> argumentList
}
if (hasFlexibleUpperBound()) {
return WildcardTypeName.subtypeOf(
flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, resolveAliases))
} else if (hasOuterType()) {
return WildcardTypeName.supertypeOf(
outerType.asTypeName(nameResolver, getTypeParameter, resolveAliases))
}
val realType = when {
hasTypeParameter() -> return getTypeParameter(typeParameter)
.asTypeName(nameResolver, getTypeParameter, resolveAliases)
hasTypeParameterName() -> typeParameterName
hasAbbreviatedType() && !resolveAliases -> abbreviatedType.typeAliasName
else -> className
}
var typeName: TypeName =
ClassName.bestGuess(nameResolver.getString(realType)
.replace("/", "."))
if (argumentList.isNotEmpty()) {
val remappedArgs: Array<TypeName> = argumentList.map {
val projection = if (it.hasProjection()) {
it.projection
} else null
if (it.hasType()) {
it.type.asTypeName(nameResolver, getTypeParameter, resolveAliases)
.let { typeName ->
projection?.let {
when (it) {
Type.Argument.Projection.IN -> WildcardTypeName.supertypeOf(
typeName)
Type.Argument.Projection.OUT -> {
if (typeName == ANY) {
// This becomes a *, which we actually don't want here.
// List<Any> works with List<*>, but List<*> doesn't work with List<Any>
typeName
} else {
WildcardTypeName.subtypeOf(typeName)
}
}
Type.Argument.Projection.STAR -> WildcardTypeName.subtypeOf(
ANY)
Type.Argument.Projection.INV -> TODO("INV projection is unsupported")
}
} ?: typeName
}
} else {
WildcardTypeName.subtypeOf(ANY)
}
}.toTypedArray()
typeName = ParameterizedTypeName.get(
typeName as ClassName, *remappedArgs)
}
if (nullable) {
typeName = typeName.asNullable()
}
return typeName
}