Skip to content

Commit

Permalink
Ktor server routes and controller interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
ulrikandersen committed Apr 18, 2024
1 parent cbb86bd commit eab370d
Show file tree
Hide file tree
Showing 17 changed files with 1,813 additions and 115 deletions.
108 changes: 55 additions & 53 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ dependencies {
//testCompileOnly("io.micronaut.security:micronaut-security:3.8.7")
testImplementation("com.squareup.okhttp3:okhttp:4.10.0")
testImplementation("org.openapitools:jackson-databind-nullable:0.2.6")
testImplementation("io.ktor:ktor-server-core:2.3.9")
testImplementation("io.ktor:ktor-server-auth:2.3.9")

testImplementation(platform("com.pinterest.ktlint:ktlint-bom:0.49.0"))
testImplementation("com.pinterest:ktlint")
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ enum class ControllerCodeGenOptionType(val description: String) {

enum class ControllerCodeGenTargetType(val description: String) {
SPRING("Generate for Spring framework."),
MICRONAUT("Generate for Micronaut framework.");
MICRONAUT("Generate for Micronaut framework."),
KTOR("Generate for Ktor server.");

override fun toString() = "`${super.toString()}` - $description"
}
Expand Down
24 changes: 21 additions & 3 deletions src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.cjbooms.fabrikt.configurations.Packages
import com.cjbooms.fabrikt.generators.MutableSettings
import com.cjbooms.fabrikt.generators.client.OkHttpClientGenerator
import com.cjbooms.fabrikt.generators.client.OpenFeignInterfaceGenerator
import com.cjbooms.fabrikt.generators.controller.KtorControllerInterfaceGenerator
import com.cjbooms.fabrikt.generators.controller.MicronautControllerInterfaceGenerator
import com.cjbooms.fabrikt.generators.controller.SpringControllerInterfaceGenerator
import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
Expand Down Expand Up @@ -42,7 +43,7 @@ class CodeGenerator(
private fun generateModels(): Collection<GeneratedFile> = sourceSet(models().files)

private fun generateControllerInterfaces(): Collection<GeneratedFile> =
sourceSet(controllers().files).plus(sourceSet(models().files))
sourceSet(controllers()).plus(sourceSet(models().files))

private fun generateClient(): Collection<GeneratedFile> {
val clientGenerator = when (MutableSettings.clientTarget()) {
Expand All @@ -67,7 +68,7 @@ class CodeGenerator(
private fun resources(models: Models): List<ResourceFile> =
listOfNotNull(QuarkusReflectionModelGenerator(models).generate())

private fun controllers(): KotlinTypes {
private fun controllers(): List<FileSpec> {
val generator =
when (MutableSettings.controllerTarget()) {
ControllerCodeGenTargetType.SPRING -> SpringControllerInterfaceGenerator(
Expand All @@ -83,7 +84,24 @@ class CodeGenerator(
MutableSettings.validationLibrary().annotations,
MutableSettings.controllerOptions(),
)

ControllerCodeGenTargetType.KTOR -> KtorControllerInterfaceGenerator(
packages,
sourceApi,
)
}
return generator.generate()

val controllerFiles: Collection<FileSpec> = generator.generate().files
val libFiles: Collection<FileSpec> = generator.generateLibrary().map {
val builder = FileSpec.builder(it.className)
.addType(it.spec)

// add imports for the library files
it.imports.forEach { import -> builder.addImport(import.packageName, import.name) }

builder.build()
}

return controllerFiles.plus(libFiles)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.cjbooms.fabrikt.model.RequestParameter
import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName
import com.cjbooms.fabrikt.util.NormalisedString.camelCase
import com.cjbooms.fabrikt.util.NormalisedString.toKotlinParameterName
import com.cjbooms.fabrikt.util.capitalized
import com.cjbooms.fabrikt.util.decapitalized
import com.reprezen.kaizen.oasparser.model3.MediaType
import com.reprezen.kaizen.oasparser.model3.Operation
import com.reprezen.kaizen.oasparser.model3.Parameter
Expand All @@ -21,8 +23,6 @@ import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import com.cjbooms.fabrikt.util.capitalized
import com.cjbooms.fabrikt.util.decapitalized
import java.util.function.Predicate

object GeneratorUtils {
Expand Down Expand Up @@ -165,6 +165,7 @@ object GeneratorUtils {
): List<IncomingParameter> {

val bodies = requestBody.contentMediaTypes.values
.filterNot { it.schema.safeName().startsWith("multipart") } // TODO: multipart form data must be handled differently
.map {
BodyParameter(
it.schema.safeName().toKotlinParameterName().ifEmpty { it.schema.toVarName() },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.cjbooms.fabrikt.generators.controller

import com.cjbooms.fabrikt.configurations.Packages
import com.cjbooms.fabrikt.generators.ValidationAnnotations
import com.cjbooms.fabrikt.model.ControllerType
import com.cjbooms.fabrikt.model.RequestParameter
import com.cjbooms.fabrikt.model.SourceApi
import com.cjbooms.fabrikt.util.KaizenParserExtensions.basePath
import com.cjbooms.fabrikt.util.toUpperCase
import com.reprezen.kaizen.oasparser.model3.Operation
import com.reprezen.kaizen.oasparser.model3.Path
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.TypeSpec

abstract class AnnotationBasedControllerInterfaceGenerator(
private val packages: Packages,
private val api: SourceApi,
private val validationAnnotations: ValidationAnnotations,
) {
abstract fun buildFunction(path: Path, op: Operation, verb: String): FunSpec

abstract fun controllerBuilder(className: String, basePath: String): TypeSpec.Builder

fun buildController(resourceName: String, paths: Collection<Path>): ControllerType {
val typeBuilder: TypeSpec.Builder = controllerBuilder(
className = ControllerGeneratorUtils.controllerName(resourceName),
basePath = api.openApi3.basePath()
)

paths.flatMap { path ->
path.operations
.filter { it.key.toUpperCase() != "HEAD" }
.map { op ->
buildFunction(
path,
op.value,
op.key,
)
}
}.forEach { typeBuilder.addFunction(it) }

return ControllerType(
typeBuilder.build(),
packages.base
)
}

fun ParameterSpec.Builder.addValidationAnnotations(parameter: RequestParameter): ParameterSpec.Builder {
if (parameter.minimum != null) this.maybeAddAnnotation(validationAnnotations.min(parameter.minimum.toInt()))
if (parameter.maximum != null) this.maybeAddAnnotation(validationAnnotations.max(parameter.maximum.toInt()))
if (parameter.typeInfo.isComplexType) this.maybeAddAnnotation(validationAnnotations.parameterValid())
return this
}

fun ParameterSpec.Builder.maybeAddAnnotation(annotation: AnnotationSpec?) =
if (annotation != null) this.addAnnotation(annotation) else this
}
Original file line number Diff line number Diff line change
@@ -1,59 +1,9 @@
package com.cjbooms.fabrikt.generators.controller

import com.cjbooms.fabrikt.configurations.Packages
import com.cjbooms.fabrikt.generators.ValidationAnnotations
import com.cjbooms.fabrikt.model.ControllerType
import com.cjbooms.fabrikt.model.ControllerLibraryType
import com.cjbooms.fabrikt.model.KotlinTypes
import com.cjbooms.fabrikt.model.RequestParameter
import com.cjbooms.fabrikt.model.SourceApi
import com.cjbooms.fabrikt.util.KaizenParserExtensions.basePath
import com.cjbooms.fabrikt.util.toUpperCase
import com.reprezen.kaizen.oasparser.model3.Operation
import com.reprezen.kaizen.oasparser.model3.Path
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.TypeSpec

abstract class ControllerInterfaceGenerator(
private val packages: Packages,
private val api: SourceApi,
private val validationAnnotations: ValidationAnnotations,
) {
abstract fun generate(): KotlinTypes
abstract fun buildFunction(path: Path, op: Operation, verb: String): FunSpec
abstract fun controllerBuilder(className: String, basePath: String): TypeSpec.Builder
fun buildController(resourceName: String, paths: Collection<Path>): ControllerType {
val typeBuilder: TypeSpec.Builder = controllerBuilder(
className = ControllerGeneratorUtils.controllerName(resourceName),
basePath = api.openApi3.basePath()
)

paths.flatMap { path ->
path.operations
.filter { it.key.toUpperCase() != "HEAD" }
.map { op ->
buildFunction(
path,
op.value,
op.key,
)
}
}.forEach { typeBuilder.addFunction(it) }

return ControllerType(
typeBuilder.build(),
packages.base
)
}

fun ParameterSpec.Builder.addValidationAnnotations(parameter: RequestParameter): ParameterSpec.Builder {
if (parameter.minimum != null) this.maybeAddAnnotation(validationAnnotations.min(parameter.minimum.toInt()))
if (parameter.maximum != null) this.maybeAddAnnotation(validationAnnotations.max(parameter.maximum.toInt()))
if (parameter.typeInfo.isComplexType) this.maybeAddAnnotation(validationAnnotations.parameterValid())
return this
}

fun ParameterSpec.Builder.maybeAddAnnotation(annotation: AnnotationSpec?) =
if (annotation != null) this.addAnnotation(annotation) else this
interface ControllerInterfaceGenerator {
fun generate(): KotlinTypes
fun generateLibrary(): Collection<ControllerLibraryType>
}
Loading

0 comments on commit eab370d

Please sign in to comment.