Skip to content

Commit

Permalink
[bug][kotlin] Fix compile of reserved word in client (OpenAPITools#5221)
Browse files Browse the repository at this point in the history
* [kotlin] Fix compile of reserved word in client

A number of places in the client code need to be escaped for reserved
words.

That is, this should be:
(git log formatted): `as` rather than as
(markdown formatted): \`as\` rather than `as`

There are only a handful of places using `{{paramName}}` which HTML
encodes backticks, rather than `{{{paramName}}}` which outputs literal
values.

Added unit test to maintain the reserved word standard for Kotlin.

* don't use kotlin-codegen-escaped parameters in parameter-map

Co-authored-by: Andreas Müller <[email protected]>
  • Loading branch information
2 people authored and MikailBag committed Mar 23, 2020
1 parent 9676746 commit d73f7d3
Show file tree
Hide file tree
Showing 9 changed files with 626 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,42 @@ import {{packageName}}.infrastructure.toMultiValue
/**
* {{summary}}
* {{notes}}
{{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
{{#allParams}}* @param {{{paramName}}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
{{/allParams}}* @return {{#returnType}}{{{returnType}}}{{#nullableReturnType}} or null{{/nullableReturnType}}{{/returnType}}{{^returnType}}void{{/returnType}}
* @throws UnsupportedOperationException If the API returns an informational or redirection response
* @throws ClientException If the API returns a client error response
* @throws ServerException If the API returns a server error response
*/{{#returnType}}
@Suppress("UNCHECKED_CAST"){{/returnType}}
@Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class)
fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to "${{paramName}}"{{#hasMore}}, {{/hasMore}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
fun {{operationId}}({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to "${{{paramName}}}"{{#hasMore}}, {{/hasMore}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf()
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, List<kotlin.String>>()
.apply {
{{#queryParams}}
{{^required}}
if ({{paramName}} != null) {
put("{{paramName}}", {{#isContainer}}toMultiValue({{paramName}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString({{paramName}}){{/isDateTime}}{{#isDate}}parseDateToQueryString({{paramName}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{paramName}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
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("{{paramName}}", {{#isContainer}}toMultiValue({{paramName}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString({{paramName}}){{/isDateTime}}{{#isDate}}parseDateToQueryString({{paramName}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{paramName}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
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("{{paramName}}", {{#isContainer}}toMultiValue({{paramName}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString({{paramName}}){{/isDateTime}}{{#isDate}}parseDateToQueryString({{paramName}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{paramName}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
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}}{{^hasHeaderParams}}){{/hasHeaderParams}}{{#hasHeaderParams}}{{#hasFormParams}}, {{/hasFormParams}}{{#headerParams}}"{{baseName}}" to {{#isContainer}}{{paramName}}.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}{{paramName}}.toString(){{/isContainer}}{{#hasMore}}, {{/hasMore}}{{/headerParams}}){{/hasHeaderParams}}
val localVariableHeaders: MutableMap<String, String> = mutableMapOf({{#hasFormParams}}"Content-Type" to {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{MediaType}}"{{/consumes.0}}{{/hasFormParams}}{{^hasHeaderParams}}){{/hasHeaderParams}}{{#hasHeaderParams}}{{#hasFormParams}}, {{/hasFormParams}}{{#headerParams}}"{{baseName}}" to {{#isContainer}}{{{paramName}}}.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}{{{paramName}}}.toString(){{/isContainer}}{{#hasMore}}, {{/hasMore}}{{/headerParams}}){{/hasHeaderParams}}
val localVariableConfig = RequestConfig(
RequestMethod.{{httpMethod}},
"{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{paramName}}"){{/pathParams}},
"{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{{paramName}}}"){{/pathParams}},
query = localVariableQuery,
headers = localVariableHeaders
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isBodyParam}}@Body {{paramName}}: {{{dataType}}}{{/isBodyParam}}
{{#isBodyParam}}@Body {{{paramName}}}: {{{dataType}}}{{/isBodyParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{paramName}}: MultipartBody.Part {{/isFile}}{{/isFormParam}}
{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{{paramName}}}: MultipartBody.Part {{/isFile}}{{/isFormParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isHeaderParam}}@Header("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isHeaderParam}}
{{#isHeaderParam}}@Header("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{/isHeaderParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isPathParam}}@Path("{{baseName}}") {{paramName}}: {{{dataType}}}{{/isPathParam}}
{{#isPathParam}}@Path("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{/isPathParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isQueryParam}}@Query("{{baseName}}") {{paramName}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{/isQueryParam}}
{{#isQueryParam}}@Query("{{baseName}}") {{{paramName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{/isQueryParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,30 @@ import kotlinx.serialization.internal.StringDescriptor
/**
* {{summary}}
* {{notes}}
{{#allParams}}* @param {{paramName}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
{{#allParams}}* @param {{{paramName}}} {{description}} {{^required}}(optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}
{{/allParams}}* @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}
*/
{{#returnType}}
@Suppress("UNCHECKED_CAST")
{{/returnType}}
suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : HttpResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> {
suspend fun {{operationId}}({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : HttpResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> {
val localVariableAuthNames = listOf<String>({{#authMethods}}"{{name}}"{{#hasMore}}, {{/hasMore}}{{/authMethods}})

val localVariableBody = {{#hasBodyParam}}{{#bodyParam}}{{#isListContainer}}{{operationIdCamelCase}}Request({{paramName}}.asList()){{/isListContainer}}{{^isListContainer}}{{#isMapContainer}}{{operationIdCamelCase}}Request({{paramName}}){{/isMapContainer}}{{^isMapContainer}}{{paramName}}{{/isMapContainer}}{{/isListContainer}}{{/bodyParam}}{{/hasBodyParam}}
val localVariableBody = {{#hasBodyParam}}{{#bodyParam}}{{#isListContainer}}{{operationIdCamelCase}}Request({{{paramName}}}.asList()){{/isListContainer}}{{^isListContainer}}{{#isMapContainer}}{{operationIdCamelCase}}Request({{{paramName}}}){{/isMapContainer}}{{^isMapContainer}}{{{paramName}}}{{/isMapContainer}}{{/isListContainer}}{{/bodyParam}}{{/hasBodyParam}}
{{^hasBodyParam}}
{{#hasFormParams}}
{{#isMultipart}}
formData {
{{#formParams}}
{{paramName}}?.apply { append("{{{baseName}}}", {{paramName}}) }
{{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}) }
{{/formParams}}
}
{{/isMultipart}}
{{^isMultipart}}
ParametersBuilder().also {
{{#formParams}}
{{paramName}}?.apply { it.append("{{{baseName}}}", {{paramName}}.toString()) }
{{{paramName}}}?.apply { it.append("{{{baseName}}}", {{{paramName}}}.toString()) }
{{/formParams}}
}.build()
{{/isMultipart}}
Expand All @@ -68,17 +68,17 @@ import kotlinx.serialization.internal.StringDescriptor

val localVariableQuery = mutableMapOf<String, List<String>>()
{{#queryParams}}
{{paramName}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{paramName}}"){{/isContainer}} }
{{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} }
{{/queryParams}}

val localVariableHeaders = mutableMapOf<String, String>()
{{#headerParams}}
{{paramName}}?.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{{paramName}}}?.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{/headerParams}}

val localVariableConfig = RequestConfig(
RequestMethod.{{httpMethod}},
"{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{paramName}}"){{/pathParams}},
"{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{{paramName}}}"){{/pathParams}},
query = localVariableQuery,
headers = localVariableHeaders
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package org.openapitools.codegen.kotlin;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.KotlinClientCodegen;
import org.openapitools.codegen.utils.StringUtils;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.HashSet;

import static org.testng.Assert.assertEquals;

@SuppressWarnings("rawtypes")
public class KotlinReservedWordsTest {
final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/kotlin/reserved_words.yaml");

@DataProvider(name = "reservedWords")
static Object[][] reservedWords() {
return new Object[][]{
{"as"},
{"break"},
{"class"},
{"continue"},
{"do"},
{"else"},
{"false"},
{"for"},
{"fun"},
{"if"},
{"in"},
{"interface"},
{"is"},
{"null"},
{"object"},
{"package"},
{"return"},
{"super"},
{"this"},
{"throw"},
{"true"},
{"try"},
{"typealias"},
{"typeof"},
{"val"},
{"var"},
{"when"},
{"while"}
};
}

@Test(dataProvider = "reservedWords")
public void testReservedWordsAsModels(String reservedWord) {
final DefaultCodegen codegen = new KotlinClientCodegen();
final Schema schema = new Schema();
final String escaped = "`" + reservedWord + "`";
final String titleCased = StringUtils.camelize(reservedWord, false);

codegen.setOpenAPI(openAPI);
CodegenModel model = codegen.fromModel(reservedWord, schema);

assertEquals(model.classname, titleCased);
if ("class".equals(reservedWord)) {
// this is a really weird "edge" case rename.
assertEquals(model.classVarName, "propertyClass");
} else {
assertEquals(model.classVarName, escaped);
}
assertEquals(model.name, escaped);
assertEquals(model.classFilename, titleCased);
}

@SuppressWarnings("OptionalGetWithoutIsPresent")
@Test(dataProvider = "reservedWords")
public void testReservedWordsAsParameters(String reservedWord) {
final DefaultCodegen codegen = new KotlinClientCodegen();
final String escaped = "`" + reservedWord + "`";
codegen.setOpenAPI(openAPI);
Operation operation = openAPI.getPaths().get("/ping").getGet();

Parameter current = operation.getParameters().stream().filter(x -> reservedWord.equals(x.getName())).findFirst().get();
CodegenParameter codegenParameter = codegen.fromParameter(current, new HashSet<>());

assertEquals(current.getName(), reservedWord);
if ("class".equals(reservedWord)) {
assertEquals(codegenParameter.paramName, "propertyClass");
} else {
assertEquals(codegenParameter.paramName, escaped);
}
}

@Test(dataProvider = "reservedWords")
public void testReservedWordsAsProperties(String reservedWord) {
final DefaultCodegen codegen = new KotlinClientCodegen();

final String escaped = "`" + reservedWord + "`";
final String titleCased = StringUtils.camelize(reservedWord, false);

Schema linked = openAPI.getComponents().getSchemas().get("Linked");

CodegenProperty property = codegen.fromProperty(reservedWord, (Schema) linked.getProperties().get(reservedWord));

if ("object".equals(reservedWord)) {
assertEquals(property.complexType, "kotlin.Any");
assertEquals(property.dataType, "kotlin.Any");
assertEquals(property.datatypeWithEnum, "kotlin.Any");
assertEquals(property.baseType, "kotlin.Any");
} else {
assertEquals(property.complexType, titleCased);
assertEquals(property.dataType, titleCased);
assertEquals(property.datatypeWithEnum, titleCased);
assertEquals(property.baseType, titleCased);
}

if ("class".equals(reservedWord)) {
// this is a really weird "edge" case rename.
assertEquals(property.name, "propertyClass");
} else {
assertEquals(property.name, escaped);
}

assertEquals(property.baseName, reservedWord);
}

}
Loading

0 comments on commit d73f7d3

Please sign in to comment.