Add OpenAPI Normalizer (#14172)

* add x-parent support

* add docstring

* add openapi normalizer rule to use ref as parent in allof

* add openapi normalizer with 1 rule

* revise wordings

* fix javadoc warnings

* better test

* fix docstring

* minor update

* minor improvements

* fix typo
This commit is contained in:
William Cheng 2022-12-30 16:03:21 +08:00 committed by GitHub
parent 3a8265b6ee
commit b71aecbe9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 771 additions and 46 deletions

View File

@ -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. 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
```

View File

@ -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") @Option(name = {"--inline-schema-name-defaults"}, title = "inline schema name defaults", description = "default values used when naming inline schema name")
private Boolean inlineSchemaNameDefaults; 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") @Option(name = {"--metadata"}, title = "metadata", description = "displays the generator metadata like the help txt for the generator and generator type etc")
private Boolean metadata; private Boolean metadata;
@ -494,6 +497,18 @@ public class ConfigHelp extends OpenApiGeneratorCommand {
sb.append(newline); 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)) { if (Boolean.TRUE.equals(instantiationTypes)) {
sb.append(newline).append("INSTANTIATION TYPES").append(newline).append(newline); sb.append(newline).append("INSTANTIATION TYPES").append(newline).append(newline);
Map<String, String> map = config.instantiationTypes() Map<String, String> map = config.instantiationTypes()

View File

@ -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.") + " 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<>(); 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( @Option(
name = {"--server-variables"}, name = {"--server-variables"},
title = "server variables", title = "server variables",
@ -447,6 +454,7 @@ public class Generate extends OpenApiGeneratorCommand {
applySchemaMappingsKvpList(schemaMappings, configurator); applySchemaMappingsKvpList(schemaMappings, configurator);
applyInlineSchemaNameMappingsKvpList(inlineSchemaNameMappings, configurator); applyInlineSchemaNameMappingsKvpList(inlineSchemaNameMappings, configurator);
applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator); applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator);
applyOpenAPINormalizerKvpList(openapiNormalizer, configurator);
applyTypeMappingsKvpList(typeMappings, configurator); applyTypeMappingsKvpList(typeMappings, configurator);
applyAdditionalPropertiesKvpList(additionalProperties, configurator); applyAdditionalPropertiesKvpList(additionalProperties, configurator);
applyLanguageSpecificPrimitivesCsvList(languageSpecificPrimitives, configurator); applyLanguageSpecificPrimitivesCsvList(languageSpecificPrimitives, configurator);

View File

@ -53,6 +53,7 @@ public final class GeneratorSettings implements Serializable {
private final Map<String, String> schemaMappings; private final Map<String, String> schemaMappings;
private final Map<String, String> inlineSchemaNameMappings; private final Map<String, String> inlineSchemaNameMappings;
private final Map<String, String> inlineSchemaNameDefaults; private final Map<String, String> inlineSchemaNameDefaults;
private final Map<String, String> openapiNormalizer;
private final Set<String> languageSpecificPrimitives; private final Set<String> languageSpecificPrimitives;
private final Map<String, String> reservedWordsMappings; private final Map<String, String> reservedWordsMappings;
private final Map<String, String> serverVariables; private final Map<String, String> serverVariables;
@ -264,6 +265,15 @@ public final class GeneratorSettings implements Serializable {
return inlineSchemaNameDefaults; 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. * Gets language specific primitives. These are in addition to the "base" primitives defined in a generator.
* <p> * <p>
@ -382,6 +392,7 @@ public final class GeneratorSettings implements Serializable {
schemaMappings = Collections.unmodifiableMap(builder.schemaMappings); schemaMappings = Collections.unmodifiableMap(builder.schemaMappings);
inlineSchemaNameMappings = Collections.unmodifiableMap(builder.inlineSchemaNameMappings); inlineSchemaNameMappings = Collections.unmodifiableMap(builder.inlineSchemaNameMappings);
inlineSchemaNameDefaults = Collections.unmodifiableMap(builder.inlineSchemaNameDefaults); inlineSchemaNameDefaults = Collections.unmodifiableMap(builder.inlineSchemaNameDefaults);
openapiNormalizer = Collections.unmodifiableMap(builder.openapiNormalizer);
languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives); languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives);
reservedWordsMappings = Collections.unmodifiableMap(builder.reservedWordsMappings); reservedWordsMappings = Collections.unmodifiableMap(builder.reservedWordsMappings);
serverVariables = Collections.unmodifiableMap(builder.serverVariables); serverVariables = Collections.unmodifiableMap(builder.serverVariables);
@ -455,6 +466,7 @@ public final class GeneratorSettings implements Serializable {
schemaMappings = Collections.unmodifiableMap(new HashMap<>(0)); schemaMappings = Collections.unmodifiableMap(new HashMap<>(0));
inlineSchemaNameMappings = Collections.unmodifiableMap(new HashMap<>(0)); inlineSchemaNameMappings = Collections.unmodifiableMap(new HashMap<>(0));
inlineSchemaNameDefaults = Collections.unmodifiableMap(new HashMap<>(0)); inlineSchemaNameDefaults = Collections.unmodifiableMap(new HashMap<>(0));
openapiNormalizer = Collections.unmodifiableMap(new HashMap<>(0));
languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0)); languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0));
reservedWordsMappings = Collections.unmodifiableMap(new HashMap<>(0)); reservedWordsMappings = Collections.unmodifiableMap(new HashMap<>(0));
serverVariables = 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) { if (copy.getInlineSchemaNameDefaults() != null) {
builder.inlineSchemaNameDefaults.putAll(copy.getInlineSchemaNameDefaults()); builder.inlineSchemaNameDefaults.putAll(copy.getInlineSchemaNameDefaults());
} }
if (copy.getOpenAPINormalizer() != null) {
builder.openapiNormalizer.putAll(copy.getOpenAPINormalizer());
}
if (copy.getLanguageSpecificPrimitives() != null) { if (copy.getLanguageSpecificPrimitives() != null) {
builder.languageSpecificPrimitives.addAll(copy.getLanguageSpecificPrimitives()); builder.languageSpecificPrimitives.addAll(copy.getLanguageSpecificPrimitives());
} }
@ -557,6 +572,7 @@ public final class GeneratorSettings implements Serializable {
private Map<String, String> schemaMappings; private Map<String, String> schemaMappings;
private Map<String, String> inlineSchemaNameMappings; private Map<String, String> inlineSchemaNameMappings;
private Map<String, String> inlineSchemaNameDefaults; private Map<String, String> inlineSchemaNameDefaults;
private Map<String, String> openapiNormalizer;
private Set<String> languageSpecificPrimitives; private Set<String> languageSpecificPrimitives;
private Map<String, String> reservedWordsMappings; private Map<String, String> reservedWordsMappings;
private Map<String, String> serverVariables; private Map<String, String> serverVariables;
@ -577,6 +593,7 @@ public final class GeneratorSettings implements Serializable {
schemaMappings = new HashMap<>(); schemaMappings = new HashMap<>();
inlineSchemaNameMappings = new HashMap<>(); inlineSchemaNameMappings = new HashMap<>();
inlineSchemaNameDefaults = new HashMap<>(); inlineSchemaNameDefaults = new HashMap<>();
openapiNormalizer = new HashMap<>();
languageSpecificPrimitives = new HashSet<>(); languageSpecificPrimitives = new HashSet<>();
reservedWordsMappings = new HashMap<>(); reservedWordsMappings = new HashMap<>();
serverVariables = new HashMap<>(); serverVariables = new HashMap<>();
@ -897,6 +914,32 @@ public final class GeneratorSettings implements Serializable {
return this; 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. * 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(getSchemaMappings(), that.getSchemaMappings()) &&
Objects.equals(getInlineSchemaNameMappings(), that.getInlineSchemaNameMappings()) && Objects.equals(getInlineSchemaNameMappings(), that.getInlineSchemaNameMappings()) &&
Objects.equals(getInlineSchemaNameDefaults(), that.getInlineSchemaNameDefaults()) && Objects.equals(getInlineSchemaNameDefaults(), that.getInlineSchemaNameDefaults()) &&
Objects.equals(getOpenAPINormalizer(), that.getOpenAPINormalizer()) &&
Objects.equals(getLanguageSpecificPrimitives(), that.getLanguageSpecificPrimitives()) && Objects.equals(getLanguageSpecificPrimitives(), that.getLanguageSpecificPrimitives()) &&
Objects.equals(getReservedWordsMappings(), that.getReservedWordsMappings()) && Objects.equals(getReservedWordsMappings(), that.getReservedWordsMappings()) &&
Objects.equals(getGitHost(), that.getGitHost()) && Objects.equals(getGitHost(), that.getGitHost()) &&
@ -1116,6 +1160,7 @@ public final class GeneratorSettings implements Serializable {
getSchemaMappings(), getSchemaMappings(),
getInlineSchemaNameMappings(), getInlineSchemaNameMappings(),
getInlineSchemaNameDefaults(), getInlineSchemaNameDefaults(),
getOpenAPINormalizer(),
getLanguageSpecificPrimitives(), getLanguageSpecificPrimitives(),
getReservedWordsMappings(), getReservedWordsMappings(),
getGitHost(), getGitHost(),

View File

@ -118,6 +118,7 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
schemaMappings.set(generate.schemaMappings) schemaMappings.set(generate.schemaMappings)
inlineSchemaNameMappings.set(generate.inlineSchemaNameMappings) inlineSchemaNameMappings.set(generate.inlineSchemaNameMappings)
inlineSchemaNameDefaults.set(generate.inlineSchemaNameDefaults) inlineSchemaNameDefaults.set(generate.inlineSchemaNameDefaults)
openapiNormalizer.set(generate.openapiNormalizer)
invokerPackage.set(generate.invokerPackage) invokerPackage.set(generate.invokerPackage)
groupId.set(generate.groupId) groupId.set(generate.groupId)
id.set(generate.id) id.set(generate.id)

View File

@ -162,6 +162,11 @@ open class OpenApiGeneratorGenerateExtension(project: Project) {
*/ */
val inlineSchemaNameDefaults = project.objects.mapProperty<String, String>() 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. * Root package for generated code.
*/ */

View File

@ -250,6 +250,13 @@ open class GenerateTask : DefaultTask() {
@Input @Input
val inlineSchemaNameDefaults = project.objects.mapProperty<String, String>() 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. * 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) { if (typeMappings.isPresent) {
typeMappings.get().forEach { entry -> typeMappings.get().forEach { entry ->
configurator.addTypeMapping(entry.key, entry.value) configurator.addTypeMapping(entry.key, entry.value)

View File

@ -315,6 +315,12 @@ public class CodeGenMojo extends AbstractMojo {
@Parameter(name = "inlineSchemaNameDefaults", property = "openapi.generator.maven.plugin.inlineSchemaNameDefaults") @Parameter(name = "inlineSchemaNameDefaults", property = "openapi.generator.maven.plugin.inlineSchemaNameDefaults")
private List<String> 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 * 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); 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 // Retained for backwards-compatibility with configOptions -> type-mappings
if (typeMappings == null && configOptions.containsKey("type-mappings")) { if (typeMappings == null && configOptions.containsKey("type-mappings")) {
applyTypeMappingsKvp(configOptions.get("type-mappings").toString(), configurator); applyTypeMappingsKvp(configOptions.get("type-mappings").toString(), configurator);
@ -753,6 +765,11 @@ public class CodeGenMojo extends AbstractMojo {
applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator); applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator);
} }
// Apply OpenAPI normalizer rules
if (openapiNormalizer != null && (configOptions == null || !configOptions.containsKey("openapi-normalizer"))) {
applyOpenAPINormalizerKvpList(openapiNormalizer, configurator);
}
// Apply Type Mappings // Apply Type Mappings
if (typeMappings != null && (configOptions == null || !configOptions.containsKey("type-mappings"))) { if (typeMappings != null && (configOptions == null || !configOptions.containsKey("type-mappings"))) {
applyTypeMappingsKvpList(typeMappings, configurator); applyTypeMappingsKvpList(typeMappings, configurator);

View File

@ -147,6 +147,8 @@ public interface CodegenConfig {
Map<String, String> inlineSchemaNameDefault(); Map<String, String> inlineSchemaNameDefault();
Map<String, String> openapiNormalizer();
Map<String, String> apiTemplateFiles(); Map<String, String> apiTemplateFiles();
Map<String, String> modelTemplateFiles(); Map<String, String> modelTemplateFiles();
@ -330,4 +332,7 @@ public interface CodegenConfig {
boolean getUseInlineModelResolver(); boolean getUseInlineModelResolver();
boolean getAddSuffixToDuplicateOperationNicknames(); boolean getAddSuffixToDuplicateOperationNicknames();
boolean getUseOpenAPINormalizer();
} }

View File

@ -167,6 +167,8 @@ public class DefaultCodegen implements CodegenConfig {
protected Map<String, String> inlineSchemaNameMapping = new HashMap<>(); protected Map<String, String> inlineSchemaNameMapping = new HashMap<>();
// a map to store the inline schema naming conventions // a map to store the inline schema naming conventions
protected Map<String, String> inlineSchemaNameDefault = new HashMap<>(); 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 modelPackage = "", apiPackage = "", fileSuffix;
protected String modelNamePrefix = "", modelNameSuffix = ""; protected String modelNamePrefix = "", modelNameSuffix = "";
protected String apiNamePrefix = "", apiNameSuffix = "Api"; protected String apiNamePrefix = "", apiNameSuffix = "Api";
@ -1137,6 +1139,11 @@ public class DefaultCodegen implements CodegenConfig {
return inlineSchemaNameDefault; return inlineSchemaNameDefault;
} }
@Override
public Map<String, String> openapiNormalizer() {
return openapiNormalizer;
}
@Override @Override
public String testPackage() { public String testPackage() {
return testPackage; return testPackage;
@ -7939,6 +7946,9 @@ public class DefaultCodegen implements CodegenConfig {
@Override @Override
public boolean getUseInlineModelResolver() { return true; } public boolean getUseInlineModelResolver() { return true; }
@Override
public boolean getUseOpenAPINormalizer() { return true; }
/* /*
A function to convert yaml or json ingested strings like property names A function to convert yaml or json ingested strings like property names
And convert special characters like newline, tab, carriage return And convert special characters like newline, tab, carriage return

View File

@ -255,6 +255,12 @@ public class DefaultGenerator implements Generator {
config.processOpts(); config.processOpts();
// normalize the spec
if (config.getUseOpenAPINormalizer()) {
OpenAPINormalizer openapiNormalizer = new OpenAPINormalizer(openAPI, config.openapiNormalizer());
openapiNormalizer.normalize();
}
// resolve inline models // resolve inline models
if (config.getUseInlineModelResolver()) { if (config.getUseInlineModelResolver()) {
InlineModelResolver inlineModelResolver = new InlineModelResolver(); InlineModelResolver inlineModelResolver = new InlineModelResolver();

View File

@ -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 =====================
}

View File

@ -71,6 +71,7 @@ public class CodegenConfigurator {
private Map<String, String> schemaMappings = new HashMap<>(); private Map<String, String> schemaMappings = new HashMap<>();
private Map<String, String> inlineSchemaNameMappings = new HashMap<>(); private Map<String, String> inlineSchemaNameMappings = new HashMap<>();
private Map<String, String> inlineSchemaNameDefaults = new HashMap<>(); private Map<String, String> inlineSchemaNameDefaults = new HashMap<>();
private Map<String, String> openapiNormalizer = new HashMap<>();
private Set<String> languageSpecificPrimitives = new HashSet<>(); private Set<String> languageSpecificPrimitives = new HashSet<>();
private Map<String, String> reservedWordsMappings = new HashMap<>(); private Map<String, String> reservedWordsMappings = new HashMap<>();
private Map<String, String> serverVariables = new HashMap<>(); private Map<String, String> serverVariables = new HashMap<>();
@ -123,6 +124,9 @@ public class CodegenConfigurator {
if(generatorSettings.getInlineSchemaNameDefaults() != null) { if(generatorSettings.getInlineSchemaNameDefaults() != null) {
configurator.inlineSchemaNameDefaults.putAll(generatorSettings.getInlineSchemaNameDefaults()); configurator.inlineSchemaNameDefaults.putAll(generatorSettings.getInlineSchemaNameDefaults());
} }
if(generatorSettings.getOpenAPINormalizer() != null) {
configurator.openapiNormalizer.putAll(generatorSettings.getOpenAPINormalizer());
}
if(generatorSettings.getLanguageSpecificPrimitives() != null) { if(generatorSettings.getLanguageSpecificPrimitives() != null) {
configurator.languageSpecificPrimitives.addAll(generatorSettings.getLanguageSpecificPrimitives()); configurator.languageSpecificPrimitives.addAll(generatorSettings.getLanguageSpecificPrimitives());
} }
@ -210,6 +214,12 @@ public class CodegenConfigurator {
return this; 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) { public CodegenConfigurator addInstantiationType(String key, String value) {
this.instantiationTypes.put(key, value); this.instantiationTypes.put(key, value);
generatorSettingsBuilder.withInstantiationType(key, value); generatorSettingsBuilder.withInstantiationType(key, value);
@ -382,6 +392,12 @@ public class CodegenConfigurator {
return this; return this;
} }
public CodegenConfigurator setOpenAPINormalizer(Map<String, String> openapiNormalizer) {
this.openapiNormalizer = openapiNormalizer;
generatorSettingsBuilder.withOpenAPINormalizer(openapiNormalizer);
return this;
}
public CodegenConfigurator setInputSpec(String inputSpec) { public CodegenConfigurator setInputSpec(String inputSpec) {
this.inputSpec = inputSpec; this.inputSpec = inputSpec;
workflowSettingsBuilder.withInputSpec(inputSpec); workflowSettingsBuilder.withInputSpec(inputSpec);
@ -661,6 +677,7 @@ public class CodegenConfigurator {
config.schemaMapping().putAll(generatorSettings.getSchemaMappings()); config.schemaMapping().putAll(generatorSettings.getSchemaMappings());
config.inlineSchemaNameMapping().putAll(generatorSettings.getInlineSchemaNameMappings()); config.inlineSchemaNameMapping().putAll(generatorSettings.getInlineSchemaNameMappings());
config.inlineSchemaNameDefault().putAll(generatorSettings.getInlineSchemaNameDefaults()); config.inlineSchemaNameDefault().putAll(generatorSettings.getInlineSchemaNameDefaults());
config.openapiNormalizer().putAll(generatorSettings.getOpenAPINormalizer());
config.languageSpecificPrimitives().addAll(generatorSettings.getLanguageSpecificPrimitives()); config.languageSpecificPrimitives().addAll(generatorSettings.getLanguageSpecificPrimitives());
config.reservedWordsMappings().putAll(generatorSettings.getReservedWordsMappings()); config.reservedWordsMappings().putAll(generatorSettings.getReservedWordsMappings());
config.additionalProperties().putAll(generatorSettings.getAdditionalProperties()); config.additionalProperties().putAll(generatorSettings.getAdditionalProperties());

View File

@ -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) { public static void applyTypeMappingsKvpList(List<String> typeMappings, CodegenConfigurator configurator) {
for (String propString : typeMappings) { for (String propString : typeMappings) {
applyTypeMappingsKvp(propString, configurator); applyTypeMappingsKvp(propString, configurator);

View File

@ -308,15 +308,15 @@ public class ModelUtils {
/** /**
* Invoke the specified visitor function for every schema that matches mimeType in the OpenAPI document. * 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, * To avoid infinite recursion, referenced schemas are visited only once. When a referenced schema is visited,
* it is added to visitedSchemas. * it is added to visitedSchemas.
* *
* @param openAPI the OpenAPI document that contains schema objects. * @param openAPI the OpenAPI document that contains schema objects.
* @param schema the root schema object to be visited. * @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 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 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) { private static void visitSchema(OpenAPI openAPI, Schema schema, String mimeType, List<String> visitedSchemas, OpenAPISchemaVisitor visitor) {
visitor.visit(schema, mimeType); 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. * 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: * A ObjectSchema differs from a MapSchema in the following way:
* - An ObjectSchema is not extensible, i.e. it has a fixed number of properties. * - 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. * - 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>
* For example, an OpenAPI schema is considered an ObjectSchema in the following scenarios: * For example, an OpenAPI schema is considered an ObjectSchema in the following scenarios:
* <p>
* *
* type: object * type: object
* additionalProperties: false * 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. * 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 * Additional properties means a Schema should support all explicitly defined properties plus any
* undeclared properties. * undeclared properties.
* * <p>
* A MapSchema differs from an ObjectSchema in the following way: * A MapSchema differs from an ObjectSchema in the following way:
* - An ObjectSchema is not extensible, i.e. it has a fixed number of properties. * - 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. * - 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 * Note that isMapSchema returns true for a composed schema (allOf, anyOf, oneOf) that also defines
* additionalproperties. * additionalproperties.
* * <p>
* For example, an OpenAPI schema is considered a MapSchema in the following scenarios: * For example, an OpenAPI schema is considered a MapSchema in the following scenarios:
* <p>
* *
* type: object * type: object
* additionalProperties: true * additionalProperties: true
@ -772,14 +774,14 @@ public class ModelUtils {
/** /**
* Check to see if the schema is a free form object. * 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: * A free form object is an object (i.e. 'type: object' in a OAS document) that:
* 1) Does not define properties, and * 1) Does not define properties, and
* 2) Is not a composed schema (no anyOf, oneOf, allOf), and * 2) Is not a composed schema (no anyOf, oneOf, allOf), and
* 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}. * 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}.
* * <p>
* Examples: * Examples:
* * <p>
* components: * components:
* schemas: * schemas:
* arbitraryObject: * arbitraryObject:
@ -798,7 +800,7 @@ public class ModelUtils {
* The value can be any type except the 'null' value. * The value can be any type except the 'null' value.
* *
* @param openAPI the object that encapsulates the OAS document. * @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 * @return true if it's a free-form object
*/ */
public static boolean isFreeFormObject(OpenAPI openAPI, Schema schema) { 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. * Return the first Schema from a specified OAS 'content' section.
* * <p>
* For example, given the following OAS, this method returns the schema * 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. * for the 'application/json' content type because it is listed first in the OAS.
* * <p>
* responses: * responses:
* '200': * '200':
* content: * content:
@ -1099,8 +1101,8 @@ public class ModelUtils {
/** /**
* Has self reference? * Has self reference?
* *
* @param openAPI OpenAPI spec. * @param openAPI OpenAPI spec.
* @param schema Schema * @param schema Schema
* @param visitedSchemaNames A set of visited schema names * @param visitedSchemaNames A set of visited schema names
* @return boolean true if it has at least one self reference * @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. * Returns the additionalProperties Schema for the specified input schema.
* * <p>
* The additionalProperties keyword is used to control the handling of additional, undeclared * 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. * properties, that is, properties whose names are not listed in the properties keyword.
* The additionalProperties keyword may be either a boolean or an object. * 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: {} * to the boolean value True or setting additionalProperties: {}
* *
* @param openAPI the object that encapsulates the OAS document. * @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 * @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) { public static Schema getAdditionalProperties(OpenAPI openAPI, Schema schema) {
Object addProps = schema.getAdditionalProperties(); Object addProps = schema.getAdditionalProperties();
@ -1380,10 +1382,10 @@ public class ModelUtils {
* that specify a determinator. * that specify a determinator.
* If there are multiple elements in the composed schema and it is not clear * If there are multiple elements in the composed schema and it is not clear
* which one should be the parent, return null. * which one should be the parent, return null.
* * <p>
* For example, given the following OAS spec, the parent of 'Dog' is Animal * For example, given the following OAS spec, the parent of 'Dog' is Animal
* because 'Animal' specifies a discriminator. * because 'Animal' specifies a discriminator.
* * <p>
* animal: * animal:
* type: object * type: object
* discriminator: * discriminator:
@ -1391,6 +1393,7 @@ public class ModelUtils {
* properties: * properties:
* type: string * type: string
* *
* <p>
* dog: * dog:
* allOf: * allOf:
* - $ref: '#/components/schemas/animal' * - $ref: '#/components/schemas/animal'
@ -1418,10 +1421,10 @@ public class ModelUtils {
LOGGER.error("Failed to obtain schema from {}", parentName); LOGGER.error("Failed to obtain schema from {}", parentName);
return "UNKNOWN_PARENT_NAME"; return "UNKNOWN_PARENT_NAME";
} else if (hasOrInheritsDiscriminator(s, allSchemas)) { } else if (hasOrInheritsDiscriminator(s, allSchemas)) {
// discriminator.propertyName is used // discriminator.propertyName is used or x-parent is used
return parentName; return parentName;
} else { } else {
// not a parent since discriminator.propertyName is not set // not a parent since discriminator.propertyName or x-parent is not set
hasAmbiguousParents = true; hasAmbiguousParents = true;
refedWithoutDiscriminator.add(parentName); refedWithoutDiscriminator.add(parentName);
} }
@ -1476,7 +1479,7 @@ public class ModelUtils {
LOGGER.error("Failed to obtain schema from {}", parentName); LOGGER.error("Failed to obtain schema from {}", parentName);
names.add("UNKNOWN_PARENT_NAME"); names.add("UNKNOWN_PARENT_NAME");
} else if (hasOrInheritsDiscriminator(s, allSchemas)) { } else if (hasOrInheritsDiscriminator(s, allSchemas)) {
// discriminator.propertyName is used // discriminator.propertyName is used or x-parent is used
names.add(parentName); names.add(parentName);
if (includeAncestors && s instanceof ComposedSchema) { if (includeAncestors && s instanceof ComposedSchema) {
names.addAll(getAllParentsName((ComposedSchema) s, allSchemas, true)); names.addAll(getAllParentsName((ComposedSchema) s, allSchemas, true));
@ -1501,7 +1504,8 @@ public class ModelUtils {
} }
private static boolean hasOrInheritsDiscriminator(Schema schema, Map<String, Schema> allSchemas) { 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; return true;
} else if (StringUtils.isNotEmpty(schema.get$ref())) { } else if (StringUtils.isNotEmpty(schema.get$ref())) {
String parentName = getSimpleRef(schema.get$ref()); String parentName = getSimpleRef(schema.get$ref());
@ -1523,18 +1527,43 @@ public class ModelUtils {
return false; 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 * 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. * 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 * 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 * schema is a 'oneOf' composed document with at most two children, and one of the children
* is the 'null' type. * is the 'null' type.
* * <p>
* The caller is responsible for resolving schema references before invoking isNullable. * 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 * 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). * 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 was introduced in OAS 3.0.
* The 'nullable' attribute is deprecated in OAS 3.1. In a OAS 3.1 document, the preferred way * 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. * 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, * 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. * and at least one of the elements is the 'null' type.
* * <p>
* The 'null' type is supported in OAS 3.1 and above. * 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' * In the example below, the 'OptionalOrder' can have the null value because the 'null'
* type is one of the elements under 'oneOf'. * type is one of the elements under 'oneOf'.
* * <p>
* OptionalOrder: * OptionalOrder:
* oneOf: * oneOf:
* - type: 'null' * - type: 'null'
@ -1591,13 +1620,13 @@ public class ModelUtils {
/** /**
* isNullType returns true if the input schema is the 'null' type. * 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 * The 'null' type is supported in OAS 3.1 and above. It is not supported
* in OAS 2.0 and OAS 3.0.x. * in OAS 2.0 and OAS 3.0.x.
* * <p>
* For example, the "null" type could be used to specify that a value must * For example, the "null" type could be used to specify that a value must
* either be null or a specified type: * either be null or a specified type:
* * <p>
* OptionalOrder: * OptionalOrder:
* oneOf: * oneOf:
* - type: 'null' * - type: 'null'
@ -1617,6 +1646,7 @@ public class ModelUtils {
* For when a type is not defined on a schema * For when a type is not defined on a schema
* Note: properties, additionalProperties, enums, validations, items, and composed schemas (oneOf/anyOf/allOf) * Note: properties, additionalProperties, enums, validations, items, and composed schemas (oneOf/anyOf/allOf)
* can be defined or omitted on these any type schemas * can be defined or omitted on these any type schemas
*
* @param schema the schema that we are checking * @param schema the schema that we are checking
* @return boolean * @return boolean
*/ */
@ -1713,7 +1743,7 @@ public class ModelUtils {
private static ObjectMapper getRightMapper(String data) { private static ObjectMapper getRightMapper(String data) {
ObjectMapper mapper; ObjectMapper mapper;
if (data.trim().startsWith("{")) { if (data.trim().startsWith("{")) {
mapper = JSON_MAPPER; mapper = JSON_MAPPER;
} else { } else {
mapper = YAML_MAPPER; mapper = YAML_MAPPER;
@ -1725,11 +1755,9 @@ public class ModelUtils {
* Parse and return a JsonNode representation of the input OAS document. * Parse and return a JsonNode representation of the input OAS document.
* *
* @param location the URL of 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.
*
* @throws java.lang.Exception if an error occurs while retrieving the OpenAPI document.
*
* @return A JsonNode representation of the input OAS document. * @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 { public static JsonNode readWithInfo(String location, List<AuthorizationValue> auths) throws Exception {
String data; String data;
@ -1756,14 +1784,13 @@ public class ModelUtils {
/** /**
* Parse the OAS document at the specified location, get the swagger or openapi version * Parse the OAS document at the specified location, get the swagger or openapi version
* as specified in the source document, and return the 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 2.0 documents, return the value of the 'swagger' attribute.
* For OAS 3.x documents, return the value of the 'openapi' 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 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. * @return the version of the OpenAPI document.
*/ */
public static SemVer getOpenApiVersion(OpenAPI openAPI, String location, List<AuthorizationValue> auths) { public static SemVer getOpenApiVersion(OpenAPI openAPI, String location, List<AuthorizationValue> auths) {

View File

@ -56,7 +56,6 @@ import java.util.stream.Collectors;
import static org.testng.Assert.*; import static org.testng.Assert.*;
public class DefaultCodegenTest { public class DefaultCodegenTest {
@Test @Test
@ -4300,4 +4299,29 @@ public class DefaultCodegenTest {
Assert.assertFalse(inlineEnumSchemaProperty.isContainer); Assert.assertFalse(inlineEnumSchemaProperty.isContainer);
Assert.assertFalse(inlineEnumSchemaProperty.isPrimitiveType); 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");
}
} }

View File

@ -1717,4 +1717,39 @@ public class JavaClientCodegenTest {
"localVarQueryParams.addAll(ApiClient.parameterToPairs(\"multi\", \"values\", queryObject.getValues()));" "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 {");
}
} }

View File

@ -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