Skip to content

Commit 1ca5b9b

Browse files
committed
Extensible enum for spring generator
1 parent b393592 commit 1ca5b9b

31 files changed

Lines changed: 1308 additions & 6 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
generatorName: spring
2+
outputDir: samples/server/petstore/springboot-useoneofextensibleenum
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/extended-enums.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
5+
additionalProperties:
6+
groupId: org.openapitools.openapi3
7+
documentationProvider: springfox
8+
useOneOfExtensibleEnums: true
9+
useOneOfInterfaces: true
10+
useBeanValidation: true
11+
artifactId: spring-boot-useoneofextensibleenum
12+
hideGenerationTimestamp: "true"
13+
generateBuilders: true

docs/generators/java-camel.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
102102
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
103103
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
104104
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
105+
|useOneOfExtensibleEnums|whether to generate custom extensible enumeration using the extensible enums with interface pattern| |false|
105106
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
106107
|useOptional|Use Optional container for optional parameters| |false|
107108
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|

docs/generators/spring.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
9595
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
9696
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
9797
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
98+
|useOneOfExtensibleEnums|whether to generate custom extensible enumeration using the extensible enums with interface pattern| |false|
9899
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
99100
|useOptional|Use Optional container for optional parameters| |false|
100101
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
514514
CodegenModel cm = mo.getModel();
515515
if (cm.oneOf.size() > 0) {
516516
cm.vendorExtensions.put("x-is-one-of-interface", true);
517+
517518
for (String one : cm.oneOf) {
518519
if (!additionalDataMap.containsKey(one)) {
519520
additionalDataMap.put(one, new OneOfImplementorAdditionalData(one));
@@ -522,6 +523,10 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
522523
}
523524
// if this is oneOf interface, make sure we include the necessary imports for it
524525
addImportsToOneOfInterface(modelsImports);
526+
boolean hasOfEnum = (cm.interfaceModels != null) && cm.interfaceModels.stream().anyMatch(m -> m.isEnum) ;
527+
if (hasOfEnum) {
528+
addExtensibleEnum(objs, cm, modelsImports);
529+
}
525530
}
526531
}
527532
}
@@ -8385,6 +8390,9 @@ public void addOneOfInterfaceModel(Schema cs, String type) {
83858390
addOneOfInterfaces.add(cm);
83868391
}
83878392

8393+
public void addExtensibleEnum(Map<String, ModelsMap> objs, CodegenModel cm, List<Map<String, String>> imports) {
8394+
}
8395+
83888396
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
83898397
}
83908398

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
101101

102102
public static final String CAMEL_CASE_DOLLAR_SIGN = "camelCaseDollarSign";
103103
public static final String USE_ONE_OF_INTERFACES = "useOneOfInterfaces";
104+
public static final String USE_ONE_OF_EXTENSIBLE_ENUMS = "useOneOfExtensibleEnums";
104105
public static final String LOMBOK = "lombok";
105106
public static final String DEFAULT_TEST_FOLDER = "${project.build.directory}/generated-test-sources/openapi";
106107
public static final String GENERATE_CONSTRUCTOR_WITH_ALL_ARGS = "generateConstructorWithAllArgs";
@@ -153,6 +154,8 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original}
153154
protected static final String ENUM_PROPERTY_NAMING_DESC = "Naming convention for enum properties: 'MACRO_CASE', 'legacy' and 'original'";
154155
@Getter protected ENUM_PROPERTY_NAMING_TYPE enumPropertyNaming = ENUM_PROPERTY_NAMING_TYPE.MACRO_CASE;
155156

157+
@Getter @Setter
158+
protected boolean useOneOfExtensibleEnums = false;
156159
/**
157160
* -- SETTER --
158161
* Set whether discriminator value lookup is case-sensitive or not.
@@ -570,6 +573,7 @@ public void processOpts() {
570573
convertPropertyToStringAndWriteBack(IMPLICIT_HEADERS_REGEX, this::setImplicitHeadersRegex);
571574
convertPropertyToBooleanAndWriteBack(CAMEL_CASE_DOLLAR_SIGN, this::setCamelCaseDollarSign);
572575
convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces);
576+
convertPropertyToBooleanAndWriteBack(USE_ONE_OF_EXTENSIBLE_ENUMS, this::setUseOneOfExtensibleEnums);
573577
convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming);
574578

575579
if (!StringUtils.isEmpty(parentGroupId) && !StringUtils.isEmpty(parentArtifactId) && !StringUtils.isEmpty(parentVersion)) {
@@ -621,6 +625,11 @@ public void processOpts() {
621625
// used later in recursive import in postProcessingModels
622626
importMapping.put("com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.annotation.JsonCreator");
623627

628+
if (useOneOfExtensibleEnums) {
629+
importMapping.put("DeserializationContext", "com.fasterxml.jackson.databind.DeserializationContext");
630+
importMapping.put("StdDeserializer", "com.fasterxml.jackson.databind.deser.std.StdDeserializer");
631+
importMapping.put("JsonParser", "com.fasterxml.jackson.core.JsonParser");
632+
}
624633
convertPropertyToBooleanAndWriteBack(SUPPORT_ASYNC, this::setSupportAsync);
625634
convertPropertyToStringAndWriteBack(DATE_LIBRARY, this::setDateLibrary);
626635

@@ -2395,16 +2404,51 @@ private boolean shouldBeImplicitHeader(CodegenParameter parameter) {
23952404
@Override
23962405
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
23972406
if (jackson) {
2398-
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) {
2399-
Map<String, String> oneImport = new HashMap<>();
2400-
oneImport.put("import", importMapping.get(i));
2401-
if (!imports.contains(oneImport)) {
2402-
imports.add(oneImport);
2403-
}
2407+
addAdditionalImports(imports, "JsonSubTypes", "JsonTypeInfo");
2408+
}
2409+
}
2410+
2411+
protected void addAdditionalImports(List<Map<String, String>> imports, String... additionalImports) {
2412+
for (String i : additionalImports) {
2413+
Map<String, String> oneImport = new HashMap<>();
2414+
oneImport.put("import", Objects.requireNonNull(importMapping.get(i), "importMapping not found " + i));
2415+
if (!imports.contains(oneImport)) {
2416+
imports.add(oneImport);
24042417
}
24052418
}
24062419
}
24072420

2421+
@Override
2422+
public void addExtensibleEnum(Map<String, ModelsMap> objs, CodegenModel cm, List<Map<String, String>> imports) {
2423+
if (jackson) {
2424+
addAdditionalImports(imports,
2425+
"DeserializationContext",
2426+
"JsonDeserialize",
2427+
"StdDeserializer",
2428+
"JsonValue",
2429+
"JsonParser",
2430+
"IOException");
2431+
2432+
ExtensibleEnumParam param = new ExtensibleEnumParam();
2433+
cm.vendorExtensions.put("x-extensible-enum", param);
2434+
if (cm.oneOf.contains("String")) {
2435+
param.useString = true;
2436+
param.property = cm.getComposedSchemas().getOneOf().stream().filter(p -> "String".equals(p.datatypeWithEnum))
2437+
.findFirst().orElseThrow();
2438+
// The name of the String property looks like oneOf1
2439+
// Use a sensible name instead
2440+
param.stringClassName = cm.classname + "String";
2441+
2442+
}
2443+
}
2444+
}
2445+
2446+
static class ExtensibleEnumParam {
2447+
public boolean useString;
2448+
public String stringClassName;
2449+
public CodegenProperty property;
2450+
}
2451+
24082452
@Override
24092453
public List<VendorExtension> getSupportedVendorExtensions() {
24102454
List<VendorExtension> extensions = super.getSupportedVendorExtensions();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ public SpringCodegen() {
237237
cliOptions
238238
.add(CliOption.newBoolean(RETURN_SUCCESS_CODE, "Generated server returns 2xx code", returnSuccessCode));
239239
cliOptions.add(CliOption.newBoolean(SPRING_CONTROLLER, "Annotate the generated API as a Spring Controller", useSpringController));
240+
cliOptions.add(CliOption.newBoolean(USE_ONE_OF_EXTENSIBLE_ENUMS, "whether to generate custom extensible enumeration using the extensible enums with interface pattern", useOneOfExtensibleEnums));
240241

241242
CliOption requestMappingOpt = new CliOption(REQUEST_MAPPING_OPTION,
242243
"Where to generate the class level @RequestMapping annotation.")

modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,82 @@
66
{{>typeInfoAnnotation}}
77
{{/discriminator}}
88
{{>generatedAnnotation}}
9+
{{#vendorExtensions.x-extensible-enum}}
10+
@JsonDeserialize(using = {{classname}}.EnumDeserializer.class)
11+
{{/vendorExtensions.x-extensible-enum}}
912
public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
1013
{{#discriminator}}
1114
public {{propertyType}} {{propertyGetter}}();
1215
{{/discriminator}}
16+
{{^discriminator}}
17+
{{#vendorExtensions.x-extensible-enum}}
18+
{{#useBeanValidation}}@Valid{{/useBeanValidation}}
19+
@JsonValue
20+
String getValue();
21+
22+
static {{classname}} fromValue(String value) {
23+
{{#interfaceModels}}
24+
try {
25+
return {{classname}}.fromValue(value);
26+
} catch (IllegalArgumentException e) {
27+
// continue
28+
}
29+
{{/interfaceModels}}
30+
{{#useString}}
31+
return new {{stringClassName}}(value);
32+
{{/useString}}
33+
{{^useString}}
34+
throw new IllegalArgumentException(value + " not supported in classes " + Arrays.asList({{#interfaceModels}}"{{classname}}"{{^-last}}, {{/-last}}{{/interfaceModels}}));
35+
{{/useString}}
36+
}
37+
38+
// custom jackson deserializer
39+
class EnumDeserializer extends StdDeserializer<{{classname}}> {
40+
41+
public EnumDeserializer() {
42+
super({{classname}}.class);
43+
}
44+
45+
@Override
46+
public {{classname}} deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
47+
String value = p.readValueAs(String.class);
48+
return {{classname}}.fromValue(value);
49+
}
50+
}
51+
{{#useString}}
52+
{{#property.description}}
53+
/**
54+
* {{{.}}}
55+
*/
56+
{{/property.description}}
57+
class {{stringClassName}} implements {{classname}} {
58+
private final String value;
59+
public {{stringClassName}}(String value) {
60+
this.value = value;
61+
}
62+
63+
{{#useBeanValidation}}@NotNull {{#property}}{{>beanValidationCore}} {{/property}}{{/useBeanValidation}}
64+
@Override
65+
public String getValue() {
66+
return value;
67+
}
68+
@Override
69+
public String toString() {
70+
return "{{stringClassName}}:" + value;
71+
}
72+
73+
@Override
74+
public final boolean equals(Object o) {
75+
if (!(o instanceof {{stringClassName}} )) return false;
76+
return value.equals((({{stringClassName}})o).value);
77+
}
78+
79+
@Override
80+
public int hashCode() {
81+
return value.hashCode();
82+
}
83+
}
84+
{{/useString}}
85+
{{/vendorExtensions.x-extensible-enum}}
86+
{{/discriminator}}
1387
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5501,4 +5501,16 @@ public void testEnumFieldShouldBeFinal_issue21018() throws IOException {
55015501
JavaFileAssert.assertThat(files.get("SomeObject.java"))
55025502
.fileContains("private final String value");
55035503
}
5504+
5505+
@Test
5506+
public void testExtensibleEnum() throws IOException {
5507+
SpringCodegen codegen = new SpringCodegen();
5508+
codegen.setUseOneOfExtensibleEnums(true);
5509+
codegen.setUseBeanValidation(true);
5510+
Map<String, File> files = generateFiles(codegen,"src/test/resources/3_0/java/extended-enums.yaml");
5511+
5512+
JavaFileAssert.assertThat(files.get("Country.java"))
5513+
.fileContains("axxx-ue;");
5514+
5515+
}
55045516
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Extended enum
4+
description: ''
5+
version: v1
6+
servers:
7+
- url: http://localhost
8+
description: test
9+
paths:
10+
/test:
11+
get:
12+
operationId: test
13+
responses:
14+
200:
15+
description: Successful operation
16+
content:
17+
application/json:
18+
schema:
19+
$ref: '#/components/schemas/Address'
20+
components:
21+
schemas:
22+
Address:
23+
properties:
24+
street:
25+
type: string
26+
countryOrString:
27+
$ref: '#/components/schemas/Country'
28+
countryOrOther:
29+
$ref: '#/components/schemas/countryOrOther'
30+
Country:
31+
oneOf:
32+
- $ref: '#/components/schemas/SubsetCountry'
33+
- type: string
34+
description: other coutries
35+
title: OtherCountry
36+
pattern: ^[A-Z]{2}$
37+
SubsetCountry:
38+
type: string
39+
enum:
40+
- BE
41+
- LU
42+
- NL
43+
countryOrOther:
44+
oneOf:
45+
- $ref: '#/components/schemas/SubsetCountry'
46+
- $ref: '#/components/schemas/OtherCountryEnum'
47+
OtherCountryEnum:
48+
type: string
49+
enum:
50+
- IT
51+
- DE
52+
- FR
53+
54+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# OpenAPI Generator Ignore
2+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
3+
4+
# Use this file to prevent files from being overwritten by the generator.
5+
# The patterns follow closely to .gitignore or .dockerignore.
6+
7+
# As an example, the C# client generator defines ApiClient.cs.
8+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9+
#ApiClient.cs
10+
11+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
12+
#foo/*/qux
13+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14+
15+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16+
#foo/**/qux
17+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18+
19+
# You can also negate patterns with an exclamation (!).
20+
# For example, you can ignore all files in a docs folder with the file extension .md:
21+
#docs/*.md
22+
# Then explicitly reverse the ignore rule for a single file:
23+
#!docs/README.md

0 commit comments

Comments
 (0)