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

com.caesarealabs.rpc4k.processor.ApiDefinitionToClientCode.kt Maven / Gradle / Ivy

There is a newer version: 0.7.1
Show newest version
package com.caesarealabs.rpc4k.processor

import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.caesarealabs.rpc4k.processor.utils.poet.*
import com.caesarealabs.rpc4k.runtime.api.GeneratedClientImplFactory
import com.caesarealabs.rpc4k.runtime.api.RpcClient
import com.caesarealabs.rpc4k.runtime.api.SerializationFormat
import com.caesarealabs.rpc4k.runtime.implementation.GeneratedCodeUtils
import com.caesarealabs.rpc4k.runtime.implementation.kotlinPoet

/**
 * Converts
 * ```
 * @Api
 * class MyApi {
 *     private val dogs = mutableListOf()
 *     fun getDogs(num: Int, type: String): List {
 *         return dogs.filter { it.type == type }.take(num)
 *     }
 *
 *     fun putDog(dog: Dog) {
 *         dogs.add(dog)
 *     }
 * }
 * ```
 * into
 * ```
 * public class MyApiClientImpl(
 *     private val client: RpcClient,
 *     private val format: SerializationFormat,
 * ) {
 *     public suspend fun getDogs(num: Int, type: String): List = request(
 *         client, format, "getDogs",
 *         listOf(num, type), listOf(Int.serializer(), String.serializer()),
 *         ListSerializer(Dog.serializer())
 *     )
 *
 *     public suspend fun putDog(dog: Dog): Unit = send(
 *         client, format, "putDog", listOf(dog),
 *         listOf(Dog.serializer())
 *     )
 * }
 * ```
 *
 * Which makes running client code much easier.
 */
object ApiDefinitionToClientCode {
    private const val ClientPropertyName = "client"
    private const val FormatPropertyName = "format"
    private val sendMethod = GeneratedCodeUtils::class.methodName("send")
    private val requestMethod = GeneratedCodeUtils::class.methodName("request")

//    context(JvmContext)
    /**
     * @param userClassIsInterface When we are generating both a client and a server, it's useful to make the generated class
     * extend the user class. We need to know if the user class is an interface or not to properly extend/implement it.
     */
    fun convert(apiDefinition: RpcApi, userClassIsInterface: Boolean): FileSpec {
        val className = "${apiDefinition.name.simple}${GeneratedCodeUtils.ClientSuffix}"
        return fileSpec(GeneratedCodeUtils.Package, className) {
            // KotlinPoet doesn't handle extension methods well
            addImport("kotlinx.serialization.builtins", "serializer")
            addImport("kotlinx.serialization.builtins", "nullable")


            addFunction(clientConstructorExtension(apiDefinition, className))

            addClass(className) {
                addType(factoryCompanionObject(apiDefinition, className))

                addPrimaryConstructor {
                    addConstructorProperty(ClientPropertyName, type = RpcClient::class, KModifier.PRIVATE)
                    addConstructorProperty(FormatPropertyName, type = SerializationFormat::class, KModifier.PRIVATE)
                }
                val userClassName = apiDefinition.name.kotlinPoet
                if (userClassIsInterface) addSuperinterface(userClassName) else superclass(userClassName)
                for (method in apiDefinition.methods) addFunction(convertMethod(method))
            }
        }
    }


    /**
     * We generate a factory for the generated client implementation for it to be easy to just pass a [GeneratedClientImplFactory]
     * The generated code looks like this:
     * ```
     *     companion object Factory: GeneratedClientImplFactory {
     *         override fun build(client: RpcClient, format: SerializationFormat): UserProtocol {
     *             return UserProtocolClientImpl(client, format)
     *         }
     *     }
     * ```
     */
//    context(JvmContext)
    private fun factoryCompanionObject(api: RpcApi, generatedClassName: String) = companionObject(GeneratedCodeUtils.FactoryName) {
        addSuperinterface(GeneratedClientImplFactory::class.asClassName().parameterizedBy(api.name.kotlinPoet))
        addFunction("build") {
            addModifiers(KModifier.OVERRIDE)
            addParameter(ClientPropertyName, RpcClient::class)
            addParameter(FormatPropertyName, SerializationFormat::class)
            returns(api.name.kotlinPoet)
            addStatement("return $generatedClassName($ClientPropertyName, $FormatPropertyName)")
        }
    }


    /**
     * Making the generated class available with an extension function makes it more resilient to name changes
     *   since you will no longer need to directly reference the generated class.
     *   Looks like:
     *   ```
     *   fun MyApi.Companion.client(client: RpcClient, format: SerializationFormat) = MyApiClientImpl(client,format)
     *   ```
     */
//    context(JvmContext)
    private fun clientConstructorExtension(api: RpcApi, generatedClassName: String) =
        extensionFunction(api.name.kotlinPoet.companion(), "client") {
            addParameter(ClientPropertyName, RpcClient::class)
            addParameter(FormatPropertyName, SerializationFormat::class)
            returns(ClassName(GeneratedCodeUtils.Package, generatedClassName))
            addStatement("return $generatedClassName($ClientPropertyName, $FormatPropertyName)")
        }

    private fun convertMethod(rpcDefinition: RpcDefinition): FunSpec = funSpec(rpcDefinition.name) {
        // We need to call network methods in this
        addModifiers(KModifier.SUSPEND, KModifier.OVERRIDE)

        for (argument in rpcDefinition.parameters) addParameter(convertArgument(argument))
        val returnType = rpcDefinition.returnType
        returns(returnType.typeName)

        val returnsValue = !returnType.isUnit
        // We use a simpler method where no return type is required
        val method = if (returnsValue) requestMethod else sendMethod

        // Example:
        //        return GeneratedCodeUtils.request(
        //            client,
        //            format,
        //            "getDogs",
        //            listOf(num, type),
        //            listOf(Int.serializer(), String.serializer()),
        //            ListSerializer(Dog.serializer())
        //        )
        val arguments = mutableListOf(
            ClientPropertyName,
            FormatPropertyName,
            "%S".formatWith(rpcDefinition.name),
            ApiDefinitionConverters.listOfFunction.withArgumentList(rpcDefinition.parameters.map { it.name }),
            ApiDefinitionConverters.listOfSerializers(rpcDefinition),
        )

        if (returnsValue) arguments.add(rpcDefinition.returnType.toSerializerString())

        this.addStatement("return ".plusFormat(method.withArgumentList(arguments)))
    }

    private fun convertArgument(arg: RpcParameter): ParameterSpec {
        return ParameterSpec(arg.name, arg.type.typeName)
    }
}






© 2015 - 2024 Weber Informatics LLC | Privacy Policy