Skip to content

Commit

Permalink
[kotlin-server][javalin6] Add Javalin 6 support (#18928)
Browse files Browse the repository at this point in the history
* [kotlin-server][javalin6] Add Javalin 6 support

Javalin 5 support was added in 13edc5d. Javalin 6 has been released, with some breaking changes. Let's add a new supportedLibrary to not break existing users of Javalin 5.

https://javalin.io/migration-guide-javalin-5-to-6

* Fix Gradle config and don't include JVM 8 CI anymore (JVM 11 is the minimum for Javalin)

* Update docs

* Fix optional query parameter handling and turn into expected type
  • Loading branch information
dennisameling committed Jun 17, 2024
1 parent c806ea5 commit 793aba7
Show file tree
Hide file tree
Showing 42 changed files with 1,355 additions and 4 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/samples-kotlin-server-jdk17.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
name: Samples Kotlin server
name: Samples Kotlin server (jdk17)

on:
push:
branches:
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/javalin/**'
- 'samples/server/petstore/kotlin-server/javalin-6/**'
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/**
pull_request:
paths:
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/javalin/**'
- 'samples/server/petstore/kotlin-server/javalin-6/**'
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/**

Expand All @@ -30,6 +32,7 @@ jobs:
- samples/server/petstore/kotlin-springboot-request
- samples/server/petstore/kotlin-springboot-request-cookie
- samples/server/petstore/kotlin-server/javalin
- samples/server/petstore/kotlin-server/javalin-6
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/
steps:
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/samples-kotlin-server-jdk21.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Samples Kotlin server (jdk21)

on:
push:
branches:
- 'samples/server/petstore/kotlin-server/javalin-6/**'
pull_request:
paths:
- 'samples/server/petstore/kotlin-server/javalin-6/**'

env:
GRADLE_VERSION: 8.8

jobs:
build:
name: Build Kotlin server
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sample:
- samples/server/petstore/kotlin-server/javalin-6
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- name: Cache maven dependencies
uses: actions/cache@v4
env:
cache-name: maven-repository
with:
path: |
~/.gradle
key: ${{ runner.os }}-${{ github.job }}-${{ env.cache-name }}-${{ hashFiles('**/pom.xml') }}
- name: Install Gradle wrapper
uses: eskatos/gradle-command-action@v3
with:
gradle-version: ${{ env.GRADLE_VERSION }}
build-root-directory: ${{ matrix.sample }}
arguments: wrapper
- name: Build
working-directory: ${{ matrix.sample }}
run: ./gradlew build -x test
7 changes: 7 additions & 0 deletions bin/configs/kotlin-server-javalin-6.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generatorName: kotlin-server
outputDir: samples/server/petstore/kotlin-server/javalin-6
library: javalin6
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
hideGenerationTimestamp: "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></dl>|ktor|
|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|
|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 @@ -27,6 +27,7 @@
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.templating.mustache.CamelCaseLambda;
import org.openapitools.codegen.templating.mustache.LowercaseLambda;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -128,6 +129,7 @@ public KotlinServerCodegen() {
supportedLibraries.put(Constants.KTOR, "ktor framework");
supportedLibraries.put(Constants.JAXRS_SPEC, "JAX-RS spec only");
supportedLibraries.put(Constants.JAVALIN5, "Javalin 5");
supportedLibraries.put(Constants.JAVALIN6, "Javalin 6");

// TODO: Configurable server engine. Defaults to netty in build.gradle.
addOption(CodegenConstants.LIBRARY, CodegenConstants.LIBRARY_DESC, DEFAULT_LIBRARY, supportedLibraries);
Expand Down Expand Up @@ -276,7 +278,7 @@ public void processOpts() {

String gradleBuildFile = "build.gradle";

if (library.equals(Constants.JAVALIN5)) {
if (isJavalin()) {
gradleBuildFile = "build.gradle.kts";
}

Expand All @@ -298,11 +300,12 @@ public void processOpts() {
final String infrastructureFolder = (sourceFolder + File.separator + packageName + File.separator + "infrastructure").replace(".", File.separator);

supportingFiles.add(new SupportingFile("ApiKeyAuth.kt.mustache", infrastructureFolder, "ApiKeyAuth.kt"));
} else if (library.equals(Constants.JAVALIN5)) {
} else if (isJavalin()) {
supportingFiles.add(new SupportingFile("Main.kt.mustache", packageFolder, "Main.kt"));
apiTemplateFiles.put("service.mustache", "Service.kt");
apiTemplateFiles.put("serviceImpl.mustache", "ServiceImpl.kt");
additionalProperties.put("lowercase", new LowercaseLambda());
additionalProperties.put("camelcase", new CamelCaseLambda());
typeMapping.put("file", "io.javalin.http.UploadedFile");
importMapping.put("io.javalin.http.UploadedFile", "io.javalin.http.UploadedFile");
}
Expand All @@ -318,6 +321,7 @@ public static class Constants {
public final static String JAXRS_SPEC = "jaxrs-spec";

public final static String JAVALIN5 = "javalin5";
public final static String JAVALIN6 = "javalin6";
public final static String AUTOMATIC_HEAD_REQUESTS = "featureAutoHead";
public final static String AUTOMATIC_HEAD_REQUESTS_DESC = "Automatically provide responses to HEAD requests for existing routes that have the GET verb defined.";
public final static String CONDITIONAL_HEADERS = "featureConditionalHeaders";
Expand Down Expand Up @@ -404,4 +408,8 @@ public void setReturnContainer(final String returnContainer) {

return objs;
}

private boolean isJavalin() {
return Constants.JAVALIN5.equals(library) || Constants.JAVALIN6.equals(library);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package {{packageName}}

import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.*

{{#apiInfo}}
{{#apis}}
{{#operations}}import {{apiPackage}}.{{classname}}
import {{apiPackage}}.{{classname}}ServiceImpl
{{/operations}}
{{/apis}}

fun main() {
{{#apis}}
{{#operations}}
val {{#camelcase}}{{classname}}{{/camelcase}} = {{classname}}({{classname}}ServiceImpl())
{{/operations}}
{{/apis}}

val app = Javalin
.create { config ->
config.router.apiBuilder {
{{#apis}}
{{#operations}}
{{#operation}}
path("{{path}}") { {{#lowercase}}{{httpMethod}}{{/lowercase}}({{#camelcase}}{{classname}}{{/camelcase}}::{{operationId}}) }
{{/operation}}
{{/operations}}

{{/apis}}
}
}

app.start({{serverPort}})
}
{{/apiInfo}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# {{packageName}} - Kotlin Server library for {{appName}}

{{#unescapedAppDescription}}
{{.}}
{{/unescapedAppDescription}}

Generated by OpenAPI Generator {{generatorVersion}}{{^hideGenerationTimestamp}} ({{generatedDate}}){{/hideGenerationTimestamp}}.

## Build

First, create the gradle wrapper script:

```
gradle wrapper
```

Then, run:

```
./gradlew check assemble
```

This runs all tests and packages the library.

## Running

The server builds as a fat jar with a main entrypoint. To start the service, run `java -jar ./build/libs/{{artifactId}}.jar`.

You may also run in docker:

```
docker build -t {{artifactId}} .
docker run -p 8080:8080 {{artifactId}}
```

## Features/Implementation Notes

* Supports JSON inputs/outputs, File inputs, and Form inputs (see ktor documentation for more info).
* ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~
* Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions.

{{#generateApiDocs}}
<a id="documentation-for-api-endpoints"></a>
## Documentation for API Endpoints

All URIs are relative to *{{{basePath}}}*

Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}}
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
{{/generateApiDocs}}

{{#generateModelDocs}}
<a id="documentation-for-models"></a>
## Documentation for Models

{{#modelPackage}}
{{#models}}{{#model}} - [{{{modelPackage}}}.{{{classname}}}]({{modelDocPath}}{{{classname}}}.md)
{{/model}}{{/models}}
{{/modelPackage}}
{{^modelPackage}}
No model defined in this package
{{/modelPackage}}
{{/generateModelDocs}}

<a id="documentation-for-authorization"></a>
## Documentation for Authorization

{{^authMethods}}Endpoints do not require authorization.{{/authMethods}}
{{#hasAuthMethods}}Authentication schemes defined for the API:{{/hasAuthMethods}}
{{#authMethods}}
<a id="{{name}}"></a>
### {{name}}

{{#isApiKey}}- **Type**: API key
- **API key parameter name**: {{keyParamName}}
- **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}}
{{/isApiKey}}
{{#isBasicBasic}}- **Type**: HTTP basic authentication
{{/isBasicBasic}}
{{#isBasicBearer}}- **Type**: HTTP Bearer Token authentication{{#bearerFormat}} ({{{.}}}){{/bearerFormat}}
{{/isBasicBearer}}
{{#isHttpSignature}}- **Type**: HTTP signature authentication
{{/isHttpSignature}}
{{#isOAuth}}- **Type**: OAuth
- **Flow**: {{flow}}
- **Authorization URL**: {{authorizationUrl}}
- **Scopes**: {{^scopes}}N/A{{/scopes}}
{{#scopes}} - {{scope}}: {{description}}
{{/scopes}}
{{/isOAuth}}

{{/authMethods}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package {{apiPackage}}

import io.javalin.http.Context
import io.javalin.http.bodyAsClass
import io.javalin.http.pathParamAsClass
import io.javalin.http.queryParamAsClass

{{#imports}}import {{import}}
{{/imports}}

{{#operations}}
class {{classname}}(private val service: {{classname}}Service) {
{{#operation}}
/**{{#summary}}
* {{.}}{{/summary}}
* {{unescapedNotes}}
{{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
{{/allParams}}*/
fun {{operationId}}(ctx: Context) {
val result = service.{{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}}, {{/-last}}{{/allParams}}{{#hasParams}}, {{/hasParams}}ctx)
ctx.status({{#responses}}{{#-first}}{{code}}{{/-first}}{{/responses}}).json(result)
}

{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isBodyParam}}{{#isArray}}ctx.bodyAsClass<List<{{{baseType}}}>>(){{/isArray}}{{^isArray}}ctx.bodyAsClass<{{{baseType}}}>(){{/isArray}}{{/isBodyParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
plugins {
kotlin("jvm") version "2.0.0"
}

group = "{{groupId}}"
version = "{{artifactVersion}}"

kotlin {
jvmToolchain(21)
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}

repositories {
mavenCentral()
}

dependencies {
implementation("io.javalin:javalin:6.1.6")
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.1")
implementation("org.slf4j:slf4j-simple:2.0.13")
testImplementation("org.jetbrains.kotlin:kotlin-test")
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isFormParam}}{{^isFile}}ctx.formParam("{{baseName}}"){{/isFile}}{{#isFile}}ctx.uploadedFile("{{baseName}}"){{/isFile}}{{/isFormParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.caching=true
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isHeaderParam}}ctx.header("{{baseName}}"){{/isHeaderParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#required}}{{{dataType}}}{{/required}}{{^required}}{{#defaultValue}}{{{dataType}}}{{/defaultValue}}{{^defaultValue}}{{{dataType}}}?{{/defaultValue}}{{/required}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isPathParam}}ctx.pathParamAsClass<{{{dataType}}}>("{{baseName}}").get(){{/isPathParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isQueryParam}}{{#isArray}}ctx.queryParams("{{baseName}}"){{/isArray}}{{^isArray}}ctx.queryParamAsClass<{{{dataType}}}>("{{baseName}}"){{^required}}.allowNullable(){{/required}}.get(){{/isArray}}{{/isQueryParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isMap}}Map<String, {{{returnType}}}>{{/isMap}}{{#isArray}}{{#reactive}}Flow{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}}
Loading

0 comments on commit 793aba7

Please sign in to comment.