Skip to content

Commit

Permalink
generateFiles function, fixes #37
Browse files Browse the repository at this point in the history
  • Loading branch information
jangalinski committed Aug 29, 2024
1 parent dd34f14 commit b4dd837
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 23 deletions.
59 changes: 58 additions & 1 deletion kotlin-code-generation/src/main/kotlin/KotlinCodeGeneration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,22 @@ import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.valueClassB
import io.toolisticon.kotlin.generation.builder.*
import io.toolisticon.kotlin.generation.poet.FormatSpecifier.asCodeBlock
import io.toolisticon.kotlin.generation.spec.*
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationContext
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationSpiRegistry
import io.toolisticon.kotlin.generation.spi.registry.KotlinCodeGenerationServiceLoader
import io.toolisticon.kotlin.generation.spi.strategy.KotlinFileSpecStrategy
import io.toolisticon.kotlin.generation.spi.strategy.executeAll
import io.toolisticon.kotlin.generation.support.SUPPRESS_CLASS_NAME
import io.toolisticon.kotlin.generation.support.SUPPRESS_MEMBER_VISIBILITY_CAN_BE_PRIVATE
import io.toolisticon.kotlin.generation.support.SUPPRESS_UNUSED
import mu.KLogging
import kotlin.reflect.KClass

/**
* Kotlin Code Generation is a wrapper lib for kotlin poet. This is the central class that allows access to builders and tools via simple static helpers.
*/
@ExperimentalKotlinPoetApi
object KotlinCodeGeneration {
object KotlinCodeGeneration : KLogging() {

/**
* Build a [KotlinAnnotationSpec] using given type and receiver fn.
Expand Down Expand Up @@ -98,6 +102,7 @@ object KotlinCodeGeneration {
* @see [KotlinConstructorPropertySpecBuilder.builder]
*/
inline fun buildConstructorProperty(name: PropertyName, type: TypeName, block: KotlinConstructorPropertySpecBuilderReceiver = {}) = constructorPropertyBuilder(name, type).also(block).build()

/**
* Build [KotlinConstructorPropertySpec].
* @see [KotlinConstructorPropertySpecBuilder.builder]
Expand All @@ -121,6 +126,7 @@ object KotlinCodeGeneration {
* @see [KotlinEnumClassSpecBuilder.builder]
*/
inline fun buildEnumClass(className: ClassName, block: KotlinEnumClassSpecBuilderReceiver = {}) = KotlinEnumClassSpecBuilder.builder(className).also(block).build()

/**
* Build [KotlinEnumClassSpec].
* @see [KotlinEnumClassSpecBuilder.builder]
Expand All @@ -132,6 +138,7 @@ object KotlinCodeGeneration {
* @see [KotlinFileSpecBuilder.builder]
*/
inline fun buildFile(className: ClassName, block: KotlinFileSpecBuilderReceiver = {}): KotlinFileSpec = fileBuilder(className).also(block).build()

/**
* Build [KotlinFileSpec].
* @see [KotlinFileSpecBuilder.builder]
Expand All @@ -149,6 +156,7 @@ object KotlinCodeGeneration {
* @see [KotlinInterfaceSpecBuilder.builder]
*/
inline fun buildInterface(className: ClassName, block: KotlinInterfaceSpecBuilderReceiver = {}): KotlinInterfaceSpec = interfaceBuilder(className).also(block).build()

/**
* Build [KotlinInterfaceSpec].
* @see [KotlinInterfaceSpecBuilder.builder]
Expand All @@ -161,6 +169,7 @@ object KotlinCodeGeneration {
* @see [KotlinObjectSpecBuilder.builder]
*/
inline fun buildObject(className: ClassName, block: KotlinObjectSpecBuilderReceiver = {}): KotlinObjectSpec = objectBuilder(className).also(block).build()

/**
* Build [KotlinObjectSpec].
* @see [KotlinObjectSpecBuilder.builder]
Expand All @@ -172,6 +181,7 @@ object KotlinCodeGeneration {
* @see [KotlinParameterSpecBuilder.builder]
*/
inline fun buildParameter(name: ParameterName, typeName: TypeName, block: KotlinParameterSpecBuilderReceiver = {}): KotlinParameterSpec = parameterBuilder(name, typeName).also(block).build()

/**
* Build [KotlinParameterSpec].
* @see [KotlinParameterSpecBuilder.builder]
Expand All @@ -183,6 +193,7 @@ object KotlinCodeGeneration {
* @see [KotlinPropertySpecBuilder.builder]
*/
inline fun buildProperty(name: PropertyName, typeName: TypeName, block: KotlinPropertySpecBuilderReceiver = {}): KotlinPropertySpec = propertyBuilder(name, typeName).also(block).build()

/**
* Build [KotlinPropertySpec].
* @see [KotlinPropertySpecBuilder.builder]
Expand All @@ -200,6 +211,7 @@ object KotlinCodeGeneration {
* @see [KotlinValueClassSpecBuilder.builder]
*/
inline fun buildValueClass(className: ClassName, block: KotlinValueClassSpecBuilderReceiver = {}): KotlinValueClassSpec = valueClassBuilder(className).also(block).build()

/**
* Build [KotlinValueClassSpec].
* @see [KotlinValueClassSpecBuilder.builder]
Expand All @@ -217,6 +229,7 @@ object KotlinCodeGeneration {
* @see KotlinAnnotationClassSpecBuilder
*/
fun annotationClassBuilder(className: ClassName) = KotlinAnnotationClassSpecBuilder.builder(className)

/**
* @see KotlinAnnotationClassSpecBuilder
*/
Expand All @@ -226,6 +239,7 @@ object KotlinCodeGeneration {
* @see KotlinAnnotationSpecBuilder
*/
fun annotationBuilder(type: ClassName) = KotlinAnnotationSpecBuilder.builder(type)

/**
* @see KotlinAnnotationSpecBuilder
*/
Expand All @@ -240,6 +254,7 @@ object KotlinCodeGeneration {
* @see KotlinClassSpecBuilder
*/
fun classBuilder(className: ClassName) = KotlinClassSpecBuilder.builder(className)

/**
* @see KotlinClassSpecBuilder
*/
Expand All @@ -259,6 +274,7 @@ object KotlinCodeGeneration {
* @see KotlinDataClassSpecBuilder
*/
fun dataClassBuilder(className: ClassName) = KotlinDataClassSpecBuilder.builder(className)

/**
* @see KotlinDataClassSpecBuilder
*/
Expand All @@ -268,10 +284,12 @@ object KotlinCodeGeneration {
* @see KotlinEnumClassSpecBuilder
*/
fun enumClassBuilder(name: SimpleName) = KotlinEnumClassSpecBuilder.builder(name)

/**
* @see KotlinEnumClassSpecBuilder
*/
fun enumClassBuilder(packageName: PackageName, name: SimpleName) = enumClassBuilder(className(packageName, name))

/**
* @see KotlinEnumClassSpecBuilder
*/
Expand All @@ -281,6 +299,7 @@ object KotlinCodeGeneration {
* @see KotlinFileSpecBuilder
*/
fun fileBuilder(className: ClassName) = KotlinFileSpecBuilder.builder(className)

/**
* @see KotlinFileSpecBuilder
*/
Expand All @@ -295,6 +314,7 @@ object KotlinCodeGeneration {
* @see KotlinInterfaceSpecBuilder
*/
fun interfaceBuilder(className: ClassName) = KotlinInterfaceSpecBuilder.builder(className)

/**
* @see KotlinInterfaceSpecBuilder
*/
Expand All @@ -304,6 +324,7 @@ object KotlinCodeGeneration {
* @see KotlinObjectSpecBuilder
*/
fun objectBuilder(className: ClassName) = KotlinObjectSpecBuilder.builder(className)

/**
* @see KotlinObjectSpecBuilder
*/
Expand All @@ -313,6 +334,7 @@ object KotlinCodeGeneration {
* @see KotlinParameterSpecBuilder
*/
fun parameterBuilder(name: ParameterName, type: TypeName) = KotlinParameterSpecBuilder.builder(name, type)

/**
* @see KotlinParameterSpecBuilder
*/
Expand All @@ -322,6 +344,7 @@ object KotlinCodeGeneration {
* @see KotlinPropertySpecBuilder
*/
fun propertyBuilder(name: PropertyName, type: TypeName) = KotlinPropertySpecBuilder.builder(name, type)

/**
* @see KotlinPropertySpecBuilder
*/
Expand All @@ -331,6 +354,7 @@ object KotlinCodeGeneration {
* @see KotlinTypeAliasSpecBuilder
*/
fun typeAliasBuilder(name: TypeAliasName, type: TypeName): KotlinTypeAliasSpecBuilder = KotlinTypeAliasSpecBuilder.builder(name, type)

/**
* @see KotlinTypeAliasSpecBuilder
*/
Expand All @@ -340,6 +364,7 @@ object KotlinCodeGeneration {
* @see KotlinValueClassSpecBuilder
*/
fun valueClassBuilder(className: ClassName) = KotlinValueClassSpecBuilder.builder(className)

/**
* @see KotlinValueClassSpecBuilder
*/
Expand All @@ -350,6 +375,7 @@ object KotlinCodeGeneration {
* Create [ClassName] for given package and simpleName.
*/
fun className(packageName: PackageName, simpleName: SimpleName) = ClassName(packageName, simpleName)

/**
* Create [ClassName] with default packageName.
*/
Expand Down Expand Up @@ -415,4 +441,35 @@ object KotlinCodeGeneration {

const val NBSP = "·"
}


/**
* Generator Function that takes an input and generates source file(s).
*
* @param INPUT the type of the input (base source of generation)
* @param CONTEXT the context (containing registry, ...) used for generation.
* @param STRATEGY the [KotlinFileSpecStrategy] to apply (using `executeAll()`
* @param input the instance of the input
* @param contextFactory fn that creates the context based on input.
* @return list of [KotlinFileSpec]
* @throws IllegalStateException when no strategy is found.
*/
inline fun <INPUT : Any,
CONTEXT : KotlinCodeGenerationContext<CONTEXT>,
reified STRATEGY : KotlinFileSpecStrategy<CONTEXT, INPUT>> generateFiles(
input: INPUT,
contextFactory: (INPUT) -> CONTEXT,
): List<KotlinFileSpec> {
val context = contextFactory.invoke(input)
val strategies: List<STRATEGY> = context.registry.strategies.filter(STRATEGY::class).mapNotNull {
if (it.test(context, input)) {
it
} else {
logger.info { "strategy-filter: removing ${it.name}" }
null
}
}
check(strategies.isNotEmpty()) { "No applicable strategy found/filtered for `${STRATEGY::class}`." }
return context.registry.strategies.filter(STRATEGY::class).executeAll(context, input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
package io.toolisticon.kotlin.generation

import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.className
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.generateFiles
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.name.asCodeBlock
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.name.asMemberName
import io.toolisticon.kotlin.generation._test.MutableSpiRegistry
import io.toolisticon.kotlin.generation._test.TestContext
import io.toolisticon.kotlin.generation._test.TestDeclaration
import io.toolisticon.kotlin.generation._test.TestDeclarationFileStrategy
import io.toolisticon.kotlin.generation._test.TestInput
import io.toolisticon.kotlin.generation.spi.strategy.KotlinFileSpecStrategy
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

Expand All @@ -18,10 +27,78 @@ internal class KotlinCodeGenerationTest {
fun `join members to array`() {
val m1 = AnnotationTarget.VALUE_PARAMETER.asMemberName()
val m2 = AnnotationTarget.ANNOTATION_CLASS.asMemberName()
val code = listOf(m1,m2).asCodeBlock()
val code = listOf(m1, m2).asCodeBlock()

assertThat(code).hasToString("[kotlin.`annotation`.AnnotationTarget.VALUE_PARAMETER, kotlin.`annotation`.AnnotationTarget.ANNOTATION_CLASS]")
}
}

@Nested
inner class GenerateFilesTest {
private val registry = MutableSpiRegistry()
private val className = className("foo", "Bar")
private val testInput = TestInput().put("name", String::class)
private val testDeclaration = TestDeclaration(className, testInput)

@Test
fun `fail when no strategy registered`() {
assertThat(registry.strategies).isEmpty()

assertThatThrownBy {
generateFiles<TestDeclaration, TestContext, KotlinFileSpecStrategy<TestContext, TestDeclaration>>(
input = testDeclaration,
contextFactory = { TestContext(it.rootClassName, registry) }
)
}.isInstanceOf(IllegalStateException::class.java)
.hasMessage(
"No applicable strategy found/filtered for " +
"`class io.toolisticon.kotlin.generation.spi.strategy.KotlinFileSpecStrategy`."
)
}

@Test
fun `fail when no strategy matches`() {
registry.strategyList.add(TestDeclarationFileStrategy().apply {
fail = true
})
assertThat(registry.strategies).isNotEmpty()

assertThatThrownBy {
generateFiles<TestDeclaration, TestContext, KotlinFileSpecStrategy<TestContext, TestDeclaration>>(
input = testDeclaration,
contextFactory = { TestContext(it.rootClassName, registry) }
)
}.isInstanceOf(IllegalStateException::class.java)
.hasMessage(
"No applicable strategy found/filtered for " +
"`class io.toolisticon.kotlin.generation.spi.strategy.KotlinFileSpecStrategy`."
)
}

@Test
fun `generate data class`() {
registry.strategyList.add(TestDeclarationFileStrategy().apply {
fail = false
})

assertThat(registry.strategies).isNotEmpty()

val file = generateFiles<TestDeclaration, TestContext, KotlinFileSpecStrategy<TestContext, TestDeclaration>>(
input = testDeclaration,
contextFactory = { TestContext(it.rootClassName, registry) }
).single()

assertThat(file.code).isEqualToIgnoringWhitespace(
"""
package foo
import kotlin.String
public data class Bar(
public val name: String,
)
""".trimIndent()
)
}
}
}
21 changes: 21 additions & 0 deletions kotlin-code-generation/src/test/kotlin/_test/MutableSpiRegistry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@file:OptIn(ExperimentalKotlinPoetApi::class)

package io.toolisticon.kotlin.generation._test

import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationSpiRegistry
import io.toolisticon.kotlin.generation.spi.UnboundKotlinCodeGenerationProcessor
import io.toolisticon.kotlin.generation.spi.UnboundKotlinCodeGenerationStrategy
import io.toolisticon.kotlin.generation.spi.processor.KotlinCodeGenerationProcessorList
import io.toolisticon.kotlin.generation.spi.strategy.KotlinCodeGenerationStrategyList
import kotlin.reflect.KClass

class MutableSpiRegistry(
val strategyList: MutableList<UnboundKotlinCodeGenerationStrategy> = mutableListOf(),
val processorList: MutableList<UnboundKotlinCodeGenerationProcessor> = mutableListOf(),
) : KotlinCodeGenerationSpiRegistry {
override val contextTypeUpperBound: KClass<*> = TestContext::class

override val strategies: KotlinCodeGenerationStrategyList get() = KotlinCodeGenerationStrategyList(strategyList)
override val processors: KotlinCodeGenerationProcessorList get() = KotlinCodeGenerationProcessorList(processorList)
}
20 changes: 2 additions & 18 deletions kotlin-code-generation/src/test/kotlin/_test/TestContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,10 @@ package io.toolisticon.kotlin.generation._test
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationContext
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationSpiRegistry
import io.toolisticon.kotlin.generation.spi.UnboundKotlinCodeGenerationProcessor
import io.toolisticon.kotlin.generation.spi.UnboundKotlinCodeGenerationStrategy
import io.toolisticon.kotlin.generation.spi.processor.KotlinCodeGenerationProcessorList
import io.toolisticon.kotlin.generation.spi.strategy.KotlinCodeGenerationStrategyList
import kotlin.reflect.KClass

class TestContext(
val rootClassName: ClassName
val rootClassName: ClassName,
override val registry: MutableSpiRegistry
) : KotlinCodeGenerationContext<TestContext> {


val strategyList = mutableListOf<UnboundKotlinCodeGenerationStrategy>()
val processorList = mutableListOf<UnboundKotlinCodeGenerationProcessor>()

override val contextType = TestContext::class
override val registry: KotlinCodeGenerationSpiRegistry = object : KotlinCodeGenerationSpiRegistry {
override val contextTypeUpperBound: KClass<*> = TestContext::class

override val strategies: KotlinCodeGenerationStrategyList = KotlinCodeGenerationStrategyList(strategyList)
override val processors: KotlinCodeGenerationProcessorList = KotlinCodeGenerationProcessorList(processorList)
}
}
10 changes: 10 additions & 0 deletions kotlin-code-generation/src/test/kotlin/_test/TestDeclaration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.toolisticon.kotlin.generation._test

import com.squareup.kotlinpoet.ClassName

data class TestDeclaration(
val rootClassName: ClassName,
val testInput: TestInput
) {

}
Loading

0 comments on commit b4dd837

Please sign in to comment.