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][jax-rs] Added support for JAX-RS library into Kotlin Server generator #10830

Merged
merged 16 commits into from
Jan 29, 2022
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
7 changes: 7 additions & 0 deletions bin/configs/kotlin-server-jaxrs-spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generatorName: kotlin-server
outputDir: samples/server/petstore/kotlin-server/jaxrs-spec
library: jaxrs-spec
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-server
additionalProperties:
useCoroutines: "true"
6 changes: 5 additions & 1 deletion docs/generators/kotlin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|featureLocations|Generates routes in a typed way, for both: constructing URLs and reading the parameters.| |true|
|featureMetrics|Enables metrics feature.| |true|
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|library|library template (sub-template)|<dl><dt>**ktor**</dt><dd>ktor framework</dd></dl>|ktor|
|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|
|modelMutable|Create mutable models| |false|
|packageName|Generated artifact package name.| |org.openapitools.server|
|parcelizeModels|toggle &quot;@Parcelize&quot; for generated models| |null|
|returnResponse|Whether generate API interface should return javax.ws.rs.core.Response instead of a deserialized entity. Only useful if interfaceOnly is true. This option is currently supported only when using jaxrs-spec library.| |false|
|serializableModel|boolean - toggle &quot;implements Serializable&quot; for generated models| |null|
|serializationLibrary|What serialization library to use: 'moshi' (default), or 'gson' or 'jackson'| |moshi|
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |null|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |null|
|sourceFolder|source folder for generated code| |src/main/kotlin|
|useBeanValidation|Use BeanValidation API annotations. This option is currently supported only when using jaxrs-spec library.| |false|
|useCoroutines|Whether to use the Coroutines. This option is currently supported only when using jaxrs-spec library.| |false|

## IMPORT MAPPING

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.meta.features.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -33,16 +34,25 @@
import java.util.List;
import java.util.Map;

public class KotlinServerCodegen extends AbstractKotlinCodegen {
public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanValidationFeatures {

public static final String INTERFACE_ONLY = "interfaceOnly";
public static final String USE_COROUTINES = "useCoroutines";
public static final String RETURN_RESPONSE = "returnResponse";
public static final String DEFAULT_LIBRARY = Constants.KTOR;
private final Logger LOGGER = LoggerFactory.getLogger(KotlinServerCodegen.class);

private Boolean autoHeadFeatureEnabled = true;
private Boolean conditionalHeadersFeatureEnabled = false;
private Boolean hstsFeatureEnabled = true;
private Boolean corsFeatureEnabled = false;
private Boolean compressionFeatureEnabled = true;
private Boolean locationsFeatureEnabled = true;
private Boolean metricsFeatureEnabled = true;
private boolean interfaceOnly = false;
private boolean useBeanValidation = false;
private boolean useCoroutines = false;
private boolean returnResponse = false;

// This is here to potentially warn the user when an option is not supported by the target framework.
private Map<String, List<String>> optionsSupportedPerFramework = new ImmutableMap.Builder<String, List<String>>()
Expand Down Expand Up @@ -102,6 +112,7 @@ public KotlinServerCodegen() {
modelPackage = packageName + ".models";

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

// TODO: Configurable server engine. Defaults to netty in build.gradle.
CliOption library = new CliOption(CodegenConstants.LIBRARY, CodegenConstants.LIBRARY_DESC);
Expand All @@ -117,6 +128,11 @@ public KotlinServerCodegen() {
addSwitch(Constants.COMPRESSION, Constants.COMPRESSION_DESC, getCompressionFeatureEnabled());
addSwitch(Constants.LOCATIONS, Constants.LOCATIONS_DESC, getLocationsFeatureEnabled());
addSwitch(Constants.METRICS, Constants.METRICS_DESC, getMetricsFeatureEnabled());

cliOptions.add(CliOption.newBoolean(INTERFACE_ONLY, "Whether to generate only API interface stubs without the server files. This option is currently supported only when using jaxrs-spec library.").defaultValue(String.valueOf(interfaceOnly)));
cliOptions.add(CliOption.newBoolean(USE_BEANVALIDATION, "Use BeanValidation API annotations. This option is currently supported only when using jaxrs-spec library.", useBeanValidation));
cliOptions.add(CliOption.newBoolean(USE_COROUTINES, "Whether to use the Coroutines. This option is currently supported only when using jaxrs-spec library."));
cliOptions.add(CliOption.newBoolean(RETURN_RESPONSE, "Whether generate API interface should return javax.ws.rs.core.Response instead of a deserialized entity. Only useful if interfaceOnly is true. This option is currently supported only when using jaxrs-spec library.").defaultValue(String.valueOf(returnResponse)));
}

public Boolean getAutoHeadFeatureEnabled() {
Expand Down Expand Up @@ -191,6 +207,37 @@ public CodegenType getTag() {
public void processOpts() {
super.processOpts();

if (additionalProperties.containsKey(CodegenConstants.LIBRARY)) {
this.setLibrary((String) additionalProperties.get(CodegenConstants.LIBRARY));
}

if (additionalProperties.containsKey(INTERFACE_ONLY)) {
interfaceOnly = Boolean.parseBoolean(additionalProperties.get(INTERFACE_ONLY).toString());
if (!interfaceOnly) {
additionalProperties.remove(INTERFACE_ONLY);
}
}

if (additionalProperties.containsKey(USE_COROUTINES)) {
useCoroutines = Boolean.parseBoolean(additionalProperties.get(USE_COROUTINES).toString());
if (!useCoroutines) {
additionalProperties.remove(USE_COROUTINES);
}
}

if (additionalProperties.containsKey(RETURN_RESPONSE)) {
returnResponse = Boolean.parseBoolean(additionalProperties.get(RETURN_RESPONSE).toString());
if (!returnResponse) {
additionalProperties.remove(RETURN_RESPONSE);
}
}

if (additionalProperties.containsKey(USE_BEANVALIDATION)) {
setUseBeanValidation(convertPropertyToBoolean(USE_BEANVALIDATION));
}

writePropertyBack(USE_BEANVALIDATION, useBeanValidation);

// set default library to "ktor"
if (StringUtils.isEmpty(library)) {
this.setLibrary(DEFAULT_LIBRARY);
Expand Down Expand Up @@ -245,29 +292,40 @@ public void processOpts() {
String resourcesFolder = "src/main/resources"; // not sure this can be user configurable.

supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("Dockerfile.mustache", "", "Dockerfile"));

if (library.equals(Constants.KTOR)) {
supportingFiles.add(new SupportingFile("Dockerfile.mustache", "", "Dockerfile"));
}

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

supportingFiles.add(new SupportingFile("AppMain.kt.mustache", packageFolder, "AppMain.kt"));
supportingFiles.add(new SupportingFile("Configuration.kt.mustache", packageFolder, "Configuration.kt"));
if (library.equals(Constants.KTOR)) {
supportingFiles.add(new SupportingFile("AppMain.kt.mustache", packageFolder, "AppMain.kt"));
supportingFiles.add(new SupportingFile("Configuration.kt.mustache", packageFolder, "Configuration.kt"));

if (generateApis && locationsFeatureEnabled) {
supportingFiles.add(new SupportingFile("Paths.kt.mustache", packageFolder, "Paths.kt"));
}
if (generateApis && locationsFeatureEnabled) {
supportingFiles.add(new SupportingFile("Paths.kt.mustache", packageFolder, "Paths.kt"));
}

supportingFiles.add(new SupportingFile("application.conf.mustache", resourcesFolder, "application.conf"));
supportingFiles.add(new SupportingFile("logback.xml", resourcesFolder, "logback.xml"));
supportingFiles.add(new SupportingFile("application.conf.mustache", resourcesFolder, "application.conf"));
supportingFiles.add(new SupportingFile("logback.xml", resourcesFolder, "logback.xml"));

final String infrastructureFolder = (sourceFolder + File.separator + packageName + File.separator + "infrastructure").replace(".", File.separator);
final String infrastructureFolder = (sourceFolder + File.separator + packageName + File.separator + "infrastructure").replace(".", File.separator);

supportingFiles.add(new SupportingFile("ApiKeyAuth.kt.mustache", infrastructureFolder, "ApiKeyAuth.kt"));
supportingFiles.add(new SupportingFile("ApiKeyAuth.kt.mustache", infrastructureFolder, "ApiKeyAuth.kt"));
}
}

@Override
public void setUseBeanValidation(boolean useBeanValidation) {
this.useBeanValidation = useBeanValidation;
}

public static class Constants {
public final static String KTOR = "ktor";
public final static String JAXRS_SPEC = "jaxrs-spec";
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{#additionalModelTypeAnnotations}}{{{.}}}
{{/additionalModelTypeAnnotations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package {{package}};

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

import javax.ws.rs.*
import javax.ws.rs.core.Response

{{#useSwaggerAnnotations}}
import io.swagger.annotations.*
{{/useSwaggerAnnotations}}

import java.io.InputStream
import java.util.Map
import java.util.List
{{#useBeanValidation}}import javax.validation.constraints.*
import javax.validation.Valid{{/useBeanValidation}}

{{#useSwaggerAnnotations}}
@Api(description = "the {{{baseName}}} API"){{/useSwaggerAnnotations}}{{#hasConsumes}}
@Consumes({ {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}}
@Produces({ {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }){{/hasProduces}}
@Path("/")
Copy link

@regispl regispl Mar 18, 2022

Choose a reason for hiding this comment

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

Hey,

First of all, big thanks @anttileppa and @wing328 , this PR came very handy for me this week, I appreciate your work on it 👏

Secondly, I realise that this PR is closed and merged, but I thought I'd ask here before I report the issue, because perhaps it's my understanding that's the issue here, not the code :-)

I'm trying to generate the code for the API that's hosted under api. For this reason I defined:

servers:
  - url: /api  

in my definition and based on my experience with other generators I'd expect it to work as a "prefix" for all REST resources. Unfortunately what's generated by the jaxrs-spec library is:

@Path("/")

(not surprising given what's in this line of the template 😉 )
meaning that the e.g. resource xyz would exist under /xyz rather than /api/xyz.

Conversely, a Kotlin client generated from the same spec prefixes all the URLs with /api - such a "mismatch" between the generated client and the server code logic makes me think that perhaps something is not right.

My question is: was "/" intentionally hardcoded here or is this perhaps a mistake? Alternatively, if that top-level "prefix" is not desired for some reason - should it perhaps be used as a prefix for each API method @Path?

(disclaimer: it's my first contact with JAX-RS, which could be where the problem is 😉 )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@regispl sounds like a mistake to me

{{>generatedAnnotation}}{{#interfaceOnly}}interface{{/interfaceOnly}}{{^interfaceOnly}}class{{/interfaceOnly}} {{classname}} {
{{#operations}}
{{#operation}}

{{#interfaceOnly}}{{>apiInterface}}{{/interfaceOnly}}{{^interfaceOnly}}{{>apiMethod}}{{/interfaceOnly}}
{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@{{httpMethod}}
@Path("{{{path}}}"){{#hasConsumes}}
@Consumes({{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}){{/hasConsumes}}{{#hasProduces}}
@Produces({{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}){{/hasProduces}}{{#useSwaggerAnnotations}}
@ApiOperation(value = "{{{summary}}}", notes = "{{{notes}}}"{{#hasAuthMethods}}, authorizations = {
{{#authMethods}}{{#isOAuth}}@Authorization(value = "{{name}}", scopes = {
{{#scopes}}@AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},
{{/-last}}{{/scopes}} }){{^-last}},{{/-last}}{{/isOAuth}}
{{^isOAuth}}@Authorization(value = "{{name}}"){{^-last}},{{/-last}}
{{/isOAuth}}{{/authMethods}} }{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} })
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#returnContainer}}, responseContainer = "{{{.}}}"{{/returnContainer}}){{^-last}},{{/-last}}{{/responses}} }){{/useSwaggerAnnotations}}
{{#useCoroutines}}suspend {{/useCoroutines}}fun {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}){{#returnResponse}}: Response{{/returnResponse}}{{^returnResponse}}{{#returnType}}: {{{returnType}}}{{/returnType}}{{/returnResponse}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@{{httpMethod}}{{#subresourceOperation}}
@Path("{{{path}}}"){{/subresourceOperation}}{{#hasConsumes}}
@Consumes({{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}){{/hasConsumes}}{{#hasProduces}}
@Produces({{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}){{/hasProduces}}{{#useSwaggerAnnotations}}
@ApiOperation(value = "{{{summary}}}", notes = "{{{notes}}}", response = {{{returnBaseType}}}.class{{#returnContainer}}, responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, authorizations = {
{{#authMethods}}{{#isOAuth}}@Authorization(value = "{{name}}", scopes = {
{{#scopes}}@AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},
{{/-last}}{{/scopes}} }){{^-last}},{{/-last}}{{/isOAuth}}
{{^isOAuth}}@Authorization(value = "{{name}}"){{^-last}},{{/-last}}
{{/isOAuth}}{{/authMethods}} }{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} })
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}
}){{/useSwaggerAnnotations}}
{{#useCoroutines}}suspend {{/useCoroutines}}fun {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): Response {
return Response.ok().entity("magic!").build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@Valid
{{#required}}
@NotNull
{{/required}}
{{>beanValidationCore}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{#pattern}} @Pattern(regexp="{{{.}}}"){{/pattern}}{{!
minLength && maxLength set
}}{{#minLength}}{{#maxLength}} @Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{!
minLength set, maxLength not
}}{{#minLength}}{{^maxLength}} @Size(min={{minLength}}){{/maxLength}}{{/minLength}}{{!
minLength not set, maxLength set
}}{{^minLength}}{{#maxLength}} @Size(max={{.}}){{/maxLength}}{{/minLength}}{{!
@Size: minItems && maxItems set
}}{{#minItems}}{{#maxItems}} @Size(min={{minItems}},max={{maxItems}}){{/maxItems}}{{/minItems}}{{!
@Size: minItems set, maxItems not
}}{{#minItems}}{{^maxItems}} @Size(min={{minItems}}){{/maxItems}}{{/minItems}}{{!
@Size: minItems not set && maxItems set
}}{{^minItems}}{{#maxItems}} @Size(max={{.}}){{/maxItems}}{{/minItems}}{{!
check for integer or long / all others=decimal type with @Decimal*
isInteger set
}}{{#isInteger}}{{#minimum}} @Min({{.}}){{/minimum}}{{#maximum}} @Max({{.}}){{/maximum}}{{/isInteger}}{{!
isLong set
}}{{#isLong}}{{#minimum}} @Min({{.}}L){{/minimum}}{{#maximum}} @Max({{.}}L){{/maximum}}{{/isLong}}{{!
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}} @DecimalMin({{#exclusiveMinimum}}value={{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}},inclusive=false{{/exclusiveMinimum}}){{/minimum}}{{#maximum}} @DecimalMax({{#exclusiveMaximum}}value={{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}},inclusive=false{{/exclusiveMaximum}}){{/maximum}}{{/isLong}}{{/isInteger}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#required}} @NotNull{{/required}}{{>beanValidationCore}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{! PathParam is always required, no @NotNull necessary }}{{>beanValidationCore}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#required}} @NotNull{{/required}}{{>beanValidationCore}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isBodyParam}}{{#useBeanValidation}}@Valid {{#required}}{{^isNullable}}@NotNull {{/isNullable}}{{/required}}{{/useBeanValidation}} {{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{/isBodyParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
group "{{groupId}}"
version "{{artifactVersion}}"

wrapper {
gradleVersion = "6.9"
distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip"
}

buildscript {
ext.kotlin_version = "1.4.32"
ext.jakarta_ws_rs_version = "2.1.6"
ext.swagger_annotations_version = "1.5.3"
ext.jakarta_annotations_version = "1.3.5"
ext.jackson_version = "2.9.9"
{{#useBeanValidation}}
ext.beanvalidation_version = "2.0.2"
{{/useBeanValidation}}
repositories {
maven { url "https://repo1.maven.org/maven2" }
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}

apply plugin: "java"
apply plugin: "kotlin"
apply plugin: "application"

sourceCompatibility = 1.8

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}

compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}

repositories {
maven { setUrl("https://repo1.maven.org/maven2") }
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
implementation("ch.qos.logback:logback-classic:1.2.1")
implementation("jakarta.ws.rs:jakarta.ws.rs-api:$jakarta_ws_rs_version")
implementation("jakarta.annotation:jakarta.annotation-api:$jakarta_annotations_version")
implementation("io.swagger:swagger-annotations:$swagger_annotations_version")
implementation("com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:$jackson_version")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version")
{{#useBeanValidation}}
implementation("jakarta.validation:jakarta.validation-api:$beanvalidation_version")
{{/useBeanValidation}}
testImplementation("junit:junit:4.13.2")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isCookieParam}}@CookieParam("{{baseName}}"){{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{^isContainer}}{{#defaultValue}} @DefaultValue("{{{.}}}"){{/defaultValue}}{{/isContainer}} {{#useSwaggerAnnotations}}{{#description}} @ApiParam("{{.}}"){{/description}}{{/useSwaggerAnnotations}} {{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{/isCookieParam}}
Loading