From 0efe839a0bf451258d39251125e56dcc71dacd66 Mon Sep 17 00:00:00 2001 From: worstell Date: Tue, 23 Jan 2024 19:06:05 -0500 Subject: [PATCH] feat: support generics in kotlin (#828) --- .../block/ftl/generator/ModuleGenerator.kt | 10 +++++++- .../ftl/generator/ModuleGeneratorTest.kt | 25 ++++++++++++++++++- .../ftl/schemaextractor/ExtractSchemaRule.kt | 18 +++++++++++++ .../schemaextractor/ExtractSchemaRuleTest.kt | 22 +++++++++++----- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/kotlin-runtime/ftl-generator/src/main/kotlin/xyz/block/ftl/generator/ModuleGenerator.kt b/kotlin-runtime/ftl-generator/src/main/kotlin/xyz/block/ftl/generator/ModuleGenerator.kt index 7a470b5bd..ce94c2b34 100644 --- a/kotlin-runtime/ftl-generator/src/main/kotlin/xyz/block/ftl/generator/ModuleGenerator.kt +++ b/kotlin-runtime/ftl-generator/src/main/kotlin/xyz/block/ftl/generator/ModuleGenerator.kt @@ -68,6 +68,7 @@ class ModuleGenerator() { private fun buildDataClass(type: Data, namespace: String): TypeSpec { val dataClassBuilder = TypeSpec.classBuilder(type.name) .addModifiers(KModifier.DATA) + .addTypeVariables(type.typeParameters.map { TypeVariableName(it.name) }) .addKdoc(type.comments.joinToString("\n")) val dataConstructorBuilder = FunSpec.constructorBuilder() @@ -146,6 +147,7 @@ class ModuleGenerator() { type.bool != null -> ClassName("kotlin", "Boolean") type.time != null -> ClassName("java.time", "OffsetDateTime") type.any != null -> ClassName("kotlin", "Any") + type.parameter != null -> TypeVariableName(type.parameter.name) type.array != null -> { val element = type.array?.element ?: throw IllegalArgumentException( "Missing element type in kotlin array generator" @@ -167,7 +169,13 @@ class ModuleGenerator() { type.dataRef != null -> { val module = if (type.dataRef.module.isEmpty()) namespace else "ftl.${type.dataRef.module}" - ClassName(module, type.dataRef.name) + ClassName(module, type.dataRef.name).let { className -> + if (type.dataRef.typeParameters.isNotEmpty()) { + className.parameterizedBy(type.dataRef.typeParameters.map { getTypeClass(it, namespace) }) + } else { + className + } + } } type.optional != null -> { diff --git a/kotlin-runtime/ftl-generator/src/test/kotlin/xyz/block/ftl/generator/ModuleGeneratorTest.kt b/kotlin-runtime/ftl-generator/src/test/kotlin/xyz/block/ftl/generator/ModuleGeneratorTest.kt index 6aa3d6bb2..373bdf9db 100644 --- a/kotlin-runtime/ftl-generator/src/test/kotlin/xyz/block/ftl/generator/ModuleGeneratorTest.kt +++ b/kotlin-runtime/ftl-generator/src/test/kotlin/xyz/block/ftl/generator/ModuleGeneratorTest.kt @@ -36,10 +36,20 @@ public class TestModule() @Test fun `should generate all Types`() { val decls = listOf( + Decl( + data_ = Data( + name = "ParamTestData", + typeParameters = listOf(TypeParameter(name = "T")), + fields = listOf( + Field(name = "t", type = Type(parameter = TypeParameter(name = "T"))), + ) + ) + ), Decl(data_ = Data(comments = listOf("Request comments"), name = "TestRequest")), Decl( data_ = Data( - comments = listOf("Response comments"), name = "TestResponse", fields = listOf( + comments = listOf("Response comments"), name = "TestResponse", + fields = listOf( Field(name = "int", type = Type(int = Int())), Field(name = "float", type = Type(float = Float())), Field(name = "string", type = Type(string = String())), @@ -73,6 +83,14 @@ public class TestModule() Field(name = "dataRef", type = Type(dataRef = DataRef(name = "TestRequest"))), Field(name = "externalDataRef", type = Type(dataRef = DataRef(module = "other", name = "TestRequest"))), Field(name = "any", type = Type(any = xyz.block.ftl.v1.schema.Any())), + Field( + name = "parameterizedDataRef", type = Type( + dataRef = DataRef( + name = "ParamTestData", + typeParameters = listOf(Type(parameter = TypeParameter(name = "T"))) + ) + ) + ), ) ) ), @@ -96,6 +114,10 @@ import kotlin.collections.ArrayList import kotlin.collections.Map import xyz.block.ftl.Ignore +public data class ParamTestData( + public val t: T, +) + /** * Request comments */ @@ -120,6 +142,7 @@ public data class TestResponse( public val dataRef: TestRequest, public val externalDataRef: ftl.other.TestRequest, public val any: Any, + public val parameterizedDataRef: ParamTestData, ) @Ignore diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt index 92b156aa6..e4d52ca25 100644 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt +++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt @@ -19,6 +19,7 @@ import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.resolve.typeBinding.createTypeBindingForReturnType import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.isTypeParameterTypeConstructor import org.jetbrains.kotlin.types.getAbbreviation import org.jetbrains.kotlin.types.isNullable import org.jetbrains.kotlin.types.typeUtil.isAny @@ -42,6 +43,7 @@ import kotlin.io.path.createDirectories data class ModuleData(val comments: List = emptyList(), val decls: MutableSet = mutableSetOf()) +data class blah(val a: T) // Helpers private fun DataRef.compare(module: String, name: String): Boolean = this.name == name && this.module == module private fun DataRef.text(): String = "${this.module}.${this.name}" @@ -377,11 +379,26 @@ class SchemaExtractor( ) }.toList(), comments = this.comments(), + typeParameters = this.children.flatMap { (it as? KtTypeParameterList)?.parameters ?: emptyList() }.map { + TypeParameter( + name = it.name!!, + pos = getLineAndColumnInPsiFile(it.containingFile, it.textRange).toPosition(it.containingKtFile.name), + ) + }.toList(), pos = getLineAndColumnInPsiFile(this.containingFile, this.textRange).toPosition(this.containingKtFile.name), ) } private fun KotlinType.toSchemaType(position: Position): Type { + if (this.unwrap().constructor.isTypeParameterTypeConstructor()) { + return Type( + parameter = TypeParameter( + name = this.constructor.declarationDescriptor?.name?.asString() ?: "T", + pos = position, + ) + ) + } + val type = when (this.fqNameOrNull()?.asString()) { String::class.qualifiedName -> Type(string = xyz.block.ftl.v1.schema.String()) Int::class.qualifiedName -> Type(int = xyz.block.ftl.v1.schema.Int()) @@ -437,6 +454,7 @@ class SchemaExtractor( name = refName, module = fqName.extractModuleName().takeIf { it != currentModuleName } ?: "", pos = position, + typeParameters = this.arguments.map { it.type.toSchemaType(position) }.toList(), ) ) } diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt index ef48ebf24..e82e7b5f0 100644 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt +++ b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt @@ -38,7 +38,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { /** * Request to echo a message. */ - data class EchoRequest(val name: String, val stuff: Any) + data class EchoRequest(val t: T, val name: String, val stuff: Any) data class EchoResponse(val messages: List) /** @@ -51,7 +51,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { @Throws(InvalidInput::class) @Verb @Ingress(Method.GET, "/echo") - fun echo(context: Context, req: EchoRequest): EchoResponse { + fun echo(context: Context, req: EchoRequest): EchoResponse { callTime(context) return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) } @@ -96,15 +96,15 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { name = "metadata", type = Type( map = Map( - key = xyz.block.ftl.v1.schema.Type(string = xyz.block.ftl.v1.schema.String()), - value_ = xyz.block.ftl.v1.schema.Type( + key = Type(string = xyz.block.ftl.v1.schema.String()), + value_ = Type( dataRef = DataRef( name = "MapValue", ) ) ) ) - ) + ), ), ), ), @@ -112,6 +112,10 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { data_ = Data( name = "EchoRequest", fields = listOf( + Field( + name = "t", + type = Type(parameter = TypeParameter(name = "T")) + ), Field( name = "name", type = Type(string = xyz.block.ftl.v1.schema.String()) @@ -126,6 +130,9 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { * Request to echo a message. */""" ), + typeParameters = listOf( + TypeParameter(name = "T") + ) ), ), Decl( @@ -136,7 +143,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { name = "messages", type = Type( array = Array( - element = xyz.block.ftl.v1.schema.Type( + element = Type( dataRef = DataRef( name = "EchoMessage", ) @@ -158,6 +165,9 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { request = Type( dataRef = DataRef( name = "EchoRequest", + typeParameters = listOf( + Type(string = xyz.block.ftl.v1.schema.String()) + ) ) ), response = Type(