[openapi-normalizer] enable some rules by default (#15501)

* enable certain normalizer rule by default

* minor fix

* better error handling

* update doc
This commit is contained in:
William Cheng 2023-05-16 14:04:52 +08:00 committed by GitHub
parent d02679b5b6
commit c5a1dbecfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 152 additions and 85 deletions

View File

@ -454,7 +454,14 @@ Note: Only arrayItemSuffix, mapItemSuffix are supported at the moment. `SKIP_SCH
## 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:
OpenAPI Normalizer transforms the input OpenAPI doc/spec (which may not perfectly conform to the specification) to make it workable with OpenAPI Generator. A few rules are switched on by default since 7.0.0 release:
- SIMPLIFY_ONEOF_ANYOF
- SIMPLIFY_BOOLEAN_ENUM
(One can use `DISABLE_ALL=true` to disable all the rules)
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).

View File

@ -254,9 +254,14 @@ public class DefaultGenerator implements Generator {
config.processOpts();
// normalize the spec
if (config.getUseOpenAPINormalizer()) {
OpenAPINormalizer openapiNormalizer = new OpenAPINormalizer(openAPI, config.openapiNormalizer());
openapiNormalizer.normalize();
try {
if (config.getUseOpenAPINormalizer()) {
OpenAPINormalizer openapiNormalizer = new OpenAPINormalizer(openAPI, config.openapiNormalizer());
openapiNormalizer.normalize();
}
} catch (Exception e) {
LOGGER.error("An exception occurred in OpenAPI Normalizer. Please report the issue via https://github.com/openapitools/openapi-generator/issues/new/: ");
e.printStackTrace();
}
// resolve inline models

View File

@ -17,6 +17,7 @@
package org.openapitools.codegen;
import com.sun.org.apache.xerces.internal.impl.dv.xs.AbstractDateTimeDV;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.media.*;
@ -34,44 +35,46 @@ import java.util.stream.Collectors;
public class OpenAPINormalizer {
private OpenAPI openAPI;
private Map<String, String> rules = new HashMap<>();
private Map<String, String> inputRules = new HashMap<>();
private Map<String, Boolean> rules = new HashMap<>();
final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class);
Set<String> ruleNames = new TreeSet<>();
Set<String> rulesDefaultToTrue = new TreeSet<>();
// ============= a list of rules =============
// when set to true, all rules (true or false) are enabled
final String ALL = "ALL";
final String ENABLE_ALL = "ENABLE_ALL";
boolean enableAll;
// when set to true, all rules (true or false) are disabled
final String DISABLE_ALL = "DISABLE_ALL";
boolean disableAll;
// 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;
// when set to true, only keep the first tag in operation if there are more than one tag defined.
final String KEEP_ONLY_FIRST_TAG_IN_OPERATION = "KEEP_ONLY_FIRST_TAG_IN_OPERATION";
boolean enableKeepOnlyFirstTagInOperation;
// when set to true, complex composed schemas (a mix of oneOf/anyOf/anyOf and properties) with
// oneOf/anyOf containing only `required` and no properties (these are properties inter-dependency rules)
// are removed as most generators cannot handle such case at the moment
final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY";
boolean removeAnyOfOneOfAndKeepPropertiesOnly;
// when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified
// to just string
final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING";
boolean simplifyAnyOfStringAndEnumString;
// when set to true, oneOf/anyOf schema with only one sub-schema is simplified to just the sub-schema
// and if sub-schema contains "null", remove it and set nullable to true instead
// and if sub-schema contains enum of "null", remove it and set nullable to true instead
final String SIMPLIFY_ONEOF_ANYOF = "SIMPLIFY_ONEOF_ANYOF";
boolean simplifyOneOfAnyOf;
// when set to true, boolean enum will be converted to just boolean
final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM";
boolean simplifyBooleanEnum;
// when set to a string value, tags in all operations will be reset to the string value provided
final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS";
@ -80,75 +83,86 @@ public class OpenAPINormalizer {
// when set to true, auto fix integer with maximum value 4294967295 (2^32-1) or long with 18446744073709551615 (2^64-1)
// by adding x-unsigned to the schema
final String ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE = "ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE";
boolean addUnsignedToIntegerWithInvalidMaxValue;
// when set to true, refactor schema with allOf and properties in the same level to a schema with allOf only and
// the allOf contains a new schema containing the properties in the top level
final String REFACTOR_ALLOF_WITH_PROPERTIES_ONLY = "REFACTOR_ALLOF_WITH_PROPERTIES_ONLY";
boolean refactorAllOfWithPropertiesOnly;
// ============= end of rules =============
/**
* Initializes OpenAPI Normalizer with a set of rules
*
* @param openAPI OpenAPI
* @param rules a map of rules
* @param openAPI OpenAPI
* @param inputRules a map of rules
*/
public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> rules) {
public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
this.openAPI = openAPI;
this.rules = rules;
parseRules(rules);
this.inputRules = inputRules;
if (Boolean.parseBoolean(inputRules.get(DISABLE_ALL))) {
LOGGER.info("Disabled all rules in OpenAPI Normalizer (DISABLE_ALL=true)");
this.disableAll = true;
return; // skip the rest
}
// a set of ruleNames
ruleNames.add(REF_AS_PARENT_IN_ALLOF);
ruleNames.add(REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY);
ruleNames.add(SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING);
ruleNames.add(SIMPLIFY_ONEOF_ANYOF);
ruleNames.add(SIMPLIFY_BOOLEAN_ENUM);
ruleNames.add(KEEP_ONLY_FIRST_TAG_IN_OPERATION);
ruleNames.add(SET_TAGS_FOR_ALL_OPERATIONS);
ruleNames.add(ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE);
ruleNames.add(REFACTOR_ALLOF_WITH_PROPERTIES_ONLY);
// rules that are default to true
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
rules.put(SIMPLIFY_BOOLEAN_ENUM, true);
processRules(inputRules);
}
/**
* Parses the rules.
* Get the rule.
*
* @param rules a map of rules
* @param ruleName the name of the rule
* @return true if the rule is set
*/
public void parseRules(Map<String, String> rules) {
if (rules == null) {
return;
public boolean getRule(String ruleName) {
if (!rules.containsKey(ruleName)) {
return false;
}
return rules.get(ruleName);
}
if ("true".equalsIgnoreCase(rules.get(ALL))) {
/**
* Process the rules.
*
* @param inputRules a map of rules
*/
public void processRules(Map<String, String> inputRules) {
if (Boolean.TRUE.equals(rules.get("enableAll"))) {
enableAll = true;
}
if (enableAll || "true".equalsIgnoreCase(rules.get(REF_AS_PARENT_IN_ALLOF))) {
enableRefAsParentInAllOf = true;
// loop through all the rules
for (Map.Entry<String, String> rule : inputRules.entrySet()) {
LOGGER.info("processing rule {} => {}", rule.getKey(), rule.getValue());
if (!ruleNames.contains(rule.getKey())) { // invalid rule name
LOGGER.warn("Invalid openapi-normalizer rule name: ", rule.getKey());
} else if (enableAll) {
rules.put(rule.getKey(), true); // set rule
} else {
rules.put(rule.getKey(), Boolean.parseBoolean(rule.getValue()));
}
}
if (enableAll || "true".equalsIgnoreCase(rules.get(KEEP_ONLY_FIRST_TAG_IN_OPERATION))) {
enableKeepOnlyFirstTagInOperation = true;
}
if (enableAll || "true".equalsIgnoreCase(rules.get(REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY))) {
removeAnyOfOneOfAndKeepPropertiesOnly = true;
}
if (enableAll || "true".equalsIgnoreCase(rules.get(SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING))) {
simplifyAnyOfStringAndEnumString = true;
}
if (enableAll || "true".equalsIgnoreCase(rules.get(SIMPLIFY_ONEOF_ANYOF))) {
simplifyOneOfAnyOf = true;
}
if (enableAll || "true".equalsIgnoreCase(rules.get(SIMPLIFY_BOOLEAN_ENUM))) {
simplifyBooleanEnum = true;
}
if (StringUtils.isNotEmpty(rules.get(SET_TAGS_FOR_ALL_OPERATIONS))) {
setTagsForAllOperations = rules.get(SET_TAGS_FOR_ALL_OPERATIONS);
}
if (enableAll || "true".equalsIgnoreCase(rules.get(ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE))) {
addUnsignedToIntegerWithInvalidMaxValue = true;
}
if (enableAll || "true".equalsIgnoreCase(rules.get(REFACTOR_ALLOF_WITH_PROPERTIES_ONLY))) {
refactorAllOfWithPropertiesOnly = true;
// non-boolean rule(s)
setTagsForAllOperations = inputRules.get(SET_TAGS_FOR_ALL_OPERATIONS);
if (setTagsForAllOperations != null) {
rules.put(SET_TAGS_FOR_ALL_OPERATIONS, true);
}
}
@ -157,7 +171,7 @@ public class OpenAPINormalizer {
* the specification.
*/
void normalize() {
if (rules == null || rules.isEmpty()) {
if (rules == null || rules.isEmpty() || disableAll) {
return;
}
@ -511,7 +525,7 @@ public class OpenAPINormalizer {
* @param schema Schema
*/
private void processUseAllOfRefAsParent(Schema schema) {
if (!enableRefAsParentInAllOf && !enableAll) {
if (!getRule(REF_AS_PARENT_IN_ALLOF)) {
return;
}
@ -554,7 +568,7 @@ public class OpenAPINormalizer {
* @param operation Operation
*/
private void processKeepOnlyFirstTagInOperation(Operation operation) {
if (!enableKeepOnlyFirstTagInOperation) {
if (!getRule(KEEP_ONLY_FIRST_TAG_IN_OPERATION)) {
return;
}
@ -587,7 +601,7 @@ public class OpenAPINormalizer {
* @param schema Schema
*/
private void processRemoveAnyOfOneOfAndKeepPropertiesOnly(Schema schema) {
if (!removeAnyOfOneOfAndKeepPropertiesOnly && !enableAll) {
if (!getRule(REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY)) {
return;
}
@ -609,7 +623,7 @@ public class OpenAPINormalizer {
* @return Schema
*/
private Schema processSimplifyAnyOfStringAndEnumString(Schema schema) {
if (!simplifyAnyOfStringAndEnumString && !enableAll) {
if (!getRule(SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING)) {
return schema;
}
@ -663,7 +677,7 @@ public class OpenAPINormalizer {
* @return Schema
*/
private Schema processSimplifyOneOf(Schema schema) {
if (!simplifyOneOfAnyOf && !enableAll) {
if (!getRule(SIMPLIFY_ONEOF_ANYOF)) {
return schema;
}
@ -709,7 +723,7 @@ public class OpenAPINormalizer {
* @return Schema
*/
private Schema processSimplifyAnyOf(Schema schema) {
if (!simplifyOneOfAnyOf && !enableAll) {
if (!getRule(SIMPLIFY_ONEOF_ANYOF)) {
return schema;
}
@ -755,7 +769,7 @@ public class OpenAPINormalizer {
* @return Schema
*/
private void processSimplifyBooleanEnum(Schema schema) {
if (!simplifyBooleanEnum && !enableAll) {
if (!getRule(SIMPLIFY_BOOLEAN_ENUM)) {
return;
}
@ -775,7 +789,7 @@ public class OpenAPINormalizer {
* @return Schema
*/
private void processAddUnsignedToIntegerWithInvalidMaxValue(Schema schema) {
if (!addUnsignedToIntegerWithInvalidMaxValue && !enableAll) {
if (!getRule(ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE)) {
return;
}
@ -802,7 +816,7 @@ public class OpenAPINormalizer {
* @return Schema
*/
private Schema processRefactorAllOfWithPropertiesOnly(Schema schema) {
if (!refactorAllOfWithPropertiesOnly && !enableAll) {
if (!getRule(REFACTOR_ALLOF_WITH_PROPERTIES_ONLY)) {
return schema;
}

View File

@ -268,7 +268,7 @@ public class OpenAPINormalizerTest {
}
@Test
public void testOpenAPINormalizerConvertEnumNullToNullable_test() {
public void testOpenAPINormalizerConvertEnumNullToNullable() {
// to test the rule SIMPLIFY_ONEOF_ANYOF, which now also covers CONVERT_ENUM_NULL_TO_NULLABLE (removed)
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/convertEnumNullToNullable_test.yaml");
@ -286,6 +286,47 @@ public class OpenAPINormalizerTest {
assertTrue(schema3.getNullable());
}
@Test
public void testOpenAPINormalizerDefaultRules() {
// to test the rule SIMPLIFY_ONEOF_ANYOF, which now also covers CONVERT_ENUM_NULL_TO_NULLABLE (removed)
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/convertEnumNullToNullable_test.yaml");
Schema schema = openAPI.getComponents().getSchemas().get("AnyOfTest");
assertEquals(schema.getAnyOf().size(), 3);
assertNull(schema.getNullable());
Map<String, String> options = new HashMap<>();
// SIMPLIFY_ONEOF_ANYOF is switched on by default as part of v7.0.0 release
//options.put("SIMPLIFY_ONEOF_ANYOF", "true");
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
openAPINormalizer.normalize();
Schema schema3 = openAPI.getComponents().getSchemas().get("AnyOfTest");
assertEquals(schema3.getAnyOf().size(), 2);
assertTrue(schema3.getNullable());
}
@Test
public void testOpenAPINormalizerDisableAll() {
// to test the rule SIMPLIFY_ONEOF_ANYOF, which now also covers CONVERT_ENUM_NULL_TO_NULLABLE (removed)
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/convertEnumNullToNullable_test.yaml");
// before test
Schema schema = openAPI.getComponents().getSchemas().get("AnyOfTest");
assertEquals(schema.getAnyOf().size(), 3);
assertNull(schema.getNullable());
Map<String, String> options = new HashMap<>();
options.put("DISABLE_ALL", "true");
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
openAPINormalizer.normalize();
// checks should be the same after test
Schema schema3 = openAPI.getComponents().getSchemas().get("AnyOfTest");
assertEquals(schema3.getAnyOf().size(), 3);
assertNull(schema3.getNullable());
}
@Test
public void testOpenAPINormalizerRefactorAllOfWithPropertiesOnly() {
// to test the rule REFACTOR_ALLOF_WITH_PROPERTIES_ONLY

View File

@ -221,7 +221,7 @@ export type PetsFilteredPatchRequestPetTypeEnum = typeof PetsFilteredPatchReques
* @type PetsPatchRequest
* @export
*/
export type PetsPatchRequest = Cat | Dog | any;
export type PetsPatchRequest = Cat | Dog;
/**
@ -265,11 +265,11 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
},
/**
*
* @param {PetsFilteredPatchRequest} [petsFilteredPatchRequest]
* @param {PetsFilteredPatchRequest | null} [petsFilteredPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
petsFilteredPatch: async (petsFilteredPatchRequest?: PetsFilteredPatchRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
petsFilteredPatch: async (petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/pets-filtered`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -298,11 +298,11 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
},
/**
*
* @param {PetsPatchRequest} [petsPatchRequest]
* @param {PetsPatchRequest | null} [petsPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
petsPatch: async (petsPatchRequest?: PetsPatchRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
petsPatch: async (petsPatchRequest?: PetsPatchRequest | null, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/pets`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -351,21 +351,21 @@ export const DefaultApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {PetsFilteredPatchRequest} [petsFilteredPatchRequest]
* @param {PetsFilteredPatchRequest | null} [petsFilteredPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.petsFilteredPatch(petsFilteredPatchRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {PetsPatchRequest} [petsPatchRequest]
* @param {PetsPatchRequest | null} [petsPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async petsPatch(petsPatchRequest?: PetsPatchRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async petsPatch(petsPatchRequest?: PetsPatchRequest | null, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.petsPatch(petsPatchRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -390,20 +390,20 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
},
/**
*
* @param {PetsFilteredPatchRequest} [petsFilteredPatchRequest]
* @param {PetsFilteredPatchRequest | null} [petsFilteredPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest, options?: any): AxiosPromise<void> {
petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options?: any): AxiosPromise<void> {
return localVarFp.petsFilteredPatch(petsFilteredPatchRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PetsPatchRequest} [petsPatchRequest]
* @param {PetsPatchRequest | null} [petsPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
petsPatch(petsPatchRequest?: PetsPatchRequest, options?: any): AxiosPromise<void> {
petsPatch(petsPatchRequest?: PetsPatchRequest | null, options?: any): AxiosPromise<void> {
return localVarFp.petsPatch(petsPatchRequest, options).then((request) => request(axios, basePath));
},
};
@ -429,23 +429,23 @@ export class DefaultApi extends BaseAPI {
/**
*
* @param {PetsFilteredPatchRequest} [petsFilteredPatchRequest]
* @param {PetsFilteredPatchRequest | null} [petsFilteredPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest, options?: AxiosRequestConfig) {
public petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).petsFilteredPatch(petsFilteredPatchRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PetsPatchRequest} [petsPatchRequest]
* @param {PetsPatchRequest | null} [petsPatchRequest]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public petsPatch(petsPatchRequest?: PetsPatchRequest, options?: AxiosRequestConfig) {
public petsPatch(petsPatchRequest?: PetsPatchRequest | null, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).petsPatch(petsPatchRequest, options).then((request) => request(this.axios, this.basePath));
}
}