From 7f59a693689e5883122317f7778561d697805ca6 Mon Sep 17 00:00:00 2001 From: khayashi4337 Date: Wed, 11 Nov 2026 16:13:53 +0900 Subject: [PATCH 1/2] fix(kotlin-client): escape $ in wire names for all libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire names containing `$` (OData-style `$filter`, `$top`) were emitted verbatim into Kotlin string literals, where `$x` starts interpolation: the generated key became the same-named parameter's value, or failed to compile. Escape `$` to `\$` in wire-name string literals across the kotlin-client libraries: query keys (incl. explode/deepObject and map forms), path literals and {param} placeholders, headers, multipart field names and apiKey names. Overlapping form/multipart emission is left to the sibling work (kotlin-form-implicit-it / kotlin-vertx-form-body); the retrofit2 exploded-property argument keeps its raw baseName identifier — only the @Query wire string is escaped, so generated argument names do not change. Add KotlinClientCodegenApiTest.testDollarWireNamesAreEscaped covering all eight kotlin libraries with a fixture exercising $param, $path and $api_key wire names. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .../libraries/jvm-ktor/api.mustache | 24 +-- .../infrastructure/ApiClient.kt.mustache | 2 +- .../libraries/jvm-okhttp/api.mustache | 2 +- .../infrastructure/ApiClient.kt.mustache | 16 +- .../libraries/jvm-retrofit2/api.mustache | 6 +- .../jvm-retrofit2/explodedQueryParam.mustache | 2 +- .../jvm-retrofit2/formParams.mustache | 2 +- .../jvm-retrofit2/headerParams.mustache | 2 +- .../infrastructure/ApiClient.kt.mustache | 2 +- .../jvm-retrofit2/pathParams.mustache | 2 +- .../jvm-retrofit2/queryParam.mustache | 2 +- .../jvm-spring-restclient/api.mustache | 14 +- .../jvm-spring-webclient/api.mustache | 14 +- .../libraries/jvm-vertx/api.mustache | 16 +- .../libraries/jvm-volley/api.mustache | 8 +- .../libraries/multiplatform/api.mustache | 16 +- .../infrastructure/ApiClient.kt.mustache | 2 +- .../kotlin/KotlinClientCodegenApiTest.java | 144 ++++++++++++++++++ .../3_0/kotlin/kotlin-dollar-wire-name.yaml | 72 +++++++++ 19 files changed, 282 insertions(+), 66 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache index 3a830ca88390..9232d1e7f6dc 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache @@ -76,19 +76,19 @@ import com.fasterxml.jackson.databind.ObjectMapper {{^isArray}} {{^isString}} {{^isNumber}} - {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) } {{/isNumber}} {{#isNumber}} - {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) } {{/isNumber}} {{/isString}} {{#isString}} - {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}) } {{/isString}} {{/isArray}} {{#isArray}} for (x in {{paramName}} ?: listOf()) { - {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{{baseName}}}", x.toString()){{/isFile}} + {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", x.toString()){{/isFile}} } {{/isArray}} {{/isFile}} @@ -135,48 +135,48 @@ import com.fasterxml.jackson.databind.ObjectMapper {{#isModel}} {{#isDeepObject}} {{#vars}} - {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{vendorExtensions.x-kotlin-param-base-name}}[{{baseName}}]"] = listOf("$it") } + {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{#lambda.escapeDollar}}{{vendorExtensions.x-kotlin-param-base-name}}{{/lambda.escapeDollar}}[{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}]"] = listOf("$it") } {{/vars}} {{/isDeepObject}} {{^isDeepObject}} {{#isExplode}} {{#vars}} - {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{baseName}}"] = listOf("$it") } + {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = listOf("$it") } {{/vars}} {{/isExplode}} {{^isExplode}} - {{{paramName}}}?.let { _model -> listOfNotNull({{#vars}}_model.{{{name}}}?.let { "{{baseName}},$it" }{{^-last}}, {{/-last}}{{/vars}}).takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.joinToString(",")) } } + {{{paramName}}}?.let { _model -> listOfNotNull({{#vars}}_model.{{{name}}}?.let { "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}},$it" }{{^-last}}, {{/-last}}{{/vars}}).takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = listOf(it.joinToString(",")) } } {{/isExplode}} {{/isDeepObject}} {{/isModel}} {{^isModel}} {{#isMap}} {{#isDeepObject}} - {{{paramName}}}?.forEach { (key, value) -> localVariableQuery["{{baseName}}[$key]"] = listOf("$value") } + {{{paramName}}}?.forEach { (key, value) -> localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}[$key]"] = listOf("$value") } {{/isDeepObject}} {{^isDeepObject}} {{#isExplode}} {{{paramName}}}?.forEach { (key, value) -> localVariableQuery[key] = listOf("$value") } {{/isExplode}} {{^isExplode}} - {{{paramName}}}?.takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.entries.joinToString(",") { (k, v) -> "$k,$v" }) } + {{{paramName}}}?.takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = listOf(it.entries.joinToString(",") { (k, v) -> "$k,$v" }) } {{/isExplode}} {{/isDeepObject}} {{/isMap}} {{^isMap}} - {{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} } + {{{paramName}}}?.apply { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} } {{/isMap}} {{/isModel}} {{/queryParams}} val localVariableHeaders = mutableMapOf() {{#headerParams}} - {{{paramName}}}?.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} } + {{{paramName}}}?.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} } {{/headerParams}} val localVariableConfig = RequestConfig( RequestMethod.{{httpMethod}}, - "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", "${{{paramName}}}"){{/pathParams}}, + "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{" + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + "}", "${{{paramName}}}"){{/pathParams}}, query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache index 43542d634bcc..764022212481 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache @@ -73,7 +73,7 @@ import {{packageName}}.auth.* mapOf({{#authMethods}}{{#isBasic}}{{#isBasicBasic}} "{{name}}" to HttpBasicAuth(){{/isBasicBasic}}{{#isBasicBearer}} "{{name}}" to HttpBearerAuth("{{scheme}}"){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}} - "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"){{/isApiKey}}{{#isOAuth}} + "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"){{/isApiKey}}{{#isOAuth}} "{{name}}" to OAuth(){{/isOAuth}}{{^-last}}, {{/-last}}{{/authMethods}}) } {{/hasAuthMethods}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache index 8f412c86ee54..609cd6d056d0 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache @@ -281,7 +281,7 @@ import {{packageName}}.infrastructure.Serializer return RequestConfig( method = RequestMethod.{{httpMethod}}, - path = "{{{path}}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}}, + path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}}, query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache index 3b9223fd5380..590622523625 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache @@ -431,25 +431,25 @@ import com.squareup.moshi.adapter {{#authMethods}} {{#isApiKey}} {{#isKeyInHeader}} - if (requestConfig.headers["{{keyParamName}}"].isNullOrEmpty()) { + if (requestConfig.headers["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"].isNullOrEmpty()) { {{/isKeyInHeader}} {{#isKeyInQuery}} - if (requestConfig.query["{{keyParamName}}"].isNullOrEmpty()) { + if (requestConfig.query["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"].isNullOrEmpty()) { {{/isKeyInQuery}} - if (apiKey["{{keyParamName}}"] != null) { - if (apiKeyPrefix["{{keyParamName}}"] != null) { + if (apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) { + if (apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) { {{#isKeyInHeader}} - requestConfig.headers["{{keyParamName}}"] = apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!! + requestConfig.headers["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! {{/isKeyInHeader}} {{#isKeyInQuery}} - requestConfig.query["{{keyParamName}}"] = listOf(apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!) + requestConfig.query["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = listOf(apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!) {{/isKeyInQuery}} } else { {{#isKeyInHeader}} - requestConfig.headers["{{keyParamName}}"] = apiKey["{{keyParamName}}"]!! + requestConfig.headers["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! {{/isKeyInHeader}} {{#isKeyInQuery}} - requestConfig.query["{{keyParamName}}"] = listOf(apiKey["{{keyParamName}}"]!!) + requestConfig.query["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = listOf(apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!) {{/isKeyInQuery}} } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache index 66c78f29844f..9b1fdc8d95e3 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache @@ -157,14 +157,14 @@ import okhttp3.MultipartBody {{/formParams}} {{^isBodyAllowed}} {{#hasBodyOrFormParams}} - @HTTP(method = "{{httpMethod}}", path = "{{{path}}}", hasBody = true) + @HTTP(method = "{{httpMethod}}", path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}", hasBody = true) {{/hasBodyOrFormParams}} {{^hasBodyOrFormParams}} - @{{httpMethod}}("{{{path}}}") + @{{httpMethod}}("{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}") {{/hasBodyOrFormParams}} {{/isBodyAllowed}} {{#isBodyAllowed}} - @{{httpMethod}}("{{{path}}}") + @{{httpMethod}}("{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}") {{/isBodyAllowed}} {{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}suspend {{/useCoroutines}}{{/doNotUseRxAndCoroutines}}fun {{operationId}}({{^allParams}}){{/allParams}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}}, {{/-last}}{{#-last}}){{/-last}}{{/allParams}}{{#returnType}}: {{/returnType}}{{^returnType}}{{#useResponseAsReturnType}}: {{/useResponseAsReturnType}}{{/returnType}}{{^doNotUseRxAndCoroutines}}{{#useRxJava}}Observable<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/useRxJava}}{{#useRxJava2}}{{#returnType}}Single<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}>{{/returnType}}{{^returnType}}Completable{{/returnType}}{{/useRxJava2}}{{#useRxJava3}}{{#returnType}}Single<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}>{{/returnType}}{{^returnType}}Completable{{/returnType}}{{/useRxJava3}}{{#useCoroutines}}{{#useResponseAsReturnType}}Response<{{/useResponseAsReturnType}}{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{^returnType}}{{#useResponseAsReturnType}}Unit{{/useResponseAsReturnType}}{{/returnType}}{{/isResponseFile}}{{#useResponseAsReturnType}}>{{/useResponseAsReturnType}}{{/useCoroutines}}{{/doNotUseRxAndCoroutines}}{{#doNotUseRxAndCoroutines}}Call<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/doNotUseRxAndCoroutines}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache index 3aaa4e67fbf4..d59382b4683a 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache @@ -1 +1 @@ -@Query("{{baseName}}") {{{baseName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}} \ No newline at end of file +@Query("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{baseName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache index fe5c9db69053..bf92485b7aea 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{{paramName}}}: {{#isCollectionFormatMulti}}List<{{/isCollectionFormatMulti}}MultipartBody.Part{{#isCollectionFormatMulti}}>{{/isCollectionFormatMulti}}{{^required}}? = null{{/required}}{{/isFile}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"){{/isMultipart}} {{{paramName}}}: {{#isCollectionFormatMulti}}List<{{/isCollectionFormatMulti}}MultipartBody.Part{{#isCollectionFormatMulti}}>{{/isCollectionFormatMulti}}{{^required}}? = null{{/required}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache index eaf1e7c3bf60..417bef8a2d79 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache @@ -1 +1 @@ -{{#isHeaderParam}}@Header("{{baseName}}") {{{paramName}}}: {{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isHeaderParam}} \ No newline at end of file +{{#isHeaderParam}}@Header("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isHeaderParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache index c48089c023d9..bd712ff3dd09 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache @@ -139,7 +139,7 @@ import okhttp3.MediaType.Companion.toMediaType val auth: Interceptor? = when (authName) { {{#authMethods}} {{#isBasicBasic}}"{{name}}" -> HttpBasicAuth() {{/isBasicBasic}}{{#isBasicBearer}}"{{name}}" -> HttpBearerAuth("{{scheme}}") - {{/isBasicBearer}}{{#isApiKey}}"{{name}}" -> ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}, "{{keyParamName}}") + {{/isBasicBearer}}{{#isApiKey}}"{{name}}" -> ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}, "{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}") {{/isApiKey}}{{#isOAuth}}"{{name}}" -> OAuth(OAuthFlow.{{flow}}, "{{authorizationUrl}}", "{{tokenUrl}}", "{{#scopes}}{{scope}}{{^-last}}, {{/-last}}{{/scopes}}") {{/isOAuth}}{{^isBasicBasic}}{{^isBasicBearer}}{{^isApiKey}}{{^isOAuth}}"{{name}}" -> null{{/isOAuth}}{{/isApiKey}}{{/isBasicBearer}}{{/isBasicBasic}}{{/authMethods}} else -> throw RuntimeException("auth name $authName not found in available auth names") diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache index 685c514e6a90..cc1ade84683c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache @@ -1 +1 @@ -{{#isPathParam}}@Path("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isPathParam}} \ No newline at end of file +{{#isPathParam}}@Path("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isPathParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache index e5c9833fb8d5..a0213c39d72d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache @@ -1 +1 @@ -@Query("{{baseName}}") {{{paramName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}@JvmSuppressWildcards {{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}} \ No newline at end of file +@Query("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}@JvmSuppressWildcards {{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache index ece096ef5788..265f3e40a1da 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache @@ -107,7 +107,7 @@ import {{packageName}}.infrastructure.* }}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{! }}{{^hasFormParams}}null{{/hasFormParams}}{{! }}{{#hasFormParams}}mapOf({{#formParams}} - "{{{baseName}}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{! + "{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{! }}{{/formParams}}){{/hasFormParams}}{{! }}{{/hasBodyParam}} val localVariableQuery = {{^hasQueryParams}}mutableMapOf>() @@ -116,17 +116,17 @@ import {{packageName}}.infrastructure.* {{#queryParams}} {{^required}} if ({{{paramName}}} != null) { - put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) + put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{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<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) + put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) } {{/isNullable}} {{^isNullable}} - put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) + put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) {{/isNullable}} {{/required}} {{/queryParams}} @@ -136,18 +136,18 @@ import {{packageName}}.infrastructure.* {{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}" {{/consumes}}{{/hasConsumes}}{{/hasFormParams}}{{#hasProduces}}localVariableHeaders["Accept"] = "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"{{/hasProduces}} {{#headerParams}} - {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} } + {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} } {{/headerParams}} val params = mutableMapOf( {{#pathParams}} - "{{baseName}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}}, + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}}, {{/pathParams}} ) return RequestConfig( method = RequestMethod.{{httpMethod}}, - path = "{{{path}}}", + path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}", params = params, query = localVariableQuery, headers = localVariableHeaders, diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache index dfe2b36cd6b9..1afc3a8e2913 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache @@ -94,7 +94,7 @@ import {{packageName}}.infrastructure.* }}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{! }}{{^hasFormParams}}null{{/hasFormParams}}{{! }}{{#hasFormParams}}mapOf({{#formParams}} - "{{{baseName}}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{! + "{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{! }}{{/formParams}}){{/hasFormParams}}{{! }}{{/hasBodyParam}} val localVariableQuery = {{^hasQueryParams}}mutableMapOf>() @@ -103,17 +103,17 @@ import {{packageName}}.infrastructure.* {{#queryParams}} {{^required}} if ({{{paramName}}} != null) { - put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) + put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{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<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) + put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) } {{/isNullable}} {{^isNullable}} - put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) + put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) {{/isNullable}} {{/required}} {{/queryParams}} @@ -123,18 +123,18 @@ import {{packageName}}.infrastructure.* {{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}" {{/consumes}}{{/hasConsumes}}{{/hasFormParams}}{{#hasProduces}}localVariableHeaders["Accept"] = "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"{{/hasProduces}} {{#headerParams}} - {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} } + {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} } {{/headerParams}} val params = mutableMapOf( {{#pathParams}} - "{{baseName}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}}, + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}}, {{/pathParams}} ) return RequestConfig( method = RequestMethod.{{httpMethod}}, - path = "{{{path}}}", + path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}", params = params, query = localVariableQuery, headers = localVariableHeaders, diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache index a82b32cbfb7a..36d8362ac5aa 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache @@ -138,7 +138,7 @@ import {{packageName}}.infrastructure.* {{/isDeprecated}} {{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}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}}) : Future> { val vertxClient = WebClient.create(vertx) - val request = vertxClient.requestAbs(HttpMethod.{{httpMethod}}, UriTemplate.of("$basePath{{{path}}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}})) + val request = vertxClient.requestAbs(HttpMethod.{{httpMethod}}, UriTemplate.of("$basePath{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}})) {{#hasFormParams}}request.putHeader("Content-Type", {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}){{/hasFormParams}} {{#headerParams}}{{{paramName}}}{{^required}}?{{/required}}.apply { request.putHeader("{{baseName}}", {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}})}{{/headerParams}} @@ -158,26 +158,26 @@ import {{packageName}}.infrastructure.* {{#hasQueryParams}} {{#queryParams}} - {{{paramName}}}{{^required}}?{{/required}}.let { request.queryParams().add("{{baseName}}", {{#isContainer}}toMultiValue(it.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>(it){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>(it){{/isDate}}{{^isDateTime}}{{^isDate}}it.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) } + {{{paramName}}}{{^required}}?{{/required}}.let { request.queryParams().add("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue(it.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>(it){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>(it){{/isDate}}{{^isDateTime}}{{^isDate}}it.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) } {{/queryParams}} {{/hasQueryParams}} {{#authMethods}} {{#isApiKey}} - if (apiKey["{{keyParamName}}"] != null) { - if (apiKeyPrefix["{{keyParamName}}"] != null) { + if (apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) { + if (apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) { {{#isKeyInHeader}} - request.putHeader("{{keyParamName}}", apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!) + request.putHeader("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!) {{/isKeyInHeader}} {{#isKeyInQuery}} - request.queryParams().add("{{keyParamName}}", apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!) + request.queryParams().add("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!) {{/isKeyInQuery}} } else { {{#isKeyInHeader}} - request.putHeader("{{keyParamName}}", apiKey["{{keyParamName}}"]!!) + request.putHeader("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!) {{/isKeyInHeader}} {{#isKeyInQuery}} - request.queryParams().add("{{keyParamName}}", apiKey["{{keyParamName}}"]!!) + request.queryParams().add("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!) {{/isKeyInQuery}} } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache index 246b5e73d169..d07755d1f7c7 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache @@ -63,10 +63,10 @@ import {{packageName}}.infrastructure.CollectionFormats.* // to make it easy to override with your own. // create path and map variables - val path = "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}IRequestFactory.escapeString({{{paramName}}}.toString()){{/isContainer}}){{/pathParams}} + val path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{" + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}IRequestFactory.escapeString({{{paramName}}}.toString()){{/isContainer}}){{/pathParams}} val formParams = mapOf({{^formParams}}){{/formParams}}{{#formParams}} - "{{baseName}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}} + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}} ){{/-last}}{{/formParams}} @@ -74,12 +74,12 @@ import {{packageName}}.infrastructure.CollectionFormats.* // TODO, if its apikey auth, then add the header names here and the hardcoded auth key // Only support hard coded apikey in query param auth for when we do this first path val queryParams = mapOf({{^queryParams}}){{/queryParams}}{{#queryParams}} - "{{baseName}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}} + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}} ){{/-last}}{{/queryParams}} .filter { it.value.isNotEmpty() } val headerParams: Map = mapOf({{^headerParams}}){{/headerParams}}{{#headerParams}} - "{{baseName}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}} + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}} ){{/-last}}{{/headerParams}} return suspendCoroutine { continuation -> diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache index 82f266017caf..d39657c2278c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache @@ -73,7 +73,7 @@ import kotlinx.serialization.encoding.* {{#formParams}} {{#isArray}} {{{paramName}}}?.onEach { - {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{{baseName}}}", it.toString()){{/isFile}} + {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", it.toString()){{/isFile}} } {{/isArray}} {{^isArray}} @@ -83,18 +83,18 @@ import kotlinx.serialization.encoding.* {{^isFile}} {{#isPrimitiveType}} {{#isString}} - {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}) } {{/isString}} {{^isString}} - {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) } {{/isString}} {{/isPrimitiveType}} {{^isPrimitiveType}} {{#isEnumOrRef}} - {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.value.toString()) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.value.toString()) } {{/isEnumOrRef}} {{^isEnumOrRef}} - {{{paramName}}}?.apply { append("{{{baseName}}}", ApiClient.JSON_DEFAULT.encodeToString({{{dataType}}}.serializer(), {{{paramName}}})) } + {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", ApiClient.JSON_DEFAULT.encodeToString({{{dataType}}}.serializer(), {{{paramName}}})) } {{/isEnumOrRef}} {{/isPrimitiveType}} {{/isFile}} @@ -116,13 +116,13 @@ import kotlinx.serialization.encoding.* {{/hasBodyParam}} val localVariableQuery = mutableMapOf>(){{#queryParams}} - {{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}){{/isContainer}} }{{/queryParams}} + {{{paramName}}}?.apply { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}){{/isContainer}} }{{/queryParams}} val localVariableHeaders = mutableMapOf(){{#headerParams}} - {{{paramName}}}?.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }{{/headerParams}} + {{{paramName}}}?.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }{{/headerParams}} val localVariableConfig = RequestConfig( RequestMethod.{{httpMethod}}, - "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}{{/isContainer}}){{/pathParams}}, + "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{" + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}{{/isContainer}}){{/pathParams}}, query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache index d2a1fe0ebf2e..3eb45309f9de 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache @@ -54,7 +54,7 @@ import {{packageName}}.auth.* mapOf({{#authMethods}}{{#isBasic}}{{#isBasicBasic}} "{{name}}" to HttpBasicAuth(){{/isBasicBasic}}{{#isBasicBearer}} "{{name}}" to HttpBearerAuth("{{scheme}}"){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}} - "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"){{/isApiKey}}{{#isOAuth}} + "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"){{/isApiKey}}{{#isOAuth}} "{{name}}" to OAuth(){{/isOAuth}}{{^-last}}, {{/-last}}{{/authMethods}}) } {{/hasAuthMethods}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java index 51208d4130e8..50967d0bcc80 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; @@ -357,4 +358,147 @@ private KotlinClientCodegen createCodegen(ClientLibrary library) throws IOExcept codegen.additionalProperties().put(KotlinClientCodegen.DATE_LIBRARY, "kotlinx-datetime"); return codegen; } + + @DataProvider(name = "librariesWithDollarWireNames") + public static Object[][] librariesWithDollarWireNames() { + return new Object[][]{ + {ClientLibrary.JVM_KTOR}, + {ClientLibrary.JVM_OKHTTP4}, + {ClientLibrary.JVM_SPRING_WEBCLIENT}, + {ClientLibrary.JVM_SPRING_RESTCLIENT}, + {ClientLibrary.JVM_RETROFIT2}, + {ClientLibrary.MULTIPLATFORM}, + {ClientLibrary.JVM_VOLLEY}, + {ClientLibrary.JVM_VERTX} + }; + } + + // Wire names containing `$` (OData-style `$filter`/`$top`) must be emitted + // as escaped Kotlin string literals (`"\$filter"`); unescaped, Kotlin + // string interpolation turns the wire key into the same-named parameter's + // value — or fails compilation outright when no such variable exists + // (`"$top"`). The same applies to `{{{path}}}` segments (`/odata/$count`), + // path-param placeholders (`{$id}`), header names, deepObject keys, + // multipart field names and apiKey names. Assertions stay within the + // lines this change actually escapes: vertx keeps master's header and + // form emission, and the retrofit2 exploded-property argument keeps its + // raw baseName identifier (only the @Query wire string is escaped). + @Test(dataProvider = "librariesWithDollarWireNames") + void testDollarWireNamesAreEscaped(ClientLibrary library) throws IOException { + OpenAPI openAPI = readOpenAPI("src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml"); + + KotlinClientCodegen codegen = createCodegen(library); + String outputPath = codegen.getOutputDir().replace('\\', '/'); + ClientOptInput input = createClientOptInput(openAPI, codegen); + DefaultGenerator generator = new DefaultGenerator(); + // ktor/multiplatform/retrofit2/okhttp4 put the apiKey name inside + // ApiClient.kt, a supporting file; the other libraries don't emit it + // at all (vertx wires the key in the api file itself) + boolean apiKeyInApiClient = library == ClientLibrary.JVM_KTOR + || library == ClientLibrary.MULTIPLATFORM + || library == ClientLibrary.JVM_RETROFIT2 + || library == ClientLibrary.JVM_OKHTTP4; + if (apiKeyInApiClient) { + // keep supporting files for ApiClient.kt; disabling models + // without enabling apis would switch api generation off too + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.API_DOCS, "false"); + } else { + enableOnlyApiGeneration(generator); + } + generator.opts(input).generate(); + + Path apiFile = Paths.get(outputPath + "/src/" + library.getSourceRoot() + "/org/openapitools/client/apis/DefaultApi.kt"); + String api = new String(Files.readAllBytes(apiFile), StandardCharsets.UTF_8); + // every library escapes the literal path and the {param} placeholder + assertFileContains(apiFile, "odata/\\$count", "\\$id", "dollarId"); + // "$top" unescaped could only come from the wire key — no `top` + // variable exists in scope, so it cannot be a legitimate interpolation + // ("$filter" appears legitimately as the `filter` param's value-side + // interpolation). The KDoc comment shows the path without a closing + // quote (`GET /odata/$count`), so `odata/$count"` means an unescaped + // string literal. + assertFileNotContains(apiFile, "\"$top\"", "odata/$count\""); + + switch (library) { + case JVM_KTOR: + assertFileContains(apiFile, + "localVariableQuery[\"\\$filter\"]", + "localVariableQuery[\"\\$top\"]", + "localVariableQuery[\"\\$a\"]", + "localVariableQuery[\"\\$dobj[\\$f]\"]", + "localVariableHeaders[\"\\$x-head\"]", + "append(\"\\$fprop\""); + break; + case JVM_OKHTTP4: + assertFileContains(apiFile, + "put(\"\\$filter\"", + "put(\"\\$top\"", + "put(\"\\$a\"", + "[\\$f]", + "localVariableHeaders[\"\\$x-head\"]"); + break; + case JVM_RETROFIT2: + assertFileContains(apiFile, + "@Query(\"\\$filter\")", + "@Query(\"\\$top\")", + "@Query(\"\\$a\")", + "@Header(\"\\$x-head\")", + "@Path(\"\\$id\")", + "@Part(\"\\$fprop\")"); + break; + case MULTIPLATFORM: + assertFileContains(apiFile, + "localVariableQuery[\"\\$filter\"]", + "localVariableQuery[\"\\$top\"]", + "localVariableQuery[\"\\$dobj\"]", + "localVariableHeaders[\"\\$x-head\"]", + "append(\"\\$fprop\""); + break; + case JVM_VERTX: + assertFileContains(apiFile, + "add(\"\\$filter\"", + "add(\"\\$top\"", + "add(\"\\$dobj\"", + "apiKey[\"\\$api_key\"]", + "putHeader(\"\\$api_key\""); + break; + case JVM_VOLLEY: + assertFileContains(apiFile, + "\"\\$filter\" to", + "\"\\$top\" to", + "\"\\$x-head\" to", + "\"\\$dobj\" to", + "\"\\$fprop\" to"); + break; + case JVM_SPRING_WEBCLIENT: + case JVM_SPRING_RESTCLIENT: + assertFileContains(apiFile, + "put(\"\\$filter\"", + "put(\"\\$top\"", + "localVariableHeaders[\"\\$x-head\"]", + "\"\\$fprop\" to PartConfig"); + break; + default: + Assert.fail("unexpected library " + library); + } + + // retrofit2 expands the exploded object's properties into @Query args, + // so the `opts` parameter itself does not appear in its api file + if (library != ClientLibrary.JVM_RETROFIT2) { + Assert.assertTrue(api.contains("opts"), + library + ": object query param must still be emitted"); + } + + if (apiKeyInApiClient) { + Path apiClient = Paths.get(outputPath + "/src/" + library.getSourceRoot() + + "/org/openapitools/client/infrastructure/ApiClient.kt"); + assertFileContains(apiClient, "\\$api_key"); + } + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml new file mode 100644 index 000000000000..32d047f723a7 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml @@ -0,0 +1,72 @@ +openapi: 3.0.3 +info: {title: Dollar, version: 1.0.0} +security: + - dollarKey: [] +paths: + /items: + get: + operationId: listItems + parameters: + - name: $filter + in: query + schema: {type: string} + - name: $top + in: query + schema: {type: integer} + - name: filter + in: query + schema: {type: string} + - name: $x-head + in: header + schema: {type: string} + - name: opts + in: query + style: form + explode: true + schema: + type: object + properties: + $a: {type: string} + b: {type: integer} + - name: $dobj + in: query + style: deepObject + explode: true + schema: + type: object + properties: + $f: {type: string} + g: {type: integer} + responses: + '200': {description: ok} + post: + operationId: submitForm + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + $fprop: {type: string} + other: {type: string} + responses: + '200': {description: ok} + /items/{$id}: + get: + operationId: getItem + parameters: + - name: $id + in: path + required: true + schema: {type: string} + responses: + '200': {description: ok} + /odata/$count: + get: + operationId: odataCount + responses: + '200': {description: ok} +components: + securitySchemes: + dollarKey: {type: apiKey, name: $api_key, in: header} From 8fab58d9c653254998822c7d498e22e97126eea7 Mon Sep 17 00:00:00 2001 From: khayashi4337 Date: Thu, 12 Nov 2026 11:30:39 +0900 Subject: [PATCH 2/2] test(kotlin-client): reuse the shared clientLibraries provider for the $-escape test The dedicated librariesWithDollarWireNames provider listed the same eight libraries as the existing clientLibraries provider; the duplication would drift the next time the shared list changes. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .../kotlin/KotlinClientCodegenApiTest.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java index 50967d0bcc80..746840f14d90 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java @@ -359,20 +359,6 @@ private KotlinClientCodegen createCodegen(ClientLibrary library) throws IOExcept return codegen; } - @DataProvider(name = "librariesWithDollarWireNames") - public static Object[][] librariesWithDollarWireNames() { - return new Object[][]{ - {ClientLibrary.JVM_KTOR}, - {ClientLibrary.JVM_OKHTTP4}, - {ClientLibrary.JVM_SPRING_WEBCLIENT}, - {ClientLibrary.JVM_SPRING_RESTCLIENT}, - {ClientLibrary.JVM_RETROFIT2}, - {ClientLibrary.MULTIPLATFORM}, - {ClientLibrary.JVM_VOLLEY}, - {ClientLibrary.JVM_VERTX} - }; - } - // Wire names containing `$` (OData-style `$filter`/`$top`) must be emitted // as escaped Kotlin string literals (`"\$filter"`); unescaped, Kotlin // string interpolation turns the wire key into the same-named parameter's @@ -383,7 +369,7 @@ public static Object[][] librariesWithDollarWireNames() { // lines this change actually escapes: vertx keeps master's header and // form emission, and the retrofit2 exploded-property argument keeps its // raw baseName identifier (only the @Query wire string is escaped). - @Test(dataProvider = "librariesWithDollarWireNames") + @Test(dataProvider = "clientLibraries") void testDollarWireNamesAreEscaped(ClientLibrary library) throws IOException { OpenAPI openAPI = readOpenAPI("src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml");