mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 16:39:22 +08:00
Kotlin Code Gen module (#435)
* Add kotlin code gen modules * Update kotlin to 1.2 * Add a serializable dummy class * Try using kapt configuration from kotlin-examples repo Still no luck! * Use proper allocated name for assignment too * Use selectName() API * Clean up constructor parameter annotations & plumbing for qualifiers * Updates poms and kotlin code gen processor to support tests. * Ignore kotlin code gen tests for now None of these are data classes tests right now, which is the only thing this supports right now * Replace $ with _ in class names for consistency * Shortcut Array types to arrayOf * Add DataClassTest * Try generated option first, fall back to maven after * More idiomatic handling * Only use nonnullable types for adapter properties * Code dump of kotshi tests * Comment out specifics to get compiling * Generics support! * Fix double primitive default * Pick up temporary snapshot for Any fix * Invariance should just be null * Better handling of nullably-bound variance * Just assume the first jvm constructor for now as jvmMethodSig is flaky * Specify types param if needed * Don't do lazy delegation * Clean up nullable typevariablename boundaries * Add type variables to extension function on companion object * Use properties instead of allocated names for more robustness Since we're already on a snapshot * If there are no type variables, make it null for simpler handling * Fix generics and Type[] handling * Fix unnecessary as casts on primitive defaults * Reference spec directly for possible bangs * Use nullSafe() adapters for anything nullable or with default values * Use object type in makeType() Types.java cares * Make TestPrimitiveDefaultValues work * Re-enable TestClassWithJavaKeyword * Ignore remaining tests that are pending decisions or JsonQualifier support * Remove customnames test as we're just going to stick with simple @Json * Add toString() implementations * Reenable default values testing, adapt to kotlin lang support * Remove primitive adapters bits since we're not using it * Clean up a bunch of leftover comments * Switch to only nullable handling, report missing properties This makes all nullable handling for local properties the same, and removes defaults for primitives in the process. It simplifies the handling a lot, and leans on kotlin language features to take care of null handling (null checking and then throwing the lazily evaluated list of missing properties). One minor change from what kotshi does - this reports the serialized name in the missing properties, not the property name. We could look at supporting this though if we want. * Implement JsonQualifier support * Use Kapt for AutoService/processor declaration * Checkstyle * Remove unused primite type checks * Add test verifying mutable and immutable collections work * Fix test name * Standardize isRequired checks * Add more nullability and mutability tests * Kotlinpoet 0.7.0 final * Switch to new vararg overload for annotation class adapter() * Make suffix just JsonAdapter without underscore * Switch to just a regular constructor for MoshiSerializableFactory * Remove constructor caching * Remove unnecessary framework class checks * Nix unnecessary superclass lookups, inline constructor lookup * Nix null token check in reads * Nix null check in writes, do !! on first value use * Nix null checks in favor of serializeNulls * Inline null checks and fail eagerly * Fix double _Adapter * First pass at simplifying adapter names * Inline names to options property, life into class and rm companion * Differentiate between absent and null, use nullSafe() as needed * Group together compile and test dependencies * Remove incorrect comment * Revert formatting * Set, not mutable set * Collapse else-if nesting to one when * Cleaner formatting test code * Collapse more to locals * Collapse more * Return a nonnullable type in fromJson * Remove redundant out variance * Use KClass where appropriate * End comment in period * Remove redundant comment * Throw on unrecognized type in simplified name * Use illegalargumentexception instead * Emit a nullcheck at the beginning of toJson instead * Remove extra newline * Simplify processing to be less abusive * Skip using asClassName() when possible * Use addComment() * Switch to declared constructors Technically more correct since we're defining these * Unmodifiable set * return adapter(type, annotationTypes[0]) * Slight optimization - check if the type is parameterized first If the type is a parameterized type, then we know they'll have the two-arg constructor. This way we don't always try and fail the single arg constructor on parameterized types * Add test for type aliases, optimize to reuse adapters if possible This is a tiny optimization to make type aliases (which did already work) reuse adapter properties if they already exist for the backing type. What this means is that if you have: typealias Foo = String and properties foo: Foo bar: String you'll only get one adapter property field for String, and both will use it * Use string templating where possible * Remove all the kotshi tests
This commit is contained in:
@@ -0,0 +1,764 @@
|
||||
/*
|
||||
* 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.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
|
||||
import me.eugeniomarletti.kotlin.metadata.extractFullName
|
||||
import me.eugeniomarletti.kotlin.metadata.isDataClass
|
||||
import me.eugeniomarletti.kotlin.metadata.isPrimary
|
||||
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
|
||||
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
|
||||
|
||||
/**
|
||||
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them.
|
||||
* This generates Kotlin code, and understands basic Kotlin language features like default values
|
||||
* and companion objects.
|
||||
*
|
||||
* The generated class will match the visibility of the given data class (i.e. if it's internal, the
|
||||
* adapter will also be internal).
|
||||
*
|
||||
* If you define a companion object, a jsonAdapter() extension function will be generated onto it.
|
||||
* If you don't want this though, you can use the runtime [MoshiSerializable] factory implementation.
|
||||
*/
|
||||
@AutoService(Processor::class)
|
||||
class MoshiKotlinCodeGenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils {
|
||||
|
||||
private val annotationName = MoshiSerializable::class.java.canonicalName
|
||||
|
||||
override fun getSupportedAnnotationTypes() = setOf(annotationName)
|
||||
|
||||
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
|
||||
|
||||
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
|
||||
val annotationElement = elementUtils.getTypeElement(annotationName)
|
||||
roundEnv.getElementsAnnotatedWith(annotationElement)
|
||||
.asSequence()
|
||||
.mapNotNull { processElement(it) }
|
||||
.forEach { it.generateAndWrite() }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun processElement(element: Element): Adapter? {
|
||||
val metadata = element.kotlinMetadata
|
||||
|
||||
if (metadata !is KotlinClassMetadata) {
|
||||
errorMustBeDataClass(element)
|
||||
return null
|
||||
}
|
||||
|
||||
val classData = metadata.data
|
||||
val (nameResolver, classProto) = classData
|
||||
|
||||
fun ProtoBuf.Type.extractFullName() = extractFullName(classData)
|
||||
|
||||
if (!classProto.isDataClass) {
|
||||
errorMustBeDataClass(element)
|
||||
return null
|
||||
}
|
||||
|
||||
val fqClassName = nameResolver.getString(classProto.fqName).replace('/', '.')
|
||||
|
||||
val packageName = nameResolver.getString(classProto.fqName).substringBeforeLast('/').replace(
|
||||
'/', '.')
|
||||
|
||||
val hasCompanionObject = classProto.hasCompanionObjectName()
|
||||
// todo allow custom constructor
|
||||
val protoConstructor = classProto.constructorList
|
||||
.single { it.isPrimary }
|
||||
val constructorJvmSignature = protoConstructor.getJvmConstructorSignature(nameResolver,
|
||||
classProto.typeTable)
|
||||
val constructor = classProto.fqName
|
||||
.let(nameResolver::getString)
|
||||
.replace('/', '.')
|
||||
.let(elementUtils::getTypeElement)
|
||||
.enclosedElements
|
||||
.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 }
|
||||
val parameters = protoConstructor
|
||||
.valueParameterList
|
||||
.mapIndexed { index, valueParameter ->
|
||||
val paramName = nameResolver.getString(valueParameter.name)
|
||||
|
||||
val nullable = valueParameter.type.nullable
|
||||
val paramFqcn = valueParameter.type.extractFullName()
|
||||
.replace("`", "")
|
||||
.removeSuffix("?")
|
||||
|
||||
val actualElement = constructor.parameters[index]
|
||||
|
||||
val serializedName = actualElement.getAnnotation(Json::class.java)?.name
|
||||
?: paramName
|
||||
|
||||
val jsonQualifiers = AnnotationMirrors.getAnnotatedAnnotations(actualElement,
|
||||
JsonQualifier::class.java)
|
||||
|
||||
Property(
|
||||
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),
|
||||
jsonQualifiers = jsonQualifiers)
|
||||
}
|
||||
|
||||
val genericTypeNames = classProto.typeParameterList
|
||||
.map {
|
||||
val variance = it.variance.asKModifier().let {
|
||||
// We don't redeclare out variance here
|
||||
if (it == OUT) {
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
TypeVariableName.invoke(
|
||||
name = nameResolver.getString(it.name),
|
||||
bounds = *(it.upperBoundList
|
||||
.map { it.asTypeName(nameResolver, classProto::getTypeParameter) }
|
||||
.toTypedArray()),
|
||||
variance = variance)
|
||||
.reified(it.reified)
|
||||
}.let {
|
||||
if (it.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return Adapter(
|
||||
fqClassName = fqClassName,
|
||||
packageName = packageName,
|
||||
propertyList = parameters,
|
||||
originalElement = element,
|
||||
hasCompanionObject = hasCompanionObject,
|
||||
visibility = classProto.visibility!!,
|
||||
genericTypeNames = genericTypeNames,
|
||||
elementUtils = elementUtils)
|
||||
}
|
||||
|
||||
private fun errorMustBeDataClass(element: Element) {
|
||||
messager.printMessage(ERROR,
|
||||
"@${MoshiSerializable::class.java.simpleName} can't be applied to $element: must be a Kotlin data class",
|
||||
element)
|
||||
}
|
||||
|
||||
private fun Adapter.generateAndWrite() {
|
||||
val adapterName = "${name}JsonAdapter"
|
||||
val outputDir = generatedDir ?: mavenGeneratedDir(adapterName)
|
||||
val fileBuilder = FileSpec.builder(packageName, adapterName)
|
||||
generate(adapterName, fileBuilder)
|
||||
fileBuilder
|
||||
.build()
|
||||
.writeTo(outputDir)
|
||||
}
|
||||
|
||||
private fun mavenGeneratedDir(adapterName: String): File {
|
||||
// Hack since the maven plugin doesn't supply `kapt.kotlin.generated` option
|
||||
// Bug filed at https://youtrack.jetbrains.com/issue/KT-22783
|
||||
val file = filer.createSourceFile(adapterName).toUri().let(::File)
|
||||
return file.parentFile.also { file.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
Reference in New Issue
Block a user