Skip to content

Commit

Permalink
add ktor2 library option to kotlin server generator
Browse files Browse the repository at this point in the history
  • Loading branch information
wing328 committed Dec 12, 2024
1 parent b218e23 commit 24ddb33
Show file tree
Hide file tree
Showing 46 changed files with 2,099 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-server-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
- samples/server/petstore/kotlin-server/javalin
- samples/server/petstore/kotlin-server/javalin-6
- samples/server/petstore/kotlin-server/ktor
- samples/server/petstore/kotlin-server/ktor2
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/
steps:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-server-jdk21.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
sample:
- samples/server/petstore/kotlin-server/javalin-6
- samples/server/petstore/kotlin-server/ktor
- samples/server/petstore/kotlin-server/ktor2
- samples/server/petstore/kotlin-server-required-and-nullable-properties
steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
- samples/server/petstore/kotlin-springboot-source-swagger2
- samples/server/petstore/kotlin-springboot-springfox
- samples/server/petstore/kotlin-server/ktor
- samples/server/petstore/kotlin-server/ktor2
- samples/server/petstore/kotlin-server/jaxrs-spec
- samples/server/petstore/kotlin-server/jaxrs-spec-mutiny
- samples/server/petstore/kotlin-server-modelMutable
Expand Down
8 changes: 8 additions & 0 deletions bin/configs/kotlin-server-ktor2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: kotlin-server
outputDir: samples/server/petstore/kotlin-server/ktor2
library: ktor2
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
hideGenerationTimestamp: "true"
serializableModel: "true"
2 changes: 1 addition & 1 deletion docs/generators/kotlin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|featureResources|Generates routes in a typed way, for both: constructing URLs and reading the parameters.| |true|
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|interfaceOnly|Whether to generate only API interface stubs without the server files. This option is currently supported only when using jaxrs-spec library.| |false|
|library|library template (sub-template)|<dl><dt>**ktor**</dt><dd>ktor framework</dd><dt>**jaxrs-spec**</dt><dd>JAX-RS spec only</dd><dt>**javalin5**</dt><dd>Javalin 5</dd><dt>**javalin6**</dt><dd>Javalin 6</dd></dl>|ktor|
|library|library template (sub-template)|<dl><dt>**ktor**</dt><dd>ktor framework</dd><dt>**ktor2**</dt><dd>ktor (2.x) framework</dd><dt>**jaxrs-spec**</dt><dd>JAX-RS spec only</dd><dt>**javalin5**</dt><dd>Javalin 5</dd><dt>**javalin6**</dt><dd>Javalin 6</dd></dl>|ktor|
|modelMutable|Create mutable models| |false|
|omitGradleWrapper|Whether to omit Gradle wrapper for creating a sub project.| |false|
|packageName|Generated artifact package name.| |org.openapitools.server|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa
Constants.METRICS,
Constants.OMIT_GRADLE_WRAPPER
))
.put(Constants.KTOR2, Arrays.asList(
Constants.AUTOMATIC_HEAD_REQUESTS,
Constants.CONDITIONAL_HEADERS,
Constants.HSTS,
Constants.CORS,
Constants.COMPRESSION,
Constants.RESOURCES,
Constants.METRICS,
Constants.OMIT_GRADLE_WRAPPER
))
.put(Constants.JAXRS_SPEC, Arrays.asList(
USE_BEANVALIDATION,
Constants.USE_COROUTINES,
Expand Down Expand Up @@ -127,6 +137,7 @@ public KotlinServerCodegen() {
modelPackage = packageName + ".models";

supportedLibraries.put(Constants.KTOR, "ktor framework");
supportedLibraries.put(Constants.KTOR2, "ktor (2.x) framework");
supportedLibraries.put(Constants.JAXRS_SPEC, "JAX-RS spec only");
supportedLibraries.put(Constants.JAVALIN5, "Javalin 5");
supportedLibraries.put(Constants.JAVALIN6, "Javalin 6");
Expand Down Expand Up @@ -323,6 +334,7 @@ public void setUseBeanValidation(boolean useBeanValidation) {

public static class Constants {
public final static String KTOR = "ktor";
public final static String KTOR2 = "ktor2";
public final static String JAXRS_SPEC = "jaxrs-spec";

public final static String JAVALIN5 = "javalin5";
Expand Down Expand Up @@ -419,6 +431,6 @@ private boolean isJavalin() {
}

private boolean isKtor() {
return Constants.KTOR.equals(library);
return Constants.KTOR.equals(library) || Constants.KTOR2.equals(library);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.openapitools.server.infrastructure

import io.ktor.http.auth.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*

enum class ApiKeyLocation(val location: String) {
QUERY("query"),
HEADER("header")
}

data class ApiKeyCredential(val value: String) : Credential
data class ApiPrincipal(val apiKeyCredential: ApiKeyCredential?) : Principal

/**
* Represents an Api Key authentication provider
*/
class ApiKeyAuthenticationProvider(configuration: Configuration) : AuthenticationProvider(configuration) {
private val authenticationFunction = configuration.authenticationFunction
private val apiKeyName: String = configuration.apiKeyName
private val apiKeyLocation: ApiKeyLocation = configuration.apiKeyLocation
override suspend fun onAuthenticate(context: AuthenticationContext) {
val call = context.call
val credentials = call.request.apiKeyAuthenticationCredentials(apiKeyName, apiKeyLocation)
val principal = credentials?.let { authenticationFunction.invoke(call, it) }

val cause = when {
credentials == null -> AuthenticationFailedCause.NoCredentials
principal == null -> AuthenticationFailedCause.InvalidCredentials
else -> null
}

if (cause != null) {
context.challenge(apiKeyName, cause) { challenge, call ->
call.respond(
UnauthorizedResponse(
HttpAuthHeader.Parameterized(
"API_KEY",
mapOf("key" to apiKeyName),
HeaderValueEncoding.QUOTED_ALWAYS
)
)
)
challenge.complete()
}
}

if (principal != null) {
context.principal(principal)
}
}

class Configuration internal constructor(name: String?) : Config(name) {
internal var authenticationFunction: suspend ApplicationCall.(ApiKeyCredential) -> Principal? = {
throw NotImplementedError(
"Api Key auth validate function is not specified. Use apiKeyAuth { validate { ... } } to fix."
)
}

var apiKeyName: String = ""

var apiKeyLocation: ApiKeyLocation = ApiKeyLocation.QUERY

/**
* Sets a validation function that will check given [ApiKeyCredential] instance and return [Principal],
* or null if credential does not correspond to an authenticated principal
*/
fun validate(body: suspend ApplicationCall.(ApiKeyCredential) -> Principal?) {
authenticationFunction = body
}
}
}

fun AuthenticationConfig.apiKeyAuth(
name: String? = null,
configure: ApiKeyAuthenticationProvider.Configuration.() -> Unit
) {
val configuration = ApiKeyAuthenticationProvider.Configuration(name).apply(configure)
val provider = ApiKeyAuthenticationProvider(configuration)
register(provider)
}

fun ApplicationRequest.apiKeyAuthenticationCredentials(
apiKeyName: String,
apiKeyLocation: ApiKeyLocation
): ApiKeyCredential? {
val value: String? = when (apiKeyLocation) {
ApiKeyLocation.QUERY -> this.queryParameters[apiKeyName]
ApiKeyLocation.HEADER -> this.headers[apiKeyName]
}
return when (value) {
null -> null
else -> ApiKeyCredential(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package {{packageName}}

import io.ktor.server.application.*
import io.ktor.serialization.gson.*
import io.ktor.http.*
{{#featureResources}}
import io.ktor.server.resources.*
{{/featureResources}}
{{#featureCORS}}
import io.ktor.server.plugins.cors.routing.*
{{/featureCORS}}
{{#featureAutoHead}}
import io.ktor.server.plugins.autohead.*
{{/featureAutoHead}}
{{#featureConditionalHeaders}}
import io.ktor.server.plugins.conditionalheaders.*
{{/featureConditionalHeaders}}
{{#featureCompression}}
import io.ktor.server.plugins.compression.*
{{/featureCompression}}
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.defaultheaders.*
{{#featureHSTS}}
import io.ktor.server.plugins.hsts.*
{{/featureHSTS}}
{{#featureMetrics}}
import com.codahale.metrics.Slf4jReporter
import io.ktor.server.metrics.dropwizard.*
import java.util.concurrent.TimeUnit
{{/featureMetrics}}
import io.ktor.server.routing.*
{{#hasAuthMethods}}
import com.typesafe.config.ConfigFactory
import io.ktor.client.HttpClient
import io.ktor.client.engine.apache.Apache
import io.ktor.server.config.HoconApplicationConfig
import io.ktor.server.auth.*
import org.openapitools.server.infrastructure.*
{{/hasAuthMethods}}
{{#generateApis}}{{#apiInfo}}{{#apis}}import {{apiPackage}}.{{classname}}
{{/apis}}{{/apiInfo}}{{/generateApis}}

{{#hasAuthMethods}}
internal val settings = HoconApplicationConfig(ConfigFactory.defaultApplication(HTTP::class.java.classLoader))

object HTTP {
val client = HttpClient(Apache)
}
{{/hasAuthMethods}}

fun Application.main() {
install(DefaultHeaders)
{{#featureMetrics}}
install(DropwizardMetrics) {
val reporter = Slf4jReporter.forRegistry(registry)
.outputTo([email protected])
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build()
reporter.start(10, TimeUnit.SECONDS)
}
{{/featureMetrics}}
{{#generateApis}}
install(ContentNegotiation) {
register(ContentType.Application.Json, GsonConverter())
}
{{#featureAutoHead}}
install(AutoHeadResponse) // see https://ktor.io/docs/autoheadresponse.html
{{/featureAutoHead}}
{{#featureConditionalHeaders}}
install(ConditionalHeaders) // see https://ktor.io/docs/conditional-headers.html
{{/featureConditionalHeaders}}
{{#featureCompression}}
install(Compression, ApplicationCompressionConfiguration()) // see https://ktor.io/docs/compression.html
{{/featureCompression}}
{{#featureCORS}}
install(CORS, ApplicationCORSConfiguration()) // see https://ktor.io/docs/cors.html
{{/featureCORS}}
{{#featureHSTS}}
install(HSTS, ApplicationHstsConfiguration()) // see https://ktor.io/docs/hsts.html
{{/featureHSTS}}
{{#featureResources}}
install(Resources)
{{/featureResources}}
{{#hasAuthMethods}}
install(Authentication) {
{{#authMethods}}
{{#isBasicBasic}}
basic("{{{name}}}") {
validate { credentials ->
// TODO: "Apply your basic authentication functionality."
// Accessible in-method via call.principal<UserIdPrincipal>()
if (credentials.name == "Swagger" && "Codegen" == credentials.password) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
{{/isBasicBasic}}
{{#isApiKey}}
// "Implement API key auth ({{{name}}}) for parameter name '{{{keyParamName}}}'."
apiKeyAuth("{{{name}}}") {
validate { apikeyCredential: ApiKeyCredential ->
when {
apikeyCredential.value == "keyboardcat" -> ApiPrincipal(apikeyCredential)
else -> null
}
}
}
{{/isApiKey}}
{{#isOAuth}}
{{#bodyAllowed}}
{{/bodyAllowed}}
{{^bodyAllowed}}
oauth("{{name}}") {
client = HttpClient(Apache)
providerLookup = { applicationAuthProvider([email protected]) }
urlProvider = { _ ->
// TODO: define a callback url here.
"/"
}
}
{{/bodyAllowed}}
{{/isOAuth}}
{{/authMethods}}
}
{{/hasAuthMethods}}
install(Routing) {
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{classname}}()
{{/operations}}
{{/apis}}
{{/apiInfo}}
}

{{/generateApis}}
}
Loading

0 comments on commit 24ddb33

Please sign in to comment.