Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,50 +1,13 @@
package com.diconium.mobile.tools.kebabkrafter.generator

import com.diconium.mobile.tools.kebabkrafter.KebabKrafterUnstableApi
import com.diconium.mobile.tools.kebabkrafter.models.BaseSpecModel
import com.diconium.mobile.tools.kebabkrafter.models.Endpoint

@KebabKrafterUnstableApi
class Transformers {
internal var endpointTransformer: EndpointTransformer = EndpointTransformer { endpoint -> endpoint }
private set

internal var ktorMapper: KtorMapper = DefaultKtorControllerMapper
private set

internal var ktorTransformer: KtorTransformer = KtorTransformer { endpoint, controller -> controller }
private set

/**
* Endpoints are the server routes extracted from the original swagger YML.
* Endpoints contains HTTP related data such as path/method/headers/body.
* This transformer allows to modify the endpoints before they are processed by the code generators.
*/
fun endpointTransformer(t: EndpointTransformer) {
endpointTransformer = t
}

/**
* Mapper that converts the Endpoint to KtorController. The controller is the basis for the code generator,
* Controller contains code related data such as package/class/kdoc.
*
* The mapper is the most complex (and powerful) part of the transformer API, hence use is discouraged.
* There is a [DefaultKtorControllerMapper] available that is used internally, but accessible for other mappers.
*/
fun ktorMapper(t: KtorMapper) {
ktorMapper = t
}

/**
* Transforms individual [KtorController] after they have been mapped.
* This is the last step before the actual code generator.
*
* The [Endpoint] provided here in this callback is for reference only.
*/
fun ktorTransformer(t: KtorTransformer) {
ktorTransformer = t
}
}
internal class Transformers(
val endpointTransformer: EndpointTransformer,
val ktorMapper: KtorMapper,
val ktorTransformer: KtorTransformer,
)

fun interface EndpointTransformer {
fun transform(endpoint: Endpoint): Endpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import java.io.File
/**
* Helper to generate server + data classes together
*/
fun generateKtorServerFor(
internal fun generateKtorServerFor(
packageName: String,
baseDir: File,
specFile: File,
contextSpec: ContextSpec,
transformers: Transformers = Transformers(),
transformers: Transformers,
installFunction: String = "installGeneratedRoutes",
) {
// clean the output folder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.diconium.mobile.tools.kebabkrafter.plugin

import com.diconium.mobile.tools.kebabkrafter.plugin.server.GenerateKtorServer
import com.diconium.mobile.tools.kebabkrafter.plugin.server.applyGenerateKtorServer
import org.gradle.api.Plugin
import org.gradle.api.Project

class KebabKrafter : Plugin<Project> {
override fun apply(target: Project) {
GenerateKtorServer.apply(target)
applyGenerateKtorServer(target)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.diconium.mobile.tools.kebabkrafter.plugin.server

import com.diconium.mobile.tools.kebabkrafter.generator.DefaultKtorControllerMapper
import com.diconium.mobile.tools.kebabkrafter.generator.EndpointTransformer
import com.diconium.mobile.tools.kebabkrafter.generator.KtorController
import com.diconium.mobile.tools.kebabkrafter.generator.KtorTransformer
import com.diconium.mobile.tools.kebabkrafter.models.Endpoint
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer

fun applyGenerateKtorServer(target: Project) {

// create extension
val ktorServerInput = target.extensions.create("ktorServer", KtorServerExtension::class.java)

// apply defaults
ktorServerInput.log.convention(false)
ktorServerInput.outputFolder.convention(target.defaultOutput)
ktorServerInput.transformerSpec.endpointTransformer.convention(DefaultEndpointTransformer::class.java)
ktorServerInput.transformerSpec.ktorMapper.convention(DefaultKtorControllerMapper::class.java)
ktorServerInput.transformerSpec.ktorTransformer.convention(DefaultKtorTransformer::class.java)

// register task
val task = target.tasks.register("generateKtorServer", GenerateKtorServerTask::class.java) {
it.group = "generator"
it.ktorServerInput.set(ktorServerInput)
}

// wire task output to the main source set
target.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
target.sourceSets { container ->
container.main.configure { sourceSet ->
sourceSet.java.srcDirs(task)
sourceSet.kotlin.srcDirs(task)
}
}
}
}

// those are copied from those auto-generated accessors files,
// just to make the usage above a bit cleaner.
private fun Project.sourceSets(configure: Action<SourceSetContainer>): Unit =
(this as ExtensionAware).extensions.configure("sourceSets", configure)

private val SourceSetContainer.main: NamedDomainObjectProvider<SourceSet>
get() = named("main")

private val SourceSet.kotlin: SourceDirectorySet
get() = (this as ExtensionAware).extensions.getByName("kotlin")
as SourceDirectorySet


private class DefaultEndpointTransformer : EndpointTransformer {
override fun transform(endpoint: Endpoint) = endpoint
}

private class DefaultKtorTransformer : KtorTransformer {
override fun transform(endpoint: Endpoint, controller: KtorController) = controller
}

private val Project.defaultOutput: Provider<Directory>
get() = this.layout.buildDirectory.dir("generated/sources/ktorServer/")
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
package com.diconium.mobile.tools.kebabkrafter.plugin.server

import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import javax.inject.Inject
import org.gradle.api.tasks.Input

/**
* Specification for the custom context where and API call is executed
*/
open class ContextSpecExtension @Inject constructor(objects: ObjectFactory) {
/**
* Package name of the custom context
*/
val packageName: Property<String> = objects.property(String::class.java)
interface ContextSpecExtension {
@get:Input
val packageName: Property<String>

/**
* Class name of the custom context
*/
val className: Property<String> = objects.property(String::class.java)
@get:Input
val className: Property<String>

/**
* Name of the factory method to build a new context
*/
val factoryName: Property<String> = objects.property(String::class.java)
@get:Input
val factoryName: Property<String>
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.diconium.mobile.tools.kebabkrafter.plugin.server

import com.diconium.mobile.tools.kebabkrafter.Log
import com.diconium.mobile.tools.kebabkrafter.generator.Transformers
import com.diconium.mobile.tools.kebabkrafter.generator.ktorserver.ContextSpec
import com.diconium.mobile.tools.kebabkrafter.generator.ktorserver.generateKtorServerFor
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.TaskAction

abstract class GenerateKtorServerTask : DefaultTask() {

@get:Nested
abstract val ktorServerInput: Property<KtorServerExtension>

@TaskAction
fun action() {
if (ktorServerInput.get().log.get()) Log.logger = logger
try {
generateKtorServerFor()
} finally {
Log.logger = null
}
}

private fun generateKtorServerFor() {
with(ktorServerInput.get()) {

generateKtorServerFor(
packageName = packageName.get(),
baseDir = outputFolder.get().asFile,
specFile = specFile.get(),
contextSpec = with(contextSpec) {
ContextSpec(
packageName = packageName.get(),
className = className.get(),
factoryName = factoryName.get(),
)
},

transformers = with(ktorServerInput.get().transformerSpec) {

// implementation notes:
//
// That's a very cheeky piece of code that I'm still unsure if genius or stupid.
// Gradle tasks uses input/outputs to define UP-TO-DATE information,
// and those inputs/outputs must be some type serializable
// but the transformers/mappers are lambas (`fun interface`) and that was my problem.
// The workaround here is that we set those inputs to `Class<out TYPE>` that are serializable.
// in the extension object we capture the anonymous inner class from the lambda
// and here we build a new instance of that lambda.
//
// This seems to work fine for simple lambdas, but, I can imagine on more complex scenario,
// (e.g. if a lambda access components from the encompassing `Project`)
// that it might not work so good, or produce unknown side effects.
//
// I think it's good that the transformers API is wrapped in an OptIn(KebabKrafterUnstableApi)

val et = endpointTransformer.get().getDeclaredConstructor()
et.isAccessible = true

val km = ktorMapper.get().getDeclaredConstructor()
km.isAccessible = true

val kt = ktorTransformer.get().getDeclaredConstructor()
kt.isAccessible = true

Transformers(et.newInstance(), km.newInstance(), kt.newInstance())
},
)
}
}
}
Loading