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 e5631074ca..359614adcc 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 @@ -8,11 +8,6 @@ import io.gitlab.arturbosch.detekt.api.Severity import io.gitlab.arturbosch.detekt.api.config import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution import io.gitlab.arturbosch.detekt.rules.fqNameOrNull -import java.io.File -import java.io.FileOutputStream -import java.nio.file.Path -import java.time.OffsetDateTime -import kotlin.io.path.createDirectories import org.jetbrains.kotlin.cfg.getDeclarationDescriptorIncludingConstructors import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange import org.jetbrains.kotlin.com.intellij.psi.PsiComment @@ -20,7 +15,6 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.impl.referencedProperty -import org.jetbrains.kotlin.descriptors.isFinalOrEnum import org.jetbrains.kotlin.diagnostics.DiagnosticUtils.getLineAndColumnInPsiFile import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils.LineAndColumn import org.jetbrains.kotlin.name.FqName @@ -28,6 +22,7 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtEnumEntry import org.jetbrains.kotlin.psi.KtExpression @@ -66,10 +61,14 @@ import xyz.block.ftl.Database import xyz.block.ftl.HttpIngress import xyz.block.ftl.Json import xyz.block.ftl.Method +import xyz.block.ftl.secrets.Secret import xyz.block.ftl.v1.schema.* -import xyz.block.ftl.v1.schema.Array import xyz.block.ftl.v1.schema.Enum -import xyz.block.ftl.v1.schema.Verb +import java.io.File +import java.io.FileOutputStream +import java.nio.file.Path +import java.time.OffsetDateTime +import kotlin.io.path.createDirectories data class ModuleData(val comments: List = emptyList(), val decls: MutableSet = mutableSetOf()) @@ -169,6 +168,8 @@ class SchemaExtractor( *extractDataDeclarations().toTypedArray(), *extractDatabases().toTypedArray(), *extractEnums().toTypedArray(), + *extractConfigs().toTypedArray(), + *extractSecrets().toTypedArray(), ), comments = moduleComments ) @@ -292,6 +293,58 @@ class SchemaExtractor( .toSet() } + private fun extractConfigs(): Set { + return extractSecretOrConfig(xyz.block.ftl.config.Config::class.qualifiedName!!).map { + Decl( + config = Config( + pos = it.position, + name = it.name, + type = it.type + ) + ) + }.toSet() + } + + private fun extractSecrets(): Set { + return extractSecretOrConfig(Secret::class.qualifiedName!!).map { + Decl( + secret = Secret( + pos = it.position, + name = it.name, + type = it.type + ) + ) + }.toSet() + } + + data class SecretConfigData(val name: String, val type: Type, val position: Position) + + private fun extractSecretOrConfig(qualifiedPropertyName: String): List { + return file.declarations + .filter { + (it as? KtProperty) + ?.getDeclarationDescriptorIncludingConstructors(bindingContext)?.referencedProperty?.returnType + ?.fqNameOrNull()?.asString() == qualifiedPropertyName + }.flatMap { it.children.asSequence() } + .map { + val position = it.getLineAndColumn().toPosition() + var type: KotlinType? = null + var name = "" + (it as? KtCallExpression)?.getResolvedCall(bindingContext)?.valueArguments?.entries?.forEach { + if (it.key.name.asString() == "name") { + name = it.value.toString().trim('"') + } else if (it.key.name.asString() == "cls") { + type = (it.key.varargElementType ?: it.key.type).arguments.single().type + } + } ?: (it as? KtDotQualifiedExpression)?.getResolvedCall(bindingContext)?.let { call -> + name = call.valueArguments.entries.single().value.toString().trim('"') + type = call.typeArguments.values.single() + } + + SecretConfigData(name, type!!.toSchemaType(position), position) + } + } + private fun extractDataDeclarations(): Set { return file.children .filter { (it is KtClass && it.isData()) || it is KtTypeAlias } 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 602c5d2c47..af9dfdfd37 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 @@ -3,9 +3,6 @@ package xyz.block.ftl.schemaextractor import io.gitlab.arturbosch.detekt.api.Config import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext -import java.io.File -import kotlin.test.AfterTest -import kotlin.test.assertContains import org.assertj.core.api.Assertions.assertThat import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots @@ -31,12 +28,16 @@ import xyz.block.ftl.v1.schema.Module import xyz.block.ftl.v1.schema.Optional import xyz.block.ftl.v1.schema.Position import xyz.block.ftl.v1.schema.Ref +import xyz.block.ftl.v1.schema.String import xyz.block.ftl.v1.schema.StringValue import xyz.block.ftl.v1.schema.Type import xyz.block.ftl.v1.schema.TypeParameter import xyz.block.ftl.v1.schema.Unit import xyz.block.ftl.v1.schema.Value import xyz.block.ftl.v1.schema.Verb +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.assertContains @KotlinCoreEnvironmentTest internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { @@ -599,8 +600,16 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { name = "StringThing", comments = listOf("Comments."), variants = listOf( - EnumVariant(name = "A", value_ = Value(stringValue = StringValue(value_ = "A")), comments = listOf("A comment.")), - EnumVariant(name = "B", value_ = Value(stringValue = StringValue(value_ = "B")), comments = listOf("B comment.")), + EnumVariant( + name = "A", + value_ = Value(stringValue = StringValue(value_ = "A")), + comments = listOf("A comment.") + ), + EnumVariant( + name = "B", + value_ = Value(stringValue = StringValue(value_ = "B")), + comments = listOf("B comment.") + ), EnumVariant(name = "C", value_ = Value(stringValue = StringValue(value_ = "C"))), ), ), @@ -624,4 +633,116 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { .ignoringFieldsMatchingRegexes(".*hashCode\$") .isEqualTo(expected) } + + + @Test + fun `extracts secrets and configs`() { + val code = """ + package ftl.test + + import ftl.time.Color + import xyz.block.ftl.Json + import xyz.block.ftl.Context + import xyz.block.ftl.Method + import xyz.block.ftl.Verb + import xyz.block.ftl.config.Config + import xyz.block.ftl.secrets.Secret + + val secret = Secret.new("secret") + val anotherSecret = Secret(String::class.java, "anotherSecret") + + val config = Config.new("config") + val anotherConfig = Config(ConfigData::class.java, "anotherConfig") + + data class ConfigData(val field: String) + + data class Request(val message: String) + + data class Response(val message: String) + + @Verb + fun something(context: Context, req: Request): Response { + return Response(message = "response") + } + """ + ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) + val file = File(OUTPUT_FILENAME) + val module = Module.ADAPTER.decode(file.inputStream()) + + val expected = Module( + name = "test", + decls = listOf( + Decl( + data_ = Data( + name = "ConfigData", + fields = listOf( + Field( + name = "field", + type = Type(string = String()) + ) + ), + ), + ), + Decl( + data_ = Data( + name = "Request", + fields = listOf( + Field( + name = "message", + type = Type(string = String()) + ), + ), + ), + ), + Decl( + data_ = Data( + name = "Response", + fields = listOf( + Field( + name = "message", + type = Type(string = String()) + ) + ), + ), + ), + Decl( + verb = Verb( + name = "something", + request = Type(ref = Ref(name = "Request", module = "test")), + response = Type(ref = Ref(name = "Response", module = "test")), + ), + ), + Decl( + config = xyz.block.ftl.v1.schema.Config( + name = "config", + type = Type(ref = Ref(name = "ConfigData", module = "test")), + ), + ), + Decl( + config = xyz.block.ftl.v1.schema.Config( + name = "anotherConfig", + type = Type(ref = Ref(name = "ConfigData", module = "test")), + ), + ), + Decl( + secret = xyz.block.ftl.v1.schema.Secret( + name = "secret", + type = Type(string = String()) + ), + ), + Decl( + secret = xyz.block.ftl.v1.schema.Secret( + name = "anotherSecret", + type = Type(string = String()) + ), + ), + ) + ) + + assertThat(module) + .usingRecursiveComparison() + .withEqualsForType({ _, _ -> true }, Position::class.java) + .ignoringFieldsMatchingRegexes(".*hashCode\$") + .isEqualTo(expected) + } }