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

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

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

import com.caesarealabs.rpc4k.processor.ApiDefinitionUtils.ignoreExperimentalWarnings
import com.caesarealabs.rpc4k.processor.ApiDefinitionUtils.listOfEventSubSerializers
import com.caesarealabs.rpc4k.processor.utils.poet.*
import com.caesarealabs.rpc4k.runtime.api.HandlerConfig
import com.caesarealabs.rpc4k.runtime.api.RpcRouter
import com.caesarealabs.rpc4k.runtime.implementation.GeneratedCodeUtils
import com.caesarealabs.rpc4k.runtime.user.RPCContext
import com.caesarealabs.rpc4k.runtime.user.Rpc4kIndex
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)
 *     }
 * }
 * ```
 * into
 * ```
 * public fun BasicApi.Companion.server(): BasicApiServerImpl = BasicApiServerImpl()
 *
 * @Suppress("UNCHECKED_CAST")
 * public class BasicApiServerImpl: GeneratedServerHelper {
 *     override suspend fun handle(request: ByteArray, method: String, setup: RpcSetup): ByteArray? = when (method) {
 *         "getDogs" -> respond(setup, request, listOf(Int.serializer(), String.serializer()), ListSerializer(Dog.serializer())
 *         ) {
 *             setup.handler.getDogs(it[0] as Int, it[1] as String)
 *         }
 *
 *         "putDog" -> respond(setup, request, listOf(Dog.serializer()),
 *             VoidUnitSerializer()
 *         ) {
 *             setup.handler.putDog(it[0] as Dog)
 *         }
 *
 *         else -> null
 *     }
 * }
 * ```
 *
 * Which makes running client code much easier.
 */
internal class ApiDefinitionToServerCode(private val api: RpcApi) {
    companion object {
        private const val Config: String = "config"
        private const val UserHandlerPropertyName = "handler"
        private const val Context = "context"

        private const val RequestParamName = "request"
        private const val MethodParamName = "method"

        private val respondUtilsMethod = GeneratedCodeUtils::class.member("respond")
        private val invokeEventUtilsMethod = GeneratedCodeUtils::class.member("invokeEvent")
        private const val InvokerSuffix = "EventInvoker"
        private const val ParticipantsParamName = "participants"
        private const val ContextParamName = "rpcContext"
        private val ParticipantsParamType = Set::class.parameterizedBy(String::class)
        private val ContextParamType = RPCContext::class
    }

    private val invokerName = "${api.name.simple}$InvokerSuffix"
    private val invokerClassName = ClassName(ApiDefinitionUtils.Package, invokerName)
    private val routerName = "${api.name.simple}${ApiDefinitionUtils.ServerSuffix}"
    private val routerClassName = ClassName(ApiDefinitionUtils.Package, routerName)
    private val clientClassName = ClassName(ApiDefinitionUtils.Package, api.name.simple + ApiDefinitionUtils.NetworkClientSuffix)
    private val serverClassName = api.name.kotlinPoet

    // Since HandlerConfig is a type alias we need to specify its name explicitly
    private val handlerConfig = ClassName(HandlerConfig::class.asClassName().packageName, "HandlerConfig").parameterizedBy(serverClassName)

    fun convert(): FileSpec {
        return fileSpec(ApiDefinitionUtils.Package, routerName) {
            // I know what I'm doing, Kotlin!
            addAnnotation(AnnotationSpec.builder(Suppress::class).addMember("%S", "UNCHECKED_CAST").build())
            ignoreExperimentalWarnings()

            // KotlinPoet doesn't handle extension methods well
            addImport("kotlinx.serialization.builtins", "serializer")
            addImport("kotlinx.serialization.builtins", "nullable")

            addProperty(rpc4kGeneratedSuiteExtension())

            addRouter()
            addInvokerClass()
        }
    }

    private fun FileSpec.Builder.addRouter() {
        addObject(routerName) {
            addSuperinterface(
                RpcRouter::class.asClassName().parameterizedBy(
                    serverClassName
                )
            )
            addFunction(handleRequestMethod())
        }
    }

    /**
     * val MyApi.Companion.server = object : GeneratedSuiteFactory {
     *     override val createInvoker = ::AllEncompassingServiceEventInvoker
     *     override val createNetworkClient = ::AllEncompassingServiceClientImpl
     * }
     *
     */
    private fun rpc4kGeneratedSuiteExtension(): PropertySpec {
        val suiteType = Rpc4kIndex::class.asTypeName().parameterizedBy(serverClassName, clientClassName, invokerClassName)
        return extensionProperty(serverClassName.companion(), "rpc4k", suiteType) {
            addCode(
                """
                            |return object: %T {
                            |   override val createInvoker = ::%T
                            |   override val createNetworkClient = ::%T
                            |   override val router = %T
                            |}
                            |""".trimMargin(),
                suiteType, invokerClassName, clientClassName, routerClassName
            )
        }
    }


    /**
     * Generates:
     * ```
     *     override suspend fun handle(request: ByteArray, method: String, setup: RpcSetup): ByteArray? = when (method) {
     *         "getDogs" -> respond(setup, request, listOf(Int.serializer(), String.serializer()), ListSerializer(Dog.serializer())
     *         ) {
     *             setup.handler.getDogs(it[0] as Int, it[1] as String)
     *         }
     *
     *         "putDog" -> respond(setup, request, listOf(Dog.serializer()),
     *             VoidUnitSerializer()
     *         ) {
     *             setup.handler.putDog(it[0] as Dog)
     *         }
     *
     *         else -> null
     *     }
     * ```
     */
    private fun handleRequestMethod(): FunSpec = funSpec("routeRequest") {
        // This overrides GeneratedServerHandler
        addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND)

        addParameter(RequestParamName, ByteArray::class)
        addParameter(MethodParamName, String::class)
        addParameter(Config, handlerConfig)
        addParameter(Context, RPCContext::class)

        returns(BYTE_ARRAY.copy(nullable = true))

        addControlFlow("return when($MethodParamName)") {
            for (method in api.methods) {
                addEndpointHandler(method)
            }
            addCode("else -> null\n")
        }
    }

    /**
     * Generates:
     * ```
     * respond(setup, request, listOf(Int.serializer(), String.serializer()), ListSerializer(Dog.serializer())) {
     *     setup.handler.getDogs(it[0] as Int, it[1] as String)
     * }
     * ```
     */
    private fun FunSpec.Builder.addEndpointHandler(rpc: RpcFunction) {
        addCode("%S -> ".formatWith(rpc.name))


        val arguments = listOf(
            Config,
            RequestParamName,
            ApiDefinitionUtils.listOfSerializers(rpc),
            rpc.returnType.toSerializerString(),
            Context
        )

        addControlFlow(respondUtilsMethod.withArgumentList(arguments)) {
            functionHandleCall(rpc)
        }
    }

    private fun FunSpec.Builder.functionHandleCall(rpc: RpcFunction) {
        addControlFlow("with($Config.$UserHandlerPropertyName)") {
            addStatement(rpc.name.withMethodArguments(functionArguments(rpc)))
        }
    }

    private fun functionArguments(rpc: RpcFunction) = rpc.parameters.mapIndexed { i, arg ->
        "it[$i] as %T".formatWith(arg.type.typeName)
    }


    /**
     * Generates something like this:
     * ```kotlin
     * class GeneratedEventInvokerExample: GeneratedEventInvoker() {
     *     suspend fun theoreticalGeneratedFunctionTest(title: String, row: AddedRow) {
     *         GeneratedCodeUtils.invokeEvent("test", listOf(row), title, setup)
     *     }
     *
     *     suspend fun theoreticalGeneratedFunctionTest2(row: TestEvent2Context) {
     *         GeneratedCodeUtils.invokeEvent("test2", listOf(row), null, setup)
     *     }
     * }
     * ```
     */
    private fun FileSpec.Builder.addInvokerClass() {
        addClass(invokerName) {

            addPrimaryConstructor {
                addConstructorProperty(Config, handlerConfig, KModifier.PRIVATE)
            }

            for (event in api.events) {
                addFunction(eventInvoker(event))
            }
        }
    }


    /**
     * ```kotlin
     *            GeneratedCodeUtils.invokeEvent(config, "eventTest",listOf(String.serializer()),String.serializer(),target.toString()) {
     *               config.handler.eventTest(dispatchParam, it[0] as String)
     *           }
     *     ```
     */
    private fun eventInvoker(event: RpcEventEndpoint) = funSpec("invoke${event.name.replaceFirstChar { it.uppercaseChar() }}") {
        val dispatchParameters = event.parameters.filter { it.isDispatch || it.isTarget }.map { it.value }
        addModifiers(KModifier.SUSPEND)
        for (parameter in dispatchParameters) {
            addParameter(parameter.name, parameter.type.typeName)
        }
        // Add support for passing RPC context
        addParameter(
            ParameterSpec.builder(ContextParamName, ContextParamType)
                // Default is RpcContext.Default
                .defaultValue("%M", RPCContext.Companion::class.member("Default"))
                .build()
        )
        // Add support for not sending events to "participants"
        addParameter(ParameterSpec.builder(ParticipantsParamName, ParticipantsParamType).defaultValue("setOf()").build())
        addKdoc("@param $ParticipantsParamName Listeners that will not be invoked as they have caused the event.")

        val targetParameter = event.targetParameter?.name

        val paramSerializers = listOfEventSubSerializers(event)


        val target = if (targetParameter != null) "${targetParameter}.toString()" else ""
        val arguments = listOf(
            Config,
            "\"${event.name}\"",
            paramSerializers,
            event.returnType.toSerializerString(),
            ContextParamName,
            ParticipantsParamName,
            target
        )
        addControlFlow(invokeEventUtilsMethod.withArgumentList(arguments)) {
            eventTransformCall(event)
        }
    }

    private fun FunSpec.Builder.eventTransformCall(rpc: RpcEventEndpoint) {
        addStatement("$Config.$UserHandlerPropertyName.${rpc.name}".withMethodArguments(eventArguments(rpc)))
    }

    private fun eventArguments(rpc: RpcEventEndpoint): List {
        // We draw from both lists, according to which parameter is a dispatch param and which is an event param.
        var eventIndex = 0
        var dispatchIndex = 0
        val dispatchOrTargetParams = rpc.parameters.filter { it.isDispatch || it.isTarget }
        return rpc.parameters.map { parameter ->
            val dispatchValue = parameter.isDispatch || parameter.isTarget
            // We use the dispatch value for the @EventTarget value, which allows us to have the real value without serialization.
//            val targetList = if (dispatchValue) DispatcherDataParamName else "it"
            val index = if (dispatchValue) dispatchIndex++ else eventIndex++
            if (dispatchValue) dispatchOrTargetParams[index].value.name.formatString()
            else "it[$index] as %T".formatWith(parameter.value.type.typeName)
        }
    }
}






© 2015 - 2024 Weber Informatics LLC | Privacy Policy