diff --git a/docs/customization.md b/docs/customization.md index b180d783c2bd58eec2468cf320b09553b14fb98e..22c34373eacbd91ff45ed80445f205ebd33c7bef 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -451,3 +451,16 @@ Another useful option is `inlineSchemaNameDefaults`, which allows you to customi ``` Note: Only arrayItemSuffix, mapItemSuffix are supported at the moment. `SKIP_SCHEMA_REUSE=true` is a special value to skip reusing inline schemas. + +## OpenAPI Normalizer + +OpenAPI Normalizer (off by default) transforms the input OpenAPI doc/spec (which may not perfectly conform to the specification) to make it workable with OpenAPI Generator. Here is a list of rules supported: + +- `REF_AS_PARENT_IN_ALLOF`: when set to `true`, child schemas in `allOf` is considered a parent if it's a `$ref` (instead of inline schema) + + +Example: +``` +java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml -o /tmp/java-okhttp/ --additional-properties hideGenerationTimestamp="true" --openapi-normalizer REF_AS_PARENT_IN_ALLOF=true +``` + diff --git a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java index d206a6749f94b6722ebced6150827e834c4142e3..25c1d3cd61c4ec3864dee20b3a67cba8e44af7de 100644 --- a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java +++ b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java @@ -80,6 +80,9 @@ public class ConfigHelp extends OpenApiGeneratorCommand { @Option(name = {"--inline-schema-name-defaults"}, title = "inline schema name defaults", description = "default values used when naming inline schema name") private Boolean inlineSchemaNameDefaults; + @Option(name = {"--openapi-normalizer"}, title = "openapi normalizer rules", description = "displays the OpenAPI normalizer rules (none)") + private Boolean openapiNormalizer; + @Option(name = {"--metadata"}, title = "metadata", description = "displays the generator metadata like the help txt for the generator and generator type etc") private Boolean metadata; @@ -494,6 +497,18 @@ public class ConfigHelp extends OpenApiGeneratorCommand { sb.append(newline); } + if (Boolean.TRUE.equals(openapiNormalizer)) { + sb.append(newline).append("OPENAPI NORMALIZER RULES").append(newline).append(newline); + Map<String, String> map = config.openapiNormalizer() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> { + throw new IllegalStateException(String.format(Locale.ROOT, "Duplicated options! %s and %s", a, b)); + }, TreeMap::new)); + writePlainTextFromMap(sb, map, optIndent, optNestedIndent, "OpenAPI normalizer rule", "Set to"); + sb.append(newline); + } + if (Boolean.TRUE.equals(instantiationTypes)) { sb.append(newline).append("INSTANTIATION TYPES").append(newline).append(newline); Map<String, String> map = config.instantiationTypes() diff --git a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java index ae78b2f9fd94043453b814e5eb417d0f74d85ec0..2d473df2a3eb668e5de8e86ca5e9d9e9c032aa48 100644 --- a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java +++ b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java @@ -180,6 +180,13 @@ public class Generate extends OpenApiGeneratorCommand { + " ONLY arrayItemSuffix, mapItemSuffix are supported at the moment. `SKIP_SCHEMA_REUSE=true` is a special value to skip reusing inline schemas.") private List<String> inlineSchemaNameDefaults = new ArrayList<>(); + @Option( + name = {"--openapi-normalizer"}, + title = "OpenAPI normalizer rules", + description = "specifies the rules to be enabled in OpenAPI normalizer in the form of RULE_1=true,RULE_2=original." + + " You can also have multiple occurrences of this option.") + private List<String> openapiNormalizer = new ArrayList<>(); + @Option( name = {"--server-variables"}, title = "server variables", @@ -447,6 +454,7 @@ public class Generate extends OpenApiGeneratorCommand { applySchemaMappingsKvpList(schemaMappings, configurator); applyInlineSchemaNameMappingsKvpList(inlineSchemaNameMappings, configurator); applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator); + applyOpenAPINormalizerKvpList(openapiNormalizer, configurator); applyTypeMappingsKvpList(typeMappings, configurator); applyAdditionalPropertiesKvpList(additionalProperties, configurator); applyLanguageSpecificPrimitivesCsvList(languageSpecificPrimitives, configurator); diff --git a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java index c14a06721e8d4e55237c96d5ad809aa7640fd5fb..207bf477580d73ff2552dcdaeb3e9da54e7ebb36 100644 --- a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java +++ b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java @@ -53,6 +53,7 @@ public final class GeneratorSettings implements Serializable { private final Map<String, String> schemaMappings; private final Map<String, String> inlineSchemaNameMappings; private final Map<String, String> inlineSchemaNameDefaults; + private final Map<String, String> openapiNormalizer; private final Set<String> languageSpecificPrimitives; private final Map<String, String> reservedWordsMappings; private final Map<String, String> serverVariables; @@ -264,6 +265,15 @@ public final class GeneratorSettings implements Serializable { return inlineSchemaNameDefaults; } + /** + * Gets OpenAPI normalizer rules + * + * @return a map of rules + */ + public Map<String, String> getOpenAPINormalizer() { + return openapiNormalizer; + } + /** * Gets language specific primitives. These are in addition to the "base" primitives defined in a generator. * <p> @@ -382,6 +392,7 @@ public final class GeneratorSettings implements Serializable { schemaMappings = Collections.unmodifiableMap(builder.schemaMappings); inlineSchemaNameMappings = Collections.unmodifiableMap(builder.inlineSchemaNameMappings); inlineSchemaNameDefaults = Collections.unmodifiableMap(builder.inlineSchemaNameDefaults); + openapiNormalizer = Collections.unmodifiableMap(builder.openapiNormalizer); languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives); reservedWordsMappings = Collections.unmodifiableMap(builder.reservedWordsMappings); serverVariables = Collections.unmodifiableMap(builder.serverVariables); @@ -455,6 +466,7 @@ public final class GeneratorSettings implements Serializable { schemaMappings = Collections.unmodifiableMap(new HashMap<>(0)); inlineSchemaNameMappings = Collections.unmodifiableMap(new HashMap<>(0)); inlineSchemaNameDefaults = Collections.unmodifiableMap(new HashMap<>(0)); + openapiNormalizer = Collections.unmodifiableMap(new HashMap<>(0)); languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0)); reservedWordsMappings = Collections.unmodifiableMap(new HashMap<>(0)); serverVariables = Collections.unmodifiableMap(new HashMap<>(0)); @@ -515,6 +527,9 @@ public final class GeneratorSettings implements Serializable { if (copy.getInlineSchemaNameDefaults() != null) { builder.inlineSchemaNameDefaults.putAll(copy.getInlineSchemaNameDefaults()); } + if (copy.getOpenAPINormalizer() != null) { + builder.openapiNormalizer.putAll(copy.getOpenAPINormalizer()); + } if (copy.getLanguageSpecificPrimitives() != null) { builder.languageSpecificPrimitives.addAll(copy.getLanguageSpecificPrimitives()); } @@ -557,6 +572,7 @@ public final class GeneratorSettings implements Serializable { private Map<String, String> schemaMappings; private Map<String, String> inlineSchemaNameMappings; private Map<String, String> inlineSchemaNameDefaults; + private Map<String, String> openapiNormalizer; private Set<String> languageSpecificPrimitives; private Map<String, String> reservedWordsMappings; private Map<String, String> serverVariables; @@ -577,6 +593,7 @@ public final class GeneratorSettings implements Serializable { schemaMappings = new HashMap<>(); inlineSchemaNameMappings = new HashMap<>(); inlineSchemaNameDefaults = new HashMap<>(); + openapiNormalizer = new HashMap<>(); languageSpecificPrimitives = new HashSet<>(); reservedWordsMappings = new HashMap<>(); serverVariables = new HashMap<>(); @@ -897,6 +914,32 @@ public final class GeneratorSettings implements Serializable { return this; } + /** + * Sets the {@code openapiNormalizer} and returns a reference to this Builder so that the methods can be chained together. + * + * @param openapiNormalizer the {@code openapiNormalizer} to set + * @return a reference to this Builder + */ + public Builder withOpenAPINormalizer(Map<String, String> openapiNormalizer) { + this.openapiNormalizer = openapiNormalizer; + return this; + } + + /** + * Sets a single {@code openapiNormalizer} and returns a reference to this Builder so that the methods can be chained together. + * + * @param key A key for the OpenAPI normalizer rule + * @param value The value of the OpenAPI normalizer rule + * @return a reference to this Builder + */ + public Builder withOpenAPINormalizer(String key, String value) { + if (this.openapiNormalizer == null) { + this.openapiNormalizer = new HashMap<>(); + } + this.openapiNormalizer.put(key, value); + return this; + } + /** * Sets the {@code languageSpecificPrimitives} and returns a reference to this Builder so that the methods can be chained together. * @@ -1085,6 +1128,7 @@ public final class GeneratorSettings implements Serializable { Objects.equals(getSchemaMappings(), that.getSchemaMappings()) && Objects.equals(getInlineSchemaNameMappings(), that.getInlineSchemaNameMappings()) && Objects.equals(getInlineSchemaNameDefaults(), that.getInlineSchemaNameDefaults()) && + Objects.equals(getOpenAPINormalizer(), that.getOpenAPINormalizer()) && Objects.equals(getLanguageSpecificPrimitives(), that.getLanguageSpecificPrimitives()) && Objects.equals(getReservedWordsMappings(), that.getReservedWordsMappings()) && Objects.equals(getGitHost(), that.getGitHost()) && @@ -1116,6 +1160,7 @@ public final class GeneratorSettings implements Serializable { getSchemaMappings(), getInlineSchemaNameMappings(), getInlineSchemaNameDefaults(), + getOpenAPINormalizer(), getLanguageSpecificPrimitives(), getReservedWordsMappings(), getGitHost(), diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt index 4975fe2166195ac76032b67cc3bc8d5e8c9bd18b..5dfccba04c944ccce13b3cc655b6a27e4a103e83 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt @@ -118,6 +118,7 @@ class OpenApiGeneratorPlugin : Plugin<Project> { schemaMappings.set(generate.schemaMappings) inlineSchemaNameMappings.set(generate.inlineSchemaNameMappings) inlineSchemaNameDefaults.set(generate.inlineSchemaNameDefaults) + openapiNormalizer.set(generate.openapiNormalizer) invokerPackage.set(generate.invokerPackage) groupId.set(generate.groupId) id.set(generate.id) diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt index 4d9cff54143fe4a6e67b2c6f204a976355893aa1..21751733b67f87a63930a3bd0dc3d97b64683835 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt @@ -162,6 +162,11 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { */ val inlineSchemaNameDefaults = project.objects.mapProperty<String, String>() + /** + * Specifies mappings (rules) in OpenAPI normalizer + */ + val openapiNormalizer = project.objects.mapProperty<String, String>() + /** * Root package for generated code. */ diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt index f3ef513a2f225936fba297cf55b0bfb5f0f244e1..b368945e77ccc85737dabec91f82701a31fe9855 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt @@ -250,6 +250,13 @@ open class GenerateTask : DefaultTask() { @Input val inlineSchemaNameDefaults = project.objects.mapProperty<String, String>() + /** + * Specifies mappings (rules) in OpenAPI normalizer + */ + @Optional + @Input + val openapiNormalizer = project.objects.mapProperty<String, String>() + /** * Root package for generated code. */ @@ -758,6 +765,12 @@ open class GenerateTask : DefaultTask() { } } + if (openapiNormalizer.isPresent) { + openapiNormalizer.get().forEach { entry -> + configurator.addOpenAPINormalizer(entry.key, entry.value) + } + } + if (typeMappings.isPresent) { typeMappings.get().forEach { entry -> configurator.addTypeMapping(entry.key, entry.value) diff --git a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java index f796ca6002a96acb3ea0dc4624a8a4be12a908e0..8eb2073faa67bd16bbee67364afbf883d90e8657 100644 --- a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java +++ b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java @@ -315,6 +315,12 @@ public class CodeGenMojo extends AbstractMojo { @Parameter(name = "inlineSchemaNameDefaults", property = "openapi.generator.maven.plugin.inlineSchemaNameDefaults") private List<String> inlineSchemaNameDefaults; + /** + * A set of rules for OpenAPI normalizer + */ + @Parameter(name = "openapiNormalizer", property = "openapi.generator.maven.plugin.openapiNormalizer") + private List<String> openapiNormalizer; + /** * A map of swagger spec types and the generated code types to use for them */ @@ -700,6 +706,12 @@ public class CodeGenMojo extends AbstractMojo { configurator); } + // Retained for backwards-compatibility with configOptions -> openapi-normalizer + if (openapiNormalizer == null && configOptions.containsKey("openapi-normalizer")) { + applyOpenAPINormalizerKvp(configOptions.get("openapi-normalizer").toString(), + configurator); + } + // Retained for backwards-compatibility with configOptions -> type-mappings if (typeMappings == null && configOptions.containsKey("type-mappings")) { applyTypeMappingsKvp(configOptions.get("type-mappings").toString(), configurator); @@ -753,6 +765,11 @@ public class CodeGenMojo extends AbstractMojo { applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator); } + // Apply OpenAPI normalizer rules + if (openapiNormalizer != null && (configOptions == null || !configOptions.containsKey("openapi-normalizer"))) { + applyOpenAPINormalizerKvpList(openapiNormalizer, configurator); + } + // Apply Type Mappings if (typeMappings != null && (configOptions == null || !configOptions.containsKey("type-mappings"))) { applyTypeMappingsKvpList(typeMappings, configurator); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index 2917c2ee0a9b385796d091feef0e9517ce44b216..9b448b01e669ee7327cc60f7371c3aefd68c019d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -147,6 +147,8 @@ public interface CodegenConfig { Map<String, String> inlineSchemaNameDefault(); + Map<String, String> openapiNormalizer(); + Map<String, String> apiTemplateFiles(); Map<String, String> modelTemplateFiles(); @@ -330,4 +332,7 @@ public interface CodegenConfig { boolean getUseInlineModelResolver(); boolean getAddSuffixToDuplicateOperationNicknames(); + + boolean getUseOpenAPINormalizer(); + } 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 30e1c099c5e36a8bde46fe2c0f056ad4f56b4f04..ae20651a4b69e61c84f510a07103d3547e072b8b 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 @@ -167,6 +167,8 @@ public class DefaultCodegen implements CodegenConfig { protected Map<String, String> inlineSchemaNameMapping = new HashMap<>(); // a map to store the inline schema naming conventions protected Map<String, String> inlineSchemaNameDefault = new HashMap<>(); + // a map to store the rules in OpenAPI Normalizer + protected Map<String, String> openapiNormalizer = new HashMap<>(); protected String modelPackage = "", apiPackage = "", fileSuffix; protected String modelNamePrefix = "", modelNameSuffix = ""; protected String apiNamePrefix = "", apiNameSuffix = "Api"; @@ -1122,6 +1124,11 @@ public class DefaultCodegen implements CodegenConfig { return inlineSchemaNameDefault; } + @Override + public Map<String, String> openapiNormalizer() { + return openapiNormalizer; + } + @Override public String testPackage() { return testPackage; @@ -7924,6 +7931,9 @@ public class DefaultCodegen implements CodegenConfig { @Override public boolean getUseInlineModelResolver() { return true; } + @Override + public boolean getUseOpenAPINormalizer() { return true; } + /* A function to convert yaml or json ingested strings like property names And convert special characters like newline, tab, carriage return diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 2da1e53f84e2360530a35556d74e1f62cb50c2d2..e13a7809676318000e06cf8f91eb13272c1c6f58 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -255,6 +255,12 @@ public class DefaultGenerator implements Generator { config.processOpts(); + // normalize the spec + if (config.getUseOpenAPINormalizer()) { + OpenAPINormalizer openapiNormalizer = new OpenAPINormalizer(openAPI, config.openapiNormalizer()); + openapiNormalizer.normalize(); + } + // resolve inline models if (config.getUseInlineModelResolver()) { InlineModelResolver inlineModelResolver = new InlineModelResolver(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java new file mode 100644 index 0000000000000000000000000000000000000000..4f1de87ba93a40336e0d63293723d4f0bb81f02c --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -0,0 +1,384 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * Copyright 2018 SmartBear Software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen; + +import io.swagger.v3.oas.models.*; +import io.swagger.v3.oas.models.callbacks.Callback; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +public class OpenAPINormalizer { + private OpenAPI openAPI; + private Map<String, String> rules = new HashMap<>(); + + final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class); + + // ============= a list of rules ============= + // when set to true, all rules are enabled + final String ALL = "ALL"; + boolean enableAll; + + // when set to true, $ref in allOf is treated as parent so that x-parent: true will be added + // to the schema in $ref (if x-parent is not present) + final String REF_AS_PARENT_IN_ALLOF = "REF_AS_PARENT_IN_ALLOF"; + boolean enableRefAsParentInAllOf; + + // ============= end of rules ============= + + /** + * Initializes OpenAPI Normalizer with a set of rules + * + * @param openAPI OpenAPI + * @param rules a map of rules + */ + public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> rules) { + this.openAPI = openAPI; + this.rules = rules; + parseRules(rules); + } + + /** + * Parses the rules. + * + * @param rules a map of rules + */ + public void parseRules(Map<String, String> rules) { + if (rules == null) { + return; + } + + if ("true".equalsIgnoreCase(rules.get(ALL))) { + enableAll = true; + } + + if (enableAll || "true".equalsIgnoreCase(rules.get(REF_AS_PARENT_IN_ALLOF))) { + enableRefAsParentInAllOf = true; + } + } + + /** + * Normalizes the OpenAPI input, which may not perfectly conform to + * the specification. + */ + void normalize() { + if (rules == null || rules.isEmpty()) { + return; + } + + if (this.openAPI.getComponents() == null) { + this.openAPI.setComponents(new Components()); + } + + if (this.openAPI.getComponents().getSchemas() == null) { + this.openAPI.getComponents().setSchemas(new HashMap<String, Schema>()); + } + + normalizePaths(); + normalizeComponents(); + } + + /** + * Normalizes inline models in Paths + */ + private void normalizePaths() { + Paths paths = openAPI.getPaths(); + if (paths == null) { + return; + } + + for (Map.Entry<String, PathItem> pathsEntry : paths.entrySet()) { + PathItem path = pathsEntry.getValue(); + List<Operation> operations = new ArrayList<>(path.readOperations()); + + // Include callback operation as well + for (Operation operation : path.readOperations()) { + Map<String, Callback> callbacks = operation.getCallbacks(); + if (callbacks != null) { + operations.addAll(callbacks.values().stream() + .flatMap(callback -> callback.values().stream()) + .flatMap(pathItem -> pathItem.readOperations().stream()) + .collect(Collectors.toList())); + } + } + + for (Operation operation : operations) { + normalizeRequestBody(operation); + normalizeParameters(operation); + normalizeResponses(operation); + } + } + } + + /** + * Normalizes schemas in content + * + * @param content target content + */ + private void normalizeContent(Content content) { + if (content == null || content.isEmpty()) { + return; + } + + for (String contentType : content.keySet()) { + MediaType mediaType = content.get(contentType); + if (mediaType == null) { + continue; + } else if (mediaType.getSchema() == null) { + continue; + } else { + normalizeSchema(mediaType.getSchema(), new HashSet<>()); + } + } + } + + /** + * Normalizes schemas in RequestBody + * + * @param operation target operation + */ + private void normalizeRequestBody(Operation operation) { + RequestBody requestBody = operation.getRequestBody(); + if (requestBody == null) { + return; + } + + // unalias $ref + if (requestBody.get$ref() != null) { + String ref = ModelUtils.getSimpleRef(requestBody.get$ref()); + requestBody = openAPI.getComponents().getRequestBodies().get(ref); + + if (requestBody == null) { + return; + } + } + + normalizeContent(requestBody.getContent()); + } + + /** + * Normalizes schemas in parameters + * + * @param operation target operation + */ + private void normalizeParameters(Operation operation) { + List<Parameter> parameters = operation.getParameters(); + if (parameters == null) { + return; + } + + for (Parameter parameter : parameters) { + if (parameter.getSchema() == null) { + continue; + } else { + normalizeSchema(parameter.getSchema(), new HashSet<>()); + } + } + } + + /** + * Normalizes schemas in ApiResponses + * + * @param operation target operation + */ + private void normalizeResponses(Operation operation) { + ApiResponses responses = operation.getResponses(); + if (responses == null) { + return; + } + + for (Map.Entry<String, ApiResponse> responsesEntry : responses.entrySet()) { + if (responsesEntry.getValue() == null) { + continue; + } else { + normalizeContent(responsesEntry.getValue().getContent()); + } + } + } + + /** + * Normalizes schemas in components + */ + private void normalizeComponents() { + Map<String, Schema> schemas = openAPI.getComponents().getSchemas(); + if (schemas == null) { + return; + } + + List<String> schemaNames = new ArrayList<String>(schemas.keySet()); + for (String schemaName : schemaNames) { + Schema schema = schemas.get(schemaName); + if (schema == null) { + LOGGER.warn("{} not fount found in openapi/components/schemas.", schemaName); + } else { + normalizeSchema(schema, new HashSet<>()); + } + } + } + + /** + * Normalizes a schema + * + * @param schema Schema + * @param visitedSchemas a set of visited schemas + */ + public void normalizeSchema(Schema schema, Set<Schema> visitedSchemas) { + if (schema == null) { + return; + } + + if (StringUtils.isNotEmpty(schema.get$ref())) { + // not need to process $ref + return; + } + + if ((visitedSchemas.contains(schema))) { + return; // skip due to circular reference + } else { + visitedSchemas.add(schema); + } + + if (schema instanceof ArraySchema) { + normalizeSchema(schema.getItems(), visitedSchemas); + } else if (schema.getAdditionalProperties() instanceof Schema) { // map + normalizeSchema((Schema) schema.getAdditionalProperties(), visitedSchemas); + } else if (ModelUtils.isComposedSchema(schema)) { + ComposedSchema m = (ComposedSchema) schema; + if (m.getAllOf() != null && !m.getAllOf().isEmpty()) { + normalizeAllOf(m, visitedSchemas); + } + + if (m.getOneOf() != null && !m.getOneOf().isEmpty()) { + normalizeOneOf(m, visitedSchemas); + } + + if (m.getAnyOf() != null && !m.getAnyOf().isEmpty()) { + normalizeAnyOf(m, visitedSchemas); + } + + if (m.getProperties() != null && !m.getProperties().isEmpty()) { + normalizeProperties(m.getProperties(), visitedSchemas); + } + + if (m.getAdditionalProperties() != null) { + // normalizeAdditionalProperties(m); + } + } else if (schema.getNot() != null) {// not schema + normalizeSchema(schema.getNot(), visitedSchemas); + } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { + normalizeProperties(schema.getProperties(), visitedSchemas); + } else if (schema instanceof Schema) { + normalizeNonComposedSchema(schema, visitedSchemas); + } else { + throw new RuntimeException("Unknown schema type found in normalizer: " + schema); + } + } + + private void normalizeNonComposedSchema(Schema schema, Set<Schema> visitedSchemas) { + // normalize non-composed schema (e.g. schema with only properties) + } + + private void normalizeProperties(Map<String, Schema> properties, Set<Schema> visitedSchemas) { + if (properties == null) { + return; + } + for (Map.Entry<String, Schema> propertiesEntry : properties.entrySet()) { + Schema property = propertiesEntry.getValue(); + normalizeSchema(property, visitedSchemas); + } + } + + private void normalizeAllOf(Schema schema, Set<Schema> visitedSchemas) { + for (Object item : schema.getAllOf()) { + if (!(item instanceof Schema)) { + throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item); + } + // normalize allOf sub schemas one by one + normalizeSchema((Schema) item, visitedSchemas); + } + // process rules here + processUseAllOfRefAsParent(schema); + } + + private void normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) { + for (Object item : schema.getAllOf()) { + if (!(item instanceof Schema)) { + throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item); + } + // normalize oenOf sub schemas one by one + normalizeSchema((Schema) item, visitedSchemas); + } + // process rules here + } + + private void normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) { + for (Object item : schema.getAllOf()) { + if (!(item instanceof Schema)) { + throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item); + } + // normalize anyOf sub schemas one by one + normalizeSchema((Schema) item, visitedSchemas); + } + // process rules here + } + + // ===================== a list of rules ===================== + // all rules (fuctions) start with the word "process" + private void processUseAllOfRefAsParent(Schema schema) { + if (!enableRefAsParentInAllOf) { + return; + } + + for (Object item : schema.getAllOf()) { + if (!(item instanceof Schema)) { + throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item); + } + Schema s = (Schema) item; + + if (StringUtils.isNotEmpty(s.get$ref())) { + String ref = ModelUtils.getSimpleRef(s.get$ref()); + // TODO need to check for requestBodies? + Schema refSchema = openAPI.getComponents().getSchemas().get(ref); + if (refSchema == null) { + throw new RuntimeException("schema cannot be null with ref " + ref); + } + if (refSchema.getExtensions() == null) { + refSchema.setExtensions(new HashMap<>()); + } + + if (refSchema.getExtensions().containsKey("x-parent")) { + // doing nothing as x-parent already exists + } else { + refSchema.getExtensions().put("x-parent", true); + } + + LOGGER.debug("processUseAllOfRefAsParent added `x-parent: true` to {}", refSchema); + } + } + } + // ===================== end of rules ===================== +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java index 3d2d9c3aaacc4f63278e8b5afb59c2dd7270a977..8b984a23bed045716eae9e71d1211d2b3e072a44 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java @@ -71,6 +71,7 @@ public class CodegenConfigurator { private Map<String, String> schemaMappings = new HashMap<>(); private Map<String, String> inlineSchemaNameMappings = new HashMap<>(); private Map<String, String> inlineSchemaNameDefaults = new HashMap<>(); + private Map<String, String> openapiNormalizer = new HashMap<>(); private Set<String> languageSpecificPrimitives = new HashSet<>(); private Map<String, String> reservedWordsMappings = new HashMap<>(); private Map<String, String> serverVariables = new HashMap<>(); @@ -123,6 +124,9 @@ public class CodegenConfigurator { if(generatorSettings.getInlineSchemaNameDefaults() != null) { configurator.inlineSchemaNameDefaults.putAll(generatorSettings.getInlineSchemaNameDefaults()); } + if(generatorSettings.getOpenAPINormalizer() != null) { + configurator.openapiNormalizer.putAll(generatorSettings.getOpenAPINormalizer()); + } if(generatorSettings.getLanguageSpecificPrimitives() != null) { configurator.languageSpecificPrimitives.addAll(generatorSettings.getLanguageSpecificPrimitives()); } @@ -210,6 +214,12 @@ public class CodegenConfigurator { return this; } + public CodegenConfigurator addOpenAPINormalizer(String key, String value) { + this.openapiNormalizer.put(key, value); + generatorSettingsBuilder.withOpenAPINormalizer(key, value); + return this; + } + public CodegenConfigurator addInstantiationType(String key, String value) { this.instantiationTypes.put(key, value); generatorSettingsBuilder.withInstantiationType(key, value); @@ -382,6 +392,12 @@ public class CodegenConfigurator { return this; } + public CodegenConfigurator setOpenAPINormalizer(Map<String, String> openapiNormalizer) { + this.openapiNormalizer = openapiNormalizer; + generatorSettingsBuilder.withOpenAPINormalizer(openapiNormalizer); + return this; + } + public CodegenConfigurator setInputSpec(String inputSpec) { this.inputSpec = inputSpec; workflowSettingsBuilder.withInputSpec(inputSpec); @@ -661,6 +677,7 @@ public class CodegenConfigurator { config.schemaMapping().putAll(generatorSettings.getSchemaMappings()); config.inlineSchemaNameMapping().putAll(generatorSettings.getInlineSchemaNameMappings()); config.inlineSchemaNameDefault().putAll(generatorSettings.getInlineSchemaNameDefaults()); + config.openapiNormalizer().putAll(generatorSettings.getOpenAPINormalizer()); config.languageSpecificPrimitives().addAll(generatorSettings.getLanguageSpecificPrimitives()); config.reservedWordsMappings().putAll(generatorSettings.getReservedWordsMappings()); config.additionalProperties().putAll(generatorSettings.getAdditionalProperties()); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java index fb708d4b2f55bcd02738b2f6327459470ccc0180..5a0b40d3a429a3b0271e7fcc2571d451bef8e0b0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java @@ -120,6 +120,19 @@ public final class CodegenConfiguratorUtils { } } + public static void applyOpenAPINormalizerKvpList(List<String> openapiNormalizer, CodegenConfigurator configurator) { + for (String propString : openapiNormalizer) { + applyOpenAPINormalizerKvp(propString, configurator); + } + } + + public static void applyOpenAPINormalizerKvp(String openapiNormalizer, CodegenConfigurator configurator) { + final Map<String, String> map = createMapFromKeyValuePairs(openapiNormalizer); + for (Map.Entry<String, String> entry : map.entrySet()) { + configurator.addOpenAPINormalizer(entry.getKey().trim(), entry.getValue().trim()); + } + } + public static void applyTypeMappingsKvpList(List<String> typeMappings, CodegenConfigurator configurator) { for (String propString : typeMappings) { applyTypeMappingsKvp(propString, configurator); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 107045d3958c4b89600f446911e7320758868a29..c1b98fc92202e49309e3b4a62d8d41abcef76a22 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -308,15 +308,15 @@ public class ModelUtils { /** * Invoke the specified visitor function for every schema that matches mimeType in the OpenAPI document. - * + * <p> * To avoid infinite recursion, referenced schemas are visited only once. When a referenced schema is visited, * it is added to visitedSchemas. * - * @param openAPI the OpenAPI document that contains schema objects. - * @param schema the root schema object to be visited. - * @param mimeType the mime type. TODO: does not seem to be used in a meaningful way. + * @param openAPI the OpenAPI document that contains schema objects. + * @param schema the root schema object to be visited. + * @param mimeType the mime type. TODO: does not seem to be used in a meaningful way. * @param visitedSchemas the list of referenced schemas that have been visited. - * @param visitor the visitor function which is invoked for every visited schema. + * @param visitor the visitor function which is invoked for every visited schema. */ private static void visitSchema(OpenAPI openAPI, Schema schema, String mimeType, List<String> visitedSchemas, OpenAPISchemaVisitor visitor) { visitor.visit(schema, mimeType); @@ -425,13 +425,14 @@ public class ModelUtils { /** * Return true if the specified schema is an object with a fixed number of properties. - * + * <p> * A ObjectSchema differs from a MapSchema in the following way: * - An ObjectSchema is not extensible, i.e. it has a fixed number of properties. * - A MapSchema is an object that can be extended with an arbitrary set of properties. * The payload may include dynamic properties. - * + * <p> * For example, an OpenAPI schema is considered an ObjectSchema in the following scenarios: + * <p> * * type: object * additionalProperties: false @@ -479,16 +480,17 @@ public class ModelUtils { * Return true if the specified 'schema' is an object that can be extended with additional properties. * Additional properties means a Schema should support all explicitly defined properties plus any * undeclared properties. - * + * <p> * A MapSchema differs from an ObjectSchema in the following way: * - An ObjectSchema is not extensible, i.e. it has a fixed number of properties. * - A MapSchema is an object that can be extended with an arbitrary set of properties. - * The payload may include dynamic properties. - * + * The payload may include dynamic properties. + * <p> * Note that isMapSchema returns true for a composed schema (allOf, anyOf, oneOf) that also defines * additionalproperties. - * + * <p> * For example, an OpenAPI schema is considered a MapSchema in the following scenarios: + * <p> * * type: object * additionalProperties: true @@ -772,14 +774,14 @@ public class ModelUtils { /** * Check to see if the schema is a free form object. - * + * <p> * A free form object is an object (i.e. 'type: object' in a OAS document) that: * 1) Does not define properties, and * 2) Is not a composed schema (no anyOf, oneOf, allOf), and * 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}. - * + * <p> * Examples: - * + * <p> * components: * schemas: * arbitraryObject: @@ -798,7 +800,7 @@ public class ModelUtils { * The value can be any type except the 'null' value. * * @param openAPI the object that encapsulates the OAS document. - * @param schema potentially containing a '$ref' + * @param schema potentially containing a '$ref' * @return true if it's a free-form object */ public static boolean isFreeFormObject(OpenAPI openAPI, Schema schema) { @@ -1054,10 +1056,10 @@ public class ModelUtils { /** * Return the first Schema from a specified OAS 'content' section. - * + * <p> * For example, given the following OAS, this method returns the schema * for the 'application/json' content type because it is listed first in the OAS. - * + * <p> * responses: * '200': * content: @@ -1099,8 +1101,8 @@ public class ModelUtils { /** * Has self reference? * - * @param openAPI OpenAPI spec. - * @param schema Schema + * @param openAPI OpenAPI spec. + * @param schema Schema * @param visitedSchemaNames A set of visited schema names * @return boolean true if it has at least one self reference */ @@ -1257,7 +1259,7 @@ public class ModelUtils { /** * Returns the additionalProperties Schema for the specified input schema. - * + * <p> * The additionalProperties keyword is used to control the handling of additional, undeclared * properties, that is, properties whose names are not listed in the properties keyword. * The additionalProperties keyword may be either a boolean or an object. @@ -1267,9 +1269,9 @@ public class ModelUtils { * to the boolean value True or setting additionalProperties: {} * * @param openAPI the object that encapsulates the OAS document. - * @param schema the input schema that may or may not have the additionalProperties keyword. + * @param schema the input schema that may or may not have the additionalProperties keyword. * @return the Schema of the additionalProperties. The null value is returned if no additional - * properties are allowed. + * properties are allowed. */ public static Schema getAdditionalProperties(OpenAPI openAPI, Schema schema) { Object addProps = schema.getAdditionalProperties(); @@ -1380,10 +1382,10 @@ public class ModelUtils { * that specify a determinator. * If there are multiple elements in the composed schema and it is not clear * which one should be the parent, return null. - * + * <p> * For example, given the following OAS spec, the parent of 'Dog' is Animal * because 'Animal' specifies a discriminator. - * + * <p> * animal: * type: object * discriminator: @@ -1391,6 +1393,7 @@ public class ModelUtils { * properties: * type: string * + * <p> * dog: * allOf: * - $ref: '#/components/schemas/animal' @@ -1418,10 +1421,10 @@ public class ModelUtils { LOGGER.error("Failed to obtain schema from {}", parentName); return "UNKNOWN_PARENT_NAME"; } else if (hasOrInheritsDiscriminator(s, allSchemas)) { - // discriminator.propertyName is used + // discriminator.propertyName is used or x-parent is used return parentName; } else { - // not a parent since discriminator.propertyName is not set + // not a parent since discriminator.propertyName or x-parent is not set hasAmbiguousParents = true; refedWithoutDiscriminator.add(parentName); } @@ -1476,7 +1479,7 @@ public class ModelUtils { LOGGER.error("Failed to obtain schema from {}", parentName); names.add("UNKNOWN_PARENT_NAME"); } else if (hasOrInheritsDiscriminator(s, allSchemas)) { - // discriminator.propertyName is used + // discriminator.propertyName is used or x-parent is used names.add(parentName); if (includeAncestors && s instanceof ComposedSchema) { names.addAll(getAllParentsName((ComposedSchema) s, allSchemas, true)); @@ -1501,7 +1504,8 @@ public class ModelUtils { } private static boolean hasOrInheritsDiscriminator(Schema schema, Map<String, Schema> allSchemas) { - if (schema.getDiscriminator() != null && StringUtils.isNotEmpty(schema.getDiscriminator().getPropertyName())) { + if ((schema.getDiscriminator() != null && StringUtils.isNotEmpty(schema.getDiscriminator().getPropertyName())) + || (isExtensionParent(schema))) { // x-parent is used return true; } else if (StringUtils.isNotEmpty(schema.get$ref())) { String parentName = getSimpleRef(schema.get$ref()); @@ -1523,18 +1527,43 @@ public class ModelUtils { return false; } + /** + * If it's a boolean, returns the value of the extension `x-parent`. + * If it's string, return true if it's non-empty. + * If the return value is `true`, the schema is a parent. + * + * @param schema Schema + * @return boolean + */ + public static boolean isExtensionParent(Schema schema) { + if (schema.getExtensions() == null) { + return false; + } else { + Object xParent = schema.getExtensions().get("x-parent"); + if (xParent == null) { + return false; + } else if (xParent instanceof Boolean) { + return (Boolean) xParent; + } else if (xParent instanceof String) { + return StringUtils.isNotEmpty((String) xParent); + } else { + return false; + } + } + } + /** * Return true if the 'nullable' attribute is set to true in the schema, i.e. if the value * of the property can be the null value. - * + * <p> * In addition, if the OAS document is 3.1 or above, isNullable returns true if the input * schema is a 'oneOf' composed document with at most two children, and one of the children * is the 'null' type. - * + * <p> * The caller is responsible for resolving schema references before invoking isNullable. * If the input schema is a $ref and the referenced schema has 'nullable: true', this method * returns false (because the nullable attribute is defined in the referenced schema). - * + * <p> * The 'nullable' attribute was introduced in OAS 3.0. * The 'nullable' attribute is deprecated in OAS 3.1. In a OAS 3.1 document, the preferred way * to specify nullable properties is to use the 'null' type. @@ -1564,11 +1593,11 @@ public class ModelUtils { /** * Return true if the specified composed schema is 'oneOf', contains one or two elements, * and at least one of the elements is the 'null' type. - * + * <p> * The 'null' type is supported in OAS 3.1 and above. * In the example below, the 'OptionalOrder' can have the null value because the 'null' * type is one of the elements under 'oneOf'. - * + * <p> * OptionalOrder: * oneOf: * - type: 'null' @@ -1591,13 +1620,13 @@ public class ModelUtils { /** * isNullType returns true if the input schema is the 'null' type. - * + * <p> * The 'null' type is supported in OAS 3.1 and above. It is not supported * in OAS 2.0 and OAS 3.0.x. - * + * <p> * For example, the "null" type could be used to specify that a value must * either be null or a specified type: - * + * <p> * OptionalOrder: * oneOf: * - type: 'null' @@ -1617,6 +1646,7 @@ public class ModelUtils { * For when a type is not defined on a schema * Note: properties, additionalProperties, enums, validations, items, and composed schemas (oneOf/anyOf/allOf) * can be defined or omitted on these any type schemas + * * @param schema the schema that we are checking * @return boolean */ @@ -1713,7 +1743,7 @@ public class ModelUtils { private static ObjectMapper getRightMapper(String data) { ObjectMapper mapper; - if (data.trim().startsWith("{")) { + if (data.trim().startsWith("{")) { mapper = JSON_MAPPER; } else { mapper = YAML_MAPPER; @@ -1725,11 +1755,9 @@ public class ModelUtils { * Parse and return a JsonNode representation of the input OAS document. * * @param location the URL of the OAS document. - * @param auths the list of authorization values to access the remote URL. - * - * @throws java.lang.Exception if an error occurs while retrieving the OpenAPI document. - * + * @param auths the list of authorization values to access the remote URL. * @return A JsonNode representation of the input OAS document. + * @throws java.lang.Exception if an error occurs while retrieving the OpenAPI document. */ public static JsonNode readWithInfo(String location, List<AuthorizationValue> auths) throws Exception { String data; @@ -1756,14 +1784,13 @@ public class ModelUtils { /** * Parse the OAS document at the specified location, get the swagger or openapi version * as specified in the source document, and return the version. - * + * <p> * For OAS 2.0 documents, return the value of the 'swagger' attribute. * For OAS 3.x documents, return the value of the 'openapi' attribute. * - * @param openAPI the object that encapsulates the OAS document. + * @param openAPI the object that encapsulates the OAS document. * @param location the URL of the OAS document. - * @param auths the list of authorization values to access the remote URL. - * + * @param auths the list of authorization values to access the remote URL. * @return the version of the OpenAPI document. */ public static SemVer getOpenApiVersion(OpenAPI openAPI, String location, List<AuthorizationValue> auths) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 05fa241e40e9a1ed6c371b3c2915ae5791dee930..95dc963329920dee788a27b79c719d04b9214f17 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -56,7 +56,6 @@ import java.util.stream.Collectors; import static org.testng.Assert.*; - public class DefaultCodegenTest { @Test @@ -4300,4 +4299,29 @@ public class DefaultCodegenTest { Assert.assertFalse(inlineEnumSchemaProperty.isContainer); Assert.assertFalse(inlineEnumSchemaProperty.isPrimitiveType); } + + @Test + public void testOpenAPINormalizer() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/allOf_extension_parent.yaml"); + + Schema schema = openAPI.getComponents().getSchemas().get("AnotherPerson"); + assertNull(schema.getExtensions()); + + Schema schema2 = openAPI.getComponents().getSchemas().get("Person"); + assertEquals(schema2.getExtensions().get("x-parent"), "abstract"); + + Map<String, String> options = new HashMap<>(); + options.put("REF_AS_PARENT_IN_ALLOF", "true"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); + openAPINormalizer.normalize(); + + Schema schema3 = openAPI.getComponents().getSchemas().get("AnotherPerson"); + assertEquals(schema3.getExtensions().get("x-parent"), true); + + Schema schema4 = openAPI.getComponents().getSchemas().get("AnotherParent"); + assertEquals(schema4.getExtensions().get("x-parent"), true); + + Schema schema5 = openAPI.getComponents().getSchemas().get("Person"); + assertEquals(schema5.getExtensions().get("x-parent"), "abstract"); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 9efb392203655960999ae134052e938c859a8a2c..b046fd2a344fb05aa9c419c95e21174bc2d9f560 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -1717,4 +1717,39 @@ public class JavaClientCodegenTest { "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"multi\", \"values\", queryObject.getValues()));" ); } + + @Test + public void testJdkHttpClientWithAndWithoutParentExtension() throws Exception { + Map<String, Object> properties = new HashMap<>(); + properties.put(CodegenConstants.API_PACKAGE, "xyz.abcdef.api"); + properties.put(CodegenConstants.MODEL_PACKAGE, "xyz.abcdef.model"); + properties.put(CodegenConstants.INVOKER_PACKAGE, "xyz.abcdef.invoker"); + + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("java") + // use default `okhttp-gson` + //.setLibrary(JavaClientCodegen.NATIVE) + .setAdditionalProperties(properties) + .setInputSpec("src/test/resources/3_0/allOf_extension_parent.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + List<File> files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 30); + validateJavaSourceFiles(files); + + TestUtils.assertFileContains(Paths.get(output + "/src/main/java/xyz/abcdef/model/Child.java"), + "public class Child extends Person {"); + TestUtils.assertFileContains(Paths.get(output + "/src/main/java/xyz/abcdef/model/Adult.java"), + "public class Adult extends Person {"); + TestUtils.assertFileContains(Paths.get(output + "/src/main/java/xyz/abcdef/model/AnotherChild.java"), + "public class AnotherChild {"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8b5e27936044d5661124f5bc286f04485f9e3fef --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml @@ -0,0 +1,87 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Example + license: + name: MIT +servers: + - url: http://api.example.xyz/v1 +paths: + /person/display/{personId}: + get: + parameters: + - name: personId + in: path + required: true + description: The id of the person to retrieve + schema: + type: string + operationId: list + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Person" +components: + schemas: + Person: + description: person using x-parent (abstract) to indicate it's a parent class + type: object + x-parent: "abstract" + properties: + $_type: + type: string + lastName: + type: string + firstName: + type: string + Adult: + description: A representation of an adult + allOf: + - $ref: '#/components/schemas/Person' + - type: object + properties: + children: + type: array + items: + $ref: "#/components/schemas/Child" + Child: + description: A representation of a child + allOf: + - type: object + properties: + age: + type: integer + format: int32 + - $ref: '#/components/schemas/Person' + AnotherChild: + description: another child class that does NOT extend/inherit AnotherPerson + allOf: + - type: object + properties: + age: + type: integer + format: int32 + - $ref: '#/components/schemas/AnotherPerson' + AnotherPerson: + description: person object without x-parent extension + type: object + allOf: + - properties: + $_type: + type: string + lastName: + type: string + firstName: + type: string + - $ref: '#/components/schemas/AnotherParent' + AnotherParent: + description: parent object without x-parent extension + type: object + properties: + isParent: + type: boolean + mum_or_dad: + type: string