diff --git a/docs/customization.md b/docs/customization.md index e79cb471e63..a31c57b8017 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -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). 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 15cc5ab410e..84adb60425c 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 @@ -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 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 index 0b3c0f245c2..5374156740b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -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 rules = new HashMap<>(); + private Map inputRules = new HashMap<>(); + private Map rules = new HashMap<>(); final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class); + Set ruleNames = new TreeSet<>(); + Set 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 rules) { + public OpenAPINormalizer(OpenAPI openAPI, Map 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 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 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 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; } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 10f3d620b8e..a51c30f2ce1 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -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 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 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 diff --git a/samples/client/petstore/typescript-axios/builds/composed-schemas/api.ts b/samples/client/petstore/typescript-axios/builds/composed-schemas/api.ts index 8a41e7028cd..bcb2ac3660d 100644 --- a/samples/client/petstore/typescript-axios/builds/composed-schemas/api.ts +++ b/samples/client/petstore/typescript-axios/builds/composed-schemas/api.ts @@ -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 => { + petsFilteredPatch: async (petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options: AxiosRequestConfig = {}): Promise => { 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 => { + petsPatch: async (petsPatchRequest?: PetsPatchRequest | null, options: AxiosRequestConfig = {}): Promise => { 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> { + async petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { 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> { + async petsPatch(petsPatchRequest?: PetsPatchRequest | null, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { 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 { + petsFilteredPatch(petsFilteredPatchRequest?: PetsFilteredPatchRequest | null, options?: any): AxiosPromise { 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 { + petsPatch(petsPatchRequest?: PetsPatchRequest | null, options?: any): AxiosPromise { 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)); } }