From d2366f6e349f422a1076db1d342cd712b1906b38 Mon Sep 17 00:00:00 2001 From: Anthony TODISCO Date: Wed, 19 Nov 2025 09:52:36 +0100 Subject: [PATCH 1/6] fix(protobuf-codegen): Fix protobuf import path with discriminator This PR fixes a critical bug in the protobuf schema generator where models using discriminators with llOf composition were generating invalid import paths when child schemas contained references to other models. --- .../languages/ProtobufSchemaCodegen.java | 13 ++++- .../protobuf/ProtobufSchemaCodegenTest.java | 37 ++++++++++++ .../3_0/allOf_composition_discriminator.yaml | 2 + ...on_discriminator_multiple_inheritance.yaml | 58 +++++++++++++++++++ .../3_0/protobuf-schema/animal.proto | 31 ++++++++++ .../resources/3_0/protobuf-schema/pet.proto | 4 ++ 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator_multiple_inheritance.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java index 314b9a2f359e..efb87d6d0aa0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java @@ -139,7 +139,7 @@ public ProtobufSchemaCodegen() { apiTemplateFiles.put("api.mustache", ".proto"); embeddedTemplateDir = templateDir = "protobuf-schema"; hideGenerationTimestamp = Boolean.TRUE; - modelPackage = "models"; + super.setModelPackage("models"); apiPackage = "services"; defaultIncludes = new HashSet<>( @@ -749,9 +749,11 @@ public void addImport(Map objs, CodegenModel cm, String impor String modelFileName = this.toModelFilename(importValue); boolean skipImport = isImportAlreadyPresentInModel(objs, cm, modelFileName); if (!skipImport) { - this.addImport(cm, importValue); + // Use toModelImport to get the correct import path with model package prefix + String processedImport = this.toModelImport(importValue); + this.addImport(cm, processedImport); Map importItem = new HashMap<>(); - importItem.put(IMPORT, modelFileName); + importItem.put(IMPORT, processedImport); objs.get(cm.getName()).getImports().add(importItem); } } @@ -1041,6 +1043,11 @@ public String toModelImport(String name) { if ("".equals(modelPackage())) { return name; } else { + // Check if the name already starts with the model package path to avoid duplication + String modelPath = modelPackage() + "/"; + if (name.startsWith(modelPath)) { + return name; + } return modelPackage() + "/" + underscore(name); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java index 573abc3124bc..0c421f67ea6d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java @@ -296,4 +296,41 @@ public void unspecifiedEnumValuesIgnoredIfAlreadyPresent() { Assert.assertEquals(enumVars1.get(1).get("value"), "FOO"); Assert.assertEquals(enumVars1.get(1).get("isString"), false); } + + @Test(description = "Support multiple level of inheritance for discriminator - ensures properties from indirect children are included") + public void testCodeGenWithAllOfDiscriminatorMultipleLevels() throws IOException { + // set line break to \n across all platforms + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/allOf_composition_discriminator_multiple_inheritance.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + TestUtils.ensureContainsFile(files, output, "models/animal.proto"); + Path path = Paths.get(output + "/models/animal.proto"); + + // Verify the generated file contains all expected properties from multi-level inheritance + String generatedContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + + // Properties from discriminated classes should be included: + // From Dog (direct child): bark + Assert.assertTrue(generatedContent.contains("string bark"), "Animal should contain 'bark' property from Dog"); + // From Feline (direct child): name, furColor + Assert.assertTrue(generatedContent.contains("string name"), "Animal should contain 'name' property from Feline"); + Assert.assertTrue(generatedContent.contains("string fur_color"), "Animal should contain 'fur_color' property from Feline"); + // From Cat (indirect child through Feline): isIndoor, careDetails + Assert.assertTrue(generatedContent.contains("bool is_indoor"), "Animal should contain 'is_indoor' property from Cat (indirect child)"); + Assert.assertTrue(generatedContent.contains("CareDetails care_details"), "Animal should contain 'care_details' property from Cat (indirect child)"); + + assertFileEquals(path, Paths.get("src/test/resources/3_0/protobuf-schema/animal.proto")); + + output.deleteOnExit(); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml index efeaa5fd02e9..e6f39388eb48 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml @@ -48,6 +48,8 @@ components: properties: name: type: string + characteristics: + $ref: '#/components/schemas/Characteristics' Reptile: allOf: - $ref: '#/components/schemas/Pet' diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator_multiple_inheritance.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator_multiple_inheritance.yaml new file mode 100644 index 000000000000..7746b66f8629 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator_multiple_inheritance.yaml @@ -0,0 +1,58 @@ +openapi: 3.0.3 +info: + title: OAI Specification example for Polymorphism + version: 1.0.0 +paths: + /status: + get: + responses: + '201': + description: desc + +components: + schemas: + Animal: + type: object + required: + - petType + properties: + petType: + type: string + discriminator: + propertyName: petType + mapping: + dog: '#/components/schemas/Dog' + cat: '#/components/schemas/Cat' + Feline: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + name: + type: string + furColor: + type: string + Cat: + allOf: + - $ref: '#/components/schemas/Feline' + - type: object + properties: + isIndoor: + type: boolean + careDetails: + $ref: '#/components/schemas/CareDetails' + Dog: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + bark: + type: string + CareDetails: + type: object + properties: + veterinarian: + type: string + lastVetVisit: + type: string + format: date \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto new file mode 100644 index 000000000000..922a4db42668 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto @@ -0,0 +1,31 @@ +/* + OAI Specification example for Polymorphism + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + + Generated by OpenAPI Generator: https://openapi-generator.tech +*/ + +syntax = "proto3"; + +package openapitools; + +import public "models/care_details.proto"; + +message Animal { + + string pet_type = 482112090; + + string bark = 3016376; + + string name = 3373707; + + string fur_color = 478695002; + + bool is_indoor = 183319801; + + CareDetails care_details = 176721135; + +} diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto index 38a86bc063f6..cad7d44d1fd8 100644 --- a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto @@ -12,12 +12,16 @@ syntax = "proto3"; package openapitools; +import public "models/characteristics.proto"; + message Pet { string pet_type = 482112090; string name = 3373707; + Characteristics characteristics = 455429578; + string bark = 3016376; bool loves_rocks = 465093427; From 33fa3beb01e3e33f7517a377a75d0c98801168bd Mon Sep 17 00:00:00 2001 From: Anthony TODISCO Date: Wed, 26 Nov 2025 16:43:44 +0100 Subject: [PATCH 2/6] fix: Add missing element in OpenAPI discriminator test case --- .../test/resources/3_0/allOf_composition_discriminator.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml index e6f39388eb48..a6c500626e1f 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml @@ -103,3 +103,8 @@ components: C: allOf: - $ref: '#/components/schemas/B' + Characteristics: + type: object + properties: + canHunt: + type: boolean \ No newline at end of file From bac320d59f95935217c41c5af5a9ca5d55423f4c Mon Sep 17 00:00:00 2001 From: Anthony TODISCO Date: Mon, 8 Dec 2025 16:04:25 +0100 Subject: [PATCH 3/6] feat(protobuf-generator): Improve protobuf generation * Improve management of inheritance * Improve management of discriminator * Allow to separate inline enums in external files * Add unit test --- .../codegen/CodegenDiscriminator.java | 4 + .../languages/ProtobufSchemaCodegen.java | 623 +++++++++++++++++- .../resources/protobuf-schema/model.mustache | 2 + .../protobuf/ProtobufSchemaCodegenTest.java | 302 ++++++++- .../3_0/allOf_composition_discriminator.yaml | 2 +- .../3_0/protobuf-schema/animal.proto | 4 +- .../3_0/protobuf-schema/extracted_enum.yaml | 32 + .../protobuf-schema/model_imported_once.yaml | 89 +++ .../resources/3_0/protobuf-schema/pet.proto | 2 + 9 files changed, 1025 insertions(+), 35 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/protobuf-schema/model_imported_once.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenDiscriminator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenDiscriminator.java index d995668041a5..753c63e3fc8e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenDiscriminator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenDiscriminator.java @@ -88,6 +88,10 @@ public MappedModel(String mappingName, String modelName, boolean explicitMapping this.explicitMapping = explicitMapping; } + public boolean isExplicitMapping() { + return explicitMapping; + } + public MappedModel(String mappingName, String modelName) { this(mappingName, modelName, false); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java index b9c5bfde0028..d89c4a3d0314 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java @@ -24,6 +24,7 @@ import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.*; +import org.openapitools.codegen.CodegenDiscriminator.MappedModel; import org.openapitools.codegen.exceptions.ProtoBufIndexComputationException; import org.openapitools.codegen.meta.GeneratorMetadata; import org.openapitools.codegen.meta.Stability; @@ -76,6 +77,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf public static final String SUPPORT_MULTIPLE_RESPONSES = "supportMultipleResponses"; + public static final String EXTRACT_ENUMS_TO_SEPARATE_FILES = "extractEnumsToSeparateFiles"; + private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class); @Setter protected String packageName = "openapitools"; @@ -100,6 +103,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf private boolean supportMultipleResponses = true; + private boolean extractEnumsToSeparateFiles = false; + @Override public CodegenType getTag() { return CodegenType.SCHEMA; @@ -212,6 +217,7 @@ public ProtobufSchemaCodegen() { addSwitch(WRAP_COMPLEX_TYPE, "Generate Additional message for complex type", wrapComplexType); addSwitch(USE_SIMPLIFIED_ENUM_NAMES, "Use a simple name for enums", useSimplifiedEnumNames); addSwitch(SUPPORT_MULTIPLE_RESPONSES, "Support multiple responses", supportMultipleResponses); + addSwitch(EXTRACT_ENUMS_TO_SEPARATE_FILES, "Extract enums to separate protobuf files and import them in models", extractEnumsToSeparateFiles); addOption(AGGREGATE_MODELS_NAME, "Aggregated model filename. If set, all generated models will be combined into this single file.", null); addOption(CUSTOM_OPTIONS_API, "Custom options for the api files.", null); addOption(CUSTOM_OPTIONS_MODEL, "Custom options for the model files.", null); @@ -279,6 +285,12 @@ public void processOpts() { additionalProperties.put(this.SUPPORT_MULTIPLE_RESPONSES, this.supportMultipleResponses); } + if (additionalProperties.containsKey(EXTRACT_ENUMS_TO_SEPARATE_FILES)) { + this.extractEnumsToSeparateFiles = convertPropertyToBooleanAndWriteBack(EXTRACT_ENUMS_TO_SEPARATE_FILES); + } else { + additionalProperties.put(EXTRACT_ENUMS_TO_SEPARATE_FILES, this.extractEnumsToSeparateFiles); + } + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); } @@ -666,6 +678,13 @@ public ModelsMap postProcessModels(ModelsMap objs) { List> enumVars = (List>) var.allowableValues.get("enumVars"); addEnumIndexes(enumVars); } + + // If extractEnumsToSeparateFiles is enabled, mark this enum property for extraction + // and set the vendor extension to prevent inline enum rendering + if (this.extractEnumsToSeparateFiles) { + var.vendorExtensions.put("x-protobuf-enum-extracted-to-file", true); + var.vendorExtensions.put("x-protobuf-enum-reference-import", true); + } } // Add x-protobuf-index, unless already specified @@ -686,6 +705,12 @@ public ModelsMap postProcessModels(ModelsMap objs) { } } } + + // Extract enums to separate files if the option is enabled + if (this.extractEnumsToSeparateFiles) { + objs = extractEnumsToSeparateFiles(objs); + } + return objs; } @@ -698,25 +723,471 @@ public Map postProcessAllModels(Map objs) Map allModels = this.getAllModels(objs); - for (CodegenModel cm : allModels.values()) { - // Replicate all attributes from children to parents in case of allof, as there is no inheritance - if (!cm.allOf.isEmpty() && cm.getParentModel() != null) { - CodegenModel parentCM = cm.getParentModel(); - for (CodegenProperty var : cm.getVars()) { - if (!parentVarsContainsVar(parentCM.vars, var)) { - parentCM.vars.add(var); - } + // Ensure models in discriminator mappings import the discriminator parent + this.addDiscriminatorParentImports(objs, allModels); + + // Manage children properties for inheritance (must be done before enum extraction) + this.manageChildrenProperties(objs, allModels); + + // Extract enum properties to separate files if enabled + if (this.extractEnumsToSeparateFiles) { + Map extractedEnums = new HashMap<>(); + + for (Entry entry : objs.entrySet()) { + ModelsMap modelsMap = entry.getValue(); + + Map extractedCodegenModelEnums = this.extractEnums(modelsMap); + for (String enumName : extractedCodegenModelEnums.keySet()) { + CodegenModel enumModel = extractedCodegenModelEnums.get(enumName); + + ModelsMap enumModelsMap = new ModelsMap(); + ModelMap enumModelMap = new ModelMap(); + enumModelMap.setModel(enumModel); + enumModelsMap.setModels(Arrays.asList(enumModelMap)); + enumModelsMap.setImports(new ArrayList<>()); + + enumModelsMap.putAll(additionalProperties); + extractedEnums.put(enumName, enumModelsMap); } - // add all imports from child - cm.getImports().stream() - // Filter self import && child import - .filter(importFromList -> !parentCM.getClassname().equalsIgnoreCase(importFromList) && !cm.getClassname().equalsIgnoreCase(importFromList)) - .forEach(importFromList -> this.addImport(objs, parentCM, importFromList)); } + + // Add all extracted enums to the objs map so they get generated + objs.putAll(extractedEnums); } + return aggregateModelsName == null ? objs : aggregateModels(objs); } + /** + * Ensures that all models in discriminator mappings import the discriminator parent. + * In protobuf, a child model needs to import the discriminator parent (root of the hierarchy), + * not just its immediate allOf parent. + * + * This method only adds imports FROM children TO the discriminator parent, + * not the other way around. + * + * @param objs the complete models map for import tracking + * @param allModels map of all CodegenModels by name + */ + private void addDiscriminatorParentImports(Map objs, Map allModels) { + for (CodegenModel model : allModels.values()) { + if (isDiscriminatorParent(model)) { + // For each child in the discriminator mapping + for (MappedModel mappedModel : model.discriminator.getMappedModels()) { + String childName = mappedModel.getModelName(); + CodegenModel childModel = allModels.get(childName); + + if (childModel == null) { + LOGGER.warn("Discriminator mapping references model '{}' which was not found", childName); + continue; + } + + // Add import FROM child TO discriminator parent + // This ensures D imports A (not just C) in multi-level inheritance + // DO NOT add imports from parent to child (that would create circular references) + this.addImport(objs, childModel, model.getClassname()); + } + } + } + } + + /** + * Manages property and import propagation from child models to parent models. + * In protobuf, inheritance doesn't exist, so all descendant properties must be + * copied to parent models to achieve pseudo-inheritance. + * + * This method uses a two-phase approach: + * 1. Bottom-up propagation: Each model copies properties to all its ancestors + * 2. Discriminator aggregation: Discriminator parents recursively collect from ALL descendants + * + * @param objs the complete models map for import tracking + * @param allModels map of all CodegenModels by name + */ + public void manageChildrenProperties(Map objs, Map allModels) { + // Phase 1: Bottom-up property propagation + // Each child copies its properties to all ancestors in the chain + for (CodegenModel model : allModels.values()) { + if (!model.allOf.isEmpty() && model.getParentModel() != null) { + // Walk up the entire parent chain + CodegenModel currentAncestor = model.getParentModel(); + + while (currentAncestor != null) { + // Copy properties from this model to the current ancestor + copyPropertiesToParent(model, currentAncestor); + + // Copy imports from this model to the current ancestor + copyImportsToParent(objs, model, currentAncestor); + + // Move to the next ancestor in the chain + currentAncestor = currentAncestor.getParentModel(); + } + } + } + + // Phase 2: Discriminator parent aggregation + // For models with discriminators, recursively collect properties from ALL descendants + // This handles multi-level inheritance (grandchildren, great-grandchildren, etc.) + for (CodegenModel model : allModels.values()) { + if (isDiscriminatorParent(model)) { + Set visited = new HashSet<>(); + collectAllDescendantProperties(objs, model, allModels, visited); + } + } + + // Phase 3: Clean up parent imports for models NOT in discriminator mappings + // In protobuf, we copy properties instead of using inheritance, so models shouldn't import their parents + // UNLESS they're explicitly in a discriminator mapping (handled by addDiscriminatorParentImports) + for (CodegenModel model : allModels.values()) { + if (model.getParentModel() != null) { + // Check if this model is in any discriminator mapping + boolean isInDiscriminatorMapping = false; + for (CodegenModel potentialParent : allModels.values()) { + if (isDiscriminatorParent(potentialParent)) { + for (MappedModel mappedModel : potentialParent.discriminator.getMappedModels()) { + if (mappedModel.getModelName().equals(model.getClassname())) { + isInDiscriminatorMapping = true; + break; + } + } + } + if (isInDiscriminatorMapping) break; + } + + // If NOT in discriminator mapping, remove parent imports + // (These were added by base OpenAPI processing but aren't needed in protobuf) + if (!isInDiscriminatorMapping) { + CodegenModel parent = model.getParentModel(); + while (parent != null) { + // Capture parent's classname for use in lambda + final String parentClassname = parent.getClassname(); + + // Remove imports matching the parent's name from both model.imports and ModelsMap + model.getImports().removeIf(importName -> { + String modelNameFromImport = importName; + if (importName.contains("/")) { + modelNameFromImport = importName.substring(importName.lastIndexOf('/') + 1); + } + return parentClassname.equalsIgnoreCase(modelNameFromImport); + }); + + // Also remove from ModelsMap + ModelsMap modelsMap = objs.get(model.getClassname()); + if (modelsMap != null && modelsMap.getImports() != null) { + modelsMap.getImports().removeIf(importDef -> { + String modelNameFromImport = importDef.get("import"); + if (modelNameFromImport != null && modelNameFromImport.contains("/")) { + modelNameFromImport = modelNameFromImport.substring(modelNameFromImport.lastIndexOf('/') + 1); + } + return parentClassname.equalsIgnoreCase(modelNameFromImport); + }); + } + + parent = parent.getParentModel(); + } + } + } + } + + // Phase 4: Remove self-imports from ALL models + // A model should never import itself (circular reference) + for (CodegenModel model : allModels.values()) { + final String modelClassname = model.getClassname(); + + // Remove self-imports from model.imports + model.getImports().removeIf(importName -> { + String modelNameFromImport = importName; + if (importName.contains("/")) { + modelNameFromImport = importName.substring(importName.lastIndexOf('/') + 1); + } + // Normalize for comparison (convert snake_case to PascalCase) + String normalizedImportName = org.openapitools.codegen.utils.StringUtils.camelize(modelNameFromImport); + return modelClassname.equalsIgnoreCase(normalizedImportName); + }); + + // Also remove from ModelsMap + ModelsMap modelsMap = objs.get(model.getClassname()); + if (modelsMap != null && modelsMap.getImports() != null) { + modelsMap.getImports().removeIf(importDef -> { + String modelNameFromImport = importDef.get("import"); + if (modelNameFromImport != null && modelNameFromImport.contains("/")) { + modelNameFromImport = modelNameFromImport.substring(modelNameFromImport.lastIndexOf('/') + 1); + } + // Normalize for comparison (convert snake_case to PascalCase) + String normalizedImportName = org.openapitools.codegen.utils.StringUtils.camelize(modelNameFromImport); + return modelClassname.equalsIgnoreCase(normalizedImportName); + }); + } + } + } + + /** + * Checks if a model is a discriminator parent (has a discriminator with mapped models). + * + * @param model the model to check + * @return true if the model has a discriminator with mapped models + */ + private boolean isDiscriminatorParent(CodegenModel model) { + return model != null + && model.discriminator != null + && model.discriminator.getMappedModels() != null + && !model.discriminator.getMappedModels().isEmpty(); + } + + /** + * Normalizes a model name for case-insensitive comparison. + * + * In protobuf generation, import paths often use snake_case format + * while classnames use PascalCase. This method + * normalizes both formats to PascalCase for accurate comparison + * + * @param modelName the model name or import path to normalize + * @return normalized PascalCase model name, or empty string if input is null/empty + */ + private String normalizeModelNameForComparison(String modelName) { + if (modelName == null || modelName.isEmpty()) { + return ""; + } + + // Extract model name from path if present + String extractedName = modelName; + if (modelName.contains("/")) { + extractedName = modelName.substring(modelName.lastIndexOf('/') + 1); + } else if (modelName.contains("\\")) { // Windows path support + extractedName = modelName.substring(modelName.lastIndexOf('\\') + 1); + } + + // Convert to PascalCase (handles snake_case, kebab-case, etc.) + return org.openapitools.codegen.utils.StringUtils.camelize(extractedName); + } + + /** + * Recursively collects properties and imports from all descendants of a discriminator parent. + * This ensures that grandchildren, great-grandchildren, etc. all have their properties + * included in the discriminator parent. + * + * @param objs the complete models map for import tracking + * @param discriminatorParent the discriminator parent model + * @param allModels map of all CodegenModels by name + * @param visited set of already processed model names to prevent infinite loops + */ + private void collectAllDescendantProperties(Map objs, + CodegenModel discriminatorParent, + Map allModels, + Set visited) { + if (objs == null || discriminatorParent == null || allModels == null || visited == null) { + LOGGER.warn("Skipping descendant property collection due to null parameter"); + return; + } + + // Mark this discriminator parent as visited to prevent infinite recursion + if (!visited.add(discriminatorParent.getClassname())) { + return; // Already processed + } + + // Get all direct children from discriminator mappings + if (discriminatorParent.discriminator == null || + discriminatorParent.discriminator.getMappedModels() == null) { + return; + } + + for (MappedModel mappedModel : discriminatorParent.discriminator.getMappedModels()) { + String childName = mappedModel.getModelName(); + CodegenModel childModel = allModels.get(childName); + + if (childModel == null) { + LOGGER.warn("Discriminator references model '{}' which was not found in allModels", childName); + continue; + } + + // Recursively collect from this child and all its descendants + collectDescendantPropertiesRecursive(objs, discriminatorParent, childModel, allModels, visited); + } + } + + /** + * Recursively collects properties from a descendant and all its children. + * + * @param objs the complete models map for import tracking + * @param discriminatorParent the root discriminator parent receiving all properties + * @param descendant the current descendant being processed + * @param allModels map of all CodegenModels by name + * @param visited set of already processed model names + */ + private void collectDescendantPropertiesRecursive(Map objs, + CodegenModel discriminatorParent, + CodegenModel descendant, + Map allModels, + Set visited) { + // Prevent infinite loops + if (visited.contains(descendant.getClassname())) { + return; + } + + visited.add(descendant.getClassname()); + + // Copy this descendant's properties to the discriminator parent + copyPropertiesToParent(descendant, discriminatorParent); + + // Copy imports from descendant to discriminator parent + // Filter out models that are in the discriminator mapping (they're siblings, not dependencies) + copyImportsToDiscriminatorParent(objs, descendant, discriminatorParent); + + // Recursively process this descendant's children + if (descendant.getChildren() != null) { + for (CodegenModel grandchild : descendant.getChildren()) { + collectDescendantPropertiesRecursive(objs, discriminatorParent, grandchild, allModels, visited); + } + } + } + + /** + * Copies imports from a descendant to a discriminator parent. + * Filters out self-references and models that are in the discriminator's mapping + * (those are siblings in the polymorphic hierarchy, not dependencies). + * Also filters out the discriminator parent itself and any intermediate parents. + * + * @param objs the complete models map for import tracking + * @param descendant the descendant model whose imports are being copied + * @param discriminatorParent the discriminator parent receiving the imports + */ + private void copyImportsToDiscriminatorParent(Map objs, + CodegenModel descendant, + CodegenModel discriminatorParent) { + if (objs == null || descendant == null || discriminatorParent == null) { + LOGGER.warn("Skipping import copy due to null parameter"); + return; + } + + // Additional safety: check if descendant has imports + if (descendant.getImports() == null || descendant.getImports().isEmpty()) { + LOGGER.debug("Descendant {} has no imports to copy", descendant.getClassname()); + return; + } + + // Build set of model names that are in the discriminator mapping + Set modelsInDiscriminatorMapping = new HashSet<>(); + if (discriminatorParent.discriminator != null && + discriminatorParent.discriminator.getMappedModels() != null) { + for (MappedModel mappedModel : discriminatorParent.discriminator.getMappedModels()) { + modelsInDiscriminatorMapping.add(mappedModel.getModelName()); + } + } + + // Build set of all parents in the chain from descendant to discriminator parent + Set parentsInChain = new HashSet<>(); + parentsInChain.add(discriminatorParent.getClassname()); + CodegenModel currentParent = descendant.getParentModel(); + while (currentParent != null) { + parentsInChain.add(currentParent.getClassname()); + if (currentParent == discriminatorParent) { + break; + } + currentParent = currentParent.getParentModel(); + } + + // Copy imports, filtering out: + // - self-references (descendant's own name) + // - discriminator siblings (other models in the discriminator mapping) + // - any parent in the inheritance chain (to avoid circular references) + descendant.getImports().stream() + .filter(importName -> { + // Normalize the import name for comparison using the helper method + String normalizedImportName = normalizeModelNameForComparison(importName); + + // Filter out self-references + if (descendant.getClassname().equalsIgnoreCase(normalizedImportName)) { + return false; + } + // Filter out discriminator siblings + for (String siblingName : modelsInDiscriminatorMapping) { + if (siblingName.equalsIgnoreCase(normalizedImportName)) { + return false; + } + } + // Filter out any parent in the chain + for (String parentName : parentsInChain) { + if (parentName.equalsIgnoreCase(normalizedImportName)) { + return false; + } + } + return true; + }) + .forEach(importName -> this.addImport(objs, discriminatorParent, importName)); + } + + /** + * Copies properties from a child model to a parent model if they don't already exist. + * + * @param child the child model whose properties are being copied + * @param parent the parent model receiving the properties + */ + private void copyPropertiesToParent(CodegenModel child, CodegenModel parent) { + if (child == null || parent == null) { + LOGGER.warn("Skipping property copy due to null parameter"); + return; + } + + for (CodegenProperty var : child.getVars()) { + if (!parentVarsContainsVar(parent.vars, var)) { + parent.vars.add(var); + } + } + } + + /** + * Copies imports from a child model to a parent model. + * Filters out self-references, circular imports, and intermediate parents in the inheritance chain. + * + * @param objs the complete models map for import tracking + * @param child the child model whose imports are being copied + * @param parent the parent model receiving the imports + */ + private void copyImportsToParent(Map objs, CodegenModel child, CodegenModel parent) { + if (objs == null || child == null || parent == null) { + LOGGER.warn("Skipping import copy due to null parameter"); + return; + } + + // Build set of model names to filter out: + // 1. Child's own name + // 2. Parent's name + // 3. All models in the inheritance chain from child to parent (intermediate parents) + // 4. All ancestors of parent (grandparents, great-grandparents, etc.) + Set modelsToFilter = new HashSet<>(); + modelsToFilter.add(child.getClassname()); + modelsToFilter.add(parent.getClassname()); + + // Add all models in the chain from child up to (but not including) parent + CodegenModel current = child.getParentModel(); + while (current != null && current != parent) { + modelsToFilter.add(current.getClassname()); + current = current.getParentModel(); + } + + // Add all ancestors of the parent to the filter set + CodegenModel currentAncestor = parent.getParentModel(); + while (currentAncestor != null) { + modelsToFilter.add(currentAncestor.getClassname()); + currentAncestor = currentAncestor.getParentModel(); + } + + // Copy imports, filtering out the models in our filter set + child.getImports().stream() + .filter(importName -> { + // Normalize the import name for comparison using the helper method + String normalizedImportName = normalizeModelNameForComparison(importName); + + // Filter out any model in our filter set + for (String filterName : modelsToFilter) { + if (filterName.equalsIgnoreCase(normalizedImportName)) { + return false; + } + } + return true; + }) + .forEach(importName -> this.addImport(objs, parent, importName)); + } + /** * Aggregates all individual model definitions into a single entry. * @@ -745,12 +1216,27 @@ public Map aggregateModels(Map objs) { return objects; } + /** + * Adds an import to a CodegenModel, preventing duplicate imports. + * + * @param objs the complete models map for import tracking + * @param cm the CodegenModel to add the import to + * @param importValue the import path (e.g., "models/pet" or "Pet") + * + * @implNote This method uses toModelImport() to normalize the import path, + * preventing duplicate model package prefixes that can occur when + * imports are propagated through discriminator inheritance chains. + */ public void addImport(Map objs, CodegenModel cm, String importValue) { - String modelFileName = this.toModelFilename(importValue); - boolean skipImport = isImportAlreadyPresentInModel(objs, cm, modelFileName); + // Use toModelImport to get the correct import path with model package prefix + if (importValue == null || importValue.trim().isEmpty()) { + LOGGER.warn("Attempted to add null or empty import to model: {}", cm.getName()); + return; + } + + String processedImport = this.toModelImport(importValue); + boolean skipImport = isImportAlreadyPresentInModel(objs, cm, processedImport); if (!skipImport) { - // Use toModelImport to get the correct import path with model package prefix - String processedImport = this.toModelImport(importValue); this.addImport(cm, processedImport); Map importItem = new HashMap<>(); importItem.put(IMPORT, processedImport); @@ -1048,6 +1534,13 @@ public String toModelImport(String name) { if (name.startsWith(modelPath)) { return name; } + // Also check if it's already a full path (contains "/") + if (name.contains("/")) { + // Extract just the model name from the path + String[] parts = name.split("/"); + String modelName = parts[parts.length - 1]; + return modelPackage() + "/" + underscore(modelName); + } return modelPackage() + "/" + underscore(name); } } @@ -1074,6 +1567,102 @@ private int generateFieldNumberFromString(String name) throws ProtoBufIndexCompu return fieldNumber; } + /** + * Extracts enum properties from models and creates separate enum model files. + * Also adds imports to the parent models for the extracted enums. + * + * @param objs the models map containing all models + * @return the modified models map with extracted enum models added + */ + private ModelsMap extractEnumsToSeparateFiles(ModelsMap objs) { + List> enumImports = new ArrayList<>(); + + Map extractedEnums = this.extractEnums(objs); + for (String enumName : extractedEnums.keySet()) { + // Add an import for this enum to the parent model + String enumImportPath = toModelImport(toModelName(enumName)); + Map importItem = new HashMap<>(); + importItem.put(IMPORT, enumImportPath); + + // Add to the list if not already present + boolean alreadyImported = enumImports.stream() + .anyMatch(imp -> imp.get(IMPORT).equals(enumImportPath)); + + if (!alreadyImported) { + enumImports.add(importItem); + } + } + + // Add all the enum imports to the model's imports + if (!enumImports.isEmpty()) { + List> existingImports = objs.getImports(); + if (existingImports == null) { + existingImports = new ArrayList<>(); + objs.setImports(existingImports); + } + + for (Map enumImport : enumImports) { + boolean alreadyExists = existingImports.stream() + .anyMatch(imp -> imp.get(IMPORT).equals(enumImport.get(IMPORT))); + if (!alreadyExists) { + existingImports.add(enumImport); + } + } + } + + return objs; + } + + /** + * Extracts enum properties from models to be used in other process. + * + * @param objs the models map containing all models + * @return the models map with extracted enum models + */ + private Map extractEnums(ModelsMap objs) { + Map extractedEnums = new HashMap<>(); + + for (ModelMap mo : objs.getModels()) { + CodegenModel cm = mo.getModel(); + + // Skip if this model itself is an enum (standalone enums are already separate files) + if (cm.isEnum) { + continue; + } + + // Find all enum properties in this model + for (CodegenProperty var : cm.vars) { + if (var.isEnum && var.vendorExtensions.containsKey("x-protobuf-enum-extracted-to-file")) { + // Create a new CodegenModel for the extracted enum + CodegenModel enumModel = new CodegenModel(); + // Use toModelName to get the properly formatted enum name in CamelCase (e.g., InlineEnumProperty) + String enumKey = toModelName(toEnumName(var)); + if (enumKey == null || enumKey.isEmpty()) { + LOGGER.warn("Enum property {} has no enum name, skipping extraction", var.getName()); + continue; + } + + if (var.allowableValues == null || var.allowableValues.isEmpty()) { + LOGGER.warn("Enum {} has no allowable values, skipping extraction", enumKey); + continue; + } + + if (!extractedEnums.containsKey(enumKey)) { + enumModel.setName(enumKey); + enumModel.setClassname(enumKey); + enumModel.setIsEnum(true); + enumModel.setAllowableValues(var.allowableValues); + // Set the base data type for the enum (string, int32, etc.) + enumModel.setDataType(var.baseType != null ? var.baseType : var.dataType); + + extractedEnums.put(enumKey, enumModel); + } + } + } + } + return extractedEnums; + } + /** * Checks if the var provided is already in the list of the parent's vars, matching the type and the name * diff --git a/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache b/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache index c0ec053732c1..420f4abc84aa 100644 --- a/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache +++ b/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache @@ -37,6 +37,7 @@ import public "{{{.}}}.proto"; {{#vendorExtensions.x-protobuf-type}}{{{.}}} {{/vendorExtensions.x-protobuf-type}}{{{vendorExtensions.x-protobuf-data-type}}} {{{name}}} = {{vendorExtensions.x-protobuf-index}}{{#vendorExtensions.x-protobuf-packed}} [packed=true]{{/vendorExtensions.x-protobuf-packed}}{{#vendorExtensions.x-protobuf-json-name}} [json_name="{{vendorExtensions.x-protobuf-json-name}}"]{{/vendorExtensions.x-protobuf-json-name}}; {{/isEnum}} {{#isEnum}} + {{^vendorExtensions.x-protobuf-enum-reference-import}} enum {{enumName}} { {{#allowableValues}} {{#enumVars}} @@ -45,6 +46,7 @@ import public "{{{.}}}.proto"; {{/allowableValues}} } + {{/vendorExtensions.x-protobuf-enum-reference-import}} {{enumName}} {{name}} = {{vendorExtensions.x-protobuf-index}}; {{/isEnum}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java index 7014c1e7c640..64887f71e63c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java @@ -48,6 +48,7 @@ import static org.openapitools.codegen.TestUtils.createCodegenModelWrapper; import static org.openapitools.codegen.languages.ProtobufSchemaCodegen.USE_SIMPLIFIED_ENUM_NAMES; import static org.testng.Assert.assertEquals; +import static org.openapitools.codegen.languages.ProtobufSchemaCodegen.EXTRACT_ENUMS_TO_SEPARATE_FILES; import static org.openapitools.codegen.languages.ProtobufSchemaCodegen.START_ENUMS_WITH_UNSPECIFIED; public class ProtobufSchemaCodegenTest { @@ -316,23 +317,53 @@ public void testCodeGenWithAllOfDiscriminatorMultipleLevels() throws IOException DefaultGenerator generator = new DefaultGenerator(); List files = generator.opts(clientOptInput).generate(); + // Verify Animal proto file (discriminator parent) TestUtils.ensureContainsFile(files, output, "models/animal.proto"); - Path path = Paths.get(output + "/models/animal.proto"); - - // Verify the generated file contains all expected properties from multi-level inheritance - String generatedContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + Path animalPath = Paths.get(output + "/models/animal.proto"); + String animalContent = new String(Files.readAllBytes(animalPath), StandardCharsets.UTF_8); + + // Properties from discriminated classes should be included in Animal: + // From Dog (direct child in discriminator mapping): bark + Assert.assertTrue(animalContent.contains("string bark"), "Animal should contain 'bark' property from Dog"); + // From Feline (intermediate parent, NOT in discriminator mapping): name, furColor + Assert.assertTrue(animalContent.contains("string name"), "Animal should contain 'name' property from Feline"); + Assert.assertTrue(animalContent.contains("string fur_color"), "Animal should contain 'fur_color' property from Feline"); + // From Cat (indirect child through Feline, IN discriminator mapping): isIndoor, careDetails + Assert.assertTrue(animalContent.contains("bool is_indoor"), "Animal should contain 'is_indoor' property from Cat (indirect child)"); + Assert.assertTrue(animalContent.contains("CareDetails care_details"), "Animal should contain 'care_details' property from Cat (indirect child)"); + + assertFileEquals(animalPath, Paths.get("src/test/resources/3_0/protobuf-schema/animal.proto")); + + // Verify Cat proto file (indirect child in discriminator mapping) + TestUtils.ensureContainsFile(files, output, "models/cat.proto"); + Path catPath = Paths.get(output + "/models/cat.proto"); + String catContent = new String(Files.readAllBytes(catPath), StandardCharsets.UTF_8); + + // Cat should import Animal (the discriminator parent), not Feline (its immediate allOf parent) + Assert.assertTrue(catContent.contains("import public \"models/animal.proto\";"), + "Cat should import Animal (discriminator parent)"); + // Cat should also import CareDetails (its dependency) + Assert.assertTrue(catContent.contains("import public \"models/care_details.proto\";"), + "Cat should import CareDetails"); + + // Verify Feline proto file (intermediate parent, NOT in discriminator mapping) + TestUtils.ensureContainsFile(files, output, "models/feline.proto"); + Path felinePath = Paths.get(output + "/models/feline.proto"); + String felineContent = new String(Files.readAllBytes(felinePath), StandardCharsets.UTF_8); + + // According to requirements: Feline inherits from Animal but is NOT in discriminator mapping + // So Feline should NOT import Animal + Assert.assertFalse(felineContent.contains("import public \"models/animal.proto\";"), + "Feline should NOT import Animal (not in discriminator mapping)"); - // Properties from discriminated classes should be included: - // From Dog (direct child): bark - Assert.assertTrue(generatedContent.contains("string bark"), "Animal should contain 'bark' property from Dog"); - // From Feline (direct child): name, furColor - Assert.assertTrue(generatedContent.contains("string name"), "Animal should contain 'name' property from Feline"); - Assert.assertTrue(generatedContent.contains("string fur_color"), "Animal should contain 'fur_color' property from Feline"); - // From Cat (indirect child through Feline): isIndoor, careDetails - Assert.assertTrue(generatedContent.contains("bool is_indoor"), "Animal should contain 'is_indoor' property from Cat (indirect child)"); - Assert.assertTrue(generatedContent.contains("CareDetails care_details"), "Animal should contain 'care_details' property from Cat (indirect child)"); - - assertFileEquals(path, Paths.get("src/test/resources/3_0/protobuf-schema/animal.proto")); + // Verify Dog proto file (direct child in discriminator mapping) + TestUtils.ensureContainsFile(files, output, "models/dog.proto"); + Path dogPath = Paths.get(output + "/models/dog.proto"); + String dogContent = new String(Files.readAllBytes(dogPath), StandardCharsets.UTF_8); + + // Dog should import Animal (the discriminator parent) + Assert.assertTrue(dogContent.contains("import public \"models/animal.proto\";"), + "Dog should import Animal (discriminator parent)"); output.deleteOnExit(); } @@ -382,4 +413,245 @@ public void enumInMapIsTreatedAsComplexType() { final CodegenProperty property = cm.vars.get(0); Assert.assertEquals(property.baseName, "colorMap"); } + + @Test(description = "Validate that a model referenced multiple times is imported only once in generated protobuf files") + public void testModelImportedOnlyOnce() throws IOException { + // set line break to \n across all platforms + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/model_imported_once.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that the main model file was generated + TestUtils.ensureContainsFile(files, output, "models/model.proto"); + Path modelPath = Paths.get(output + "/models/model.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8); + + // Count occurrences of the import statement for model A (using "public" keyword as generated) + String importStatement = "import public \"models/a.proto\";"; + int importCount = countOccurrences(modelContent, importStatement); + + // Assert that model A is imported exactly once despite being used in: + // - direct field in Model (directA) + // - field in SubModel which is nested in Model (SubModel.aReference) + Assert.assertEquals(importCount, 1, "Model A should be imported exactly once in model.proto"); + + // Check the SubModel file - it also references A directly + TestUtils.ensureContainsFile(files, output, "models/sub_model.proto"); + Path subModelPath = Paths.get(output + "/models/sub_model.proto"); + String subModelContent = new String(Files.readAllBytes(subModelPath), StandardCharsets.UTF_8); + int subModelImportCount = countOccurrences(subModelContent, importStatement); + Assert.assertEquals(subModelImportCount, 1, "Model A should be imported exactly once in sub_model.proto"); + + // Check the ExtensibleModel file + TestUtils.ensureContainsFile(files, output, "models/extensible_model.proto"); + Path extensiblePath = Paths.get(output + "/models/extensible_model.proto"); + String extensibleContent = new String(Files.readAllBytes(extensiblePath), StandardCharsets.UTF_8); + + // Count occurrences in ExtensibleModel + int extensibleImportCount = countOccurrences(extensibleContent, importStatement); + + // Assert that model A is imported exactly once in ExtensibleModel despite being used in: + // - direct field in ExtensibleModel (extensibleA) + // - field in ChildModel_1 (childA) which extends ExtensibleModel via discriminator + // - field in ChildModel_2 (directA) which also extends ExtensibleModel via discriminator + Assert.assertEquals(extensibleImportCount, 1, "Model A should be imported exactly once in extensible_model.proto"); + + output.deleteOnExit(); + } + + private int countOccurrences(String content, String substring) { + int count = 0; + int index = 0; + while ((index = content.indexOf(substring, index)) != -1) { + count++; + index += substring.length(); + } + return count; + } + + @Test(description = "Validate that enums are extracted to separate files when extractEnumsToSeparateFiles option is enabled") + public void testExtractEnumsToSeparateFiles() throws IOException { + // set line break to \n across all platforms + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that separate enum files were generated + TestUtils.ensureContainsFile(files, output, "models/separated_enum.proto"); + TestUtils.ensureContainsFile(files, output, "models/inline_enum_property.proto"); + TestUtils.ensureContainsFile(files, output, "models/another_inline_enum_property.proto"); + + // Check that the model file was generated + TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); + Path modelPath = Paths.get(output + "/models/model_with_enums.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8); + + // Verify that enums are NOT defined inline in the model + Assert.assertFalse(modelContent.contains("enum Inline_enum_property"), + "Inline enum should be extracted to separate file"); + + // Verify that the model imports the separated enum files + Assert.assertTrue(modelContent.contains("import public \"models/separated_enum.proto\";"), + "Model should import the separated enum file"); + Assert.assertTrue(modelContent.contains("import public \"models/inline_enum_property.proto\";"), + "Model should import the inline enum file"); + + // Check the AllOfModel file + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums.proto"); + Path allOfModelPath = Paths.get(output + "/models/all_of_model_with_enums.proto"); + String allOfModelContent = new String(Files.readAllBytes(allOfModelPath), StandardCharsets.UTF_8); + + // Verify that the allOf model imports the separated enum files + Assert.assertTrue(allOfModelContent.contains("import public \"models/another_inline_enum_property.proto\";"), + "AllOf model should import its inline enum file"); + + // Verify the separated enum file content + Path separatedEnumPath = Paths.get(output + "/models/separated_enum.proto"); + String separatedEnumContent = new String(Files.readAllBytes(separatedEnumPath), StandardCharsets.UTF_8); + Assert.assertTrue(separatedEnumContent.contains("package openapitools;"), + "Separated enum file should contain a valid package declaration"); + Assert.assertTrue(separatedEnumContent.contains("enum SeparatedEnum"), + "Separated enum file should contain the enum definition"); + Assert.assertTrue(separatedEnumContent.contains("VALUE1"), + "Separated enum should contain VALUE1"); + Assert.assertTrue(separatedEnumContent.contains("VALUE2"), + "Separated enum should contain VALUE2"); + + // Verify the inline enum file content + Path inlineEnumPath = Paths.get(output + "/models/inline_enum_property.proto"); + String inlineEnumContent = new String(Files.readAllBytes(inlineEnumPath), StandardCharsets.UTF_8); + Assert.assertTrue(inlineEnumContent.contains("package openapitools;"), + "Inline enum file should contain a valid package declaration"); + Assert.assertTrue(inlineEnumContent.contains("enum InlineEnumProperty"), + "Inline enum file should contain the enum definition"); + Assert.assertTrue(inlineEnumContent.contains("VALUE3"), + "Inline enum should contain VALUE3"); + Assert.assertTrue(inlineEnumContent.contains("VALUE4"), + "Inline enum should contain VALUE4"); + + output.deleteOnExit(); + } + + @Test(description = "Validate that enums are extracted to separate files when extractEnumsToSeparateFiles option is enabled") + public void testExtractEnumsToSeparateFilesWithOtherEnumOptions() throws IOException { + // set line break to \n across all platforms + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true) + .addAdditionalProperty(USE_SIMPLIFIED_ENUM_NAMES, true) + .addAdditionalProperty(START_ENUMS_WITH_UNSPECIFIED, true); + + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that separate enum files were generated + TestUtils.ensureContainsFile(files, output, "models/separated_enum.proto"); + TestUtils.ensureContainsFile(files, output, "models/inline_enum_property.proto"); + TestUtils.ensureContainsFile(files, output, "models/another_inline_enum_property.proto"); + + // Check that the model file was generated + TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); + Path modelPath = Paths.get(output + "/models/model_with_enums.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8); + + // Verify that enums are NOT defined inline in the model + Assert.assertFalse(modelContent.contains("enum Inline_enum_property"), + "Inline enum should be extracted to separate file"); + + // Verify that the model imports the separated enum files + Assert.assertTrue(modelContent.contains("import public \"models/separated_enum.proto\";"), + "Model should import the separated enum file"); + Assert.assertTrue(modelContent.contains("import public \"models/inline_enum_property.proto\";"), + "Model should import the inline enum file"); + + // Check the AllOfModel file + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums.proto"); + Path allOfModelPath = Paths.get(output + "/models/all_of_model_with_enums.proto"); + String allOfModelContent = new String(Files.readAllBytes(allOfModelPath), StandardCharsets.UTF_8); + + // Verify that the allOf model imports the separated enum files + Assert.assertTrue(allOfModelContent.contains("import public \"models/another_inline_enum_property.proto\";"), + "AllOf model should import its inline enum file"); + + // Verify the separated enum file content + Path separatedEnumPath = Paths.get(output + "/models/separated_enum.proto"); + String separatedEnumContent = new String(Files.readAllBytes(separatedEnumPath), StandardCharsets.UTF_8); + Assert.assertTrue(separatedEnumContent.contains("package openapitools;"), + "Separated enum file should contain a valid package declaration"); + Assert.assertTrue(separatedEnumContent.contains("enum SeparatedEnum"), + "Separated enum file should contain the enum definition"); + Assert.assertTrue(separatedEnumContent.contains("UNSPECIFIED"), + "Separated enum should contain UNSPECIFIED"); + Assert.assertTrue(separatedEnumContent.contains("VALUE1"), + "Separated enum should contain VALUE1"); + Assert.assertTrue(separatedEnumContent.contains("VALUE2"), + "Separated enum should contain VALUE2"); + + // Verify the inline enum file content + Path inlineEnumPath = Paths.get(output + "/models/inline_enum_property.proto"); + String inlineEnumContent = new String(Files.readAllBytes(inlineEnumPath), StandardCharsets.UTF_8); + Assert.assertTrue(inlineEnumContent.contains("package openapitools;"), + "Inline enum file should contain a valid package declaration"); + Assert.assertTrue(inlineEnumContent.contains("enum InlineEnumProperty"), + "Inline enum file should contain the enum definition"); + Assert.assertTrue(inlineEnumContent.contains("UNSPECIFIED"), + "Inline enum should contain UNSPECIFIED"); + Assert.assertTrue(inlineEnumContent.contains("VALUE2"), + "Inline enum should contain VALUE2"); + Assert.assertTrue(inlineEnumContent.contains("VALUE3"), + "Inline enum should contain VALUE3"); + Assert.assertTrue(inlineEnumContent.contains("VALUE4"), + "Inline enum should contain VALUE4"); + + output.deleteOnExit(); + } + + @Test(description = "Test toModelImport with various input formats") + public void testToModelImportVariations() { + final ProtobufSchemaCodegen codegen = new ProtobufSchemaCodegen(); + codegen.setModelPackage("models"); + + // Normal case + Assert.assertEquals(codegen.toModelImport("Pet"), "models/pet"); + + // Already prefixed - should not duplicate + Assert.assertEquals(codegen.toModelImport("models/pet"), "models/pet"); + + // With different casing + Assert.assertEquals(codegen.toModelImport("PetStore"), "models/pet_store"); + + // With numbers + Assert.assertEquals(codegen.toModelImport("Pet123"), "models/pet123"); + + // Empty model package + codegen.setModelPackage(""); + Assert.assertEquals(codegen.toModelImport("Pet"), "Pet"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml index a6c500626e1f..f21bb53f4c74 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml @@ -80,7 +80,7 @@ components: - $ref: '#/components/schemas/Lizard' discriminator: propertyName: petType - # per https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject + # per https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminator-object # this discriminator must be included to use it as a hint to pick a schema MyPetsNoDisc: oneOf: diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto index 922a4db42668..c9953ea3bdde 100644 --- a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/animal.proto @@ -18,8 +18,6 @@ message Animal { string pet_type = 482112090; - string bark = 3016376; - string name = 3373707; string fur_color = 478695002; @@ -28,4 +26,6 @@ message Animal { CareDetails care_details = 176721135; + string bark = 3016376; + } diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml new file mode 100644 index 000000000000..036ae92e4aa8 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml @@ -0,0 +1,32 @@ +openapi: 3.0.0 +info: + title: Enum extraction test + version: '2.0' +paths: {} +components: + schemas: + SeparatedEnum: + type: string + enum: + - VALUE1 + - VALUE2 + ModelWithEnums: + properties: + referenceEnumProperty: + $ref: "#/components/schemas/SeparatedEnum" + inlineEnumProperty: + type: string + enum: + - VALUE2 + - VALUE3 + - VALUE4 + AllOfModelWithEnums: + allOf: + - $ref: "#/components/schemas/ModelWithEnums" + - type: object + properties: + anotherInlineEnumProperty: + type: string + enum: + - VALUE5 + - VALUE6 \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/model_imported_once.yaml b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/model_imported_once.yaml new file mode 100644 index 000000000000..ec4cd8fec94c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/model_imported_once.yaml @@ -0,0 +1,89 @@ +openapi: 3.0.0 +info: + title: Model Import Test + version: 1.0.0 +paths: {} +components: + schemas: + A: + type: object + description: Base model A that will be referenced multiple times + properties: + id: + type: string + value: + type: string + required: + - id + + SubModel: + type: object + description: SubModel that uses model A + allOf: + - $ref: '#/components/schemas/ExtensibleModel' + - type: object + properties: + subField: + type: string + aReference: + $ref: '#/components/schemas/A' + required: + - subField + + Model: + type: object + description: Main model that uses A directly and via SubModel + properties: + name: + type: string + directA: + $ref: '#/components/schemas/A' + nestedSubModel: + $ref: '#/components/schemas/SubModel' + required: + - name + + ExtensibleModel: + type: object + description: Extensible model with discriminator that uses model A + discriminator: + propertyName: type + mapping: + child1: '#/components/schemas/ChildModel_1' + child2: '#/components/schemas/ChildModel_2' + properties: + type: + type: string + extensibleA: + $ref: '#/components/schemas/A' + required: + - type + + ChildModel_1: + allOf: + - $ref: '#/components/schemas/ExtensibleModel' + - type: object + properties: + childSpecificField: + type: string + childA: + $ref: '#/components/schemas/A' + ChildModel_2: + allOf: + - $ref: '#/components/schemas/SubExtensibleModel' + - type: object + properties: + anotherChildSpecificField: + type: integer + directA: + $ref: '#/components/schemas/A' + + SubExtensibleModel: + allOf: + - $ref: '#/components/schemas/ExtensibleModel' + - type: object + properties: + aSubExtensibleField: + type: boolean + subDirectA: + $ref: '#/components/schemas/A' \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto index cad7d44d1fd8..86bbde0b29f2 100644 --- a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/pet.proto @@ -26,4 +26,6 @@ message Pet { bool loves_rocks = 465093427; + bool has_legs = 140596906; + } From dbf98abc52686191017409f79b2e4be5a02e375e Mon Sep 17 00:00:00 2001 From: Anthony TODISCO Date: Thu, 18 Dec 2025 10:41:05 +0100 Subject: [PATCH 4/6] fix: Improve logic when extracting enums to avoid collision in enum values --- .../languages/ProtobufSchemaCodegen.java | 258 ++++++++++++-- .../resources/protobuf-schema/enum.mustache | 31 +- .../resources/protobuf-schema/model.mustache | 2 +- .../protobuf/ProtobufSchemaCodegenTest.java | 326 +++++++++++++++--- .../3_0/protobuf-schema/extracted_enum.yaml | 36 +- 5 files changed, 568 insertions(+), 85 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java index d89c4a3d0314..b1dc937cb657 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java @@ -79,6 +79,22 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf public static final String EXTRACT_ENUMS_TO_SEPARATE_FILES = "extractEnumsToSeparateFiles"; + /** + * The inner enum name used when wrapping extracted enums in message containers. + * This prevents enum value name collisions in the Protocol Buffers global namespace. + */ + public static final String ENUM_WRAPPER_INNER_NAME = "Enum"; + + /** + * Vendor extension key indicating that an enum property has been extracted to a separate file. + */ + private static final String VENDOR_EXT_ENUM_EXTRACTED = "x-protobuf-enum-extracted-to-file"; + + /** + * Vendor extension key for the wrapper message name of an extracted enum. + */ + private static final String VENDOR_EXT_ENUM_WRAPPER_MESSAGE = "x-protobuf-enum-wrapper-message"; + private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class); @Setter protected String packageName = "openapitools"; @@ -628,7 +644,37 @@ public String getNameFromDataType(CodegenProperty property) { } } - + /** + * Post-processes CodegenModel objects to apply protobuf-specific transformations. + * + *

This method performs several critical operations: + *

    + *
  1. Processes enum definitions and optionally extracts them to separate files
  2. + *
  3. Sets vendor extensions for protobuf field types and data types
  4. + *
  5. Assigns field numbers based on configuration
  6. + *
  7. Handles oneOf/anyOf composition schemas
  8. + *
+ * + *

Enum Extraction Behavior: When {@code extractEnumsToSeparateFiles} is enabled, + * inline enum properties are extracted to separate .proto files wrapped in message containers. + * This prevents enum value name collisions in the protobuf global namespace. The resulting + * format is: + *

+     * message EnumName {
+     *   enum Enum {
+     *     VALUE1 = 0;
+     *     VALUE2 = 1;
+     *   }
+     * }
+     * 
+ * + * References to these enums use the qualified name {@code EnumName.Enum}. + * + * @param objs the models map containing all CodegenModel objects + * @return the modified models map with protobuf-specific transformations applied + * @see #extractEnumsToSeparateFiles(ModelsMap) + * @see #extractEnums(ModelsMap) + */ @Override public ModelsMap postProcessModels(ModelsMap objs) { objs = postProcessModelsEnum(objs); @@ -652,56 +698,87 @@ public ModelsMap postProcessModels(ModelsMap objs) { cm.vars = processOneOfAnyOfItems(cm.getComposedSchemas().getAnyOf()); } int index = 1; - for (CodegenProperty var : cm.vars) { + for (CodegenProperty property : cm.vars) { // add x-protobuf-type: repeated if it's an array - if (Boolean.TRUE.equals(var.isArray)) { - var.vendorExtensions.put("x-protobuf-type", "repeated"); - } else if (Boolean.TRUE.equals(var.isNullable && var.isPrimitiveType)) { - var.vendorExtensions.put("x-protobuf-type", "optional"); + if (Boolean.TRUE.equals(property.isArray)) { + property.vendorExtensions.put("x-protobuf-type", "repeated"); + } else if (Boolean.TRUE.equals(property.isNullable && property.isPrimitiveType)) { + property.vendorExtensions.put("x-protobuf-type", "optional"); } // add x-protobuf-data-type // ref: https://developers.google.com/protocol-buffers/docs/proto3 - if (!var.vendorExtensions.containsKey("x-protobuf-data-type")) { - if (var.isArray) { - var.vendorExtensions.put("x-protobuf-data-type", var.items.dataType); + if (!property.vendorExtensions.containsKey("x-protobuf-data-type")) { + if (property.isArray) { + property.vendorExtensions.put("x-protobuf-data-type", property.items.dataType); } else { - var.vendorExtensions.put("x-protobuf-data-type", var.dataType); + property.vendorExtensions.put("x-protobuf-data-type", property.dataType); } } - if (var.isEnum) { - addUnspecifiedToAllowableValues(var.allowableValues); - addEnumValuesPrefix(var.allowableValues, var.getEnumName()); + if (property.isEnum) { + addUnspecifiedToAllowableValues(property.allowableValues); + addEnumValuesPrefix(property.allowableValues, property.getEnumName()); - if (var.allowableValues.containsKey("enumVars")) { - List> enumVars = (List>) var.allowableValues.get("enumVars"); + if (property.allowableValues.containsKey("enumVars")) { + List> enumVars = (List>) property.allowableValues.get("enumVars"); addEnumIndexes(enumVars); } - // If extractEnumsToSeparateFiles is enabled, mark this enum property for extraction - // and set the vendor extension to prevent inline enum rendering + // Process enum extraction when EXTRACT_ENUMS_TO_SEPARATE_FILES is enabled. + // This prevents naming collisions when multiple models have inline enums with the same field name. + // + // Naming Convention: + // Wrapper message name format: ParentModelName_FieldName + // File name format: snake_case(wrapper_message_name).proto + // Enum reference in model: ParentModelName_FieldName.Enum + // + // Example: + // Model A has field "inlineEnumProperty" -> ModelA_InlineEnumProperty.proto + // Model B has field "inlineEnumProperty" -> ModelB_InlineEnumProperty.proto + // (Without parent prefix, both would create InlineEnumProperty.proto - causing collision) + // + // Vendor Extensions Set: + // x-protobuf-enum-extracted-to-file: Boolean flag for extraction + // x-protobuf-enum-wrapper-message: The wrapper message name (for later use in extractEnums()) + // x-protobuf-enum-reference-import: Prevents inline rendering in template + // x-protobuf-data-type: Full reference with .Enum suffix for property type if (this.extractEnumsToSeparateFiles) { - var.vendorExtensions.put("x-protobuf-enum-extracted-to-file", true); - var.vendorExtensions.put("x-protobuf-enum-reference-import", true); + property.vendorExtensions.put("x-protobuf-enum-extracted-to-file", true); + property.vendorExtensions.put("x-protobuf-enum-reference-import", true); + + // Compute the wrapper message name: ParentModelName_FieldName + // This naming scheme ensures uniqueness across models + String enumTypeName = cm.getClassname() + "_" + toModelName(toEnumName(property)); + if (StringUtils.isBlank(enumTypeName)) { + LOGGER.warn("Unable to determine enum type name for property: {}", property.name); + continue; + } + + // Store wrapper message name for use in extractEnums() method + property.vendorExtensions.put("x-protobuf-enum-wrapper-message", enumTypeName); + + // Set property data type to reference the extracted enum wrapper's inner Enum + // The template will use this to reference: ParentModelName_FieldName.Enum + property.vendorExtensions.put("x-protobuf-data-type", enumTypeName + "." + ENUM_WRAPPER_INNER_NAME); } } // Add x-protobuf-index, unless already specified if (this.numberedFieldNumberList) { - var.vendorExtensions.putIfAbsent("x-protobuf-index", index); + property.vendorExtensions.putIfAbsent("x-protobuf-index", index); index++; } else { try { - var.vendorExtensions.putIfAbsent("x-protobuf-index", generateFieldNumberFromString(var.getName())); + property.vendorExtensions.putIfAbsent("x-protobuf-index", generateFieldNumberFromString(property.getName())); } catch (ProtoBufIndexComputationException e) { LOGGER.error("Exception when assigning a index to a protobuf field", e); - var.vendorExtensions.putIfAbsent("x-protobuf-index", "Generated field number is in reserved range (19000, 19999)"); + property.vendorExtensions.putIfAbsent("x-protobuf-index", "Generated field number is in reserved range (19000, 19999)"); } } - if (addJsonNameAnnotation && !var.baseName.equals(var.name)) { - var.vendorExtensions.put("x-protobuf-json-name", var.baseName); + if (addJsonNameAnnotation && !property.baseName.equals(property.name)) { + property.vendorExtensions.put("x-protobuf-json-name", property.baseName); } } } @@ -731,6 +808,9 @@ public Map postProcessAllModels(Map objs) // Extract enum properties to separate files if enabled if (this.extractEnumsToSeparateFiles) { + // First, update property data types for referenced enum models + this.updateReferencedEnumPropertyDataTypes(objs, allModels); + Map extractedEnums = new HashMap<>(); for (Entry entry : objs.entrySet()) { @@ -758,6 +838,75 @@ public Map postProcessAllModels(Map objs) return aggregateModelsName == null ? objs : aggregateModels(objs); } + /** + * Updates property data types for properties that reference enum models. + * When extractEnumsToSeparateFiles is enabled, referenced enum properties + * need to have their data types updated to include the .Enum suffix + * to reference the inner enum within the message wrapper. + * + * This method handles: + * - Direct references to enum models (e.g., property: {$ref: 'MyEnum'}) + * - Arrays of enums (e.g., items with enum dataType) + * - Maps with enum values + * + * @param objs the complete models map + * @param allModels map of all CodegenModels by name + */ + private void updateReferencedEnumPropertyDataTypes(Map objs, Map allModels) { + for (CodegenModel model : allModels.values()) { + if (model.vars == null || model.vars.isEmpty()) { + continue; + } + + for (CodegenProperty property : model.vars) { + // Skip properties that are already marked as inline extracted enums + if (property.vendorExtensions.containsKey(VENDOR_EXT_ENUM_EXTRACTED)) { + continue; + } + + // For array/map types, check the inner item's dataType + String dataTypeToCheck = null; + CodegenProperty itemToCheck = property; + + if (property.isArray && property.items != null) { + dataTypeToCheck = property.items.dataType; + itemToCheck = property.items; + } else if (property.isMap && property.items != null) { + dataTypeToCheck = property.items.dataType; + itemToCheck = property.items; + } else { + dataTypeToCheck = property.dataType; + itemToCheck = property; + } + + if (StringUtils.isBlank(dataTypeToCheck)) { + continue; + } + + // Look for an enum model that matches this dataType + CodegenModel referencedModel = allModels.get(dataTypeToCheck); + if (referencedModel != null && referencedModel.isEnum) { + // This property references an enum model, update its data type to include .Enum suffix + // Also mark it as a referenced extracted enum and add import + property.vendorExtensions.put(VENDOR_EXT_ENUM_EXTRACTED, true); + property.vendorExtensions.put("x-protobuf-enum-reference-import", true); + String enumWrapperName = dataTypeToCheck; // The message wrapper name is the enum model name + property.vendorExtensions.put(VENDOR_EXT_ENUM_WRAPPER_MESSAGE, enumWrapperName); + + // Update the data type of the item to include .Enum suffix + String wrappedEnumType = enumWrapperName + "." + ENUM_WRAPPER_INNER_NAME; + itemToCheck.dataType = wrappedEnumType; + + // Also update the x-protobuf-data-type for protobuf rendering + property.vendorExtensions.put("x-protobuf-data-type", wrappedEnumType); + + // Add import for the referenced enum to the current model + this.addImport(objs, model, enumWrapperName); + } + } + } + } + /** * Ensures that all models in discriminator mappings import the discriminator parent. * In protobuf, a child model needs to import the discriminator parent (root of the hierarchy), @@ -813,7 +962,7 @@ public void manageChildrenProperties(Map objs, Map objs, visited.add(descendant.getClassname()); // Copy this descendant's properties to the discriminator parent - copyPropertiesToParent(descendant, discriminatorParent); + copyPropertiesToParent(objs, descendant, discriminatorParent); // Copy imports from descendant to discriminator parent // Filter out models that are in the discriminator mapping (they're siblings, not dependencies) @@ -1117,19 +1266,51 @@ private void copyImportsToDiscriminatorParent(Map objs, /** * Copies properties from a child model to a parent model if they don't already exist. + * When a property is an extracted enum (identified by the x-protobuf-enum-extracted-to-file vendor extension), + * also adds the corresponding enum import to the parent model. * + * @param objs the complete models map for import tracking * @param child the child model whose properties are being copied * @param parent the parent model receiving the properties */ - private void copyPropertiesToParent(CodegenModel child, CodegenModel parent) { + private void copyPropertiesToParent(Map objs, CodegenModel child, CodegenModel parent) { if (child == null || parent == null) { LOGGER.warn("Skipping property copy due to null parameter"); return; } - for (CodegenProperty var : child.getVars()) { - if (!parentVarsContainsVar(parent.vars, var)) { - parent.vars.add(var); + // Add null safety for collections + if (child.getVars() == null || parent.vars == null) { + LOGGER.warn("Skipping property copy due to null vars: child.vars={}, parent.vars={}", + child.getVars(), parent.vars); + return; + } + + for (CodegenProperty property : child.getVars()) { + if (property == null) { + LOGGER.warn("Skipping null property in child model {}", child.getClassname()); + continue; + } + + if (!parentVarsContainsVar(parent.vars, property)) { + parent.vars.add(property); + + // Guard against null vendorExtensions + if (this.extractEnumsToSeparateFiles && + property.vendorExtensions != null && + property.vendorExtensions.containsKey(VENDOR_EXT_ENUM_EXTRACTED) && + Boolean.TRUE.equals(property.vendorExtensions.get(VENDOR_EXT_ENUM_EXTRACTED))) { + + // Get the enum wrapper message name from vendor extensions + String enumWrapperMessage = (String) property.vendorExtensions.get(VENDOR_EXT_ENUM_WRAPPER_MESSAGE); + if (StringUtils.isNotBlank(enumWrapperMessage)) { + // Add import for the extracted enum file + addImport(objs, parent, enumWrapperMessage); + } else { + LOGGER.warn("Property {} in model {} is marked as extracted enum but has no wrapper message name", + property.name, child.getClassname()); + } + } } } } @@ -1615,6 +1796,7 @@ private ModelsMap extractEnumsToSeparateFiles(ModelsMap objs) { /** * Extracts enum properties from models to be used in other process. + * For inline enums, uses the naming scheme ParentModelName_FieldName to avoid collisions. * * @param objs the models map containing all models * @return the models map with extracted enum models @@ -1631,18 +1813,18 @@ private Map extractEnums(ModelsMap objs) { } // Find all enum properties in this model - for (CodegenProperty var : cm.vars) { - if (var.isEnum && var.vendorExtensions.containsKey("x-protobuf-enum-extracted-to-file")) { + for (CodegenProperty property : cm.vars) { + if (property.isEnum && property.vendorExtensions.containsKey("x-protobuf-enum-extracted-to-file")) { // Create a new CodegenModel for the extracted enum CodegenModel enumModel = new CodegenModel(); - // Use toModelName to get the properly formatted enum name in CamelCase (e.g., InlineEnumProperty) - String enumKey = toModelName(toEnumName(var)); + // Use ParentModelName_FieldName for inline enums to avoid collisions + String enumKey = (String) property.vendorExtensions.get("x-protobuf-enum-wrapper-message"); if (enumKey == null || enumKey.isEmpty()) { - LOGGER.warn("Enum property {} has no enum name, skipping extraction", var.getName()); + LOGGER.warn("Enum property {} has no wrapper message name, skipping extraction", property.getName()); continue; } - if (var.allowableValues == null || var.allowableValues.isEmpty()) { + if (property.allowableValues == null || property.allowableValues.isEmpty()) { LOGGER.warn("Enum {} has no allowable values, skipping extraction", enumKey); continue; } @@ -1651,9 +1833,9 @@ private Map extractEnums(ModelsMap objs) { enumModel.setName(enumKey); enumModel.setClassname(enumKey); enumModel.setIsEnum(true); - enumModel.setAllowableValues(var.allowableValues); + enumModel.setAllowableValues(property.allowableValues); // Set the base data type for the enum (string, int32, etc.) - enumModel.setDataType(var.baseType != null ? var.baseType : var.dataType); + enumModel.setDataType(property.baseType != null ? property.baseType : property.dataType); extractedEnums.put(enumKey, enumModel); } diff --git a/modules/openapi-generator/src/main/resources/protobuf-schema/enum.mustache b/modules/openapi-generator/src/main/resources/protobuf-schema/enum.mustache index aa26522c8809..6d78fbc0e6fc 100644 --- a/modules/openapi-generator/src/main/resources/protobuf-schema/enum.mustache +++ b/modules/openapi-generator/src/main/resources/protobuf-schema/enum.mustache @@ -1,7 +1,26 @@ -enum {{classname}} { - {{#allowableValues}} - {{#enumVars}} - {{{name}}} = {{{protobuf-enum-index}}}; - {{/enumVars}} - {{/allowableValues}} +{{! + Generates an extracted enum wrapped in a message container. + + This wrapper pattern is used to prevent enum value name collisions + in the Protocol Buffers global enum namespace. Multiple enums can + have values with the same name when wrapped in separate messages. + + Generated format: + message EnumName { + enum Enum { + VALUE1 = 0; + VALUE2 = 1; + } + } + + Usage in models: EnumName.Enum field_name = 1; +}} +message {{classname}} { + enum Enum { + {{#allowableValues}} + {{#enumVars}} + {{{name}}} = {{{protobuf-enum-index}}}; + {{/enumVars}} + {{/allowableValues}} + } } diff --git a/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache b/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache index 420f4abc84aa..be8dd9c0126b 100644 --- a/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache +++ b/modules/openapi-generator/src/main/resources/protobuf-schema/model.mustache @@ -47,7 +47,7 @@ import public "{{{.}}}.proto"; } {{/vendorExtensions.x-protobuf-enum-reference-import}} - {{enumName}} {{name}} = {{vendorExtensions.x-protobuf-index}}; + {{#vendorExtensions.x-protobuf-type}}{{{.}}} {{/vendorExtensions.x-protobuf-type}}{{{vendorExtensions.x-protobuf-data-type}}} {{name}} = {{vendorExtensions.x-protobuf-index}}; {{/isEnum}} {{/vars}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java index 64887f71e63c..449192a340f6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java @@ -484,6 +484,7 @@ public void testExtractEnumsToSeparateFiles() throws IOException { System.setProperty("line.separator", "\n"); File output = Files.createTempDirectory("test").toFile(); + System.out.println("ExtractEnums Temporary output directory: " + output.getAbsolutePath()); final CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName("protobuf-schema") @@ -496,9 +497,10 @@ public void testExtractEnumsToSeparateFiles() throws IOException { List files = generator.opts(clientOptInput).generate(); // Check that separate enum files were generated + // With the new naming scheme, inline enum names include the parent model prefix TestUtils.ensureContainsFile(files, output, "models/separated_enum.proto"); - TestUtils.ensureContainsFile(files, output, "models/inline_enum_property.proto"); - TestUtils.ensureContainsFile(files, output, "models/another_inline_enum_property.proto"); + TestUtils.ensureContainsFile(files, output, "models/model_with_enums_inline_enum_property.proto"); + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums_another_inline_enum_property.proto"); // Check that the model file was generated TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); @@ -512,8 +514,13 @@ public void testExtractEnumsToSeparateFiles() throws IOException { // Verify that the model imports the separated enum files Assert.assertTrue(modelContent.contains("import public \"models/separated_enum.proto\";"), "Model should import the separated enum file"); - Assert.assertTrue(modelContent.contains("import public \"models/inline_enum_property.proto\";"), - "Model should import the inline enum file"); + Assert.assertTrue(modelContent.contains("import public \"models/model_with_enums_inline_enum_property.proto\";"), + "Model should import the inline enum file with parent model prefix"); + + // Verify that the model uses the correct enum reference with .Enum suffix + // With the new naming scheme, inline enums use ParentModelName_FieldName format + Assert.assertTrue(modelContent.contains("ModelWithEnums_InlineEnumProperty.Enum"), + "Model should reference extracted enum with .Enum suffix and parent model prefix"); // Check the AllOfModel file TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums.proto"); @@ -521,32 +528,40 @@ public void testExtractEnumsToSeparateFiles() throws IOException { String allOfModelContent = new String(Files.readAllBytes(allOfModelPath), StandardCharsets.UTF_8); // Verify that the allOf model imports the separated enum files - Assert.assertTrue(allOfModelContent.contains("import public \"models/another_inline_enum_property.proto\";"), - "AllOf model should import its inline enum file"); + Assert.assertTrue(allOfModelContent.contains("import public \"models/all_of_model_with_enums_another_inline_enum_property.proto\";"), + "AllOf model should import its inline enum file with parent model prefix"); // Verify the separated enum file content Path separatedEnumPath = Paths.get(output + "/models/separated_enum.proto"); String separatedEnumContent = new String(Files.readAllBytes(separatedEnumPath), StandardCharsets.UTF_8); Assert.assertTrue(separatedEnumContent.contains("package openapitools;"), "Separated enum file should contain a valid package declaration"); - Assert.assertTrue(separatedEnumContent.contains("enum SeparatedEnum"), - "Separated enum file should contain the enum definition"); - Assert.assertTrue(separatedEnumContent.contains("VALUE1"), - "Separated enum should contain VALUE1"); - Assert.assertTrue(separatedEnumContent.contains("VALUE2"), - "Separated enum should contain VALUE2"); - - // Verify the inline enum file content - Path inlineEnumPath = Paths.get(output + "/models/inline_enum_property.proto"); + Assert.assertTrue(separatedEnumContent.contains("message SeparatedEnum"), + "Separated enum file should contain the message wrapper"); + Assert.assertTrue(separatedEnumContent.contains("enum Enum {"), + "Separated enum file should contain inner Enum definition"); + Assert.assertTrue(separatedEnumContent.contains("SEPARATED_ENUM_VALUE1"), + "Separated enum should contain SEPARATED_ENUM_VALUE1"); + Assert.assertTrue(separatedEnumContent.contains("SEPARATED_ENUM_VALUE2"), + "Separated enum should contain SEPARATED_ENUM_VALUE2"); + + // Verify the inline enum file content - uses parent model name prefix + Path inlineEnumPath = Paths.get(output + "/models/model_with_enums_inline_enum_property.proto"); String inlineEnumContent = new String(Files.readAllBytes(inlineEnumPath), StandardCharsets.UTF_8); Assert.assertTrue(inlineEnumContent.contains("package openapitools;"), "Inline enum file should contain a valid package declaration"); - Assert.assertTrue(inlineEnumContent.contains("enum InlineEnumProperty"), - "Inline enum file should contain the enum definition"); - Assert.assertTrue(inlineEnumContent.contains("VALUE3"), - "Inline enum should contain VALUE3"); - Assert.assertTrue(inlineEnumContent.contains("VALUE4"), - "Inline enum should contain VALUE4"); + Assert.assertTrue(inlineEnumContent.contains("message ModelWithEnums_InlineEnumProperty"), + "Inline enum file should contain the message wrapper with parent model prefix"); + Assert.assertTrue(inlineEnumContent.contains("enum Enum {"), + "Inline enum file should contain inner Enum definition"); + // Note: Enum values keep the prefixes from the original field name, not parent+field + // since they are already scoped within the message wrapper + Assert.assertTrue(inlineEnumContent.contains("INLINE_ENUM_PROPERTY_VALUE2"), + "Inline enum should contain enum value"); + Assert.assertTrue(inlineEnumContent.contains("INLINE_ENUM_PROPERTY_VALUE3"), + "Inline enum should contain enum value"); + Assert.assertTrue(inlineEnumContent.contains("INLINE_ENUM_PROPERTY_VALUE4"), + "Inline enum should contain enum value"); output.deleteOnExit(); } @@ -557,6 +572,7 @@ public void testExtractEnumsToSeparateFilesWithOtherEnumOptions() throws IOExcep System.setProperty("line.separator", "\n"); File output = Files.createTempDirectory("test").toFile(); + System.out.println("With combined options Temporary output directory: " + output.getAbsolutePath()); final CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName("protobuf-schema") @@ -571,10 +587,10 @@ public void testExtractEnumsToSeparateFilesWithOtherEnumOptions() throws IOExcep DefaultGenerator generator = new DefaultGenerator(); List files = generator.opts(clientOptInput).generate(); - // Check that separate enum files were generated + // Check that separate enum files were generated with parent model prefix TestUtils.ensureContainsFile(files, output, "models/separated_enum.proto"); - TestUtils.ensureContainsFile(files, output, "models/inline_enum_property.proto"); - TestUtils.ensureContainsFile(files, output, "models/another_inline_enum_property.proto"); + TestUtils.ensureContainsFile(files, output, "models/model_with_enums_inline_enum_property.proto"); + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums_another_inline_enum_property.proto"); // Check that the model file was generated TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); @@ -588,8 +604,13 @@ public void testExtractEnumsToSeparateFilesWithOtherEnumOptions() throws IOExcep // Verify that the model imports the separated enum files Assert.assertTrue(modelContent.contains("import public \"models/separated_enum.proto\";"), "Model should import the separated enum file"); - Assert.assertTrue(modelContent.contains("import public \"models/inline_enum_property.proto\";"), - "Model should import the inline enum file"); + Assert.assertTrue(modelContent.contains("import public \"models/model_with_enums_inline_enum_property.proto\";"), + "Model should import the inline enum file with parent model prefix"); + + // Verify that the model uses the correct enum reference with .Enum suffix + // With the new naming scheme, inline enums use ParentModelName_FieldName format + Assert.assertTrue(modelContent.contains("ModelWithEnums_InlineEnumProperty.Enum"), + "Model should reference extracted enum with .Enum suffix and parent model prefix"); // Check the AllOfModel file TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums.proto"); @@ -597,40 +618,45 @@ public void testExtractEnumsToSeparateFilesWithOtherEnumOptions() throws IOExcep String allOfModelContent = new String(Files.readAllBytes(allOfModelPath), StandardCharsets.UTF_8); // Verify that the allOf model imports the separated enum files - Assert.assertTrue(allOfModelContent.contains("import public \"models/another_inline_enum_property.proto\";"), - "AllOf model should import its inline enum file"); + Assert.assertTrue(allOfModelContent.contains("import public \"models/all_of_model_with_enums_another_inline_enum_property.proto\";"), + "AllOf model should import its inline enum file with parent model prefix"); // Verify the separated enum file content Path separatedEnumPath = Paths.get(output + "/models/separated_enum.proto"); String separatedEnumContent = new String(Files.readAllBytes(separatedEnumPath), StandardCharsets.UTF_8); Assert.assertTrue(separatedEnumContent.contains("package openapitools;"), "Separated enum file should contain a valid package declaration"); - Assert.assertTrue(separatedEnumContent.contains("enum SeparatedEnum"), - "Separated enum file should contain the enum definition"); - Assert.assertTrue(separatedEnumContent.contains("UNSPECIFIED"), + Assert.assertTrue(separatedEnumContent.contains("message SeparatedEnum"), + "Separated enum file should contain the message wrapper"); + Assert.assertTrue(separatedEnumContent.contains("enum Enum {"), + "Separated enum file should contain inner Enum definition"); + Assert.assertTrue(separatedEnumContent.contains("UNSPECIFIED"), "Separated enum should contain UNSPECIFIED"); Assert.assertTrue(separatedEnumContent.contains("VALUE1"), "Separated enum should contain VALUE1"); Assert.assertTrue(separatedEnumContent.contains("VALUE2"), "Separated enum should contain VALUE2"); - // Verify the inline enum file content - Path inlineEnumPath = Paths.get(output + "/models/inline_enum_property.proto"); + // Verify the inline enum file content - uses parent model name prefix + Path inlineEnumPath = Paths.get(output + "/models/model_with_enums_inline_enum_property.proto"); String inlineEnumContent = new String(Files.readAllBytes(inlineEnumPath), StandardCharsets.UTF_8); Assert.assertTrue(inlineEnumContent.contains("package openapitools;"), "Inline enum file should contain a valid package declaration"); - Assert.assertTrue(inlineEnumContent.contains("enum InlineEnumProperty"), - "Inline enum file should contain the enum definition"); + Assert.assertTrue(inlineEnumContent.contains("message ModelWithEnums_InlineEnumProperty"), + "Inline enum file should contain the message wrapper with parent model prefix"); + Assert.assertTrue(inlineEnumContent.contains("enum Enum {"), + "Inline enum file should contain inner Enum definition"); Assert.assertTrue(inlineEnumContent.contains("UNSPECIFIED"), "Inline enum should contain UNSPECIFIED"); + // With simplified names, enum values don't have prefixes, just the base values Assert.assertTrue(inlineEnumContent.contains("VALUE2"), - "Inline enum should contain VALUE2"); + "Inline enum should contain enum value"); Assert.assertTrue(inlineEnumContent.contains("VALUE3"), - "Inline enum should contain VALUE3"); + "Inline enum should contain enum value"); Assert.assertTrue(inlineEnumContent.contains("VALUE4"), - "Inline enum should contain VALUE4"); + "Inline enum should contain enum value"); - output.deleteOnExit(); + //output.deleteOnExit(); } @Test(description = "Test toModelImport with various input formats") @@ -654,4 +680,226 @@ public void testToModelImportVariations() { codegen.setModelPackage(""); Assert.assertEquals(codegen.toModelImport("Pet"), "Pet"); } + + @Test(description = "Validate that enum imports are added to discriminator parent when extractEnumsToSeparateFiles is enabled") + public void testDiscriminatorWithExtractedEnums() throws IOException { + // set line break to \n across all platforms + String originalLineSeparator = System.getProperty("line.separator"); + try { + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that the discriminator parent file was generated + TestUtils.ensureContainsFile(files, output, "models/discriminated_model.proto"); + Path discriminatorPath = Paths.get(output + "/models/discriminated_model.proto"); + String discriminatorContent = new String(Files.readAllBytes(discriminatorPath), StandardCharsets.UTF_8); + + // The discriminator parent should have properties from both children + // From ModelTypeAWithInlineEnum: specificPropertyA, inlineEnumPropertyA + Assert.assertTrue(discriminatorContent.contains("string specific_property_a"), + "Discriminator parent should contain 'specific_property_a' from child A"); + + // The enum property should be present with the correct .Enum suffix + // With the new naming scheme, inline enums use ParentModelName_FieldName format + Assert.assertTrue(discriminatorContent.contains("ModelTypeAWithInlineEnum_InlineEnumProperty.Enum inline_enum_property"), + "Discriminator parent should contain extracted enum property from child A with .Enum suffix and parent model prefix"); + + // CRITICAL: The discriminator parent should import the extracted enum file + Assert.assertTrue(discriminatorContent.contains("import public \"models/model_type_a_with_inline_enum_inline_enum_property.proto\";"), + "Discriminator parent MUST import the extracted enum from child A with parent model prefix"); + + // From ModelTypeBWithInlineEnum: specificPropertyB, referenceEnumPropertyB + Assert.assertTrue(discriminatorContent.contains("string specific_property_b"), + "Discriminator parent should contain 'specific_property_b' from child B"); + + // Child B references SeparatedEnum (not inline), so it should be imported + Assert.assertTrue(discriminatorContent.contains("import public \"models/separated_enum.proto\";"), + "Discriminator parent should import the separated enum referenced by child B"); + + // Verify the extracted enum file for inline enum from child A was created + // The file name uses snake_case of the full wrapper message name + TestUtils.ensureContainsFile(files, output, "models/model_type_a_with_inline_enum_inline_enum_property.proto"); + Path enumPath = Paths.get(output + "/models/model_type_a_with_inline_enum_inline_enum_property.proto"); + String enumContent = new String(Files.readAllBytes(enumPath), StandardCharsets.UTF_8); + + Assert.assertTrue(enumContent.contains("message ModelTypeAWithInlineEnum_InlineEnumProperty"), + "Extracted enum file should contain the message wrapper with parent model prefix"); + Assert.assertTrue(enumContent.contains("enum Enum {"), + "Extracted enum file should contain inner Enum definition"); + // Note: Enum values keep the prefixes from the original field name, not parent+field + // since they are already scoped within the message wrapper + Assert.assertTrue(enumContent.contains("INLINE_ENUM_PROPERTY_VALUE7"), + "Extracted enum should contain enum value"); + Assert.assertTrue(enumContent.contains("INLINE_ENUM_PROPERTY_VALUE8"), + "Extracted enum should contain enum value"); + + output.deleteOnExit(); + } finally { + // Restore original property to avoid side effects on other tests + System.setProperty("line.separator", originalLineSeparator); + } + } + + @SuppressWarnings("unchecked") + @Test(description = "Validate that referenced enum models are correctly wrapped with .Enum suffix when extractEnumsToSeparateFiles is enabled") + public void testReferencedEnumsWithExtraction() throws IOException { + // set line break to \n across all platforms + String originalLineSeparator = System.getProperty("line.separator"); + try { + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that the model file was generated + TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); + Path modelPath = Paths.get(output + "/models/model_with_enums.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace("\r", "\n"); + + // CRITICAL: The model should have the inline enum property with .Enum suffix + // With the new naming scheme, inline enums use ParentModelName_FieldName format + Assert.assertTrue(modelContent.contains("ModelWithEnums_InlineEnumProperty.Enum inline_enum_property"), + "Model should reference inline extracted enum with .Enum suffix and parent model prefix"); + + // CRITICAL: The model should have the referenced enum property with .Enum suffix + // This is the bug being fixed - referenced enums should also get wrapped + Assert.assertTrue(modelContent.contains("SeparatedEnum.Enum reference_enum_property"), + "Model should reference referenced enum with .Enum suffix (THIS IS THE FIX)"); + + // Verify that both enum files are imported + Assert.assertTrue(modelContent.contains("import public \"models/separated_enum.proto\";"), + "Model should import the separated enum file"); + Assert.assertTrue(modelContent.contains("import public \"models/model_with_enums_inline_enum_property.proto\";"), + "Model should import the inline enum file with parent model prefix"); + + // Verify the separated enum file was correctly generated + TestUtils.ensureContainsFile(files, output, "models/separated_enum.proto"); + Path separatedEnumPath = Paths.get(output + "/models/separated_enum.proto"); + String separatedEnumContent = new String(Files.readAllBytes(separatedEnumPath), StandardCharsets.UTF_8); + + Assert.assertTrue(separatedEnumContent.contains("message SeparatedEnum"), + "Separated enum file should contain the message wrapper"); + Assert.assertTrue(separatedEnumContent.contains("enum Enum {"), + "Separated enum file should contain inner Enum definition"); + + // Verify the inline enum file was correctly generated with parent model prefix + TestUtils.ensureContainsFile(files, output, "models/model_with_enums_inline_enum_property.proto"); + Path inlineEnumPath = Paths.get(output + "/models/model_with_enums_inline_enum_property.proto"); + String inlineEnumContent = new String(Files.readAllBytes(inlineEnumPath), StandardCharsets.UTF_8); + + Assert.assertTrue(inlineEnumContent.contains("message ModelWithEnums_InlineEnumProperty"), + "Inline enum file should contain the message wrapper with parent model prefix"); + Assert.assertTrue(inlineEnumContent.contains("enum Enum {"), + "Inline enum file should contain inner Enum definition"); + + output.deleteOnExit(); + } finally { + // Restore original property to avoid side effects on other tests + System.setProperty("line.separator", originalLineSeparator); + } + } + + @SuppressWarnings("unchecked") + @Test(description = "Validate backward compatibility: extracted_enum.yaml generates inline enums when EXTRACT_ENUMS_TO_SEPARATE_FILES is NOT enabled") + public void testEnumsRemainsInlineWithoutExtraction() throws IOException { + // set line break to \n across all platforms + String originalLineSeparator = System.getProperty("line.separator"); + try { + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + System.out.println("InlineEnums Temporary output directory: " + output.getAbsolutePath()); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that the model file was generated + TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); + Path modelPath = Paths.get(output + "/models/model_with_enums.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace("\r", "\n"); + + // WITHOUT extraction: Enums should be defined INLINE in the model file + // Check for inline enum definitions (not extracted to separate files) + // The inline enum should contain the enum values (not a message wrapper) + Assert.assertTrue(modelContent.contains("enum") && modelContent.contains("VALUE2"), + "Without extraction, inline enums should be defined inline in the model"); + + // Verify that inline enums use simple type references (NOT wrapped with .Enum) + // When enums are inline, they're referenced as the enum name directly + // (not as ParentModel_FieldName.Enum which is only used for extracted enums) + Assert.assertTrue(!modelContent.contains(".Enum"), + "Without extraction, enum properties should NOT reference .Enum suffix"); + + // Without extraction: Inline enum files should NOT be generated + // (Note: SeparatedEnum.proto WILL be generated because SeparatedEnum is a top-level schema, + // but inline_enum_property.proto should NOT be generated because it's an inline enum) + List inlineEnumFiles = new java.util.ArrayList<>(); + if (Files.exists(Paths.get(output + "/models/inline_enum_property.proto"))) { + inlineEnumFiles.add(Paths.get(output + "/models/inline_enum_property.proto")); + } + if (Files.exists(Paths.get(output + "/models/another_inline_enum_property.proto"))) { + inlineEnumFiles.add(Paths.get(output + "/models/another_inline_enum_property.proto")); + } + Assert.assertTrue(inlineEnumFiles.isEmpty(), + "Without extraction option enabled, inline enum files should NOT be created as separate files"); + + // Verify that imports for inline enum files are NOT present + Assert.assertFalse(modelContent.contains("import public \"models/inline_enum_property.proto\""), + "Without extraction, there should be no inline enum file imports"); + Assert.assertFalse(modelContent.contains("import public \"models/another_inline_enum_property.proto\""), + "Without extraction, there should be no inline enum file imports"); + + // Check the AllOfModel file + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums.proto"); + Path allOfModelPath = Paths.get(output + "/models/all_of_model_with_enums.proto"); + String allOfModelContent = new String(Files.readAllBytes(allOfModelPath), StandardCharsets.UTF_8); + + // IMPORTANT: AllOf composition in protobuf has different semantics than OpenAPI allOf. + // In protobuf, allOf models only contain direct properties of the model, not inherited/composed properties. + // This differs from OpenAPI where allOf models would include properties from all composed schemas. + // Therefore, we validate backward compatibility by ensuring: + // 1. The model file is generated (proving structure is correct) + // 2. No .Enum suffix is used (proving extraction mode is OFF - not using extracted enum references) + // 3. Inline enums are defined inline (proving backward compatibility works with inline enums) + Assert.assertTrue(!allOfModelContent.isEmpty(), + "Without extraction option, the allOf model file should be generated"); + Assert.assertFalse(allOfModelContent.contains(".Enum"), + "Without extraction option, enums should NOT use .Enum suffix (should be inline)"); + Assert.assertTrue(allOfModelContent.contains("enum") && allOfModelContent.contains("VALUE"), + "Without extraction option, model should contain inline enum definitions"); + + output.deleteOnExit(); + } finally { + // Restore original property to avoid side effects on other tests + System.setProperty("line.separator", originalLineSeparator); + } + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml index 036ae92e4aa8..9abbee8eb72d 100644 --- a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml @@ -11,6 +11,7 @@ components: - VALUE1 - VALUE2 ModelWithEnums: + type: object properties: referenceEnumProperty: $ref: "#/components/schemas/SeparatedEnum" @@ -29,4 +30,37 @@ components: type: string enum: - VALUE5 - - VALUE6 \ No newline at end of file + - VALUE6 + DiscriminatedModel: + type: object + properties: + commonProperty: + type: string + modelType: + type: string + discriminator: + propertyName: modelType + mapping: + typeA: "#/components/schemas/ModelTypeAWithInlineEnum" + typeB: "#/components/schemas/ModelTypeBWithInlineEnum" + ModelTypeAWithInlineEnum: + allOf: + - $ref: "#/components/schemas/DiscriminatedModel" + - type: object + properties: + specificPropertyA: + type: string + inlineEnumProperty: + type: string + enum: + - VALUE7 + - VALUE8 + ModelTypeBWithInlineEnum: + allOf: + - $ref: "#/components/schemas/DiscriminatedModel" + - type: object + properties: + specificPropertyB: + type: string + referenceEnumPropertyB: + $ref: "#/components/schemas/SeparatedEnum" \ No newline at end of file From a9d13a602feb63294b9ab03dca6f1bcbd797291e Mon Sep 17 00:00:00 2001 From: Anthony TODISCO Date: Fri, 19 Dec 2025 16:04:11 +0100 Subject: [PATCH 5/6] fix: Manage case with Enum in lists --- .../languages/ProtobufSchemaCodegen.java | 34 +++++-- .../protobuf/ProtobufSchemaCodegenTest.java | 97 +++++++++++++++++++ .../3_0/protobuf-schema/extracted_enum.yaml | 11 +++ 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java index b1dc937cb657..1c469cb3a7b5 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java @@ -716,12 +716,34 @@ public ModelsMap postProcessModels(ModelsMap objs) { } } - if (property.isEnum) { - addUnspecifiedToAllowableValues(property.allowableValues); - addEnumValuesPrefix(property.allowableValues, property.getEnumName()); + // Check if this property is an enum or an array of enums. + // + // This handles two distinct cases: + // 1. Direct enum property: property.isEnum=true + // Example OpenAPI: myStatus: {$ref: '#/components/schemas/Status'} + // Generated Protobuf: Status.Enum my_status = N; + // + // 2. Array of enums: property.isArray=true && property.items.isEnum=true + // Example OpenAPI: tags: {type: array, items: {$ref: '#/components/schemas/Tag'}} + // Generated Protobuf: repeated Tag.Enum tags = N; + // + // Both cases require the same enum extraction and wrapper processing when + // EXTRACT_ENUMS_TO_SEPARATE_FILES is enabled. This fix ensures arrays of enums + // are handled consistently with direct enum references (previously arrays were + // not being wrapped with the .Enum suffix). + boolean isDirectEnum = property.isEnum; + boolean isArrayOfEnums = property.isArray && property.items != null && property.items.isEnum; + + if (isDirectEnum || isArrayOfEnums) { + // For arrays of enums, extract enum values from items property; + // for direct enums, extract from the property itself. + CodegenProperty enumProperty = isArrayOfEnums ? property.items : property; + + addUnspecifiedToAllowableValues(enumProperty.allowableValues); + addEnumValuesPrefix(enumProperty.allowableValues, enumProperty.getEnumName()); - if (property.allowableValues.containsKey("enumVars")) { - List> enumVars = (List>) property.allowableValues.get("enumVars"); + if (enumProperty.allowableValues.containsKey("enumVars")) { + List> enumVars = (List>) enumProperty.allowableValues.get("enumVars"); addEnumIndexes(enumVars); } @@ -749,7 +771,7 @@ public ModelsMap postProcessModels(ModelsMap objs) { // Compute the wrapper message name: ParentModelName_FieldName // This naming scheme ensures uniqueness across models - String enumTypeName = cm.getClassname() + "_" + toModelName(toEnumName(property)); + String enumTypeName = cm.getClassname() + "_" + toModelName(toEnumName(enumProperty)); if (StringUtils.isBlank(enumTypeName)) { LOGGER.warn("Unable to determine enum type name for property: {}", property.name); continue; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java index 449192a340f6..8b90db5e1209 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java @@ -902,4 +902,101 @@ public void testEnumsRemainsInlineWithoutExtraction() throws IOException { System.setProperty("line.separator", originalLineSeparator); } } + + @Test(description = "Validate that enums in arrays are correctly handled with .Enum suffix when extractEnumsToSeparateFiles is enabled") + public void testEnumsInArraysWithExtraction() throws IOException { + // set line break to \n across all platforms + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + System.out.println("EnumsInArrays Temporary output directory: " + output.getAbsolutePath()); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that the model file was generated + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums.proto"); + Path modelPath = Paths.get(output + "/models/all_of_model_with_enums.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace("\r", "\n"); + + // Test Case 1: Array with REFERENCED enum (listOfReferencedEnums) + // Should have .Enum suffix: repeated SeparatedEnum.Enum + // Use regex to validate structure without depending on specific field numbers + java.util.regex.Pattern referencedEnumPattern = java.util.regex.Pattern.compile( + "repeated\\s+SeparatedEnum\\.Enum\\s+list_of_referenced_enums\\s*=\\s*\\d+"); + Assert.assertTrue( + referencedEnumPattern.matcher(modelContent).find(), + "Array with referenced enum should use .Enum suffix with pattern: repeated SeparatedEnum.Enum list_of_referenced_enums = "); + + // Test Case 2: Array with INLINE enum (listOfEnums) - in AllOfModelWithEnums + // Should have .Enum suffix: repeated AllOfModelWithEnums_ListOfEnums.Enum + // Use regex to validate structure without depending on specific field numbers + java.util.regex.Pattern inlineEnumPattern = java.util.regex.Pattern.compile( + "repeated\\s+AllOfModelWithEnums_ListOfEnums\\.Enum\\s+list_of_enums\\s*=\\s*\\d+"); + Assert.assertTrue( + inlineEnumPattern.matcher(modelContent).find(), + "Array with inline enum should use .Enum suffix with pattern: repeated AllOfModelWithEnums_ListOfEnums.Enum list_of_enums = "); + + // Verify the imported enum file for referenced enum exists + TestUtils.ensureContainsFile(files, output, "models/separated_enum.proto"); + + // Verify the extracted inline enum file for arrays exists + TestUtils.ensureContainsFile(files, output, "models/all_of_model_with_enums_list_of_enums.proto"); + Path listOfEnumsPath = Paths.get(output + "/models/all_of_model_with_enums_list_of_enums.proto"); + String listOfEnumsContent = new String(Files.readAllBytes(listOfEnumsPath), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace("\r", "\n"); + + // Verify the wrapper message structure + Assert.assertTrue(listOfEnumsContent.contains("message AllOfModelWithEnums_ListOfEnums"), + "Extracted inline enum file should contain wrapper message"); + Assert.assertTrue(listOfEnumsContent.contains("enum Enum"), + "Wrapper message should contain inner Enum"); + Assert.assertTrue(listOfEnumsContent.contains("VALUE10") && listOfEnumsContent.contains("VALUE11"), + "Wrapper message should contain enum values"); + + output.deleteOnExit(); + } + + @Test(description = "Validate that enums in arrays remain inline when extractEnumsToSeparateFiles is NOT enabled") + public void testEnumsInArraysWithoutExtraction() throws IOException { + // set line break to \n across all platforms + System.setProperty("line.separator", "\n"); + + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("protobuf-schema") + .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + // Note: EXTRACT_ENUMS_TO_SEPARATE_FILES is NOT enabled + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + // Check that the model file was generated + TestUtils.ensureContainsFile(files, output, "models/model_with_enums.proto"); + Path modelPath = Paths.get(output + "/models/model_with_enums.proto"); + String modelContent = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8) + .replace("\r\n", "\n").replace("\r", "\n"); + + // Without extraction: Arrays of enums should NOT use .Enum suffix + // Instead, they should use the type directly or inline definition + Assert.assertFalse(modelContent.contains(".Enum"), + "Without extraction, enums should NOT use .Enum suffix"); + + // Extracted inline enum files should NOT exist + Assert.assertFalse(Files.exists(Paths.get(output + "/models/model_with_enums_list_of_enums.proto")), + "Without extraction, inline enum array files should NOT be generated"); + + output.deleteOnExit(); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml index 9abbee8eb72d..f493a4582c62 100644 --- a/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf-schema/extracted_enum.yaml @@ -21,6 +21,13 @@ components: - VALUE2 - VALUE3 - VALUE4 + listOfEnums: + type: array + items: + type: string + enum: + - VALUE10 + - VALUE11 AllOfModelWithEnums: allOf: - $ref: "#/components/schemas/ModelWithEnums" @@ -31,6 +38,10 @@ components: enum: - VALUE5 - VALUE6 + listOfReferencedEnums: + type: array + items: + $ref: '#/components/schemas/SeparatedEnum' DiscriminatedModel: type: object properties: From 79b9578705eadbb2bdfd2ec2ce5f06f7abf38cb3 Mon Sep 17 00:00:00 2001 From: Anthony TODISCO Date: Tue, 20 Jan 2026 11:33:47 +0100 Subject: [PATCH 6/6] fix: Fix issue on enum extraction Fix issue linked to enum in array when there is inheritance or discriminator --- .../languages/ProtobufSchemaCodegen.java | 45 ++++++++++++++++--- .../protobuf/ProtobufSchemaCodegenTest.java | 11 ++--- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java index 1c469cb3a7b5..d228b4bdf343 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java @@ -830,6 +830,10 @@ public Map postProcessAllModels(Map objs) // Extract enum properties to separate files if enabled if (this.extractEnumsToSeparateFiles) { + // Recompute allModels after managing children properties + // This ensures inherited properties are included in the vars + allModels = this.getAllModels(objs); + // First, update property data types for referenced enum models this.updateReferencedEnumPropertyDataTypes(objs, allModels); @@ -890,6 +894,7 @@ private void updateReferencedEnumPropertyDataTypes(Map objs, String dataTypeToCheck = null; CodegenProperty itemToCheck = property; + // Determine what data type to check if (property.isArray && property.items != null) { dataTypeToCheck = property.items.dataType; itemToCheck = property.items; @@ -906,21 +911,49 @@ private void updateReferencedEnumPropertyDataTypes(Map objs, } // Look for an enum model that matches this dataType - CodegenModel referencedModel = allModels.get(dataTypeToCheck); + // Note: Need to apply toModelName to ensure correct key lookup since getAllModels() uses this + String modelNameKey = toModelName(dataTypeToCheck); + + // If the dataType already ends with .Enum, it's already been wrapped + // Strip the .Enum suffix to find the underlying enum model + if (dataTypeToCheck.endsWith("." + ENUM_WRAPPER_INNER_NAME)) { + // Strip the .Enum suffix for lookup + modelNameKey = toModelName(dataTypeToCheck.substring(0, dataTypeToCheck.length() - ("." + ENUM_WRAPPER_INNER_NAME).length())); + } + + CodegenModel referencedModel = allModels.get(modelNameKey); if (referencedModel != null && referencedModel.isEnum) { + + // Use the actual classname from the enum model for consistency with how it's used in templates + String enumWrapperName = referencedModel.getClassname(); + + // For referenced enums, the wrapper name is just the enum model's classname + // (not a composite like "ModelName_PropertyName" which is for inline enums) + + // Check if already properly wrapped with .Enum suffix + String expectedWrappedType = enumWrapperName + "." + ENUM_WRAPPER_INNER_NAME; + // This property references an enum model, update its data type to include .Enum suffix // Also mark it as a referenced extracted enum and add import property.vendorExtensions.put(VENDOR_EXT_ENUM_EXTRACTED, true); property.vendorExtensions.put("x-protobuf-enum-reference-import", true); - String enumWrapperName = dataTypeToCheck; // The message wrapper name is the enum model name property.vendorExtensions.put(VENDOR_EXT_ENUM_WRAPPER_MESSAGE, enumWrapperName); // Update the data type of the item to include .Enum suffix - String wrappedEnumType = enumWrapperName + "." + ENUM_WRAPPER_INNER_NAME; - itemToCheck.dataType = wrappedEnumType; + itemToCheck.dataType = expectedWrappedType; - // Also update the x-protobuf-data-type for protobuf rendering - property.vendorExtensions.put("x-protobuf-data-type", wrappedEnumType); + // Update the vendor extension for protobuf rendering + // This is critical for arrays, as the template uses x-protobuf-data-type + // For arrays: x-protobuf-data-type must reference the wrapped enum type + // For direct properties: the dataType in the template is used + if (property.isArray) { + // For array properties, the template uses x-protobuf-data-type which comes from items.dataType + // Make sure it's updated there + property.vendorExtensions.put("x-protobuf-data-type", expectedWrappedType); + } else { + // For non-array properties, also update vendor extension for consistency + property.vendorExtensions.put("x-protobuf-data-type", expectedWrappedType); + } // Add import for the referenced enum to the current model this.addImport(objs, model, enumWrapperName); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java index 8b90db5e1209..8e3ac79854d6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/protobuf/ProtobufSchemaCodegenTest.java @@ -484,7 +484,6 @@ public void testExtractEnumsToSeparateFiles() throws IOException { System.setProperty("line.separator", "\n"); File output = Files.createTempDirectory("test").toFile(); - System.out.println("ExtractEnums Temporary output directory: " + output.getAbsolutePath()); final CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName("protobuf-schema") @@ -572,15 +571,15 @@ public void testExtractEnumsToSeparateFilesWithOtherEnumOptions() throws IOExcep System.setProperty("line.separator", "\n"); File output = Files.createTempDirectory("test").toFile(); - System.out.println("With combined options Temporary output directory: " + output.getAbsolutePath()); final CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName("protobuf-schema") .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") .setOutputDir(output.getAbsolutePath().replace("\\", "/")) - .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true) + .addAdditionalProperty("removeEnumValuePrefix", false) + .addAdditionalProperty(START_ENUMS_WITH_UNSPECIFIED, true) .addAdditionalProperty(USE_SIMPLIFIED_ENUM_NAMES, true) - .addAdditionalProperty(START_ENUMS_WITH_UNSPECIFIED, true); + .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true); final ClientOptInput clientOptInput = configurator.toClientOptInput(); @@ -829,7 +828,6 @@ public void testEnumsRemainsInlineWithoutExtraction() throws IOException { System.setProperty("line.separator", "\n"); File output = Files.createTempDirectory("test").toFile(); - System.out.println("InlineEnums Temporary output directory: " + output.getAbsolutePath()); final CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName("protobuf-schema") @@ -915,6 +913,9 @@ public void testEnumsInArraysWithExtraction() throws IOException { .setGeneratorName("protobuf-schema") .setInputSpec("src/test/resources/3_0/protobuf-schema/extracted_enum.yaml") .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty("removeEnumValuePrefix", false) + .addAdditionalProperty(START_ENUMS_WITH_UNSPECIFIED, true) + .addAdditionalProperty(USE_SIMPLIFIED_ENUM_NAMES, true) .addAdditionalProperty(EXTRACT_ENUMS_TO_SEPARATE_FILES, true); final ClientOptInput clientOptInput = configurator.toClientOptInput();