Skip to content

Commit

Permalink
[kotlin] New 'jvm-spring-webclient' library (#15568)
Browse files Browse the repository at this point in the history
* Added library 'jvm-spring-webclient' to Kotlin client generator

* Reran all generators and generated docs

* Changed target of kotlin-jvm-spring-webclient sample from 2_0 to 3_0

* Added build of kotlin-jvm-spring-webclient to github workflow
  • Loading branch information
stefankoppier committed May 23, 2023
1 parent 5299935 commit 9358ab9
Show file tree
Hide file tree
Showing 43 changed files with 3,104 additions and 3 deletions.
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
- samples/client/petstore/kotlin-jvm-vertx-jackson
- samples/client/petstore/kotlin-jvm-vertx-jackson-coroutines
- samples/client/petstore/kotlin-jvm-vertx-moshi
- samples/client/petstore/kotlin-jvm-spring-webclient
- samples/client/petstore/kotlin-spring-cloud
steps:
- uses: actions/checkout@v3
Expand Down
9 changes: 9 additions & 0 deletions bin/configs/kotlin-jvm-spring-webclient.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: kotlin
outputDir: samples/client/petstore/kotlin-jvm-spring-webclient
library: jvm-spring-webclient
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
additionalProperties:
artifactId: kotlin-petstore-spring-webclient
enumUnknownDefaultCase: true
serializationLibrary: jackson
2 changes: 1 addition & 1 deletion docs/generators/kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|generateRoomModels|Generate Android Room database models in addition to API models (JVM Volley library only)| |false|
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|idea|Add IntellJ Idea plugin and mark Kotlin main and test folders as source folders.| |false|
|library|Library template (sub-template) to use|<dl><dt>**jvm-ktor**</dt><dd>Platform: Java Virtual Machine. HTTP client: Ktor 1.6.7. JSON processing: Gson, Jackson (default).</dd><dt>**jvm-okhttp4**</dt><dd>[DEFAULT] Platform: Java Virtual Machine. HTTP client: OkHttp 4.2.0 (Android 5.0+ and Java 8+). JSON processing: Moshi 1.8.0.</dd><dt>**jvm-okhttp3**</dt><dd>Platform: Java Virtual Machine. HTTP client: OkHttp 3.12.4 (Android 2.3+ and Java 7+). JSON processing: Moshi 1.8.0.</dd><dt>**jvm-retrofit2**</dt><dd>Platform: Java Virtual Machine. HTTP client: Retrofit 2.6.2.</dd><dt>**multiplatform**</dt><dd>Platform: Kotlin multiplatform. HTTP client: Ktor 1.6.7. JSON processing: Kotlinx Serialization: 1.2.1.</dd><dt>**jvm-volley**</dt><dd>Platform: JVM for Android. HTTP client: Volley 1.2.1. JSON processing: gson 2.8.9</dd><dt>**jvm-vertx**</dt><dd>Platform: Java Virtual Machine. HTTP client: Vert.x Web Client. JSON processing: Moshi, Gson or Jackson.</dd></dl>|jvm-okhttp4|
|library|Library template (sub-template) to use|<dl><dt>**jvm-ktor**</dt><dd>Platform: Java Virtual Machine. HTTP client: Ktor 1.6.7. JSON processing: Gson, Jackson (default).</dd><dt>**jvm-okhttp4**</dt><dd>[DEFAULT] Platform: Java Virtual Machine. HTTP client: OkHttp 4.2.0 (Android 5.0+ and Java 8+). JSON processing: Moshi 1.8.0.</dd><dt>**jvm-okhttp3**</dt><dd>Platform: Java Virtual Machine. HTTP client: OkHttp 3.12.4 (Android 2.3+ and Java 7+). JSON processing: Moshi 1.8.0.</dd><dt>**jvm-spring-webclient**</dt><dd>Platform: Java Virtual Machine. HTTP: Spring 5 WebClient. JSON processing: Jackson.</dd><dt>**jvm-retrofit2**</dt><dd>Platform: Java Virtual Machine. HTTP client: Retrofit 2.6.2.</dd><dt>**multiplatform**</dt><dd>Platform: Kotlin multiplatform. HTTP client: Ktor 1.6.7. JSON processing: Kotlinx Serialization: 1.2.1.</dd><dt>**jvm-volley**</dt><dd>Platform: JVM for Android. HTTP client: Volley 1.2.1. JSON processing: gson 2.8.9</dd><dt>**jvm-vertx**</dt><dd>Platform: Java Virtual Machine. HTTP client: Vert.x Web Client. JSON processing: Moshi, Gson or Jackson.</dd></dl>|jvm-okhttp4|
|modelMutable|Create mutable models| |false|
|moshiCodeGen|Whether to enable codegen with the Moshi library. Refer to the [official Moshi doc](https://github.com/square/moshi#codegen) for more info.| |false|
|omitGradlePluginVersions|Whether to declare Gradle plugin versions in build files.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
protected static final String MULTIPLATFORM = "multiplatform";
protected static final String JVM_VOLLEY = "jvm-volley";
protected static final String JVM_VERTX = "jvm-vertx";
protected static final String JVM_SPRING_WEBCLIENT = "jvm-spring-webclient";

public static final String USE_RX_JAVA = "useRxJava";
public static final String USE_RX_JAVA2 = "useRxJava2";
Expand Down Expand Up @@ -215,6 +216,7 @@ public KotlinClientCodegen() {
supportedLibraries.put(JVM_KTOR, "Platform: Java Virtual Machine. HTTP client: Ktor 1.6.7. JSON processing: Gson, Jackson (default).");
supportedLibraries.put(JVM_OKHTTP4, "[DEFAULT] Platform: Java Virtual Machine. HTTP client: OkHttp 4.2.0 (Android 5.0+ and Java 8+). JSON processing: Moshi 1.8.0.");
supportedLibraries.put(JVM_OKHTTP3, "Platform: Java Virtual Machine. HTTP client: OkHttp 3.12.4 (Android 2.3+ and Java 7+). JSON processing: Moshi 1.8.0.");
supportedLibraries.put(JVM_SPRING_WEBCLIENT, "Platform: Java Virtual Machine. HTTP: Spring 5 WebClient. JSON processing: Jackson.");
supportedLibraries.put(JVM_RETROFIT2, "Platform: Java Virtual Machine. HTTP client: Retrofit 2.6.2.");
supportedLibraries.put(MULTIPLATFORM, "Platform: Kotlin multiplatform. HTTP client: Ktor 1.6.7. JSON processing: Kotlinx Serialization: 1.2.1.");
supportedLibraries.put(JVM_VOLLEY, "Platform: JVM for Android. HTTP client: Volley 1.2.1. JSON processing: gson 2.8.9");
Expand Down Expand Up @@ -455,6 +457,9 @@ public void processOpts() {
case JVM_RETROFIT2:
processJVMRetrofit2Library(infrastructureFolder);
break;
case JVM_SPRING_WEBCLIENT:
processJVMSpringWebClientLibrary(infrastructureFolder);
break;
case MULTIPLATFORM:
processMultiplatformLibrary(infrastructureFolder);
break;
Expand Down Expand Up @@ -724,6 +729,18 @@ private void processJVMOkHttpLibrary(final String infrastructureFolder) {
supportingFiles.add(new SupportingFile("infrastructure/ApiResponse.kt.mustache", infrastructureFolder, "ApiResponse.kt"));
}

private void processJVMSpringWebClientLibrary(final String infrastructureFolder) {
if (getSerializationLibrary() != SERIALIZATION_LIBRARY_TYPE.jackson) {
throw new RuntimeException("This library currently only supports jackson serialization. Try adding '--additional-properties serializationLibrary=jackson' to your command.");
}

commonJvmMultiplatformSupportingFiles(infrastructureFolder);
addSupportingSerializerAdapters(infrastructureFolder);

additionalProperties.put(JVM, true);
additionalProperties.put(JVM_SPRING_WEBCLIENT, true);
}

private void processMultiplatformLibrary(final String infrastructureFolder) {
commonJvmMultiplatformSupportingFiles(infrastructureFolder);
additionalProperties.put(MULTIPLATFORM, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ buildscript {
{{#jvm-vertx}}
ext.vertx_version = "4.3.3"
{{/jvm-vertx}}
{{#jvm-spring-webclient}}
ext.spring_boot_version = "2.7.11"
ext.reactor_version = "3.5.6"
{{/jvm-spring-webclient}}

repositories {
maven { url "https://repo1.maven.org/maven2" }
Expand Down Expand Up @@ -122,6 +126,10 @@ dependencies {
{{#jvm-okhttp4}}
implementation "com.squareup.okhttp3:okhttp:4.10.0"
{{/jvm-okhttp4}}
{{#jvm-spring-webclient}}
implementation "org.springframework.boot:spring-boot-starter-webflux:$spring_boot_version"
implementation "io.projectreactor:reactor-core:$reactor_version"
{{/jvm-spring-webclient}}
{{#threetenbp}}
implementation "org.threeten:threetenbp:1.5.1"
{{/threetenbp}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{{>licenseInfo}}
package {{apiPackage}}

{{#jackson}}
import com.fasterxml.jackson.annotation.JsonProperty
{{/jackson}}

import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import org.springframework.http.ResponseEntity
import org.springframework.util.MultiValueMap
import org.springframework.web.util.UriComponentsBuilder
import reactor.core.publisher.Mono

{{#imports}}import {{import}}
{{/imports}}
import {{packageName}}.infrastructure.*

{{#operations}}
{{#nonPublicApi}}internal {{/nonPublicApi}}class {{classname}}(client: WebClient) : ApiClient(client) {
{{#operation}}
{{#allParams}}
{{#isEnum}}
/**
* enum for parameter {{paramName}}
*/
{{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{enumName}}{{operationIdCamelCase}}(val value: {{^isContainer}}{{dataType}}{{/isContainer}}{{#isContainer}}kotlin.String{{/isContainer}}) {
{{^enumUnknownDefaultCase}}
{{#allowableValues}}
{{#enumVars}}
{{#jackson}}
@JsonProperty(value = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) {{&name}}({{{value}}}){{^-last}},{{/-last}}
{{/jackson}}
{{/enumVars}}
{{/allowableValues}}
{{/enumUnknownDefaultCase}}
{{#enumUnknownDefaultCase}}
{{#allowableValues}}
{{#enumVars}}
{{^-last}}
{{#jackson}}
@JsonProperty(value = {{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}}) {{&name}}({{{value}}}),
{{/jackson}}
{{/-last}}
{{/enumVars}}
{{/allowableValues}}
{{/enumUnknownDefaultCase}}
}

{{/isEnum}}
{{/allParams}}

@Throws(WebClientResponseException::class)
{{#isDeprecated}}
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
fun {{operationId}}({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): Mono<{{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> {
return {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
.map { it.body }
}

@Throws(WebClientResponseException::class)
{{#isDeprecated}}
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
fun {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): Mono<ResponseEntity<{{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>> {
val localVariableConfig = {{operationId}}RequestConfig({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
return request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, PartConfig<*>>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
localVariableConfig
)
}

{{#isDeprecated}}
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
fun {{operationId}}RequestConfig({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : RequestConfig<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, PartConfig<*>>{{/hasFormParams}}{{/hasBodyParam}}> {
val localVariableBody = {{#hasBodyParam}}{{!
}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{!
}}{{^hasFormParams}}null{{/hasFormParams}}{{!
}}{{#hasFormParams}}mapOf({{#formParams}}
"{{{baseName}}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
}}{{/formParams}}){{/hasFormParams}}{{!
}}{{/hasBodyParam}}
val localVariableQuery = {{^hasQueryParams}}mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
.apply {
{{#queryParams}}
{{^required}}
if ({{{paramName}}} != null) {
put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
}
{{/required}}
{{#required}}
{{#isNullable}}
if ({{{paramName}}} != null) {
put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
}
{{/isNullable}}
{{^isNullable}}
put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
{{/isNullable}}
{{/required}}
{{/queryParams}}
}
{{/hasQueryParams}}
val localVariableHeaders: MutableMap<String, String> = mutableMapOf({{#hasFormParams}}"Content-Type" to {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{/hasFormParams}})
{{#headerParams}}
{{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{/headerParams}}
{{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}"
{{/consumes}}{{/hasConsumes}}{{/hasFormParams}}{{#hasProduces}}localVariableHeaders["Accept"] = "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"
{{/hasProduces}}

return RequestConfig(
method = RequestMethod.{{httpMethod}},
path = "{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}},
query = localVariableQuery,
headers = localVariableHeaders,
requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
body = localVariableBody
)
}

{{/operation}}
private fun encodeURIComponent(uriComponent: kotlin.String): kotlin.String =
UriComponentsBuilder.newInstance()
.scheme("http")
.host("localhost")
.pathSegment(uriComponent)
.build()
.encode()
.pathSegments
.first()
}
{{/operations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package {{packageName}}.infrastructure;

import org.springframework.http.HttpMethod
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.http.ResponseEntity
import reactor.core.publisher.Mono

open class ApiClient(protected val client: WebClient) {
companion object {
protected const val ContentType = "Content-Type"
protected const val Accept = "Accept"
protected const val JsonMediaType = "application/json"
}

protected inline fun <reified I : Any, reified T: Any?> request(requestConfig: RequestConfig<I>): Mono<ResponseEntity<T>> {
return prepare(defaults(requestConfig))
.retrieve()
.toEntity(T::class.java)
}

protected fun <I : Any> prepare(requestConfig: RequestConfig<I>) =
client.method(requestConfig)
.uri(requestConfig)
.headers(requestConfig)
.body(requestConfig)

protected fun <I> defaults(requestConfig: RequestConfig<I>) =
requestConfig.apply {
if (body != null && headers[ContentType].isNullOrEmpty()) {
headers[ContentType] = JsonMediaType
}
if (headers[Accept].isNullOrEmpty()) {
headers[Accept] = JsonMediaType
}
}

private fun <I> WebClient.method(requestConfig: RequestConfig<I>)=
method(HttpMethod.valueOf(requestConfig.method.name))

private fun <I> WebClient.RequestBodyUriSpec.uri(requestConfig: RequestConfig<I>) =
uri { builder ->
builder.path(requestConfig.path).apply {
requestConfig.query.forEach { (name, value) ->
queryParam(name, value)
}
}.build()
}

private fun <I> WebClient.RequestBodySpec.headers(requestConfig: RequestConfig<I>) =
apply { requestConfig.headers.forEach { (name, value) -> header(name, value) } }

private fun <I : Any> WebClient.RequestBodySpec.body(requestConfig: RequestConfig<I>) =
apply { if (requestConfig.body != null) bodyValue(requestConfig.body) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator

# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs

# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux

# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux

# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
Loading

0 comments on commit 9358ab9

Please sign in to comment.