Skip to content

Commit decf2cf

Browse files
committed
[typescript] Fix inner enum reference in multi-map property type
Fixes #20877, #22747
1 parent b112c18 commit decf2cf

6 files changed

Lines changed: 216 additions & 101 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,65 @@
1717

1818
package org.openapitools.codegen.languages;
1919

20-
import io.swagger.v3.oas.models.OpenAPI;
21-
import io.swagger.v3.oas.models.media.Schema;
22-
import io.swagger.v3.oas.models.parameters.Parameter;
23-
import lombok.Getter;
24-
import lombok.Setter;
20+
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.form;
21+
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.simple;
22+
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
23+
import static org.openapitools.codegen.utils.StringUtils.camelize;
24+
import static org.openapitools.codegen.utils.StringUtils.underscore;
25+
26+
import java.io.File;
27+
import java.text.SimpleDateFormat;
28+
import java.util.ArrayList;
29+
import java.util.Arrays;
30+
import java.util.Collections;
31+
import java.util.Date;
32+
import java.util.EnumSet;
33+
import java.util.HashMap;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Locale;
37+
import java.util.Map;
38+
import java.util.Set;
39+
import java.util.TreeSet;
40+
import java.util.function.BiPredicate;
41+
import java.util.function.Function;
42+
import java.util.regex.Pattern;
43+
import java.util.stream.Collectors;
44+
import java.util.stream.Stream;
45+
2546
import org.apache.commons.io.FilenameUtils;
2647
import org.apache.commons.lang3.StringUtils;
2748
import org.checkerframework.checker.nullness.qual.Nullable;
28-
import org.openapitools.codegen.*;
49+
import org.openapitools.codegen.CliOption;
50+
import org.openapitools.codegen.CodegenConfig;
51+
import org.openapitools.codegen.CodegenConstants;
2952
import org.openapitools.codegen.CodegenConstants.ENUM_PROPERTY_NAMING_TYPE;
3053
import org.openapitools.codegen.CodegenConstants.MODEL_PROPERTY_NAMING_TYPE;
3154
import org.openapitools.codegen.CodegenConstants.PARAM_NAMING_TYPE;
32-
import org.openapitools.codegen.meta.features.*;
55+
import org.openapitools.codegen.CodegenModel;
56+
import org.openapitools.codegen.CodegenOperation;
57+
import org.openapitools.codegen.CodegenParameter;
58+
import org.openapitools.codegen.CodegenProperty;
59+
import org.openapitools.codegen.CodegenType;
60+
import org.openapitools.codegen.DefaultCodegen;
61+
import org.openapitools.codegen.GeneratorLanguage;
62+
import org.openapitools.codegen.meta.features.ClientModificationFeature;
63+
import org.openapitools.codegen.meta.features.DocumentationFeature;
64+
import org.openapitools.codegen.meta.features.GlobalFeature;
65+
import org.openapitools.codegen.meta.features.SchemaSupportFeature;
66+
import org.openapitools.codegen.meta.features.SecurityFeature;
67+
import org.openapitools.codegen.meta.features.WireFormatFeature;
3368
import org.openapitools.codegen.model.ModelMap;
3469
import org.openapitools.codegen.model.ModelsMap;
3570
import org.openapitools.codegen.utils.ModelUtils;
3671
import org.slf4j.Logger;
3772
import org.slf4j.LoggerFactory;
3873

39-
import java.io.File;
40-
import java.text.SimpleDateFormat;
41-
import java.util.*;
42-
import java.util.function.BiPredicate;
43-
import java.util.function.Function;
44-
import java.util.regex.Pattern;
45-
import java.util.stream.Collectors;
46-
import java.util.stream.Stream;
47-
48-
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.form;
49-
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.simple;
50-
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
51-
import static org.openapitools.codegen.utils.StringUtils.camelize;
52-
import static org.openapitools.codegen.utils.StringUtils.underscore;
74+
import io.swagger.v3.oas.models.OpenAPI;
75+
import io.swagger.v3.oas.models.media.Schema;
76+
import io.swagger.v3.oas.models.parameters.Parameter;
77+
import lombok.Getter;
78+
import lombok.Setter;
5379

5480
public abstract class AbstractTypeScriptClientCodegen extends DefaultCodegen implements CodegenConfig {
5581

@@ -1023,22 +1049,97 @@ protected void addImport(CodegenModel m, String type) {
10231049
}
10241050
}
10251051

1052+
/**
1053+
* Override to fix the inner enum naming issue for maps/arrays of enums.
1054+
* <p>
1055+
* The parent implementation uses toEnumName(baseItem) which produces a generic name
1056+
* like "InnerEnum" based on the inner item's name. This override first calculates
1057+
* the correct property.enumName, then uses it in the datatypeWithEnum replacement.
1058+
* </p>
1059+
*
1060+
* @param property Codegen property
1061+
*/
1062+
@Override
1063+
protected void updateDataTypeWithEnumForArray(CodegenProperty property) {
1064+
CodegenProperty baseItem = property.items;
1065+
while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap)
1066+
|| Boolean.TRUE.equals(baseItem.isArray))) {
1067+
baseItem = baseItem.items;
1068+
}
1069+
1070+
if (baseItem != null) {
1071+
// First, set the property's enumName using the property itself (not the inner item)
1072+
// This ensures the correct enum name (e.g., "OptionsEnum") is used
1073+
// instead of the generic inner item name (e.g., "InnerEnum")
1074+
property.enumName = toEnumName(property);
1075+
1076+
// Now use property.enumName for datatypeWithEnum
1077+
property.datatypeWithEnum = property.datatypeWithEnum.replace(baseItem.baseType, property.enumName);
1078+
1079+
// set default value for variable with inner enum
1080+
if (property.defaultValue != null) {
1081+
property.defaultValue = property.defaultValue.replace(baseItem.baseType, property.enumName);
1082+
}
1083+
1084+
updateCodegenPropertyEnum(property);
1085+
}
1086+
}
1087+
1088+
/**
1089+
* Override to fix the inner enum naming issue for map properties.
1090+
* <p>
1091+
* The parent implementation uses {@code toEnumName(baseItem)} which produces "InnerEnum"
1092+
* for properties whose inner item has a generic name like "inner". We instead
1093+
* calculate the enumName from the property itself first, then use it in the replacement.
1094+
* </p>
1095+
*
1096+
* @param property Codegen property
1097+
*/
1098+
@Override
1099+
protected void updateDataTypeWithEnumForMap(CodegenProperty property) {
1100+
CodegenProperty baseItem = property.items;
1101+
while (baseItem != null && (Boolean.TRUE.equals(baseItem.isMap)
1102+
|| Boolean.TRUE.equals(baseItem.isArray))) {
1103+
baseItem = baseItem.items;
1104+
}
1105+
1106+
if (baseItem != null) {
1107+
// First, set the property's enumName using the property itself (not the inner item)
1108+
property.enumName = toEnumName(property);
1109+
1110+
// Now use property.enumName for datatypeWithEnum instead of toEnumName(baseItem)
1111+
// This ensures the correct enum name (e.g., "ProjectRolesEnum") is used
1112+
// instead of the generic inner item name (e.g., "InnerEnum")
1113+
property.datatypeWithEnum = property.datatypeWithEnum.replace(baseItem.baseType + ">", property.enumName + ">");
1114+
1115+
// set default value for variable with inner enum
1116+
if (property.defaultValue != null) {
1117+
property.defaultValue = property.defaultValue.replace(", " + property.items.baseType, ", " + property.enumName);
1118+
}
1119+
1120+
updateCodegenPropertyEnum(property);
1121+
}
1122+
}
1123+
10261124
@Override
10271125
public ModelsMap postProcessModels(ModelsMap objs) {
10281126
// process enum in models
10291127
List<ModelMap> models = postProcessModelsEnum(objs).getModels();
10301128
for (ModelMap mo : models) {
10311129
CodegenModel cm = mo.getModel();
10321130
cm.imports = new TreeSet<>(cm.imports);
1131+
10331132
// name enum with model name, e.g. StatusEnum => Pet.StatusEnum
1133+
// This applies to both direct enum properties (isEnum) and properties containing
1134+
// inner enums (isInnerEnum) like maps or arrays of enums.
10341135
for (CodegenProperty var : cm.vars) {
1035-
if (Boolean.TRUE.equals(var.isEnum)) {
1136+
if (Boolean.TRUE.equals(var.isEnum) || Boolean.TRUE.equals(var.isInnerEnum)) {
10361137
var.datatypeWithEnum = var.datatypeWithEnum.replace(var.enumName, cm.classname + classEnumSeparator + var.enumName);
10371138
}
10381139
}
10391140
if (cm.parent != null) {
10401141
for (CodegenProperty var : cm.allVars) {
1041-
if (Boolean.TRUE.equals(var.isEnum)) {
1142+
if (Boolean.TRUE.equals(var.isEnum) || Boolean.TRUE.equals(var.isInnerEnum)) {
10421143
var.datatypeWithEnum = var.datatypeWithEnum
10431144
.replace(var.enumName, cm.classname + classEnumSeparator + var.enumName);
10441145
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAngularClientCodegen.java

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -406,31 +406,6 @@ private String applyLocalTypeMapping(String type) {
406406
return type;
407407
}
408408

409-
@Override
410-
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
411-
super.postProcessModelProperty(model, property);
412-
if (property.isInnerEnum && property.items != null && property.enumName != null) {
413-
// Fix maps/arrays of inner enums to use the correct enum name.
414-
// The DefaultCodegen.updateDataTypeWithEnumForMap uses toEnumName(innerItem) which
415-
// returns a name based on the inner item's name (e.g., "InnerEnum" for "inner").
416-
// But the generated TypeScript enum uses property.enumName (e.g., "RuleMsgOperationReportingOptionsEnum").
417-
// We replace the wrong inner enum name with the correct property enum name.
418-
// The classname prefix will be added later by postProcessModels in AbstractTypeScriptClientCodegen.
419-
CodegenProperty innerMostItem = property.items;
420-
while (innerMostItem != null && (Boolean.TRUE.equals(innerMostItem.isMap)
421-
|| Boolean.TRUE.equals(innerMostItem.isArray))) {
422-
innerMostItem = innerMostItem.items;
423-
}
424-
if (innerMostItem != null && innerMostItem.enumName != null
425-
&& !innerMostItem.enumName.equals(property.enumName)) {
426-
// Replace the wrong enum name with the correct one (without classname prefix)
427-
property.datatypeWithEnum = property.datatypeWithEnum.replace(
428-
innerMostItem.enumName, property.enumName);
429-
property.dataType = property.datatypeWithEnum;
430-
}
431-
}
432-
}
433-
434409
@Override
435410
public void postProcessParameter(CodegenParameter parameter) {
436411
super.postProcessParameter(parameter);

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Sc
383383

384384
@Override
385385
public ModelsMap postProcessModels(ModelsMap objs) {
386+
// postProcessModelsEnum applies inner enum fixes via the parent class override
386387
List<ModelMap> models = postProcessModelsEnum(objs).getModels();
387388

388389
// process enum and custom properties in models
@@ -800,7 +801,8 @@ private ExtendedCodegenModel processCodeGenModel(ExtendedCodegenModel cm) {
800801
for (CodegenProperty cpVar : cm.allVars) {
801802
ExtendedCodegenProperty var = (ExtendedCodegenProperty) cpVar;
802803

803-
if (Boolean.TRUE.equals(var.isEnum)) {
804+
// Handle both direct enum properties and inner enums (maps/arrays of enums)
805+
if (Boolean.TRUE.equals(var.isEnum) || Boolean.TRUE.equals(var.isInnerEnum)) {
804806
var.datatypeWithEnum = var.datatypeWithEnum
805807
.replace(var.enumName, cm.classname + var.enumName);
806808
}
@@ -845,7 +847,9 @@ private ExtendedCodegenModel processCodeGenModel(ExtendedCodegenModel cm) {
845847

846848
private boolean processCodegenProperty(ExtendedCodegenProperty var, String parentClassName, Object xEntityId) {
847849
// name enum with model name, e.g. StatusEnum => PetStatusEnum
848-
if (Boolean.TRUE.equals(var.isEnum)) {
850+
// This applies to both direct enum properties (isEnum) and properties containing
851+
// inner enums (isInnerEnum) like maps or arrays of enums.
852+
if (Boolean.TRUE.equals(var.isEnum) || Boolean.TRUE.equals(var.isInnerEnum)) {
849853
// behaviour for enum names is specific for Typescript Fetch, not using namespaces
850854
var.datatypeWithEnum = var.datatypeWithEnum.replace(var.enumName, parentClassName + var.enumName);
851855

@@ -1525,11 +1529,11 @@ public class ExtendedCodegenModel extends CodegenModel {
15251529
public Set<CodegenProperty> oneOfPrimitives = new HashSet<>();
15261530
@Getter @Setter
15271531
public CodegenDiscriminator.MappedModel selfReferencingDiscriminatorMapping;
1528-
1532+
15291533
public boolean isEntity; // Is a model containing an "id" property marked as isUniqueId
15301534
public String returnPassthrough;
15311535
public boolean hasReturnPassthroughVoid;
1532-
1536+
15331537
public boolean hasSelfReferencingDiscriminatorMapping(){
15341538
return selfReferencingDiscriminatorMapping != null;
15351539
}

modules/openapi-generator/src/main/resources/typescript-fetch/modelGenericInterfaces.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{
1616
* @deprecated
1717
{{/deprecated}}
1818
*/
19-
{{#isReadOnly}}readonly {{/isReadOnly}}{{name}}{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{#isNullable}} | null{{/isNullable}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{#isNullable}} | null{{/isNullable}}{{/isEnum}};
19+
{{#isReadOnly}}readonly {{/isReadOnly}}{{name}}{{^required}}?{{/required}}: {{{datatypeWithEnum}}}{{#isNullable}} | null{{/isNullable}};
2020
{{/vars}}
2121
}{{#hasEnums}}
2222

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/SharedTypeScriptTest.java

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
package org.openapitools.codegen.typescript;
22

3-
import org.apache.commons.io.FileUtils;
4-
import org.apache.commons.lang3.StringUtils;
5-
import org.openapitools.codegen.ClientOptInput;
6-
import org.openapitools.codegen.DefaultGenerator;
7-
import org.openapitools.codegen.Generator;
8-
import org.openapitools.codegen.TestUtils;
9-
import org.openapitools.codegen.config.CodegenConfigurator;
10-
import org.openapitools.codegen.languages.TypeScriptAxiosClientCodegen;
11-
import org.openapitools.codegen.languages.TypeScriptFetchClientCodegen;
12-
import org.testng.Assert;
13-
import org.testng.annotations.Test;
3+
import static org.openapitools.codegen.typescript.TypeScriptGroups.TYPESCRIPT;
144

155
import java.io.File;
166
import java.io.IOException;
@@ -21,8 +11,20 @@
2111
import java.util.HashMap;
2212
import java.util.List;
2313
import java.util.Map;
14+
import java.util.regex.Pattern;
15+
import java.util.stream.Stream;
2416

25-
import static org.openapitools.codegen.typescript.TypeScriptGroups.TYPESCRIPT;
17+
import org.apache.commons.io.FileUtils;
18+
import org.apache.commons.lang3.StringUtils;
19+
import org.openapitools.codegen.ClientOptInput;
20+
import org.openapitools.codegen.DefaultGenerator;
21+
import org.openapitools.codegen.Generator;
22+
import org.openapitools.codegen.TestUtils;
23+
import org.openapitools.codegen.config.CodegenConfigurator;
24+
import org.openapitools.codegen.languages.TypeScriptAxiosClientCodegen;
25+
import org.openapitools.codegen.languages.TypeScriptFetchClientCodegen;
26+
import org.testng.Assert;
27+
import org.testng.annotations.Test;
2628

2729
@Test(groups = {TYPESCRIPT})
2830
public class SharedTypeScriptTest {
@@ -67,6 +69,24 @@ private void checkAPIFile(List<File> files, String apiFileName) throws IOExcepti
6769
Assert.assertEquals(StringUtils.countMatches(apiFileContent, "import { GetCustomer200Response }"), 1);
6870
}
6971

72+
private Path findModelDefinitionFile(Path root, Pattern modelPattern) throws IOException {
73+
try (Stream<Path> paths = Files.walk(root)) {
74+
return paths
75+
.filter(Files::isRegularFile)
76+
.filter(path -> path.toString().endsWith(".ts"))
77+
.filter(path -> {
78+
try {
79+
String content = Files.readString(path);
80+
return modelPattern.matcher(content).find();
81+
} catch (IOException e) {
82+
return false;
83+
}
84+
})
85+
.findFirst()
86+
.orElseThrow(() -> new IOException("Unable to locate model definition in " + root));
87+
}
88+
}
89+
7090
@Test
7191
public void oldImportsStillPresentTest() throws IOException {
7292
Path output = Files.createTempDirectory("test");
@@ -184,4 +204,55 @@ public void givenTypeMappingContainsGenericAndMappedTypeIsUtilityTypeThenTypeIsN
184204
TestUtils.assertFileNotContains(axiosApiFile.toPath(), "AxiosPromise<UserSummary>");
185205
}
186206

207+
@Test(description = "Issue #20877, #22747 - Maps/arrays of inner enums should use correct enum name")
208+
public void givenMapWithArrayOfEnumsThenCorrectEnumNameIsUsed() throws Exception {
209+
// This tests the fix for the issue where maps with array of enums generated
210+
// "InnerEnum" type reference instead of the correct qualified enum name.
211+
// The fix is in AbstractTypeScriptClientCodegen and applies to all TypeScript generators.
212+
final String specPath = "src/test/resources/3_0/issue_19393_map_of_inner_enum.yaml";
213+
214+
List<String> generators = Arrays.asList(
215+
"typescript",
216+
"typescript-angular",
217+
"typescript-axios",
218+
"typescript-aurelia",
219+
"typescript-fetch",
220+
"typescript-inversify",
221+
"typescript-jquery",
222+
"typescript-nestjs",
223+
"typescript-nestjs-server",
224+
"typescript-node",
225+
"typescript-redux-query",
226+
"typescript-rxjs"
227+
);
228+
229+
Pattern modelDefinition = Pattern.compile("\\b(interface|type|class)\\s+EmployeeWithMultiMapOfEnum\\b");
230+
Pattern namespacedEnumRef = Pattern.compile("projectRoles[^\\n]*ProjectRolesEnum");
231+
Pattern flatEnumRef = Pattern.compile("projectRoles[^\\n]*EmployeeWithMultiMapOfEnumProjectRolesEnum");
232+
233+
for (String generatorName : generators) {
234+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
235+
output.deleteOnExit();
236+
237+
CodegenConfigurator configurator = new CodegenConfigurator()
238+
.setGeneratorName(generatorName)
239+
.setInputSpec(specPath)
240+
.setOutputDir(output.getAbsolutePath());
241+
242+
Generator generator = new DefaultGenerator();
243+
generator.opts(configurator.toClientOptInput()).generate();
244+
245+
Path modelFile = findModelDefinitionFile(output.toPath(), modelDefinition);
246+
String fileContents = Files.readString(modelFile);
247+
248+
Assert.assertFalse(fileContents.contains("InnerEnum"),
249+
generatorName + ": Should not contain 'InnerEnum' reference in " + modelFile);
250+
251+
boolean hasEnumRef = namespacedEnumRef.matcher(fileContents).find()
252+
|| flatEnumRef.matcher(fileContents).find();
253+
Assert.assertTrue(hasEnumRef,
254+
generatorName + ": Expected enum reference not found in " + modelFile);
255+
}
256+
}
257+
187258
}

0 commit comments

Comments
 (0)