forked from loafle/openapi-generator-original
Add option to generate a fully sealed
model in the JavaSpring
generator (#20503)
* Generated sealed interfaces for oneOf * Add generated data * Add also modifier * Allow sealed for everything * Fully seal model * Disable html escaping * Update modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com> * Update docs * Check all oneOf scenarios * Fix failed scenario * Fix comments * Remove unused import * Adapt pom.xml also * Add comment and remove unused function --------- Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
This commit is contained in:
parent
d5866feb2d
commit
cd7cdb1e24
@ -103,6 +103,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|
||||
|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|
|
||||
|useOptional|Use Optional container for optional parameters| |false|
|
||||
|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|
|
||||
|useSealed|Whether to generate sealed model interfaces and classes| |false|
|
||||
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot 3.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|
||||
|useSpringController|Annotate the generated API as a Spring Controller| |false|
|
||||
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
|
||||
|
@ -96,6 +96,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|
||||
|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|
|
||||
|useOptional|Use Optional container for optional parameters| |false|
|
||||
|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|
|
||||
|useSealed|Whether to generate sealed model interfaces and classes| |false|
|
||||
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot 3.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|
||||
|useSpringController|Annotate the generated API as a Spring Controller| |false|
|
||||
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
|
||||
|
30
flake.lock
generated
30
flake.lock
generated
@ -1,12 +1,15 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -17,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1678083093,
|
||||
"narHash": "sha256-eTkS9GcdSAYA3cE9zCAAs9wY3+oM2zT45ydIkAcEFFQ=",
|
||||
"lastModified": 1737989155,
|
||||
"narHash": "sha256-TFJAGK7tt/jj1v747xNOzopxZ4odBIeDi6EJlYDg/bI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "684306b246d05168e42425a3610df7e2c4d51fcd",
|
||||
"rev": "5861228f6e9e9dd5d3f8e0a26411f682fdade93a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -35,6 +38,21 @@
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
@ -59,6 +59,9 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
|
||||
public Set<String> oneOf = new TreeSet<>();
|
||||
public Set<String> allOf = new TreeSet<>();
|
||||
|
||||
// direct descendants that are allowed to extend the current model
|
||||
public List<String> permits = new ArrayList<>();
|
||||
|
||||
// The schema name as written in the OpenAPI document
|
||||
// If it's a reserved word, it will be escaped.
|
||||
@Getter @Setter
|
||||
@ -922,6 +925,7 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
|
||||
Objects.equals(parentModel, that.parentModel) &&
|
||||
Objects.equals(interfaceModels, that.interfaceModels) &&
|
||||
Objects.equals(children, that.children) &&
|
||||
Objects.equals(permits, that.permits) &&
|
||||
Objects.equals(anyOf, that.anyOf) &&
|
||||
Objects.equals(oneOf, that.oneOf) &&
|
||||
Objects.equals(allOf, that.allOf) &&
|
||||
@ -975,7 +979,7 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getParent(), getParentSchema(), getInterfaces(), getAllParents(), getParentModel(),
|
||||
getInterfaceModels(), getChildren(), anyOf, oneOf, allOf, getName(), getSchemaName(), getClassname(), getTitle(),
|
||||
getInterfaceModels(), getChildren(), permits, anyOf, oneOf, allOf, getName(), getSchemaName(), getClassname(), getTitle(),
|
||||
getDescription(), getClassVarName(), getModelJson(), getDataType(), getXmlPrefix(), getXmlNamespace(),
|
||||
getXmlName(), getClassFilename(), getUnescapedDescription(), getDiscriminator(), getDefaultValue(),
|
||||
getArrayModelType(), isAlias, isString, isInteger, isLong, isNumber, isNumeric, isFloat, isDouble,
|
||||
@ -1005,6 +1009,7 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
|
||||
sb.append(", allParents=").append(allParents);
|
||||
sb.append(", parentModel=").append(parentModel);
|
||||
sb.append(", children=").append(children != null ? children.size() : "[]");
|
||||
sb.append(", permits=").append(permits != null ? permits.size() : "[]");
|
||||
sb.append(", anyOf=").append(anyOf);
|
||||
sb.append(", oneOf=").append(oneOf);
|
||||
sb.append(", allOf=").append(allOf);
|
||||
|
@ -621,16 +621,25 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
|
||||
// Let parent know about all its children
|
||||
for (Map.Entry<String, CodegenModel> allModelsEntry : allModels.entrySet()) {
|
||||
String name = allModelsEntry.getKey();
|
||||
CodegenModel cm = allModelsEntry.getValue();
|
||||
CodegenModel parent = allModels.get(cm.getParent());
|
||||
if (parent != null) {
|
||||
if (!parent.permits.contains(cm.classname) && parent.permits.stream()
|
||||
.noneMatch(name -> name.equals(cm.getName()))) {
|
||||
parent.permits.add(cm.classname);
|
||||
}
|
||||
}
|
||||
// if a discriminator exists on the parent, don't add this child to the inheritance hierarchy
|
||||
// TODO Determine what to do if the parent discriminator name == the grandparent discriminator name
|
||||
while (parent != null) {
|
||||
if (parent.getChildren() == null) {
|
||||
parent.setChildren(new ArrayList<>());
|
||||
}
|
||||
parent.getChildren().add(cm);
|
||||
if (parent.getChildren().stream().map(CodegenModel::getName)
|
||||
.noneMatch(name -> name.equals(cm.getName()))) {
|
||||
parent.getChildren().add(cm);
|
||||
}
|
||||
|
||||
parent.hasChildren = true;
|
||||
Schema parentSchema = this.openAPI.getComponents().getSchemas().get(parent.schemaName);
|
||||
if (parentSchema == null) {
|
||||
@ -2704,7 +2713,6 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType);
|
||||
} else {
|
||||
m.anyOf.add(languageType);
|
||||
|
||||
}
|
||||
} else if (composed.getOneOf() != null) {
|
||||
if (m.oneOf.contains(languageType)) {
|
||||
@ -2751,6 +2759,9 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
m.anyOf.add(modelName);
|
||||
} else if (composed.getOneOf() != null) {
|
||||
m.oneOf.add(modelName);
|
||||
if (!m.permits.contains(modelName)) {
|
||||
m.permits.add(modelName);
|
||||
}
|
||||
} else if (composed.getAllOf() != null) {
|
||||
m.allOf.add(modelName);
|
||||
} else {
|
||||
|
@ -113,6 +113,7 @@ public class SpringCodegen extends AbstractJavaCodegen
|
||||
public static final String REQUEST_MAPPING_OPTION = "requestMappingMode";
|
||||
public static final String USE_REQUEST_MAPPING_ON_CONTROLLER = "useRequestMappingOnController";
|
||||
public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface";
|
||||
public static final String USE_SEALED = "useSealed";
|
||||
|
||||
@Getter public enum RequestMappingMode {
|
||||
api_interface("Generate the @RequestMapping annotation on the generated Api Interface."),
|
||||
@ -151,6 +152,7 @@ public class SpringCodegen extends AbstractJavaCodegen
|
||||
protected boolean performBeanValidation = false;
|
||||
@Setter protected boolean apiFirst = false;
|
||||
protected boolean useOptional = false;
|
||||
@Setter protected boolean useSealed = false;
|
||||
@Setter protected boolean virtualService = false;
|
||||
@Setter protected boolean hateoas = false;
|
||||
@Setter protected boolean returnSuccessCode = false;
|
||||
@ -229,6 +231,8 @@ public class SpringCodegen extends AbstractJavaCodegen
|
||||
.add(CliOption.newBoolean(USE_BEANVALIDATION, "Use BeanValidation API annotations", useBeanValidation));
|
||||
cliOptions.add(CliOption.newBoolean(PERFORM_BEANVALIDATION,
|
||||
"Use Bean Validation Impl. to perform BeanValidation", performBeanValidation));
|
||||
cliOptions.add(CliOption.newBoolean(USE_SEALED,
|
||||
"Whether to generate sealed model interfaces and classes"));
|
||||
cliOptions.add(CliOption.newBoolean(API_FIRST,
|
||||
"Generate the API from the OAI spec at server compile time (API first approach)", apiFirst));
|
||||
cliOptions
|
||||
@ -423,6 +427,7 @@ public class SpringCodegen extends AbstractJavaCodegen
|
||||
convertPropertyToBooleanAndWriteBack(GENERATE_CONSTRUCTOR_WITH_REQUIRED_ARGS, value -> this.generatedConstructorWithRequiredArgs=value);
|
||||
convertPropertyToBooleanAndWriteBack(RETURN_SUCCESS_CODE, this::setReturnSuccessCode);
|
||||
convertPropertyToBooleanAndWriteBack(USE_SWAGGER_UI, this::setUseSwaggerUI);
|
||||
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);
|
||||
if (getDocumentationProvider().equals(DocumentationProvider.NONE)) {
|
||||
this.setUseSwaggerUI(false);
|
||||
}
|
||||
|
@ -6,7 +6,12 @@
|
||||
<name>{{artifactId}}</name>
|
||||
<version>{{artifactVersion}}</version>
|
||||
<properties>
|
||||
{{#useSealed}}
|
||||
<java.version>17</java.version>
|
||||
{{/useSealed}}
|
||||
{{^useSealed}}
|
||||
<java.version>1.8</java.version>
|
||||
{{/useSealed}}
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
@ -6,7 +6,12 @@
|
||||
<name>{{artifactId}}</name>
|
||||
<version>{{artifactVersion}}</version>
|
||||
<properties>
|
||||
{{#useSealed}}
|
||||
<java.version>17</java.version>
|
||||
{{/useSealed}}
|
||||
{{^useSealed}}
|
||||
<java.version>8</java.version>
|
||||
{{/useSealed}}
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
@ -6,7 +6,12 @@
|
||||
<name>{{artifactId}}</name>
|
||||
<version>{{artifactVersion}}</version>
|
||||
<properties>
|
||||
{{#useSealed}}
|
||||
<java.version>17</java.version>
|
||||
{{/useSealed}}
|
||||
{{^useSealed}}
|
||||
<java.version>1.8</java.version>
|
||||
{{/useSealed}}
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
@ -6,7 +6,7 @@
|
||||
{{>typeInfoAnnotation}}
|
||||
{{/discriminator}}
|
||||
{{>generatedAnnotation}}
|
||||
public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
|
||||
public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
|
||||
{{#discriminator}}
|
||||
public {{propertyType}} {{propertyGetter}}();
|
||||
{{/discriminator}}
|
||||
|
1
modules/openapi-generator/src/main/resources/JavaSpring/permits.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/JavaSpring/permits.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{#useSealed}}{{#permits}}{{#-first}}permits {{/-first}}{{{.}}}{{^-last}}, {{/-last}}{{/permits}} {{/useSealed}}
|
@ -31,7 +31,7 @@
|
||||
{{#vendorExtensions.x-class-extra-annotation}}
|
||||
{{{vendorExtensions.x-class-extra-annotation}}}
|
||||
{{/vendorExtensions.x-class-extra-annotation}}
|
||||
public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
|
||||
public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
|
||||
{{#serializableModel}}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
1
modules/openapi-generator/src/main/resources/JavaSpring/sealed.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/JavaSpring/sealed.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{#useSealed}}{{#permits.0}}sealed {{/permits.0}}{{^permits.0}}{{^vendorExtensions.x-is-one-of-interface}}final {{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}
|
@ -2259,6 +2259,78 @@ public class SpringCodegenTest {
|
||||
.assertParameter("pageable").hasType("Pageable");
|
||||
}
|
||||
|
||||
@DataProvider(name = "sealedScenarios")
|
||||
public static Object[][] sealedScenarios() {
|
||||
return new Object[][]{
|
||||
{"oneof_polymorphism_and_inheritance.yaml", Map.of(
|
||||
"Foo.java", "public final class Foo extends Entity implements FooRefOrValue",
|
||||
"FooRef.java", "public final class FooRef extends EntityRef implements FooRefOrValue",
|
||||
"FooRefOrValue.java", "public sealed interface FooRefOrValue permits Foo, FooRef ",
|
||||
"Entity.java", "public sealed class Entity extends RepresentationModel<Entity> permits Bar, BarCreate, Foo, Pasta, Pizza {")},
|
||||
{"oneOf_additionalProperties.yaml", Map.of(
|
||||
"SchemaA.java", "public final class SchemaA extends RepresentationModel<SchemaA> implements PostRequest {",
|
||||
"PostRequest.java", "public sealed interface PostRequest permits SchemaA {")},
|
||||
{"oneOf_array.yaml", Map.of(
|
||||
"MyExampleGet200Response.java", "public interface MyExampleGet200Response")},
|
||||
{"oneOf_duplicateArray.yaml", Map.of(
|
||||
"Example.java", "public interface Example {")},
|
||||
{"oneOf_nonPrimitive.yaml", Map.of(
|
||||
"Example.java", "public interface Example {")},
|
||||
{"oneOf_primitive.yaml", Map.of(
|
||||
"Child.java", "public final class Child extends RepresentationModel<Child> implements Example {",
|
||||
"Example.java", "public sealed interface Example permits Child {")},
|
||||
{"oneOf_primitiveAndArray.yaml", Map.of(
|
||||
"Example.java", "public interface Example {")},
|
||||
{"oneOf_reuseRef.yaml", Map.of(
|
||||
"Fruit.java", "public sealed interface Fruit permits Apple, Banana {",
|
||||
"Banana.java", "public final class Banana extends RepresentationModel<Banana> implements Fruit {",
|
||||
"Apple.java", "public final class Apple extends RepresentationModel<Apple> implements Fruit {")},
|
||||
{"oneOf_twoPrimitives.yaml", Map.of(
|
||||
"MyExamplePostRequest.java", "public interface MyExamplePostRequest {")},
|
||||
{"oneOfArrayMapImport.yaml", Map.of(
|
||||
"Fruit.java", "public interface Fruit {",
|
||||
"Grape.java", "public final class Grape extends RepresentationModel<Grape> {",
|
||||
"Apple.java", "public final class Apple extends RepresentationModel<Apple> {")},
|
||||
{"oneOfDiscriminator.yaml", Map.of(
|
||||
"FruitAllOfDisc.java", "public sealed interface FruitAllOfDisc permits AppleAllOfDisc, BananaAllOfDisc {",
|
||||
"FruitReqDisc.java", "public sealed interface FruitReqDisc permits AppleReqDisc, BananaReqDisc {\n")}
|
||||
};
|
||||
}
|
||||
|
||||
@Test(dataProvider = "sealedScenarios", description = "sealed scenarios")
|
||||
public void sealedScenarios(String apiFile, Map<String, String> definitions) throws IOException {
|
||||
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
|
||||
output.deleteOnExit();
|
||||
String outputPath = output.getAbsolutePath().replace('\\', '/');
|
||||
OpenAPI openAPI = new OpenAPIParser()
|
||||
.readLocation("src/test/resources/3_0/" + apiFile, null, new ParseOptions()).getOpenAPI();
|
||||
|
||||
SpringCodegen codegen = new SpringCodegen();
|
||||
codegen.setOutputDir(output.getAbsolutePath());
|
||||
codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
|
||||
codegen.setUseOneOfInterfaces(true);
|
||||
codegen.setUseSealed(true);
|
||||
|
||||
ClientOptInput input = new ClientOptInput();
|
||||
input.openAPI(openAPI);
|
||||
input.config(codegen);
|
||||
|
||||
DefaultGenerator generator = new DefaultGenerator();
|
||||
codegen.setHateoas(true);
|
||||
generator.setGenerateMetadata(false); // skip metadata and ↓ only generate models
|
||||
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
|
||||
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
|
||||
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
|
||||
generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false");
|
||||
|
||||
codegen.setLegacyDiscriminatorBehavior(false);
|
||||
|
||||
generator.opts(input).generate();
|
||||
|
||||
definitions.forEach((file, check) ->
|
||||
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/" + file), check));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSetDefaultValueForMultipleArrayItems() throws IOException {
|
||||
Map<String, Object> additionalProperties = new HashMap<>();
|
||||
|
Loading…
x
Reference in New Issue
Block a user