All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.ansman.kotshi.ksp.generators.AdapterGenerator.kt Maven / Gradle / Ivy

Go to download

An annotations processor that generates Moshi adapters from Kotlin data classes

There is a newer version: 3.0.0
Show newest version
package se.ansman.kotshi.ksp.generators

import com.google.devtools.ksp.getAllSuperTypes
import com.google.devtools.ksp.isInternal
import com.google.devtools.ksp.isLocal
import com.google.devtools.ksp.isPublic
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.Modifier
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import se.ansman.kotshi.GeneratedAdapter
import se.ansman.kotshi.GlobalConfig
import se.ansman.kotshi.Polymorphic
import se.ansman.kotshi.PolymorphicLabel
import se.ansman.kotshi.applyEachIndexed
import se.ansman.kotshi.applyIf
import se.ansman.kotshi.kapt.generators.internalKotshiApi
import se.ansman.kotshi.kapt.generators.jsonReaderOptions
import se.ansman.kotshi.kapt.generators.moshiParameter
import se.ansman.kotshi.kapt.generators.namedJsonAdapter
import se.ansman.kotshi.kapt.generators.typesParameter
import se.ansman.kotshi.ksp.KspProcessingError
import se.ansman.kotshi.ksp.addOriginatingKSFile
import se.ansman.kotshi.ksp.getAnnotation
import se.ansman.kotshi.ksp.getValue
import se.ansman.kotshi.ksp.toClassName
import se.ansman.kotshi.ksp.toTypeName
import se.ansman.kotshi.ksp.writeTo
import se.ansman.kotshi.nullable

@Suppress("UnstableApiUsage")
abstract class AdapterGenerator(
    protected val environment: SymbolProcessorEnvironment,
    protected val resolver: Resolver,
    protected val element: KSClassDeclaration,
    protected val globalConfig: GlobalConfig,
) {
    protected val nameAllocator = NameAllocator().apply {
        newName("options")
        newName("value")
        newName("writer")
        newName("reader")
        newName("stringBuilder")
        newName("it")
    }

    protected val className = element.toClassName()

    protected val typeVariables = element.typeParameters.map { typeParameter ->
        TypeVariableName(
            typeParameter.name.getShortName(),
            typeParameter.bounds
                .map { it.resolve().toTypeName() }
                .toList(),
        )
    }

    protected val typeName = if (typeVariables.isEmpty()) {
        className
    } else {
        className.parameterizedBy(typeVariables)
    }

    protected val value = ParameterSpec.builder("value", typeName.nullable()).build()

    fun generateAdapter(): GeneratedAdapter {
        when {
            Modifier.INNER in element.modifiers ->
                throw KspProcessingError("@JsonSerializable can't be applied to inner classes", element)
            element.isLocal() ->
                throw KspProcessingError("@JsonSerializable can't be applied to local classes", element)
            !element.isPublic() && !element.isInternal() ->
                throw KspProcessingError("Classes annotated with @JsonSerializable must public or internal", element)
        }

        val adapterClassName =
            ClassName(className.packageName, "Kotshi${className.simpleNames.joinToString("_")}JsonAdapter")

        val typeSpec = TypeSpec.classBuilder(adapterClassName)
            .addModifiers(KModifier.INTERNAL)
            .addOriginatingKSFile(element.containingFile!!)
            .addAnnotation(internalKotshiApi)
            // TODO
//            .maybeAddGeneratedAnnotation(elements, sourceVersion)
            .addTypeVariables(typeVariables)
            .superclass(namedJsonAdapter.plusParameter(typeName))
            .addSuperclassConstructorParameter("%S", "KotshiJsonAdapter(${className.simpleNames.joinToString(".")})")
            .apply { addMethods() }
            .build()

        FileSpec.builder(adapterClassName.packageName, adapterClassName.simpleName)
            .addComment("Code generated by Kotshi. Do not edit.")
            .addType(typeSpec)
            .build()
            .writeTo(environment.codeGenerator)

        return GeneratedAdapter(
            targetType = className,
            className = adapterClassName,
            typeVariables = typeVariables,
            requiresMoshi = typeSpec.primaryConstructor
                ?.parameters
                ?.contains(moshiParameter)
                ?: false,
            requiresTypes = typeSpec.primaryConstructor
                ?.parameters
                ?.contains(typesParameter)
                ?: false
        )
    }

    protected abstract fun TypeSpec.Builder.addMethods()

    protected fun TypeSpec.Builder.maybeAddCompanion(jsonNames: Collection): TypeSpec.Builder =
        applyIf(jsonNames.isNotEmpty()) {
            addOptions(jsonNames)
        }

    protected fun TypeSpec.Builder.addOptions(jsonNames: Collection): TypeSpec.Builder =
        addProperty(PropertySpec.builder("options", jsonReaderOptions, KModifier.PRIVATE)
            .initializer(CodeBlock.Builder()
                .add("%T.of(«", jsonReaderOptions)
                .applyIf(jsonNames.size > 1) { add("\n") }
                .applyEachIndexed(jsonNames) { index, name ->
                    if (index > 0) {
                        add(",\n")
                    }
                    add("%S", name)
                }
                .applyIf(jsonNames.size > 1) { add("\n") }
                .add("»)")
                .build())
            .build())


    protected fun getPolymorphicLabels(): Map = element.getPolymorphicLabels()

    private fun KSClassDeclaration.getPolymorphicLabels(): Map {
        val output = LinkedHashMap()
        val isPolymorphic = getAnnotation() != null

        for (type in getAllSuperTypes().map { it.declaration } + sequenceOf(this)) {
            if (type is KSClassDeclaration) {
                val labelKey = type.nearestPolymorpicLabelKey()
                    ?: continue
                output[labelKey] = type.getAnnotation()?.getValue("value")
                    ?: continue
            } else {
                throw KspProcessingError("Unknown super type ${type.javaClass}", type)
            }
        }
        if (isPolymorphic && output.isEmpty()) {
            throw KspProcessingError("Found no polymorphic labels", this)
        }
        return output
    }

    private fun KSClassDeclaration.nearestPolymorpicLabelKey(): String? {
        for (superType in getAllSuperTypes()/*.toList().reversed()*/) {
            superType.declaration.getAnnotation()
                ?.getValue("labelKey")
                ?.let { return it }
        }
        return null
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy