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

jvmMain.com.caesarealabs.rpc4k.processor.ApiDefinitionToNetworkClient.kt Maven / Gradle / Ivy

The newest version!
package com.caesarealabs.rpc4k.processor

import com.caesarealabs.rpc4k.processor.ApiDefinitionUtils.ignoreExperimentalWarnings
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.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.MemberName.Companion.member
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy

/**
 * 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)
 *     }
 *
 *     @RpcEvent fun dogEvent(@Dispatch dispatchParam: Int, @EventTarget target: String, clientParam: Boolean): Int {
 *          return if (clientParam) dispatchParam else dispatchParam + 2
 *     }
 * }
 * ```
 * into
 * ```
 * class MyApiNetworkClient(
 *     private val client: RpcClient,
 *     private val format: SerializationFormat,
 * ) {
 *     suspend fun getDogs(num: Int, type: String): List = request(
 *         client, format, "getDogs",
 *         listOf(num, type), listOf(Int.serializer(), String.serializer()),
 *         ListSerializer(Dog.serializer())
 *     )
 *
 *     suspend fun putDog(dog: Dog): Unit = send(
 *         client, format, "putDog", listOf(dog),
 *         listOf(Dog.serializer())
 *     )
 *
 *      override fun dogEvent(target: String, clientParam: Boolean): EventSubscription =
 *       coldEventFlow(client, format, "dogEvent", listOf(clientParam), listOf(Boolean.serializer()),
 *       Int.serializer(), target)
 * }
 * ```
 *
 * Which makes running client code much easier.
 */
internal object ApiDefinitionToNetworkClient {
    private const val ClientPropertyName = "client"
    private const val FormatPropertyName = "format"
    private val sendMethod = GeneratedCodeUtils::class.member("send")
    private val requestMethod = GeneratedCodeUtils::class.member("request")
    private val coldEventFlow = GeneratedCodeUtils::class.member("coldEventFlow")

    fun convert(apiDefinition: RpcApi): FileSpec {
        val className = "${apiDefinition.name.simple}${ApiDefinitionUtils.NetworkClientSuffix}"
        return fileSpec(ApiDefinitionUtils.Package, className) {
            // KotlinPoet doesn't handle extension methods well
            addImport("kotlinx.serialization.builtins", "serializer")
            addImport("kotlinx.serialization.builtins", "nullable")

            ignoreExperimentalWarnings()

            addClass(className) {
                //LOWPRIO: Improve server testing with "in-memory-server" client generation
                // addSuperinterface(ClassName(ApiDefinitionUtils.Package, ApiDefinitionToClientInterface.interfaceName(apiDefinition)))
                // addType(factoryCompanionObject(className))

                addPrimaryConstructor {
                    addConstructorProperty(ClientPropertyName, type = RpcClient::class, KModifier.PRIVATE)
                    addConstructorProperty(FormatPropertyName, type = SerializationFormat::class, KModifier.PRIVATE)
                }
                for (method in apiDefinition.methods) addFunction(requestMethod(method))
                for (event in apiDefinition.events) addFunction(eventSubMethod(event))
            }
        }
    }


//    /**
//     * 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)
//     *         }
//     *     }
//     * ```
//     */
//    private fun factoryCompanionObject(generatedClassName: String) = companionObject(ApiDefinitionUtils.FactoryName) {
//        val generatedClientClass = ClassName(ApiDefinitionUtils.Package, generatedClassName)
//        addSuperinterface(GeneratedClientImplFactory::class.asClassName().parameterizedBy(generatedClientClass))
//        addFunction("build") {
//            addModifiers(KModifier.OVERRIDE)
//            addParameter(ClientPropertyName, RpcClient::class)
//            addParameter(FormatPropertyName, SerializationFormat::class)
//            returns(generatedClientClass)
//            addStatement("return $generatedClassName($ClientPropertyName, $FormatPropertyName)")
//        }
//    }


    /**
     * Creates a method like
     * ```
     *       suspend fun getDogs(num: Int, type: String): List = request(
     *           client, format, "getDogs",
     *           listOf(num, type), listOf(Int.serializer(), String.serializer()),
     *           ListSerializer(Dog.serializer())
     *       )
     *
     *  ```
     */
    private fun requestMethod(rpcDefinition: RpcFunction): FunSpec = ApiDefinitionToClient.createRequest(rpcDefinition) {
        //LOWPRIO: Improve server testing with "in-memory-server" client generation
        // addModifiers(KModifier.OVERRIDE)
        val returnsValue = !rpcDefinition.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),
            ApiDefinitionUtils.listOfFunction.withArgumentList(rpcDefinition.parameters.map { it.name }),
            ApiDefinitionUtils.listOfSerializers(rpcDefinition),
        )

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

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


    /**
     * Creates a method akin to
     * ```kotlin
     *         suspend fun eventTargetTest(normal: String, target: Int): EventSubscription {
     *         return createFlow(client, format, "eventTargetTest", listOf(normal), listOf(String.serializer()), String.serializer(), target)
     *     }
     * ```
     */
    private fun eventSubMethod(event: RpcEventEndpoint): FunSpec = ApiDefinitionToClient.createEventSubscription(event) {
        //LOWPRIO: Improve server testing with "in-memory-server" client generation
        // addModifiers(KModifier.OVERRIDE)
        val normalArgs = event.parameters.filter { !it.isDispatch && !it.isTarget }.map { it.value.name }


        val arguments = mutableListOf(
            ClientPropertyName,
            FormatPropertyName,
            "%S".formatWith(event.name),
//            We only need the dispatch parameters
            if (normalArgs.isEmpty()) "listOf()" else ApiDefinitionUtils.listOfFunction.withArgumentList(normalArgs),
            ApiDefinitionUtils.listOfEventSubSerializers(event),
            event.returnType.toSerializerString()
        )

        val targetParameter = event.parameters.find { it.isTarget }

        // Pass the target if its relevant
        if (targetParameter != null) arguments.add(targetParameter.value.name)

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

}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy