Skip to content

Commit

Permalink
feat: extract configs/secrets in kotlin (#1111)
Browse files Browse the repository at this point in the history
fixes #1092
  • Loading branch information
worstell authored Mar 20, 2024
1 parent 0858fd7 commit 80b91c3
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,21 @@ 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
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
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
Expand Down Expand Up @@ -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<String> = emptyList(), val decls: MutableSet<Decl> = mutableSetOf())

Expand Down Expand Up @@ -169,6 +168,8 @@ class SchemaExtractor(
*extractDataDeclarations().toTypedArray(),
*extractDatabases().toTypedArray(),
*extractEnums().toTypedArray(),
*extractConfigs().toTypedArray(),
*extractSecrets().toTypedArray(),
),
comments = moduleComments
)
Expand Down Expand Up @@ -292,6 +293,70 @@ class SchemaExtractor(
.toSet()
}

private fun extractConfigs(): Set<Decl> {
return extractSecretsOrConfigs(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<Decl> {
return extractSecretsOrConfigs(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 extractSecretsOrConfigs(qualifiedPropertyName: String): List<SecretConfigData> {
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 = ""
when (it) {
is KtCallExpression -> {
it.getResolvedCall(bindingContext)?.valueArguments?.entries?.forEach { arg ->
if (arg.key.name.asString() == "name") {
name = arg.value.toString().trim('"')
} else if (arg.key.name.asString() == "cls") {
type = (arg.key.varargElementType ?: arg.key.type).arguments.single().type
}
}
}

is KtDotQualifiedExpression -> {
it.getResolvedCall(bindingContext)?.let { call ->
name = call.valueArguments.entries.single().value.toString().trim('"')
type = call.typeArguments.values.single()
}
}

else -> {
throw IllegalArgumentException("$position: Could not extract secret or config")
}
}

SecretConfigData(name, type!!.toSchemaType(position), position)
}
}

private fun extractDataDeclarations(): Set<Decl> {
return file.children
.filter { (it is KtClass && it.isData()) || it is KtTypeAlias }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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"))),
),
),
Expand All @@ -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<String>("secret")
val anotherSecret = Secret(String::class.java, "anotherSecret")
val config = Config.new<ConfigData>("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)
}
}

0 comments on commit 80b91c3

Please sign in to comment.