Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG][KOTLIN] Sanitize names of the adapter variables in anyOf and oneOf model template to avoid compilation errors #19981

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import java.io.IOException
{{#anyOf}}
{{^isArray}}
{{^vendorExtensions.x-duplicated-data-type}}
val adapter{{{dataType}}} = gson.getDelegateAdapter(this, TypeToken.get({{{dataType}}}::class.java))
val adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}} = gson.getDelegateAdapter(this, TypeToken.get({{{dataType}}}::class.java))
{{/vendorExtensions.x-duplicated-data-type}}
{{/isArray}}
{{#isArray}}
Expand Down Expand Up @@ -122,7 +122,7 @@ import java.io.IOException
return
{{/isPrimitiveType}}
{{^isPrimitiveType}}
val element = adapter{{{dataType}}}.toJsonTree(value.actualInstance as {{{dataType}}}?)
val element = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}.toJsonTree(value.actualInstance as {{{dataType}}}?)
elementAdapter.write(out, element)
return
{{/isPrimitiveType}}
Expand Down Expand Up @@ -185,7 +185,7 @@ import java.io.IOException
for(element in jsonElement.getAsJsonArray()) {
{{#items}}
{{#isNumber}}
require(jsonElement.getAsJsonPrimitive().isNumber) {
require(element.getAsJsonPrimitive().isNumber) {
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
}
{{/isNumber}}
Expand Down Expand Up @@ -238,4 +238,96 @@ import java.io.IOException
}.nullSafe() as TypeAdapter<T>
}
}

companion object {
/**
* Validates the JSON Element and throws an exception if issues found
*
* @param jsonElement JSON Element
* @throws IOException if the JSON Element is invalid with respect to {{classname}}
*/
@Throws(IOException::class)
fun validateJsonElement(jsonElement: JsonElement?) {
var match = 0
val errorMessages = ArrayList<String>()
{{#composedSchemas}}
{{#anyOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{^hasVars}}
// validate the json string with {{{dataType}}}
try {
// validate the JSON object to see if any exception is thrown
{{^isArray}}
{{#isNumber}}
require(jsonElement!!.getAsJsonPrimitive().isNumber()) {
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
}
{{/isNumber}}
{{^isNumber}}
{{#isPrimitiveType}}
require(jsonElement!!.getAsJsonPrimitive().is{{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}}()) {
String.format("Expected json element to be of type {{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}} in the JSON string but got `%s`", jsonElement.toString())
}
{{/isPrimitiveType}}
{{/isNumber}}
{{^isNumber}}
{{^isPrimitiveType}}
{{{dataType}}}.validateJsonElement(jsonElement)
{{/isPrimitiveType}}
{{/isNumber}}
{{/isArray}}
{{#isArray}}
require(jsonElement!!.isJsonArray) {
String.format("Expected json element to be a array type in the JSON string but got `%s`", jsonElement.toString())
}

// validate array items
for(element in jsonElement.getAsJsonArray()) {
{{#items}}
{{#isNumber}}
require(element.getAsJsonPrimitive().isNumber) {
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
}
{{/isNumber}}
{{^isNumber}}
{{#isPrimitiveType}}
require(element.getAsJsonPrimitive().is{{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}}) {
String.format("Expected array items to be of type {{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}} in the JSON string but got `%s`", jsonElement.toString())
}
{{/isPrimitiveType}}
{{/isNumber}}
{{^isNumber}}
{{^isPrimitiveType}}
{{{dataType}}}.validateJsonElement(element)
{{/isPrimitiveType}}
{{/isNumber}}
{{/items}}
}
{{/isArray}}
match++
} catch (e: Exception) {
// Validation failed, continue
errorMessages.add(String.format("Validation for {{{dataType}}} failed with `%s`.", e.message))
}
{{/hasVars}}
{{#hasVars}}
// validate json string for {{{.}}}
try {
// validate the JSON object to see if any exception is thrown
{{.}}.validateJsonElement(jsonElement)
match++
} catch (e: Exception) {
// validation failed, continue
errorMessages.add(String.format("Validation for {{{.}}} failed with `%s`.", e.message))
}
{{/hasVars}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/anyOf}}
{{/composedSchemas}}

if (match != 1) {
throw IOException(String.format("Failed validation for {{classname}}: %d classes match result, expected 1. Detailed failure message for oneOf schemas: %s. JSON: %s", match, errorMessages, jsonElement.toString()))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import java.io.IOException
{{#oneOf}}
{{^isArray}}
{{^vendorExtensions.x-duplicated-data-type}}
val adapter{{{dataType}}} = gson.getDelegateAdapter(this, TypeToken.get({{{dataType}}}::class.java))
val adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}} = gson.getDelegateAdapter(this, TypeToken.get({{{dataType}}}::class.java))
{{/vendorExtensions.x-duplicated-data-type}}
{{/isArray}}
{{#isArray}}
Expand Down Expand Up @@ -122,7 +122,7 @@ import java.io.IOException
return
{{/isPrimitiveType}}
{{^isPrimitiveType}}
val element = adapter{{{dataType}}}.toJsonTree(value.actualInstance as {{{dataType}}}?)
val element = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}.toJsonTree(value.actualInstance as {{{dataType}}}?)
elementAdapter.write(out, element)
return
{{/isPrimitiveType}}
Expand Down Expand Up @@ -179,7 +179,7 @@ import java.io.IOException
for(element in jsonElement.getAsJsonArray()) {
{{#items}}
{{#isNumber}}
require(jsonElement.getAsJsonPrimitive().isNumber) {
require(element.getAsJsonPrimitive().isNumber) {
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
}
{{/isNumber}}
Expand Down Expand Up @@ -236,4 +236,96 @@ import java.io.IOException
}.nullSafe() as TypeAdapter<T>
}
}

companion object {
/**
* Validates the JSON Element and throws an exception if issues found
*
* @param jsonElement JSON Element
* @throws IOException if the JSON Element is invalid with respect to {{classname}}
*/
@Throws(IOException::class)
fun validateJsonElement(jsonElement: JsonElement?) {
var match = 0
val errorMessages = ArrayList<String>()
{{#composedSchemas}}
{{#oneOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{^hasVars}}
// validate the json string with {{{dataType}}}
try {
// validate the JSON object to see if any exception is thrown
{{^isArray}}
{{#isNumber}}
require(jsonElement!!.getAsJsonPrimitive().isNumber()) {
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
}
{{/isNumber}}
{{^isNumber}}
{{#isPrimitiveType}}
require(jsonElement!!.getAsJsonPrimitive().is{{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}}()) {
String.format("Expected json element to be of type {{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}} in the JSON string but got `%s`", jsonElement.toString())
}
{{/isPrimitiveType}}
{{/isNumber}}
{{^isNumber}}
{{^isPrimitiveType}}
{{{dataType}}}.validateJsonElement(jsonElement)
{{/isPrimitiveType}}
{{/isNumber}}
{{/isArray}}
{{#isArray}}
require(jsonElement!!.isJsonArray) {
String.format("Expected json element to be a array type in the JSON string but got `%s`", jsonElement.toString())
}

// validate array items
for(element in jsonElement!!.getAsJsonArray()) {
{{#items}}
{{#isNumber}}
require(jsonElement!!.getAsJsonPrimitive().isNumber) {
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
}
{{/isNumber}}
{{^isNumber}}
{{#isPrimitiveType}}
require(element.getAsJsonPrimitive().is{{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}}) {
String.format("Expected array items to be of type {{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}} in the JSON string but got `%s`", jsonElement.toString())
}
{{/isPrimitiveType}}
{{/isNumber}}
{{^isNumber}}
{{^isPrimitiveType}}
{{{dataType}}}.validateJsonElement(element)
{{/isPrimitiveType}}
{{/isNumber}}
{{/items}}
}
{{/isArray}}
match++
} catch (e: Exception) {
// Validation failed, continue
errorMessages.add(String.format("Validation for {{{dataType}}} failed with `%s`.", e.message))
}
{{/hasVars}}
{{#hasVars}}
// validate json string for {{{.}}}
try {
// validate the JSON object to see if any exception is thrown
{{.}}.validateJsonElement(jsonElement)
match++
} catch (e: Exception) {
// validation failed, continue
errorMessages.add(String.format("Validation for {{{.}}} failed with `%s`.", e.message))
}
{{/hasVars}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/oneOf}}
{{/composedSchemas}}

if (match != 1) {
throw IOException(String.format("Failed validation for {{classname}}: %d classes match result, expected 1. Detailed failure message for oneOf schemas: %s. JSON: %s", match, errorMessages, jsonElement.toString()))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.openapitools.codegen.kotlin;

import lombok.Getter;
import org.jetbrains.kotlin.com.intellij.openapi.util.text.Strings;

@Getter
enum ClientLibrary {
JVM_KTOR("main/kotlin"),
JVM_OKHTTP4("main/kotlin"),
JVM_SPRING_WEBCLIENT("main/kotlin"),
JVM_SPRING_RESTCLIENT("main/kotlin"),
JVM_RETROFIT2("main/kotlin"),
MULTIPLATFORM("commonMain/kotlin"),
JVM_VOLLEY("gson", "main/java"),
JVM_VERTX("main/kotlin");
private final String serializationLibrary;
private final String libraryName;
private final String sourceRoot;

ClientLibrary(String serializationLibrary, String sourceRoot) {
this.serializationLibrary = serializationLibrary;
this.sourceRoot = sourceRoot;
this.libraryName = Strings.toLowerCase(this.name()).replace("_", "-");
}

ClientLibrary(String sourceRoot) {
this("jackson", sourceRoot);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

public class KotlinClientCodegenApiTest {

@DataProvider(name = "pathResponses")
@DataProvider(name = "clientLibraries")
public Object[][] pathResponses() {
return new Object[][]{
{ClientLibrary.JVM_KTOR},
Expand All @@ -36,7 +36,7 @@ public Object[][] pathResponses() {
};
}

@Test(dataProvider = "pathResponses")
@Test(dataProvider = "clientLibraries")
void testPathVariableIsNotEscaped_19930(ClientLibrary library) throws IOException {

OpenAPI openAPI = new OpenAPIParser()
Expand Down Expand Up @@ -76,29 +76,4 @@ private KotlinClientCodegen createCodegen(ClientLibrary library) throws IOExcept
codegen.additionalProperties().put(KotlinClientCodegen.DATE_LIBRARY, "kotlinx-datetime");
return codegen;
}

@Getter
private enum ClientLibrary {
JVM_KTOR("main/kotlin"),
JVM_OKHTTP4("main/kotlin"),
JVM_SPRING_WEBCLIENT("main/kotlin"),
JVM_SPRING_RESTCLIENT("main/kotlin"),
JVM_RETROFIT2("main/kotlin"),
MULTIPLATFORM("commonMain/kotlin"),
JVM_VOLLEY("gson", "main/java"),
JVM_VERTX("main/kotlin");
private final String serializationLibrary;
private final String libraryName;
private final String sourceRoot;

ClientLibrary(String serializationLibrary, String sourceRoot) {
this.serializationLibrary = serializationLibrary;
this.sourceRoot = sourceRoot;
this.libraryName = Strings.toLowerCase(this.name()).replace("_", "-");
}

ClientLibrary(String sourceRoot) {
this("jackson", sourceRoot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,66 @@ public void testFailOnUnknownPropertiesAdditionalProperty() {
configAssert.assertValue(KotlinClientCodegen.FAIL_ON_UNKNOWN_PROPERTIES, codegen::isFailOnUnknownProperties, Boolean.FALSE);
}

@DataProvider(name = "gsonClientLibraries")
public Object[][] pathResponses() {
return new Object[][]{
{ClientLibrary.JVM_KTOR},
{ClientLibrary.JVM_OKHTTP4},
{ClientLibrary.JVM_RETROFIT2},
{ClientLibrary.MULTIPLATFORM},
{ClientLibrary.JVM_VOLLEY},
{ClientLibrary.JVM_VERTX}
};
}

@Test(dataProvider = "gsonClientLibraries")
public void testLocalVariablesUseSanitizedDataTypeNamesForOneOfProperty_19942(ClientLibrary clientLibrary) throws IOException {
File output = Files.createTempDirectory("test").toFile();
String path = output.getAbsolutePath();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("kotlin")
.setLibrary(clientLibrary.getLibraryName())
.setInputSpec("src/test/resources/3_0/issue_19942.json")
.addAdditionalProperty("omitGradleWrapper", true)
.addAdditionalProperty("serializationLibrary", "gson")
.addAdditionalProperty("dateLibrary", "kotlinx-datetime")
.addAdditionalProperty("useSpringBoot3", "true")
.addAdditionalProperty("generateOneOfAnyOfWrappers", true)
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
DefaultGenerator generator = new DefaultGenerator();

generator.opts(configurator.toClientOptInput()).generate();

TestUtils.assertFileNotContains(Paths.get(path + "/src/" + clientLibrary.getSourceRoot() + "/org/openapitools/client/models/ObjectWithComplexOneOfId.kt"),
"val adapterkotlin.String", "val adapterjava.math.BigDecimal");
}

@Test(dataProvider = "gsonClientLibraries")
public void testLocalVariablesUseSanitizedDataTypeNamesForAnyOfProperty_19942(ClientLibrary clientLibrary) throws IOException {
File output = Files.createTempDirectory("test").toFile();
String path = output.getAbsolutePath();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("kotlin")
.setLibrary(clientLibrary.getLibraryName())
.setInputSpec("src/test/resources/3_0/issue_19942.json")
.addAdditionalProperty("omitGradleWrapper", true)
.addAdditionalProperty("serializationLibrary", "gson")
.addAdditionalProperty("dateLibrary", "kotlinx-datetime")
.addAdditionalProperty("useSpringBoot3", "true")
.addAdditionalProperty("generateOneOfAnyOfWrappers", true)
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
DefaultGenerator generator = new DefaultGenerator();

generator.opts(configurator.toClientOptInput()).generate();

TestUtils.assertFileNotContains(Paths.get(path + "/src/" + clientLibrary.getSourceRoot() + "/org/openapitools/client/models/ObjectWithComplexAnyOfId.kt"),
"val adapterkotlin.String", "val adapterjava.math.BigDecimal");
}

private static class ModelNameTest {
private final String expectedName;
private final String expectedClassName;
Expand Down
Loading
Loading