From 08617249abad3c6ea3f5ed43b99d9ddd52f13d53 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:00:56 +0200 Subject: [PATCH] refactor(enum-utils): move normalization logic for enum handling to EnumUtils --- .../codegen/OpenAPINormalizer.java | 97 ++------------ .../openapitools/codegen/utils/EnumUtils.java | 126 ++++++++++++++++-- 2 files changed, 126 insertions(+), 97 deletions(-) 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 6ee7949ec60e..9a304b529503 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 @@ -41,6 +41,8 @@ import java.util.stream.Collectors; import static org.openapitools.codegen.CodegenConstants.*; +import static org.openapitools.codegen.utils.EnumUtils.ANY_OF; +import static org.openapitools.codegen.utils.EnumUtils.ONE_OF; import static org.openapitools.codegen.utils.ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema; import static org.openapitools.codegen.utils.StringUtils.getUniqueString; @@ -170,6 +172,8 @@ public class OpenAPINormalizer { // ============= end of rules ============= + private static final String ONE_OF_ANY_OF_ENUM_SIMPLIFIED = "Simplified {} with enum sub-schemas to single enum: {} since rule {} was enabled"; + /** * Factory constructor for OpenAPINormalizer. * @@ -1642,96 +1646,19 @@ protected Schema processSimplifyOneOfEnum(Schema schema) { * @return Simplified schema */ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List subSchemas, String composedType) { - Map enumExtensions = new LinkedHashMap<>(); - - if(schema.getTypes() != null && schema.getTypes().size() > 1) { - // we cannot handle enums with multiple types - return schema; + Schema enumSchema = EnumUtils.simplifyComposedSchemaWithEnums(schema, subSchemas, composedType, openAPI); + if (hasComposedSchemaWithEnumsBeenSimplified(schema, composedType)) { + LOGGER.debug(ONE_OF_ANY_OF_ENUM_SIMPLIFIED, composedType, enumSchema, SIMPLIFY_ONEOF_ANYOF_ENUM); } - - if(subSchemas.size() < 2) { - //do not process if there's less than 2 sub-schemas. It will be normalized later, and this prevents - //named enum schemas from being converted to inline enum schemas - return schema; - } - String schemaType = ModelUtils.getType(schema); - - for (Object item : subSchemas) { - if (!(item instanceof Schema)) { - return schema; - } - - Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item); - - // Check if this sub-schema has an enum or const value (OAS 3.1 uses const for single-value enums) - boolean definesEnum = ModelUtils.hasEnum(subSchema); - if (!definesEnum && subSchema.getConst() == null) { - return schema; - } - // If const is present but enum is not, treat const as a single enum value - List subSchemaEnumValues = definesEnum - ? subSchema.getEnum() - : Arrays.asList(subSchema.getConst()); - - // Ensure all sub-schemas have the same type (if type is specified) - if(subSchema.getTypes() != null && subSchema.getTypes().size() > 1) { - // we cannot handle enums with multiple types - return schema; - } - String subSchemaType = ModelUtils.getType(subSchema); - if (subSchemaType != null) { - if (schemaType == null) { - schemaType = subSchemaType; - } else if (!schemaType.equals(subSchema.getType())) { - return schema; - } - } - boolean subSchemaDeprecated = Boolean.TRUE.equals(subSchema.getDeprecated()); - // Add all enum values from this sub-schema to our collection - if(subSchemaEnumValues.size() == 1) { - String description = subSchema.getTitle() == null ? "" : subSchema.getTitle(); - if(subSchema.getDescription() != null) { - if(!description.isEmpty()) { - description += " - "; - } - description += subSchema.getDescription(); - } - enumExtensions.put(subSchemaEnumValues.get(0), new EnumUtils.EnumExtensions(description, subSchemaDeprecated)); - } else { - for(Object e: subSchemaEnumValues) { - enumExtensions.put(e, new EnumUtils.EnumExtensions("", subSchemaDeprecated)); - } - } - } - - return createSimplifiedEnumSchema(schema, enumExtensions, schemaType, composedType); + return enumSchema; } - /** - * Creates a simplified enum schema from collected enum values. - * - * @param originalSchema Original schema to modify - * @param enumExtensions Collected enum values - * @param schemaType Consistent type across sub-schemas - * @param composedType Type of composed schema being simplified - * @return Simplified enum schema - */ - protected Schema createSimplifiedEnumSchema(Schema originalSchema, Map enumExtensions, String schemaType, String composedType) { - // Clear the composed schema type - if ("oneOf".equals(composedType)) { - originalSchema.setOneOf(null); - } else if ("anyOf".equals(composedType)) { - originalSchema.setAnyOf(null); - } - - EnumUtils.createSimplifiedEnumSchema(originalSchema, enumExtensions, schemaType); - - LOGGER.debug("Simplified {} with enum sub-schemas to single enum: {}", composedType, originalSchema); - - return originalSchema; + private boolean hasComposedSchemaWithEnumsBeenSimplified(Schema schema, String composedType) { + boolean oneOfSimplified = ONE_OF.equals(composedType) && schema.getOneOf() == null; + boolean anyOfSimplified = ANY_OF.equals(composedType) && schema.getAnyOf() == null; + return oneOfSimplified || anyOfSimplified; } - /** * If the schema is oneOf and the sub-schemas is null, set `nullable: true` * instead. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/EnumUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/EnumUtils.java index d0d8ab6eb7d2..9287c5c86408 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/EnumUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/EnumUtils.java @@ -1,10 +1,9 @@ package org.openapitools.codegen.utils; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.Schema; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import static org.openapitools.codegen.CodegenConstants.X_ENUM_DEPRECATED; @@ -12,30 +11,133 @@ public class EnumUtils { - public static Schema createSimplifiedEnumSchema(Schema originalSchema, + public static final String ONE_OF = "oneOf"; + public static final String ANY_OF = "anyOf"; + + /** + * Simplifies a composed schema (oneOf/anyOf) where all sub-schemas are enums + * to a single enum schema containing all the values. + * + * @param schema Schema to modify + * @param subSchemas List of sub-schemas to check + * @param composedType Type of composed schema ("oneOf" or "anyOf") + * @param openAPI The OpenAPI object to resolve references + * @return Simplified schema + */ + public static Schema simplifyComposedSchemaWithEnums(Schema schema, + List subSchemas, + String composedType, + OpenAPI openAPI) { + Map enumExtensions = new LinkedHashMap<>(); + + if (schema.getTypes() != null && schema.getTypes().size() > 1) { + // we cannot handle enums with multiple types + return schema; + } + + if (subSchemas.size() < 2) { + //do not process if there's less than 2 sub-schemas. It will be normalized later, and this prevents + //named enum schemas from being converted to inline enum schemas + return schema; + } + String schemaType = ModelUtils.getType(schema); + + for (Object item : subSchemas) { + if (!(item instanceof Schema)) { + return schema; + } + + Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item); + + // Check if this sub-schema has an enum or const value (OAS 3.1 uses const for single-value enums) + boolean definesEnum = ModelUtils.hasEnum(subSchema); + if (!definesEnum && subSchema.getConst() == null) { + return schema; + } + // If const is present but enum is not, treat const as a single enum value + List subSchemaEnumValues = definesEnum + ? subSchema.getEnum() + : Arrays.asList(subSchema.getConst()); + + // Ensure all sub-schemas have the same type (if type is specified) + if (subSchema.getTypes() != null && subSchema.getTypes().size() > 1) { + // we cannot handle enums with multiple types + return schema; + } + String subSchemaType = ModelUtils.getType(subSchema); + if (subSchemaType != null) { + if (schemaType == null) { + schemaType = subSchemaType; + } else if (!schemaType.equals(subSchema.getType())) { + return schema; + } + } + enumExtensions.putAll(getEnumExtensions(subSchema, subSchemaEnumValues)); + } + + // Clear the composed schema type since we were able to successfully process all subSchemas + if (ONE_OF.equals(composedType)) { + schema.setOneOf(null); + } else if (ANY_OF.equals(composedType)) { + schema.setAnyOf(null); + } + return createSimplifiedEnumSchema(schema, enumExtensions, schemaType); + } + + private static Map getEnumExtensions(Schema schema, + List schemaEnumValues) { + Map enumExtensions = new LinkedHashMap<>(); + boolean schemaDeprecated = Boolean.TRUE.equals(schema.getDeprecated()); + // Add all enum values from this sub-schema to our collection + if (schemaEnumValues.size() == 1) { + String description = schema.getTitle() == null ? "" : schema.getTitle(); + if (schema.getDescription() != null) { + if (!description.isEmpty()) { + description += " - "; + } + description += schema.getDescription(); + } + enumExtensions.put(schemaEnumValues.get(0), new EnumExtensions(description, schemaDeprecated)); + } else { + for (Object enumValue : schemaEnumValues) { + enumExtensions.put(enumValue, new EnumExtensions("", schemaDeprecated)); + } + } + return enumExtensions; + } + + /** + * Creates a simplified enum schema from collected enum values. + * + * @param schema schema to modify + * @param enums Collected enum values + * @param schemaType Consistent type across sub-schemas + * @return Simplified enum schema + */ + private static Schema createSimplifiedEnumSchema(Schema schema, Map enums, String schemaType) { - if (ModelUtils.getType(originalSchema) == null && schemaType != null) { + if (ModelUtils.getType(schema) == null && schemaType != null) { //if type was specified in subschemas, keep it in the main schema - ModelUtils.setType(originalSchema, schemaType); + ModelUtils.setType(schema, schemaType); } - originalSchema.setEnum(new ArrayList<>(enums.keySet())); + schema.setEnum(new ArrayList<>(enums.keySet())); List enumDescriptions = enums.values().stream().map(EnumExtensions::getDescription).collect(Collectors.toList()); List enumDeprecations = enums.values().stream().map(EnumExtensions::isDeprecated).collect(Collectors.toList()); - if(enumDescriptions.stream().anyMatch(e -> !e.isEmpty())) { + if (enumDescriptions.stream().anyMatch(e -> !e.isEmpty())) { //set x-enum-descriptions only if there's at least one non-empty description - originalSchema.addExtension(X_ENUM_DESCRIPTIONS, new ArrayList<>(enumDescriptions)); + schema.addExtension(X_ENUM_DESCRIPTIONS, new ArrayList<>(enumDescriptions)); } if (enumDeprecations.stream().anyMatch(Boolean.TRUE::equals)) { // preserve per-value deprecated flags from OAS 3.1 oneOf/anyOf + const sub-schemas - originalSchema.addExtension(X_ENUM_DEPRECATED, new ArrayList<>(enumDeprecations)); + schema.addExtension(X_ENUM_DEPRECATED, new ArrayList<>(enumDeprecations)); } - return originalSchema; + return schema; } - public static class EnumExtensions { + private static class EnumExtensions { private final String description; private final boolean deprecated;