Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kotlin-server: Add support for Javalin #17596

Merged
merged 5 commits into from
Jan 15, 2024
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please update the kotlin server github workflow with the new sample directory so that CI can test it moving forward?

.github/workflows/samples-kotlin-server-jdk17.yaml 
.github/workflows/samples-kotlin-server.yaml

Copy link
Contributor Author

@dennisameling dennisameling Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. Done! ✅

It looks like one of the Spring Boot-related tests is now failing. Looks like that sample is already broken on the master branch:

fun getInventory(, serverHttpRequest: ServerHttpRequest): ResponseEntity<Map<String, kotlin.Int>> {

Please let me know if there's anything else I can do within the scope of this PR 😊 thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed it's not related to this change

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
Loading