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

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

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

import com.caesarealabs.rpc4k.processor.utils.appendIf
import com.caesarealabs.rpc4k.processor.utils.poet.kotlinPoet
import com.caesarealabs.rpc4k.runtime.user.Dispatch
import com.caesarealabs.rpc4k.runtime.user.EventTarget
import com.caesarealabs.rpc4k.runtime.implementation.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeName
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder


@Serializable
internal data class RpcApi(
    /**
     * We need the full name in kotlin , and only want the simple name in other languages accessing the generated file.
     *
     * Note: maybe this means i should have a different model for the generated stuff?
     */
    @Serializable(SimpleNameOnlyKotlinNameSerializer::class) val name: KotlinClassName,
    val methods: List,
    val events: List,
    val models: List
)

internal sealed interface RpcEndpoint {
    val name: String
    val returnType: KotlinTypeReference
}

@Serializable
internal data class RpcFunction(override val name: String, val parameters: List,
                                override val returnType: KotlinTypeReference
) : RpcEndpoint


@Serializable internal data class RpcEventEndpoint(
    override val name: String, val parameters: List, override val returnType: KotlinTypeReference
) : RpcEndpoint {
    val targetParameter get() = parameters.find { it.isTarget }?.value
}

@Serializable
internal data class EventParameter(
    /**
     * Whether this parameter is annotated with [Dispatch]
     */
    val isDispatch: Boolean,
    val value: RpcParameter,
    /**
     * Whether this parameter is annotated with [EventTarget]
     */
    val isTarget: Boolean
)

@Serializable
//NiceToHave: support optional parameters
internal data class RpcParameter(val name: String, val type: KotlinTypeReference /*val isOptional: Boolean*//* = false*/)

@Serializable
internal sealed interface RpcModel {
    val name: String
    val typeParameters: List

    @Serializable
    @SerialName("inline")
    data class Inline(override val name: String, override val typeParameters: List = listOf(), val inlinedType: KotlinTypeReference) :
        RpcModel


    @Serializable
    @SerialName("struct")
    data class Struct(
        val hasTypeDiscriminator: Boolean,
        override val name: String,
        // BLOCKED: Get rid of this, i don't want it in the final spec
        val packageName: String,
        override val typeParameters: List = listOf(),
        val properties: List,
    ) : RpcModel {
        /**
         * @param isOptional BLOCKED: Support optional parameters and properties
         */
        @Serializable

        data class Property(val name: String, val type: KotlinTypeReference/* val isOptional: Boolean*//* = false*/)
    }

    @Serializable
    @SerialName("enum")
    data class Enum(override val name: String, val options: List) : RpcModel {
        override val typeParameters: List = listOf()
    }

    /**
     * Important note: Languages implementing this MUST add a `type: ` field to the structs referenced by `options` so the other side
     * may know which of the union options a union value is
     * @param [options] a list of possible types this union can evaluate to.
     */
    @Serializable
    @SerialName("union")
    data class Union(override val name: String, val options: List, override val typeParameters: List = listOf()) :
        RpcModel
}


/**
 * More precise description of an [RpcType] that comes from the JVM, and makes it easier to generate kotlin code as it includes the package name of the type
 * and uses kotlin class names and not RPC class names
 */
@Serializable(with = KotlinTypeReferenceSerializer::class)
internal data class KotlinTypeReference(
    val name: KotlinClassName,
//    val packageName: String,
//    val simpleName: String,
    val isNullable: Boolean = false,
    // True in cases where the value is initialized by a default value
    val hasDefaultValue: Boolean = false,
    val typeArguments: List = listOf(),
    val isTypeParameter: Boolean = false,
    val inlinedType: KotlinTypeReference? = null
) {
    companion object {
        val string = KotlinTypeReference(KotlinClassName("kotlin", "String"))
    }

    // Inner classes are dot seperated
//    val qualifiedName = "$packageName.$simpleName"

    val rawTypeName = name.kotlinPoet

    val typeName: TypeName = rawTypeName.let { name ->
        if (typeArguments.isEmpty()) name else name.parameterizedBy(typeArguments.map { it.typeName })
    }.copy(nullable = isNullable)
    val isUnit get() = name.isUnit
}


// BLOCKED: get rid of packageName here
@Serializable
internal data class RpcType(
    val name: String,
    val isTypeParameter: Boolean = false,
    val isNullable: Boolean = false,
    val typeArguments: List = listOf(),
    val inlinedType: RpcType? = null
) {

    init {
        // Kotlin doesn't have higher-kinded types yet
        if (isTypeParameter) check(typeArguments.isEmpty()
        ) { "It doesn't make sense for type parameter <$name> to have type parameters: <${typeArguments.joinToString()}>" }
        if (isTypeParameter) check(inlinedType == null) { "It doesn't make sense for type parameter <$name> to be an inlined type: $inlinedType" }
    }

    override fun toString(): String {
        val string = if (isTypeParameter) {
            name
        } else {
            name
                .appendIf(typeArguments.isNotEmpty()) { "<${typeArguments.joinToString()}>" }
                .appendIf(inlinedType != null) { "(Inlining $inlinedType)" }
        }
        return string.appendIf(isNullable) { "?" }
    }

    companion object BuiltinNames {
        const val Bool = "bool"
        const val I8 = "i8"
        const val I16 = "i16"
        const val I32 = "i32"
        const val I64 = "i64"
        const val I8Array = "i8array"
        const val UnsignedI8Array = "ui8array"
        const val UUID = "uuid"
        const val F32 = "f32"
        const val F64 = "f64"
        const val Char = "char"
        const val String = "string"
        const val Void = "void"
        const val Array = "array"
        const val Record = "record"
        const val Tuple = "tuple"
        const val Date = "date"
        const val Duration = "duration"
    }
}


internal const val GeneratedModelsPackage = "${ApiDefinitionUtils.Package}.models"


/**
 * [KotlinTypeReference] is serialized to a [RpcType]
 */
internal class KotlinTypeReferenceSerializer : KSerializer {
    override val descriptor = RpcType.serializer().descriptor

    override fun deserialize(decoder: Decoder): KotlinTypeReference {
        return decoder.decodeSerializableValue(RpcType.serializer()).toKotlinTypeReference(GeneratedModelsPackage)
    }

    override fun serialize(encoder: Encoder, value: KotlinTypeReference) {
        encoder.encodeSerializableValue(RpcType.serializer(), value.toRpcType())
    }
}

private fun RpcType.toKotlinTypeReference(packageName: String): KotlinTypeReference {
    // NiceToHave: Generate Kotlin clients from non-kotlin servers
    throw UnsupportedOperationException("Generate Kotlin clients from non-kotlin servers")
}


private fun KotlinTypeReference.toRpcType() = when (name.pkg) {
    "kotlin" -> toBuiltinRpcType()
    "kotlin.collections" -> toBuiltinCollectionType()
    "kotlin.time" -> toDurationType()
    "kotlinx.datetime" -> toDateType()
    // com.benasher44.uuid.Uuid typealias still sometimes annoyingly evaluates as java.util.UUID so we need to treat them as the same
    "com.benasher44.uuid" , "java.util" -> toUUID()
    else -> toUserType()
}



/**
 * Converts Kotlin types like Int to RPC types like i32.
 */
private fun KotlinTypeReference.toBuiltinRpcType(): RpcType {
    val primitive = primitiveKotlinToRpcTypes[name.simple]
    if (primitive != null) return buildRpcType(name = primitive)
    val array = arrayKotlinToRpcTypes[name.simple]
    if (array != null) return buildRpcType(name = RpcType.Array, typeArguments = listOf(RpcType(name = array)))

    return when (name.simple) {
        "Pair", "Triple", "Array" -> {
            // Types that use normal type arguments - pair, triple, array
            val name = if (name.simple == "Array") RpcType.Array else RpcType.Tuple
            buildRpcType(name = name)
        }

        else -> error(
            "Unexpected kotlin builtin type: ${rawTypeName}." +
                " Was a custom class declared in kotlin.*? This is probably a bug in RPC4K."
        )
    }
}

private fun KotlinTypeReference.toBuiltinCollectionType(): RpcType {
    val name = when (name.simple) {
        "List", "Set", "MutableList", "MutableSet" -> RpcType.Array
        "Map", "MutableMap" -> RpcType.Record
        "Map.Entry" -> RpcType.Tuple
        else -> error(
            "Unexpected kotlin builtin collection type: ${rawTypeName}." +
                " Was a custom class declared in kotlin.collections.*? This is probably a bug in RPC4K."
        )
    }

    return buildRpcType(name = name)
}


private val arrayKotlinToRpcTypes: Map = buildMap {
    putTwo("ShortArray", "UShortArray", RpcType.I16)
    putTwo("IntArray", "UIntArray", RpcType.I32)
    putTwo("LongArray", "ULongArray", RpcType.I64)
    put("CharArray", RpcType.Char)
}

private val primitiveKotlinToRpcTypes: Map = buildMap {
    put("Boolean", RpcType.Bool)
    putTwo("Byte", "UByte", RpcType.I8)
    putTwo("Short", "UShort", RpcType.I16)
    putTwo("Int", "UInt", RpcType.I32)
    putTwo("Long", "ULong", RpcType.I64)
    put("Char", RpcType.Char)
    put("String", RpcType.String)
    put("Unit", RpcType.Void)
    put("Float", RpcType.F32)
    put("Double", RpcType.F64)
    // We add special support for i8 arrays because they are useful
    put("ByteArray", RpcType.I8Array)
    put("UByteArray", RpcType.UnsignedI8Array)
}

private fun MutableMap.putTwo(key1: String, key2: String, value: String) {
    put(key1, value)
    put(key2, value)
}


private fun KotlinTypeReference.buildRpcType(
    name: String,
    isNullable: Boolean = this.isNullable,
    typeArguments: List = this.typeArguments.map { it.toRpcType() }
): RpcType {
    return RpcType(name = name, isNullable = isNullable, typeArguments = typeArguments)
}

private fun KotlinTypeReference.toDateType(): RpcType {
    if (name.simple == "Instant") {
        return buildRpcType(name = RpcType.Date, typeArguments = listOf())
    } else {
        error("Unexpected kotlin date type: ${name.simple}. These shouldn't be accepted by the compiler.")
    }
}

private fun KotlinTypeReference.toUUID(): RpcType {
    if (name.simple == "Uuid" || name.simple == "UUID") {
        return buildRpcType(name = RpcType.UUID, typeArguments = listOf())
    } else {
        error("Unexpected kotlin java.util type: ${name.simple}. These shouldn't be accepted by the compiler.")
    }
}

private fun KotlinTypeReference.toDurationType(): RpcType {
    if (name.simple == "Duration") {
        return buildRpcType(name = RpcType.Duration, typeArguments = listOf())
    } else {
        error("Unexpected kotlin kotlin.time type: ${name.simple}. These shouldn't be accepted by the compiler.")
    }
}


private fun KotlinTypeReference.toUserType(): RpcType {
    return RpcType(
        name = name.simple,
        isNullable = isNullable,
        isTypeParameter = isTypeParameter,
        typeArguments = typeArguments.map { it.toRpcType() },
        inlinedType = inlinedType?.toRpcType()
    )
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy