Skip to content

Commit

Permalink
generateFiles via KotlinFileSpecIterable
Browse files Browse the repository at this point in the history
  • Loading branch information
jangalinski committed Sep 12, 2024
1 parent 2f5277a commit 274551f
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.toolisticon.kotlin.generation.test.model
import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import com.tschuchort.compiletesting.SourceFile
import io.toolisticon.kotlin.generation.spec.KotlinFileSpec
import io.toolisticon.kotlin.generation.spec.KotlinFileSpecs
import io.toolisticon.kotlin.generation.spec.KotlinFileSpecList
import io.toolisticon.kotlin.generation.test.KotlinCodeGenerationTest.sourceFile
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi

Expand All @@ -13,10 +13,10 @@ import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
@ExperimentalKotlinPoetApi
@ExperimentalCompilerApi
data class KotlinCompilationCommand(
val fileSpecs: KotlinFileSpecs
val fileSpecs: KotlinFileSpecList
) {

constructor(fileSpec: KotlinFileSpec) : this(KotlinFileSpecs(fileSpec))
constructor(fileSpec: KotlinFileSpec) : this(KotlinFileSpecList(fileSpec))

operator fun plus(fileSpec: KotlinFileSpec) = copy(fileSpecs = fileSpecs + fileSpec)

Expand Down
62 changes: 44 additions & 18 deletions kotlin-code-generation/src/main/kotlin/KotlinCodeGeneration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.propertyBui
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.runtimeExceptionClassBuilder
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.typeAliasBuilder
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.valueClassBuilder
import io.toolisticon.kotlin.generation.KotlinCodeGeneration.className
import io.toolisticon.kotlin.generation.builder.*
import io.toolisticon.kotlin.generation.builder.extra.*
import io.toolisticon.kotlin.generation.builder.extra.DelegateMapValueClassSpecBuilder.Companion.DEFAULT_KEY_TYPE
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.KotlinCodeGenerationStrategy
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_MEMBER_VISIBILITY_CAN_BE_PRIVATE
import mu.KLogging
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf

/**
* 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.
Expand Down Expand Up @@ -559,34 +559,60 @@ object KotlinCodeGeneration : KLogging() {
const val NBSP = "·"
}

/**
* Generator Function that takes a context and an input, finds matching strategies and generates source file(s).
*
* Invokes the contextFactory and calls `generateFiles(context, input)`.
*
* @param INPUT the type of the input (base source of generation)
* @param CONTEXT the context (containing registry, ...) used for generation.
* @param input the instance of the input
* @param contextFactory factory fn to create the context (containing the spi registry) used for generation from input
* @return [KotlinFileSpecList] containing the generated files
* @throws IllegalStateException when no matching strategy is found.
*/
inline fun <reified CONTEXT : KotlinCodeGenerationContext<CONTEXT>, reified INPUT : Any> generateFiles(contextFactory: (INPUT) -> CONTEXT, input: INPUT): KotlinFileSpecList = generateFiles(
context = contextFactory.invoke(input),
input = input
)

/**
* Generator Function that takes an input and generates source file(s).
* Generator Function that takes a context and an input, finds matching strategies 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 {
* @param context the context (containing the spi registry) used for generation
* @return [KotlinFileSpecList] containing the generated files
* @throws IllegalStateException when no matching strategy is found.
*/
inline fun <reified CONTEXT : KotlinCodeGenerationContext<CONTEXT>, reified INPUT : Any> generateFiles(context: CONTEXT, input: INPUT): KotlinFileSpecList {
val strategyCandidates = context.registry.strategies.filter { it.specType.isSubclassOf(KotlinFileSpecIterable::class) }
.filter { it.contextType.isSubclassOf(CONTEXT::class) }
.filter { it.inputType.isSubclassOf(INPUT::class) }
.map {
@Suppress("UNCHECKED_CAST")
it as KotlinCodeGenerationStrategy<CONTEXT, INPUT, KotlinFileSpecIterable>
}
// find all matching strategies
val matchingStrategies: List<KotlinCodeGenerationStrategy<CONTEXT, INPUT, KotlinFileSpecIterable>> = strategyCandidates.mapNotNull {
if (it.test(context, input)) {
it
} else {
logger.info { "strategy-filter: removing ${it.name}" }
null
}
}.also {
check(it.isNotEmpty()) { "No applicable strategy found/filtered for context=`${CONTEXT::class.simpleName}`, input=`${input::class.simpleName}`." }
}

// generate files
val sourceFiles = matchingStrategies.executeAll(context, input).flatten().also {
check(it.isNotEmpty()) { "No files where generated for context=`${CONTEXT::class.simpleName}`, input=`${input::class.simpleName}`." }
}
check(strategies.isNotEmpty()) { "No applicable strategy found/filtered for `${STRATEGY::class}`." }
return context.registry.strategies.filter(STRATEGY::class).executeAll(context, input)

// wrap to spec list
return KotlinFileSpecList(sourceFiles)
}

}
27 changes: 15 additions & 12 deletions kotlin-code-generation/src/main/kotlin/spec/KotlinFileSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlin.reflect.KClass
*/
data class KotlinFileSpec(
private val spec: FileSpec
) : KotlinGeneratorSpec<KotlinFileSpec, FileSpec, FileSpecSupplier>, KotlinFileSpecSupplier, TaggableSpec {
) : KotlinGeneratorSpec<KotlinFileSpec, FileSpec, FileSpecSupplier>, KotlinFileSpecSupplier, TaggableSpec, KotlinFileSpecIterable {

val packageName: String = spec.packageName
val rootName: String = spec.name
Expand All @@ -20,6 +20,8 @@ data class KotlinFileSpec(
val fqn: String = "$packageName.$rootName"
val fileName: String = "$fqn.kt"

override fun iterator(): Iterator<KotlinFileSpec> = listOf(this).listIterator()

override fun spec(): KotlinFileSpec = this
override fun get(): FileSpec = spec
}
Expand All @@ -35,15 +37,17 @@ interface KotlinFileSpecSupplier : KotlinGeneratorSpecSupplier<KotlinFileSpec>,
* List that contains multiple [KotlinFileSpec]s.
*/
@JvmInline
value class KotlinFileSpecs(private val fileSpecs: List<KotlinFileSpec>) : List<KotlinFileSpec> by fileSpecs {
value class KotlinFileSpecList(private val fileSpecs: List<KotlinFileSpec>) : List<KotlinFileSpec> by fileSpecs, KotlinFileSpecIterable {
companion object {

/**
* Empty Specs.
*/
val EMPTY = KotlinFileSpecs(emptyList())
val EMPTY = KotlinFileSpecList(emptyList())

fun collect(vararg fns: () -> KotlinFileSpec) = collect(fns.toList())

fun collect(vararg fns: () -> KotlinFileSpec) = fns.fold(EMPTY) { acc, cur -> acc + cur() }
fun collect(specs: List<() -> KotlinFileSpec>) = specs.fold(EMPTY) { acc, cur -> acc + cur() }
}

/**
Expand All @@ -52,13 +56,12 @@ value class KotlinFileSpecs(private val fileSpecs: List<KotlinFileSpec>) : List<
constructor(fileSpec: KotlinFileSpec) : this(listOf(fileSpec))

/**
* Create copy of this list and add new spec.
*/
operator fun plus(fileSpec: KotlinFileSpec) = KotlinFileSpecs(fileSpecs + fileSpec)

/**
* Create copy of this list and add all others.
* Create copy of this list and add new spec(s).
*/
operator fun plus(other: KotlinFileSpecs) = other.fold(this, KotlinFileSpecs::plus)

operator fun plus(fileSpec: KotlinFileSpecIterable) = KotlinFileSpecList(fileSpecs + fileSpec)
}

/**
* Iterable that provides instances of [KotlinFileSpec].
*/
sealed interface KotlinFileSpecIterable : Iterable<KotlinFileSpec>
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ interface KotlinCodeGenerationContext<SELF : KotlinCodeGenerationContext<SELF>>
processorType: KClass<PROCESSOR>
): List<PROCESSOR> = registry.processors.filterIsInstance(processorType.java)
}

@OptIn(ExperimentalKotlinPoetApi::class)
fun interface KotlinCodeGenerationContextFactory<CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any> : (INPUT) -> CONTEXT
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package io.toolisticon.kotlin.generation.spi.processor
import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationContext
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationProcessor
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationStrategy
import io.toolisticon.kotlin.generation.spi.UnboundKotlinCodeGenerationProcessor
import kotlin.reflect.KClass

Expand All @@ -12,22 +11,32 @@ import kotlin.reflect.KClass
*/
@ExperimentalKotlinPoetApi
@JvmInline
value class KotlinCodeGenerationProcessorList(val list: List<UnboundKotlinCodeGenerationProcessor>) :
List<UnboundKotlinCodeGenerationProcessor> by list {
value class KotlinCodeGenerationProcessorList(
@PublishedApi
internal val list: List<UnboundKotlinCodeGenerationProcessor>
) : List<UnboundKotlinCodeGenerationProcessor> by list {

constructor(vararg processors: UnboundKotlinCodeGenerationProcessor) : this(processors.toList())

/**
* Filter the current list and return instances of given type.
* @param strategyType defining which concrete implementations to use
* @return list containing only instances of given [strategyType]
*
* @param processorType defining which concrete implementations to use
* @return list containing only instances of given [processorType]
*/
fun <PROCESSOR : KotlinCodeGenerationProcessor<CONTEXT, INPUT, BUILDER>, CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any, BUILDER : Any> filter(
processorType: KClass<PROCESSOR>
): List<PROCESSOR> {
return list.filterIsInstance(processorType.java)
}

/**
* Filter the current list and return instances of given type.
* @return list containing only instances of reified type.
*/
inline fun <reified PROCESSOR : KotlinCodeGenerationProcessor<CONTEXT, INPUT, SPEC>, CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any, SPEC : Any> filter() = filter(PROCESSOR::class)


override fun toString(): String = "KotlinCodeGenerationProcessorList(processors=${list.map { it.name }})"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import kotlin.reflect.KClass
*/
@ExperimentalKotlinPoetApi
@JvmInline
value class KotlinCodeGenerationStrategyList(private val list: List<UnboundKotlinCodeGenerationStrategy>) :
List<UnboundKotlinCodeGenerationStrategy> by list {
value class KotlinCodeGenerationStrategyList(
@PublishedApi
internal val list: List<UnboundKotlinCodeGenerationStrategy>
) : List<UnboundKotlinCodeGenerationStrategy> by list {

constructor(vararg strategy: UnboundKotlinCodeGenerationStrategy) : this(strategy.toList())

/**
* Filter the current list and return instances of given type.
*
* @param strategyType defining which concrete implementations to use
* @return list containing only instances of given [strategyType]
*/
Expand All @@ -27,6 +30,12 @@ value class KotlinCodeGenerationStrategyList(private val list: List<UnboundKotli
return list.filterIsInstance(strategyType.java)
}

/**
* Filter the current list and return instances of given type.
* @return list containing only instances of reified type.
*/
inline fun <reified STRATEGY : KotlinCodeGenerationStrategy<CONTEXT, INPUT, SPEC>, CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any, SPEC : Any> filter() = filter(STRATEGY::class)

override fun toString(): String = "KotlinCodeGenerationStrategyList(strategies=${list.map { it.name }})"
}

Expand All @@ -40,6 +49,4 @@ fun <STRATEGY : KotlinCodeGenerationStrategy<CONTEXT, INPUT, SPEC>, CONTEXT : Ko
fun <STRATEGY : KotlinCodeGenerationStrategy<CONTEXT, INPUT, SPEC>, CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any, SPEC : Any> List<STRATEGY>.executeAll(
context: CONTEXT,
input: INPUT
): List<SPEC> = map {
it.execute(context, input)
}.filterNotNull()
): List<SPEC> = mapNotNull { it.execute(context, input) }
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
package io.toolisticon.kotlin.generation.spi.strategy

import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import io.toolisticon.kotlin.generation.spec.KotlinFileSpecs
import io.toolisticon.kotlin.generation.spec.KotlinFileSpecList
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationContext
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationSpi
import kotlin.reflect.KClass

/**
* Used to implement a [io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationStrategy]
* that will generate multiple [io.toolisticon.kotlin.generation.spec.KotlinFileSpec]s wrapped in [KotlinFileSpecs].
* that will generate multiple [io.toolisticon.kotlin.generation.spec.KotlinFileSpec]s wrapped in [KotlinFileSpecList].
*
* Implementations should override the `invoke` function to call multiple [KotlinFileSpecStrategy]s and collect the
* results.
*/
@ExperimentalKotlinPoetApi
abstract class KotlinFileSpecsStrategy<CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any>(
abstract class KotlinFileSpecListStrategy<CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any>(
contextType: KClass<CONTEXT>,
override val inputType: KClass<INPUT>,
order: Int = KotlinCodeGenerationSpi.DEFAULT_ORDER,
) : KotlinCodeGenerationStrategyBase<CONTEXT, INPUT, KotlinFileSpecs>(
) : KotlinFileSpecIterableStrategy<CONTEXT, INPUT, KotlinFileSpecList>(
contextType = contextType,
inputType = inputType,
specType = KotlinFileSpecs::class,
specType = KotlinFileSpecList::class,
order = order
) {

abstract override fun invoke(context: CONTEXT, input: INPUT): KotlinFileSpecs
abstract override fun invoke(context: CONTEXT, input: INPUT): KotlinFileSpecList

override fun test(context: CONTEXT, input: Any): Boolean = super.test(context, input)

override fun execute(context: CONTEXT, input: INPUT): KotlinFileSpecList = super.execute(context, input) ?: KotlinFileSpecList.EMPTY
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ abstract class KotlinFileSpecStrategy<CONTEXT : KotlinCodeGenerationContext<CONT
contextType: KClass<CONTEXT>,
override val inputType: KClass<INPUT>,
order: Int = KotlinCodeGenerationSpi.DEFAULT_ORDER,
) : KotlinCodeGenerationStrategyBase<CONTEXT, INPUT, KotlinFileSpec>(
) : KotlinFileSpecIterableStrategy<CONTEXT, INPUT, KotlinFileSpec>(
contextType = contextType,
inputType = inputType,
specType = KotlinFileSpec::class,
Expand Down
29 changes: 29 additions & 0 deletions kotlin-code-generation/src/main/kotlin/spi/strategy/_abstract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.toolisticon.kotlin.generation.spi.strategy

import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
import io.toolisticon.kotlin.generation.spec.KotlinFileSpecIterable
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationContext
import io.toolisticon.kotlin.generation.spi.KotlinCodeGenerationSpi
import kotlin.reflect.KClass

/**
* Base class for [KotlinFileSpecStrategy] and [KotlinFileSpecListStrategy] so we can join single and multi results
* during generation.
*/
@ExperimentalKotlinPoetApi
sealed class KotlinFileSpecIterableStrategy<CONTEXT : KotlinCodeGenerationContext<CONTEXT>, INPUT : Any, SPEC : KotlinFileSpecIterable>(
contextType: KClass<CONTEXT>,
override val inputType: KClass<INPUT>,
specType: KClass<SPEC>,
order: Int = KotlinCodeGenerationSpi.DEFAULT_ORDER,
) : KotlinCodeGenerationStrategyBase<CONTEXT, INPUT, SPEC>(
contextType = contextType,
inputType = inputType,
specType = specType,
order = order
) {

abstract override fun invoke(context: CONTEXT, input: INPUT): SPEC

override fun test(context: CONTEXT, input: Any): Boolean = super.test(context, input)
}
Loading

0 comments on commit 274551f

Please sign in to comment.