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:
parent
96e074d030
commit
d045947ea7
5 changed files with 708 additions and 580 deletions
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue