Skip to content

OpenAPI Generator Kotlin Client & Server #191

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,32 @@

public class KoraCodegen extends DefaultCodegen {

private final Logger LOGGER = LoggerFactory.getLogger(KoraCodegen.class);
private static final Logger LOGGER = LoggerFactory.getLogger(KoraCodegen.class);

private enum Mode {
JAVA_CLIENT, JAVA_SERVER, REACTIVE_CLIENT, REACTIVE_SERVER, KOTLIN_CLIENT, KOTLIN_SERVER;
JAVA_CLIENT, JAVA_SERVER, JAVA_REACTIVE_CLIENT, JAVA_REACTIVE_SERVER,
KOTLIN_CLIENT, KOTLIN_SERVER, KOTLIN_COROUTINE_CLIENT, KOTLIN_COROUTINE_SERVER;

public boolean isServer() {
return switch (this) {
case JAVA_CLIENT, REACTIVE_CLIENT, KOTLIN_CLIENT -> false;
case JAVA_SERVER, REACTIVE_SERVER, KOTLIN_SERVER -> true;
case JAVA_SERVER, JAVA_REACTIVE_SERVER, KOTLIN_SERVER, KOTLIN_COROUTINE_SERVER -> true;
default -> false;
};
}

public boolean isClient() {
return !isServer();
}

public boolean isJava() {
return this != KOTLIN_CLIENT && this != KOTLIN_SERVER;
return switch (this) {
case JAVA_CLIENT, JAVA_REACTIVE_CLIENT, JAVA_SERVER, JAVA_REACTIVE_SERVER -> true;
default -> false;
};
}

public boolean isKotlin() {
return !isJava();
}
}

Expand Down Expand Up @@ -228,6 +240,14 @@ public void processOpts() {
apiTemplateFiles.put("javaClientResponseMappers.mustache", "ClientResponseMappers.java");
apiTemplateFiles.put("javaClientRequestMappers.mustache", "ClientRequestMappers.java");
}
case JAVA_REACTIVE_CLIENT -> {
this.additionalProperties.put("isClient", true);
modelTemplateFiles.put("javaModel.mustache", ".java");
apiTemplateFiles.put("javaReactiveClientApi.mustache", ".java");
apiTemplateFiles.put("javaApiResponses.mustache", "Responses.java");
apiTemplateFiles.put("javaClientResponseMappers.mustache", "ClientResponseMappers.java");
apiTemplateFiles.put("javaClientRequestMappers.mustache", "ClientRequestMappers.java");
}
case JAVA_SERVER -> {
this.additionalProperties.put("isClient", false);
apiTemplateFiles.put("javaServerApi.mustache", "Controller.java");
Expand All @@ -237,18 +257,10 @@ public void processOpts() {
apiTemplateFiles.put("javaServerResponseMappers.mustache", "ServerResponseMappers.java");
modelTemplateFiles.put("javaModel.mustache", ".java");
}
case REACTIVE_CLIENT -> {
this.additionalProperties.put("isClient", true);
modelTemplateFiles.put("javaModel.mustache", ".java");
apiTemplateFiles.put("javaApiResponses.mustache", "Responses.java");
apiTemplateFiles.put("javaClientResponseMappers.mustache", "ClientResponseMappers.java");
apiTemplateFiles.put("javaClientRequestMappers.mustache", "ClientRequestMappers.java");
apiTemplateFiles.put("reactiveClientApi.mustache", ".java");
}
case REACTIVE_SERVER -> {
case JAVA_REACTIVE_SERVER -> {
this.additionalProperties.put("isClient", false);
apiTemplateFiles.put("reactiveServerApi.mustache", "Controller.java");
apiTemplateFiles.put("reactiveServerApiDelegate.mustache", "Delegate.java");
apiTemplateFiles.put("javaReactiveServerApi.mustache", "Controller.java");
apiTemplateFiles.put("javaReactiveServerApiDelegate.mustache", "Delegate.java");
apiTemplateFiles.put("javaApiResponses.mustache", "Responses.java");
apiTemplateFiles.put("javaServerRequestMappers.mustache", "ServerRequestMappers.java");
apiTemplateFiles.put("javaServerResponseMappers.mustache", "ServerResponseMappers.java");
Expand All @@ -262,6 +274,14 @@ public void processOpts() {
apiTemplateFiles.put("kotlinClientRequestMappers.mustache", "ClientRequestMappers.kt");
apiTemplateFiles.put("kotlinClientResponseMappers.mustache", "ClientResponseMappers.kt");
}
case KOTLIN_COROUTINE_CLIENT -> {
this.additionalProperties.put("isClient", true);
modelTemplateFiles.put("kotlinModel.mustache", ".kt");
apiTemplateFiles.put("kotlinCoroutineClientApi.mustache", ".kt");
apiTemplateFiles.put("kotlinApiResponses.mustache", "Responses.kt");
apiTemplateFiles.put("kotlinClientRequestMappers.mustache", "ClientRequestMappers.kt");
apiTemplateFiles.put("kotlinClientResponseMappers.mustache", "ClientResponseMappers.kt");
}
case KOTLIN_SERVER -> {
this.additionalProperties.put("isClient", false);
modelTemplateFiles.put("kotlinModel.mustache", ".kt");
Expand All @@ -271,6 +291,15 @@ public void processOpts() {
apiTemplateFiles.put("kotlinServerRequestMappers.mustache", "ServerRequestMappers.kt");
apiTemplateFiles.put("kotlinServerResponseMappers.mustache", "ServerResponseMappers.kt");
}
case KOTLIN_COROUTINE_SERVER -> {
this.additionalProperties.put("isClient", false);
modelTemplateFiles.put("kotlinModel.mustache", ".kt");
apiTemplateFiles.put("kotlinCoroutineServerApi.mustache", "Controller.kt");
apiTemplateFiles.put("kotlinCoroutineServerApiDelegate.mustache", "Delegate.kt");
apiTemplateFiles.put("kotlinApiResponses.mustache", "Responses.kt");
apiTemplateFiles.put("kotlinServerRequestMappers.mustache", "ServerRequestMappers.kt");
apiTemplateFiles.put("kotlinServerResponseMappers.mustache", "ServerResponseMappers.kt");
}
}
if (additionalProperties.containsKey(ENABLE_VALIDATION) && this.codegenMode.isServer()) {
this.enableValidation = Boolean.parseBoolean(additionalProperties.get(ENABLE_VALIDATION).toString());
Expand All @@ -280,7 +309,7 @@ public void processOpts() {
}

embeddedTemplateDir = templateDir = "openapi/templates/kora";
if (this.codegenMode == Mode.KOTLIN_CLIENT || this.codegenMode == Mode.KOTLIN_SERVER) {
if (this.codegenMode.isKotlin()) {
languageSpecificPrimitives = new HashSet<>(
Arrays.asList(
"ByteArray",
Expand Down Expand Up @@ -1343,7 +1372,7 @@ record AuthMethodGroup(String name, List<CodegenSecurity> methods) {}
response.vendorExtensions.put("singleResponse", op.responses.size() == 1);
}
if (op.hasAuthMethods) {
if (this.codegenMode.name().contains("SERVER")) {
if (this.codegenMode.isServer()) {
var operationAuthMethods = new TreeSet<String>();
for (var authMethod : op.authMethods) {
tags.add(upperCase(toVarName(authMethod.name)));
Expand Down Expand Up @@ -1449,7 +1478,7 @@ record AuthMethodGroup(String name, List<CodegenSecurity> methods) {}
}
}
}
if (this.codegenMode == Mode.JAVA_CLIENT || this.codegenMode == Mode.REACTIVE_CLIENT || this.codegenMode == Mode.KOTLIN_CLIENT) {
if (this.codegenMode.isClient()) {
var annotationParams = httpClientAnnotationParams.entrySet()
.stream()
.map(e -> e.getKey() + " = " + e.getValue())
Expand Down Expand Up @@ -1522,19 +1551,19 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
var securitySchemas = openAPI.getComponents().getSecuritySchemes();
if (!Objects.requireNonNullElse(securitySchemas, Map.of()).isEmpty()) {
switch (this.codegenMode) {
case JAVA_CLIENT, REACTIVE_CLIENT -> {
case JAVA_CLIENT, JAVA_REACTIVE_CLIENT -> {
var securitySchemaClass = apiFileFolder() + File.separator + "ApiSecurity.java";
this.supportingFiles.add(new SupportingFile("javaClientSecuritySchema.mustache", securitySchemaClass));
}
case JAVA_SERVER, REACTIVE_SERVER -> {
case JAVA_SERVER, JAVA_REACTIVE_SERVER -> {
var securitySchemaClass = apiFileFolder() + File.separator + "ApiSecurity.java";
this.supportingFiles.add(new SupportingFile("javaServerSecuritySchema.mustache", securitySchemaClass));
}
case KOTLIN_CLIENT -> {
case KOTLIN_CLIENT, KOTLIN_COROUTINE_CLIENT -> {
var securitySchemaClass = apiFileFolder() + File.separator + "ApiSecurity.kt";
this.supportingFiles.add(new SupportingFile("kotlinClientSecuritySchema.mustache", securitySchemaClass));
}
case KOTLIN_SERVER -> {
case KOTLIN_SERVER, KOTLIN_COROUTINE_SERVER -> {
var securitySchemaClass = apiFileFolder() + File.separator + "ApiSecurity.kt";
this.supportingFiles.add(new SupportingFile("kotlinServerSecuritySchema.mustache", securitySchemaClass));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface {{classname}} {
{{/implicitHeaders}}{{#responses}}
@ResponseCodeMapper(code = {{#isDefault}}-1{{/isDefault}}{{^isDefault}}{{code}}{{/isDefault}}, mapper = {{classname}}ClientResponseMappers.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}{{code}}ApiResponseMapper::class){{/responses}}{{#hasAuthMethods}}
@InterceptWith(tag = Tag(ApiSecurity.{{vendorExtensions.authInterceptorTag}}::class), value = HttpClientInterceptor::class){{/hasAuthMethods}}
suspend fun {{operationId}}({{>kotlinAnnotatedParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse
fun {{operationId}}({{>kotlinAnnotatedParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse

{{#hasFormParams}}
data class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}FormParam(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package {{package}}

{{#imports}}import {{import}}
{{/imports}}
import reactor.core.publisher.Mono
import ru.tinkoff.kora.http.client.common.annotation.HttpClient;
import ru.tinkoff.kora.http.common.annotation.HttpRoute;
import ru.tinkoff.kora.http.client.common.annotation.ResponseCodeMapper;
import ru.tinkoff.kora.common.Mapping;
import ru.tinkoff.kora.http.common.annotation.Path;
import ru.tinkoff.kora.http.common.annotation.Header;
import ru.tinkoff.kora.http.common.annotation.Query;
{{#hasAuthMethods}}
import ru.tinkoff.kora.common.Tag;
import ru.tinkoff.kora.http.common.annotation.InterceptWith;
import ru.tinkoff.kora.http.client.common.interceptor.HttpClientInterceptor;
{{/hasAuthMethods}}

@ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client"){{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}
@HttpClient{{{annotationParams}}}
interface {{classname}} {
{{#operations}}
{{#operation}}
/**
* {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
{{#notes}}
* {{.}}
{{/notes}}
*
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
@HttpRoute(method = "{{httpMethod}}", path = "{{path}}"){{#implicitHeaders}}
@ApiImplicitParams({
{{#headerParams}}
TODO implisit header
{{>implicitHeader}}
{{/headerParams}}
})
{{/implicitHeaders}}{{#responses}}
@ResponseCodeMapper(code = {{#isDefault}}-1{{/isDefault}}{{^isDefault}}{{code}}{{/isDefault}}, mapper = {{classname}}ClientResponseMappers.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}{{code}}ApiResponseMapper::class){{/responses}}{{#hasAuthMethods}}
@InterceptWith(tag = Tag(ApiSecurity.{{vendorExtensions.authInterceptorTag}}::class), value = HttpClientInterceptor::class){{/hasAuthMethods}}
suspend fun {{operationId}}({{>kotlinAnnotatedParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse

{{#hasFormParams}}
data class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}FormParam(
{{#formParams}}
val {{paramName}}: {{#isFile}}ru.tinkoff.kora.http.common.form.FormMultipart.FormPart{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}}{{^required}}?{{/required}}{{^-last}},{{/-last}}
{{/formParams}}
){}
{{/hasFormParams}}


{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package {{package}}

import ru.tinkoff.kora.common.Mapping;
import ru.tinkoff.kora.http.common.annotation.HttpRoute;
import ru.tinkoff.kora.http.common.annotation.Path;
import ru.tinkoff.kora.http.common.annotation.Header;
import ru.tinkoff.kora.http.common.annotation.Query;
import javax.annotation.Nullable;
import ru.tinkoff.kora.http.server.common.annotation.HttpController;

{{#imports}}import {{import}}
{{/imports}}
{{#hasAuthMethods}}
import ru.tinkoff.kora.common.Tag;
import ru.tinkoff.kora.http.common.annotation.InterceptWith;
import ru.tinkoff.kora.http.server.common.HttpServerInterceptor;
{{/hasAuthMethods}}{{#vendorExtensions.enableValidation}}
import ru.tinkoff.kora.http.common.annotation.InterceptWith;
import ru.tinkoff.kora.validation.common.annotation.Validate;
import ru.tinkoff.kora.validation.common.annotation.Valid;
import ru.tinkoff.kora.validation.common.annotation.Size;
import ru.tinkoff.kora.validation.common.annotation.Pattern;
import ru.tinkoff.kora.validation.common.annotation.Range;
{{/vendorExtensions.enableValidation}}

@ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client"){{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}
@HttpController{{#vendorExtensions.enableValidation}}
@ru.tinkoff.kora.common.Component{{/vendorExtensions.enableValidation}}
{{#vendorExtensions.enableValidation}}open {{/vendorExtensions.enableValidation}}class {{classname}}Controller(private val delegate: {{classname}}Delegate) {
{{#operations}}
{{#operation}}
/**
* {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
{{#notes}}
* {{.}}
{{/notes}}
*
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
@HttpRoute(method = "{{httpMethod}}", path = "{{path}}"){{#implicitHeaders}}
@ApiImplicitParams({
{{#headerParams}}
TODO implicit header
{{>implicitHeader}}
{{/headerParams}}
})
{{/implicitHeaders}}{{#hasAuthMethods}}
@InterceptWith(tag = Tag(ApiSecurity.{{vendorExtensions.authInterceptorTag}}::class), value = HttpServerInterceptor::class){{/hasAuthMethods}}{{#vendorExtensions.x-validate}}
@InterceptWith(ru.tinkoff.kora.validation.module.http.server.ValidationHttpServerInterceptor::class)
@Validate
{{/vendorExtensions.x-validate}}
@Mapping({{classname}}ServerResponseMappers.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ResponseMapper::class)
{{#vendorExtensions.enableValidation}}open {{/vendorExtensions.enableValidation}}suspend fun {{operationId}}({{>kotlinAnnotatedParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse {
return this.delegate.{{operationId}}(
{{#allParams}}{{#lambda.trim}}
{{#hasFormParams}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/hasFormParams}}
{{^hasFormParams}}{{paramName}}{{^-last}},{{/-last}}{{/hasFormParams}}{{/lambda.trim}}{{/allParams}}{{#hasFormParams}}
form{{/hasFormParams}}
)
}

{{#hasFormParams}}
data class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}FormParam(
{{#formParams}}
val {{paramName}}: {{#isFile}}ru.tinkoff.kora.http.common.form.FormMultipart.FormPart.MultipartFile{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}}{{^required}}?{{/required}}{{^-last}},{{/-last}}
{{/formParams}}
){}
{{/hasFormParams}}


{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package {{package}}

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

@ru.tinkoff.kora.common.annotation.Generated("openapi generator kora client"){{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}
interface {{classname}}Delegate {
{{#operations}}
{{#operation}}
suspend fun {{operationId}}(
{{#allParams}} {{^isFormParam}}
{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasFormParams}},{{/hasFormParams}}{{^hasFormParams}}{{^-last}},{{/-last}}{{/hasFormParams}}
{{/isFormParam}}{{/allParams}} {{#hasFormParams}}
form: {{classname}}Controller.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}FormParam
{{/hasFormParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse

{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import ru.tinkoff.kora.validation.common.annotation.Range;
@Validate
{{/vendorExtensions.x-validate}}
@Mapping({{classname}}ServerResponseMappers.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ResponseMapper::class)
{{#vendorExtensions.enableValidation}}open {{/vendorExtensions.enableValidation}}suspend fun {{operationId}}({{>kotlinAnnotatedParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse {
{{#vendorExtensions.enableValidation}}open {{/vendorExtensions.enableValidation}}fun {{operationId}}({{>kotlinAnnotatedParams}}): {{classname}}Responses.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}ApiResponse {
return this.delegate.{{operationId}}(
{{#allParams}}{{#lambda.trim}}
{{#hasFormParams}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/hasFormParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package {{package}}
interface {{classname}}Delegate {
{{#operations}}
{{#operation}}
suspend fun {{operationId}}(
fun {{operationId}}(
{{#allParams}} {{^isFormParam}}
{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasFormParams}},{{/hasFormParams}}{{^hasFormParams}}{{^-last}},{{/-last}}{{/hasFormParams}}
{{/isFormParam}}{{/allParams}} {{#hasFormParams}}
Expand Down
Loading