Skip to content

Commit

Permalink
kotlin-server: Add support for Javalin (#17596)
Browse files Browse the repository at this point in the history
* kotlin-server: Add support for Javalin

* kotlin-server: Add Javalin generated sample code

* Add output from build scripts

* Address MR feedback

* Update CI to include new Javalin sample
  • Loading branch information
dennisameling authored Jan 15, 2024
1 parent 36e4e4f commit 13edc5d
Show file tree
Hide file tree
Showing 44 changed files with 1,362 additions and 45 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/samples-kotlin-server-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ on:
push:
branches:
- 'samples/server/petstore/kotlin-springboot-3*/**'
- 'samples/server/petstore/kotlin-server/javalin/**'
# 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/**'
# comment out due to gradle build failure
# - samples/server/petstore/kotlin-spring-default/**

Expand All @@ -26,6 +28,7 @@ jobs:
# server
- samples/server/petstore/kotlin-springboot-3
- samples/server/petstore/kotlin-springboot-request
- samples/server/petstore/kotlin-server/javalin
# 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.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- samples/server/petstore/kotlin-server/jaxrs-spec
- samples/server/petstore/kotlin-server/jaxrs-spec-mutiny
- samples/server/petstore/kotlin-server-modelMutable
- samples/server/petstore/kotlin-server/javalin
- samples/server/others/kotlin-server/jaxrs-spec
# comment out due to gradle build failure
#- samples/server/petstore/kotlin-spring-default
Expand Down
7 changes: 7 additions & 0 deletions bin/configs/kotlin-server-javalin.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
library: javalin5
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></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></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 @@ -1143,4 +1143,44 @@ protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
return super.addMustacheLambdas()
.put("escapeDollar", new EscapeChar("(?<!\\\\)\\$", "\\\\\\$"));
}

protected interface DataTypeAssigner {
void setReturnType(String returnType);

void setReturnContainer(String returnContainer);
}

/**
* @param returnType The return type that needs to be converted
* @param dataTypeAssigner An object that will assign the data to the respective fields in the model.
*/
protected void doDataTypeAssignment(final String returnType, DataTypeAssigner dataTypeAssigner) {
if (returnType == null) {
dataTypeAssigner.setReturnType("Unit");
} else if (returnType.startsWith("kotlin.collections.List")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.List<".length(), end).trim());
dataTypeAssigner.setReturnContainer("List");
}
} else if (returnType.startsWith("kotlin.collections.MutableList")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.MutableList<".length(), end).trim());
dataTypeAssigner.setReturnContainer("List");
}
} else if (returnType.startsWith("kotlin.collections.Map")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.Map<".length(), end).split(",")[1].trim());
dataTypeAssigner.setReturnContainer("Map");
}
} else if (returnType.startsWith("kotlin.collections.MutableMap")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.MutableMap<".length(), end).split(",")[1].trim());
dataTypeAssigner.setReturnContainer("Map");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.templating.mustache.LowercaseLambda;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -119,6 +121,7 @@ public KotlinServerCodegen() {

supportedLibraries.put(Constants.KTOR, "ktor framework");
supportedLibraries.put(Constants.JAXRS_SPEC, "JAX-RS spec only");
supportedLibraries.put(Constants.JAVALIN5, "Javalin 5");

// TODO: Configurable server engine. Defaults to netty in build.gradle.
addOption(CodegenConstants.LIBRARY, CodegenConstants.LIBRARY_DESC, DEFAULT_LIBRARY, supportedLibraries);
Expand Down Expand Up @@ -322,7 +325,13 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("Dockerfile.mustache", "", "Dockerfile"));
}

supportingFiles.add(new SupportingFile("build.gradle.mustache", "", "build.gradle"));
String gradleBuildFile = "build.gradle";

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

supportingFiles.add(new SupportingFile(gradleBuildFile + ".mustache", "", gradleBuildFile));
supportingFiles.add(new SupportingFile("settings.gradle.mustache", "", "settings.gradle"));
supportingFiles.add(new SupportingFile("gradle.properties", "", "gradle.properties"));

Expand All @@ -340,6 +349,13 @@ 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)) {
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());
typeMapping.put("file", "io.javalin.http.UploadedFile");
importMapping.put("io.javalin.http.UploadedFile", "io.javalin.http.UploadedFile");
}
}

Expand All @@ -351,6 +367,8 @@ public void setUseBeanValidation(boolean useBeanValidation) {
public static class Constants {
public final static String KTOR = "ktor";
public final static String JAXRS_SPEC = "jaxrs-spec";

public final static String JAVALIN5 = "javalin5";
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 @@ -390,4 +408,50 @@ public void postProcess() {
System.out.println("# Please support his work directly via https://patreon.com/jimschubert \uD83D\uDE4F #");
System.out.println("################################################################################");
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
OperationMap operations = objs.getOperations();
if (operations != null) {
List<CodegenOperation> ops = operations.getOperation();
ops.forEach(operation -> {
List<CodegenResponse> responses = operation.responses;
if (responses != null) {
responses.forEach(resp -> {

if ("0".equals(resp.code)) {
resp.code = "200";
}

doDataTypeAssignment(resp.dataType, new DataTypeAssigner() {
@Override
public void setReturnType(final String returnType) {
resp.dataType = returnType;
}

@Override
public void setReturnContainer(final String returnContainer) {
resp.containerType = returnContainer;
}
});
});
}

doDataTypeAssignment(operation.returnType, new DataTypeAssigner() {

@Override
public void setReturnType(final String returnType) {
operation.returnType = returnType;
}

@Override
public void setReturnContainer(final String returnContainer) {
operation.returnContainer = returnContainer;
}
});
});
}

return objs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -949,46 +949,6 @@ private String getNonMutableContainerTypeIfNeeded(String type) {
return type;
}

private interface DataTypeAssigner {
void setReturnType(String returnType);

void setReturnContainer(String returnContainer);
}

/**
* @param returnType The return type that needs to be converted
* @param dataTypeAssigner An object that will assign the data to the respective fields in the model.
*/
private void doDataTypeAssignment(final String returnType, DataTypeAssigner dataTypeAssigner) {
if (returnType == null) {
dataTypeAssigner.setReturnType("Unit");
} else if (returnType.startsWith("kotlin.collections.List")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.List<".length(), end).trim());
dataTypeAssigner.setReturnContainer("List");
}
} else if (returnType.startsWith("kotlin.collections.MutableList")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.MutableList<".length(), end).trim());
dataTypeAssigner.setReturnContainer("List");
}
} else if (returnType.startsWith("kotlin.collections.Map")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.Map<".length(), end).split(",")[1].trim());
dataTypeAssigner.setReturnContainer("Map");
}
} else if (returnType.startsWith("kotlin.collections.MutableMap")) {
int end = returnType.lastIndexOf(">");
if (end > 0) {
dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.MutableMap<".length(), end).split(",")[1].trim());
dataTypeAssigner.setReturnContainer("Map");
}
}
}

private static String sanitizeDirectory(String in) {
return in.replace(".", File.separator);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package {{packageName}}

import io.javalin.Javalin
import io.javalin.community.routing.dsl.routing

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

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

val app = Javalin
.create { config ->
config.routing {
{{#apis}}
{{#operations}}
{{#operation}}
{{#lowercase}}{{httpMethod}}{{/lowercase}}("{{path}}", {{classname}}::{{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}}
Loading

0 comments on commit 13edc5d

Please sign in to comment.