diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index d83cdef420cf..ad421b50a3ba 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -17,6 +17,8 @@ package org.openapitools.codegen; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Ticker; @@ -69,6 +71,10 @@ import org.openapitools.codegen.utils.ExamplesUtils; import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.OneOfImplementorAdditionalData; +import org.openapitools.codegen.validations.oas.ModelVendorExtensionAdd; +import org.openapitools.codegen.validations.oas.ModelVendorExtensionRemove; +import org.openapitools.codegen.validations.oas.OperationVendorExtensionAdd; +import org.openapitools.codegen.validations.oas.OperationVendorExtensionRemove; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,6 +107,29 @@ public class DefaultCodegen implements CodegenConfig { public static FeatureSet DefaultFeatureSet; + private final ObjectMapper mapper = new ObjectMapper(); + + public static final String MODEL_VENDOR_EXTENSION_REMOVE = "modelVendorExtensionRemove"; + public static final String MODEL_VENDOR_EXTENSION_ADD = "modelVendorExtensionAdd"; + public static final String OPERATION_VENDOR_EXTENSION_REMOVE = "operationVendorExtensionRemove"; + public static final String OPERATION_VENDOR_EXTENSION_ADD = "operationVendorExtensionAdd"; + + @Getter + @Setter + private Map modelVendorExtensionRemove = new HashMap<>(); + + @Getter + @Setter + private Map modelVendorExtensionAdd = new HashMap<>(); + + @Getter + @Setter + private Map operationVendorExtensionRemove = new HashMap<>(); + + @Getter + @Setter + private Map operationVendorExtensionAdd = new HashMap<>(); + // A cache of sanitized words. The sanitizeName() method is invoked many times with the same // arguments, this cache is used to optimized performance. private static final Cache sanitizedNameCache; @@ -430,6 +459,28 @@ public void processOpts() { parseDefaultToEmptyContainer((String) additionalProperties.get(DEFAULT_TO_EMPTY_CONTAINER)); defaultToEmptyContainer = true; } + + if(additionalProperties.containsKey(MODEL_VENDOR_EXTENSION_REMOVE)) { + setModelVendorExtensionRemove( + mapper.convertValue(additionalProperties.get(MODEL_VENDOR_EXTENSION_REMOVE), new TypeReference>() {}) + ); + } + if(additionalProperties.containsKey(OPERATION_VENDOR_EXTENSION_REMOVE)) { + setOperationVendorExtensionRemove( + mapper.convertValue(additionalProperties.get(OPERATION_VENDOR_EXTENSION_REMOVE), new TypeReference>() {}) + ); + } + + if(additionalProperties.containsKey(MODEL_VENDOR_EXTENSION_ADD)) { + setModelVendorExtensionAdd( + mapper.convertValue(additionalProperties.get(MODEL_VENDOR_EXTENSION_ADD), new TypeReference<>() {}) + ); + } + if(additionalProperties.containsKey(OPERATION_VENDOR_EXTENSION_ADD)) { + setOperationVendorExtensionAdd( + mapper.convertValue(additionalProperties.get(OPERATION_VENDOR_EXTENSION_ADD), new TypeReference<>() {}) + ); + } } /*** @@ -1136,6 +1187,101 @@ public void preprocessOpenAPI(OpenAPI openAPI) { @Override @SuppressWarnings("unused") public void processOpenAPI(OpenAPI openAPI) { + Map operationsByOperationId = openAPI.getPaths() != null + ? openAPI.getPaths().entrySet().stream() + .flatMap(path -> path.getValue().readOperations().stream()) + .collect(Collectors.toMap(Operation::getOperationId, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new)) + : Map.of(); + Map modelsByName = (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) + ? openAPI.getComponents().getSchemas() + : Map.of(); + + // Remove model vendor extensions + modelVendorExtensionRemove.forEach((name, ext) -> { + if (modelsByName.containsKey(name)) { + Schema schema = modelsByName.get(name); + if (schema.getExtensions() != null) { + ext.getClazz().forEach(schema.getExtensions()::remove); + } + ext.getFields().forEach((fieldName, fieldExtensionsToRemove) -> { + Map props = schema.getProperties(); + if (props != null && props.get(fieldName) != null && props.get(fieldName).getExtensions() != null) { + Map fieldExtensions = props.get(fieldName).getExtensions(); + fieldExtensionsToRemove.forEach(fieldExtensions::remove); + } + }); + } + }); + + // Add model vendor extensions + modelVendorExtensionAdd.forEach((name, ext) -> { + if (modelsByName.containsKey(name)) { + var schema = modelsByName.get(name); + // retrieve the extensions map or initialize + Map classExt = Objects.requireNonNullElse((Map) schema.getExtensions(), new HashMap<>()); + ext.getClazz().forEach((extName, extVals) -> mergeExtensions(classExt, extName, extVals)); + // put the modified map back + schema.setExtensions(classExt); + var props = schema.getProperties(); + if (props != null) { + ext.getFields().forEach((fieldName, fieldExts) -> { + if (props.containsKey(fieldName)) { + Schema fieldSchema = (Schema) props.get(fieldName); + // retrieve the extensions map or initialize + Map fieldExt = Objects.requireNonNullElse((Map) fieldSchema.getExtensions(), new HashMap<>()); + fieldExts.forEach((extName, extVals) -> mergeExtensions(fieldExt, extName, extVals)); + // put the modified map back + fieldSchema.setExtensions(fieldExt); + } + }); + } + } + }); + + // Remove operation vendor extensions + operationVendorExtensionRemove.forEach((opId, ext) -> { + if (operationsByOperationId.containsKey(opId)) { + var op = operationsByOperationId.get(opId); + if (op.getExtensions() != null) { + ext.getMethod().forEach(op.getExtensions()::remove); + } + ext.getParams().forEach((paramName, paramExtensionsToRemove) -> { + var params = op.getParameters().stream().collect(Collectors.toMap(Parameter::getName, Function.identity())); + if (params.containsKey(paramName) && params.get(paramName).getExtensions() != null) { + var paramsExtensions = params.get(paramName).getExtensions(); + paramExtensionsToRemove.forEach(paramsExtensions::remove); + } + }); + } + }); + + // Add operation vendor extensions + operationVendorExtensionAdd.forEach((opId, ext) -> { + if (operationsByOperationId.containsKey(opId)) { + var op = operationsByOperationId.get(opId); + // retrieve the extensions map or initialize + Map operationExtensions = Objects.requireNonNullElse(op.getExtensions(), new HashMap<>()); + ext.getMethod().forEach((extName, extVals) -> mergeExtensions(operationExtensions, extName, extVals)); + // put the modified map back + op.setExtensions(operationExtensions); + var params = op.getParameters().stream().collect(Collectors.toMap(Parameter::getName, Function.identity())); + ext.getParameters().forEach((paramName, paramExts) -> { + if (params.containsKey(paramName)) { + Parameter parameter = params.get(paramName); + // retrieve the extensions map or initialize + Map paramExt = Objects.requireNonNullElse(parameter.getExtensions(), new HashMap<>()); + paramExts.forEach((extName, extVals) -> mergeExtensions(paramExt, extName, extVals)); + // put the modified map back + parameter.setExtensions(paramExt); + } + }); + } + }); + } + + private void mergeExtensions(Map extensionsMap, String name, List values) { + var existing = getObjectAsStringList(extensionsMap.get(name)); + extensionsMap.put(name, Stream.concat(existing.stream(), values.stream()).collect(Collectors.toList())); } // override with any special handling of the JMustache compiler diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/ModelVendorExtensionAdd.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/ModelVendorExtensionAdd.java new file mode 100644 index 000000000000..e90d6129547c --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/ModelVendorExtensionAdd.java @@ -0,0 +1,22 @@ +package org.openapitools.codegen.validations.oas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ModelVendorExtensionAdd { + + @Getter + @Setter + @JsonProperty("class") + private Map> clazz = new HashMap<>(); + + @Getter + @Setter + private Map>> fields; + +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/ModelVendorExtensionRemove.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/ModelVendorExtensionRemove.java new file mode 100644 index 000000000000..3d60faa77ad0 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/ModelVendorExtensionRemove.java @@ -0,0 +1,22 @@ +package org.openapitools.codegen.validations.oas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ModelVendorExtensionRemove { + + @Getter + @Setter + @JsonProperty("class") + private List clazz = new ArrayList<>(); + + @Getter + @Setter + private Map> fields = new HashMap<>(); +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OperationVendorExtensionAdd.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OperationVendorExtensionAdd.java new file mode 100644 index 000000000000..6cf52d2e27ac --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OperationVendorExtensionAdd.java @@ -0,0 +1,19 @@ +package org.openapitools.codegen.validations.oas; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OperationVendorExtensionAdd { + + @Getter + @Setter + private Map> method = new HashMap<>(); + + @Getter + @Setter + private Map>> parameters = new HashMap<>(); +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OperationVendorExtensionRemove.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OperationVendorExtensionRemove.java new file mode 100644 index 000000000000..619ff5bb36f9 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OperationVendorExtensionRemove.java @@ -0,0 +1,20 @@ +package org.openapitools.codegen.validations.oas; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OperationVendorExtensionRemove { + + @Getter + @Setter + private List method = new ArrayList<>(); + + @Getter + @Setter + private Map> params = new HashMap<>(); +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java deleted file mode 100644 index e66638589e7a..000000000000 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.openapitools.codegen.kotlin; - -import org.openapitools.codegen.ClientOptInput; -import org.openapitools.codegen.DefaultGenerator; -import org.openapitools.codegen.TestUtils; -import org.openapitools.codegen.languages.KotlinSpringServerCodegen; -import org.testng.annotations.Test; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.testng.Assert.assertTrue; - -public class KotlinSpringServerCodegenTest { - - @Test - public void gradleWrapperIsGenerated() throws IOException { - File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); - output.deleteOnExit(); - - KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); - - codegen.setOutputDir(output.getAbsolutePath()); - new DefaultGenerator().opts( - new ClientOptInput().openAPI(TestUtils.parseSpec("src/test/resources/3_0/petstore.yaml")) - .config(codegen) - ).generate(); - String outputPath = output.getAbsolutePath(); - Path gradleWrapperProperties = Paths.get(outputPath + "/gradle/wrapper/gradle-wrapper.properties"); - Path gradleWrapperJar = Paths.get(outputPath + "/gradle/wrapper/gradle-wrapper.jar"); - Path gradleWrapper = Paths.get(outputPath + "/gradlew"); - Path gradleWrapperBat = Paths.get(outputPath + "/gradlew.bat"); - TestUtils.assertFileExists(gradleWrapperProperties); - TestUtils.assertFileExists(gradleWrapper); - TestUtils.assertFileExists(gradleWrapperBat); - //Different because file is not a text file - assertTrue(Files.exists(gradleWrapperJar)); - - //Spring Cloud - File outputCloud = Files.createTempDirectory("testCloud").toFile().getCanonicalFile(); - outputCloud.deleteOnExit(); - codegen.setLibrary(KotlinSpringServerCodegen.SPRING_CLOUD_LIBRARY); - codegen.setOutputDir(outputCloud.getAbsolutePath()); - new DefaultGenerator().opts( - new ClientOptInput().openAPI(TestUtils.parseSpec("src/test/resources/3_0/petstore.yaml")) - .config(codegen) - ).generate(); - - String outputPathCloud = outputCloud.getAbsolutePath(); - Path gradleWrapperPropertiesCloud = Paths.get(outputPathCloud + "/gradle/wrapper/gradle-wrapper.properties"); - Path gradleWrapperJarCloud = Paths.get(outputPathCloud + "/gradle/wrapper/gradle-wrapper.jar"); - Path gradleWrapperCloud = Paths.get(outputPathCloud + "/gradlew"); - Path gradleWrapperBatCloud = Paths.get(outputPathCloud + "/gradlew.bat"); - TestUtils.assertFileExists(gradleWrapperPropertiesCloud); - TestUtils.assertFileExists(gradleWrapperCloud); - TestUtils.assertFileExists(gradleWrapperBatCloud); - //Different because file is not a text file - assertTrue(Files.exists(gradleWrapperJarCloud)); - } - - @Test(description = "generate polymorphic jackson model") - public void polymorphicJacksonSerialization() throws IOException { - File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); - output.deleteOnExit(); - - KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen() ; - codegen.setOutputDir(output.getAbsolutePath()); - - new DefaultGenerator().opts( - new ClientOptInput() - .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/polymorphism.yaml")) - .config(codegen) - ).generate(); - - final Path animalKt = Paths.get(output + "/src/main/kotlin/org/openapitools/model/Animal.kt"); - // base has extra jackson imports - TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonIgnoreProperties"); - TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonSubTypes"); - TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonTypeInfo"); - // and these are being used - TestUtils.assertFileContains(animalKt, "@JsonIgnoreProperties"); - TestUtils.assertFileContains(animalKt, "@JsonSubTypes"); - TestUtils.assertFileContains(animalKt, "@JsonTypeInfo"); - // base is interface - TestUtils.assertFileContains(animalKt, "interface Animal"); - // base properties are present - TestUtils.assertFileContains(animalKt, "val id"); - TestUtils.assertFileContains(animalKt, "val optionalProperty"); - TestUtils.assertFileContains(animalKt, "val stringArray: kotlin.collections.List"); - TestUtils.assertFileContains(animalKt, "val stringSet: kotlin.collections.Set"); - // base doesn't contain discriminator - TestUtils.assertFileNotContains(animalKt, "val discriminator"); - - final Path birdKt = Paths.get(output + "/src/main/kotlin/org/openapitools/model/Bird.kt"); - // derived has serial name set to mapping key - TestUtils.assertFileContains(birdKt, "data class Bird"); - // derived properties are overridden - TestUtils.assertFileContains(birdKt, "override val id"); - TestUtils.assertFileContains(birdKt, "override val optionalProperty"); - TestUtils.assertFileContains(birdKt, "override val stringArray: kotlin.collections.List"); - TestUtils.assertFileContains(birdKt, "override val stringSet: kotlin.collections.Set"); - // derived doesn't contain disciminator - TestUtils.assertFileNotContains(birdKt, "val discriminator"); - } -} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index 353c5141c910..4d220d6a9664 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -47,9 +47,100 @@ import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DOCUMENTATION_PROVIDER; +import static org.testng.Assert.assertTrue; public class KotlinSpringServerCodegenTest { + @Test + public void gradleWrapperIsGenerated() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + + codegen.setOutputDir(output.getAbsolutePath()); + new DefaultGenerator().opts( + new ClientOptInput().openAPI(TestUtils.parseSpec("src/test/resources/3_0/petstore.yaml")) + .config(codegen) + ).generate(); + String outputPath = output.getAbsolutePath(); + Path gradleWrapperProperties = Paths.get(outputPath + "/gradle/wrapper/gradle-wrapper.properties"); + Path gradleWrapperJar = Paths.get(outputPath + "/gradle/wrapper/gradle-wrapper.jar"); + Path gradleWrapper = Paths.get(outputPath + "/gradlew"); + Path gradleWrapperBat = Paths.get(outputPath + "/gradlew.bat"); + TestUtils.assertFileExists(gradleWrapperProperties); + TestUtils.assertFileExists(gradleWrapper); + TestUtils.assertFileExists(gradleWrapperBat); + //Different because file is not a text file + assertTrue(Files.exists(gradleWrapperJar)); + + //Spring Cloud + File outputCloud = Files.createTempDirectory("testCloud").toFile().getCanonicalFile(); + outputCloud.deleteOnExit(); + codegen.setLibrary(KotlinSpringServerCodegen.SPRING_CLOUD_LIBRARY); + codegen.setOutputDir(outputCloud.getAbsolutePath()); + new DefaultGenerator().opts( + new ClientOptInput().openAPI(TestUtils.parseSpec("src/test/resources/3_0/petstore.yaml")) + .config(codegen) + ).generate(); + + String outputPathCloud = outputCloud.getAbsolutePath(); + Path gradleWrapperPropertiesCloud = Paths.get(outputPathCloud + "/gradle/wrapper/gradle-wrapper.properties"); + Path gradleWrapperJarCloud = Paths.get(outputPathCloud + "/gradle/wrapper/gradle-wrapper.jar"); + Path gradleWrapperCloud = Paths.get(outputPathCloud + "/gradlew"); + Path gradleWrapperBatCloud = Paths.get(outputPathCloud + "/gradlew.bat"); + TestUtils.assertFileExists(gradleWrapperPropertiesCloud); + TestUtils.assertFileExists(gradleWrapperCloud); + TestUtils.assertFileExists(gradleWrapperBatCloud); + //Different because file is not a text file + assertTrue(Files.exists(gradleWrapperJarCloud)); + } + + @Test(description = "generate polymorphic jackson model") + public void polymorphicJacksonSerialization() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen() ; + codegen.setOutputDir(output.getAbsolutePath()); + + new DefaultGenerator().opts( + new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/polymorphism.yaml")) + .config(codegen) + ).generate(); + + final Path animalKt = Paths.get(output + "/src/main/kotlin/org/openapitools/model/Animal.kt"); + // base has extra jackson imports + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonIgnoreProperties"); + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonSubTypes"); + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonTypeInfo"); + // and these are being used + TestUtils.assertFileContains(animalKt, "@JsonIgnoreProperties"); + TestUtils.assertFileContains(animalKt, "@JsonSubTypes"); + TestUtils.assertFileContains(animalKt, "@JsonTypeInfo"); + // base is interface + TestUtils.assertFileContains(animalKt, "interface Animal"); + // base properties are present + TestUtils.assertFileContains(animalKt, "val id"); + TestUtils.assertFileContains(animalKt, "val optionalProperty"); + TestUtils.assertFileContains(animalKt, "val stringArray: kotlin.collections.List"); + TestUtils.assertFileContains(animalKt, "val stringSet: kotlin.collections.Set"); + // base doesn't contain discriminator + TestUtils.assertFileNotContains(animalKt, "val discriminator"); + + final Path birdKt = Paths.get(output + "/src/main/kotlin/org/openapitools/model/Bird.kt"); + // derived has serial name set to mapping key + TestUtils.assertFileContains(birdKt, "data class Bird"); + // derived properties are overridden + TestUtils.assertFileContains(birdKt, "override val id"); + TestUtils.assertFileContains(birdKt, "override val optionalProperty"); + TestUtils.assertFileContains(birdKt, "override val stringArray: kotlin.collections.List"); + TestUtils.assertFileContains(birdKt, "override val stringSet: kotlin.collections.Set"); + // derived doesn't contain disciminator + TestUtils.assertFileNotContains(birdKt, "val discriminator"); + } + @Test(description = "test embedded enum array") public void embeddedEnumArrayTest() throws Exception { String baseModelPackage = "zz"; @@ -1049,8 +1140,9 @@ public void generateSerializableModelWithXimplementsSkip() throws Exception { KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); codegen.setOutputDir(output.getAbsolutePath()); codegen.additionalProperties().put(CodegenConstants.SERIALIZABLE_MODEL, true); - codegen.additionalProperties().put(X_KOTLIN_IMPLEMENTS_SKIP, List.of("com.some.pack.Fetchable")); - codegen.additionalProperties().put(X_KOTLIN_IMPLEMENTS_FIELDS_SKIP, Map.of("Dog", List.of("likesFetch"))); +// codegen.additionalProperties().put(X_KOTLIN_IMPLEMENTS_SKIP, List.of("com.some.pack.Fetchable")); +// codegen.additionalProperties().put(X_KOTLIN_IMPLEMENTS_FIELDS_SKIP, Map.of("Dog", List.of("likesFetch"))); + codegen.additionalProperties().put(MODEL_VENDOR_EXTENSION_REMOVE, Map.of("Dog", Map.of("class", List.of("x-kotlin-implements", "x-kotlin-implements-fields")))); ClientOptInput input = new ClientOptInput() .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/petstore-with-x-kotlin-implements.yaml")) @@ -1187,6 +1279,72 @@ public void generateSerializableModelWithXimplementsSkipAndSchemaImplements() th ); } + @Test + public void generateSerializableModelWithRemovedXimplements() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CodegenConstants.SERIALIZABLE_MODEL, true); + codegen.additionalProperties().put(MODEL_VENDOR_EXTENSION_REMOVE, Map.of("Dog", Map.of("class", List.of("x-kotlin-implements", "x-kotlin-implements-fields")))); + + ClientOptInput input = new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/petstore-with-x-kotlin-implements.yaml")) + .config(codegen); + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Dog.kt"); + assertFileContains( + path, + "@get:JsonProperty(\"likesFetch\", required = true) val likesFetch: kotlin.Boolean,", + ") : Pet, java.io.Serializable {", + "private const val serialVersionUID: kotlin.Long = 1" + ); + } + + @Test + public void generateSerializableModelWithExternallyAddedXimplements() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CodegenConstants.SERIALIZABLE_MODEL, true); + codegen.additionalProperties().put(MODEL_VENDOR_EXTENSION_ADD, Map.of("Dog", Map.of("class", Map.of("x-kotlin-implements", List.of("com.some.pack.Fetchable"), "x-kotlin-implements-fields", List.of("likesFetch"))))); + + ClientOptInput input = new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/petstore-without-x-kotlin-implements.yaml")) + .config(codegen); + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Dog.kt"); + assertFileContains( + path, + "@get:JsonProperty(\"likesFetch\", required = true) override val likesFetch: kotlin.Boolean,", + ") : Pet, com.some.pack.Fetchable, java.io.Serializable {", + "private const val serialVersionUID: kotlin.Long = 1" + ); + } + @Test public void generateHttpInterfaceReactiveWithReactorResponseEntity() throws Exception { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-without-x-kotlin-implements.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-without-x-kotlin-implements.yaml new file mode 100644 index 000000000000..62df93359eaf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-without-x-kotlin-implements.yaml @@ -0,0 +1,1035 @@ +openapi: 3.0.3 +info: + title: OpenAPI Petstore + version: 1.0.0 + description: > + This is a sample server Petstore server. + For this sample, you can use the api key `special-key` to test the authorization filters. + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +servers: + - url: https://petstore.swagger.io/v2 +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + operationId: addPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + put: + tags: + - pet + summary: Update an existing pet + operationId: updatePet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: [ available ] + style: form + explode: false + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + schema: + type: array + items: + type: string + style: form + explode: false + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [ ] + post: + tags: + - pet + summary: Updates a pet in the store with form data + operationId: updatePetWithForm + requestBody: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + type: string + description: Updated name of the pet + status: + type: string + description: Updated status of the pet + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + operationId: deletePet + parameters: + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + - name: api_key + in: header + required: false + schema: + type: string + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: Uploads an image + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + requestBody: + required: false + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + type: string + file: + type: string + format: binary + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [ ] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + operationId: placeOrder + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID + operationId: getOrderById + parameters: + - name: orderId + in: path + required: true + schema: + type: integer + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + operationId: deleteOrder + parameters: + - name: orderId + in: path + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + operationId: createUser + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: successful operation + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + operationId: createUsersWithArrayInput + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + '200': + description: successful operation + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + operationId: createUsersWithListInput + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + '200': + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + operationId: loginUser + parameters: + - name: username + in: query + required: true + schema: + type: string + - name: password + in: query + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: string + headers: + X-Rate-Limit: + schema: + type: integer + X-Expires-After: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + operationId: logoutUser + responses: + '200': + description: successful operation + /user/{username}: + get: + tags: + - user + summary: Get user by user name + operationId: getUserByName + parameters: + - name: username + in: path + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + operationId: updateUser + parameters: + - name: username + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + delete: + tags: + - user + summary: Delete user + operationId: deleteUser + parameters: + - name: username + in: path + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + # Comprehensive x-spring-paginated test endpoints + /pet/paginated/all: + get: + tags: + - pet + summary: List all pets with pagination (only pagination params) + description: Tests x-spring-paginated with ONLY page/size/sort params that will be removed + operationId: listAllPetsPaginated + parameters: + - name: page + in: query + description: Page number - will be removed when x-spring-paginated is true + schema: + type: integer + format: int32 + default: 0 + - name: size + in: query + description: Page size - will be removed when x-spring-paginated is true + schema: + type: integer + format: int32 + default: 20 + - name: sort + in: query + description: Sort order - will be removed when x-spring-paginated is true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/withParams: + get: + tags: + - pet + summary: List pets with filtering and pagination + description: Tests x-spring-paginated with regular params + pagination params + operationId: listPetsWithFilterPaginated + parameters: + - name: status + in: query + description: Filter by status - will be kept + schema: + type: string + enum: + - available + - pending + - sold + - name: name + in: query + description: Filter by name - will be kept + schema: + type: string + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + format: int32 + - name: size + in: query + description: Page size - will be removed + schema: + type: integer + format: int32 + - name: sort + in: query + description: Sort order - will be removed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/headerSize: + get: + tags: + - pet + summary: List pets with header param named 'size' + description: Tests that header param 'size' is preserved while query param 'size' is removed + operationId: listPetsWithHeaderSize + parameters: + - name: size + in: header + description: Size header - must NOT be removed (different from query param) + schema: + type: string + - name: category + in: query + description: Filter by category - will be kept + schema: + type: string + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + - name: size + in: query + description: Page size query param - will be removed + schema: + type: integer + - name: sort + in: query + description: Sort order - will be removed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/noPagination: + get: + tags: + - pet + summary: List pets WITHOUT x-spring-paginated + description: Tests that operations without x-spring-paginated keep all params + operationId: listPetsNoPagination + parameters: + - name: status + in: query + description: Filter by status + schema: + type: string + - name: page + in: query + description: Page number - will be KEPT (no x-spring-paginated) + schema: + type: integer + - name: size + in: query + description: Page size - will be KEPT (no x-spring-paginated) + schema: + type: integer + - name: sort + in: query + description: Sort order - will be KEPT (no x-spring-paginated) + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + # NO x-spring-paginated - all params should remain + /pet/paginated/{ownerId}: + get: + tags: + - pet + summary: List pets by owner with pagination + description: Tests x-spring-paginated with path param + query params + operationId: listPetsByOwnerPaginated + parameters: + - name: ownerId + in: path + description: Owner ID - will be kept + required: true + schema: + type: integer + format: int64 + - name: includeAdopted + in: query + description: Include adopted pets - will be kept + schema: + type: boolean + default: false + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + - name: size + in: query + description: Page size - will be removed + schema: + type: integer + - name: sort + in: query + description: Sort order - will be removed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/multiParam: + get: + tags: + - pet + summary: List pets with many query params + description: Tests x-spring-paginated removes only page/size/sort + operationId: listPetsMultipleParams + parameters: + - name: name + in: query + description: Filter by name - will be kept + schema: + type: string + - name: minAge + in: query + description: Minimum age - will be kept + schema: + type: integer + - name: maxAge + in: query + description: Maximum age - will be kept + schema: + type: integer + - name: tags + in: query + description: Filter by tags - will be kept + schema: + type: array + items: + type: string + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + - name: size + in: query + description: Page size - will be removed + schema: + type: integer + - name: sort + in: query + description: Sort order - will be removed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/partialParams: + get: + tags: + - pet + summary: List pets with only some pagination params + description: Tests x-spring-paginated with only page and size (no sort) + operationId: listPetsPartialPagination + parameters: + - name: status + in: query + description: Filter by status - will be kept + schema: + type: string + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + - name: size + in: query + description: Page size - will be removed + schema: + type: integer + # Intentionally NO 'sort' parameter + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/pathOnly: + get: + tags: + - pet + summary: List pets with path and header params only + description: Tests x-spring-paginated with no query params except pagination + operationId: listPetsByIdPaginated + parameters: + - name: X-Request-ID + in: header + description: Request ID for tracing + schema: + type: string + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + - name: size + in: query + description: Page size - will be removed + schema: + type: integer + - name: sort + in: query + description: Sort order - will be removed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + /pet/paginated/mixed: + get: + tags: + - pet + summary: Mixed params - path, header, query, and pagination + description: Comprehensive test with all parameter types + operationId: listPetsMixedParams + parameters: + - name: Authorization + in: header + description: Authorization header + schema: + type: string + - name: X-Tenant-ID + in: header + description: Tenant ID + schema: + type: string + - name: status + in: query + description: Status filter + schema: + type: string + - name: includeInactive + in: query + description: Include inactive pets + schema: + type: boolean + - name: page + in: query + description: Page number - will be removed + schema: + type: integer + - name: size + in: query + description: Page size - will be removed + schema: + type: integer + - name: sort + in: query + description: Sort order - will be removed + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + x-spring-paginated: true + +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://petstore.swagger.io/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Pet: + type: object + discriminator: + propertyName: petType + mapping: + Dog: '#/components/schemas/Dog' + Cat: '#/components/schemas/Cat' + required: + - name + - photoUrls + - petType + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + photoUrls: + type: array + items: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + color: + $ref: '#/components/schemas/Color' + petType: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + properties: + bark: + type: boolean + breed: + type: string + enum: + - Dingo + - Husky + - Retriever + - Shepherd + likesFetch: + type: boolean + description: Whether the dog enjoys fetching + required: [ bark, breed, likesFetch ] + type: object + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + properties: + hunts: + type: boolean + age: + type: integer + type: object + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Color: + type: string + enum: + - black + - white + - brown + - yellow + - violet + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string