Skip to content

Commit

Permalink
[Feature][Java/Spring] Support Discriminator Based OneOf Interface (#…
Browse files Browse the repository at this point in the history
…11650)

* Fix #5381
added x-is-one-of-interface extension for oneOf interface in mustache
template

* Fix #5381
fixed name of model from UNKNOWN_BASE_TYPE to right one in api: operationId + OneOf

Fix #5381
parcelableModel is not required

* Fix #5381
removed not needed methods

* Fix #5381
catch NPE cases in preprocessOpenAPI
updated samples

* Fix #5381
fixed generation of oneOf Models

* Fix #5381
addOneOfInterfaceModel only for cases when useOneOfInterfaces is true and for spring

* Fix #5381
NPE fix

* Fix #5381
spring: fixed use of oneOf Models in API

* Fix #5381
implementing oneOf for spring lib overriding methods with different behavior from default

* Fix #5381
added x-is-one-of-interface extension for oneOf interface in mustache
template

* Fix #5381
fixed name of model from UNKNOWN_BASE_TYPE to right one in api: operationId + OneOf

Fix #5381
removed not needed methods

Fix #5381
fixed generation of oneOf Models

Fix #5381
addOneOfInterfaceModel only for cases when useOneOfInterfaces is true and for spring

Fix #5381
NPE fix for tests

* Fix #5381
fixed handing of composed schema with array

* Fix #5381
fixed NPE in addOneOfInterfaceModel

* Fix #5381
fixed generation of oneOf models with descriminator

* Initial merge of 5.0

* Aligned with master formatting

* Corrected spacing for class names to align with samples.

* Merged master

* Updated samples

* Consolidate methods from JavaClient and SpringCodegen (mov up to AbstractJavaCodegen)

* set useLegacyDiscriminator to false, format templates

* Suport JsonTypeName, fq class name for spring.io.Resource

* Generate Samples

* Test full qualified usage of the spring Resource interface.

* Add java-camel to samples.circleci.spring profile

* Add more complex example combining inheritance and oneof-interface

* Remove x-implements Serializable from JavaClientCodegen (moved to AbstractJavaCodegen)

* Fix spacing before opening brace after extends/implements

* Generate Samples

* Add more complex example combining inheritance and oneof-interface

* Generate Samples

* Fix JsonTypeName annotation handling in Java and JavaSpring

* Content mediatype is hardcoded in api.mustache #11511

* Generate Samples

* OAS3 incorrect data type when providing a default value #11367

* Generate Samples

* Fix JsonTypeName annotation handling in Java and JavaSpring

* Generate Samples

* getIsClassnameSanitized: use null safe equals

* Fix JsonTypeName annotation handling in Java and JavaSpring (merge)

* Generate Samples

* Generate Samples

* Add oneof sample

* Generate Samples

* Giv example oas spec a meaningful name, demo usage of oneOf in Model

* Generate Samples

* Remove unnecessary JsonTypeName include, add example for JsonTypeName (Bar_Create)

* Generate Samples

* Generate Samples

Co-authored-by: Alexej <[email protected]>
Co-authored-by: JBurgess <[email protected]>
Co-authored-by: William Cheng <[email protected]>
  • Loading branch information
4 people authored Mar 16, 2022
1 parent a3fb571 commit e07c7d1
Show file tree
Hide file tree
Showing 1,030 changed files with 4,087 additions and 1,027 deletions.
1 change: 1 addition & 0 deletions .github/workflows/samples-spring.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
- samples/openapi3/server/petstore/springboot-delegate
- samples/server/petstore/spring-boot-nullable-set
- samples/server/petstore/spring-boot-defaultInterface-unhandledException
- samples/openapi3/server/petstore/spring-boot-oneof
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
Expand Down
10 changes: 10 additions & 0 deletions bin/configs/spring-boot-oneof.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/spring-boot-oneof
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
groupId: org.openapitools.openapi3
documentationProvider: springdoc
artifactId: springboot-oneof
snapshotVersion: "true"
hideGenerationTimestamp: "true"
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ public void postProcessParameter(CodegenParameter parameter) {
@Override
@SuppressWarnings("unused")
public void preprocessOpenAPI(OpenAPI openAPI) {
if (useOneOfInterfaces) {
if (useOneOfInterfaces && openAPI.getComponents() != null) {
// we process the openapi schema here to find oneOf schemas and create interface models for them
Map<String, Schema> schemas = new HashMap<>(openAPI.getComponents().getSchemas());
if (schemas == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,6 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
}
}

@SuppressWarnings("unchecked")
@Override
public Map<String, Object> postProcessModels(Map<String, Object> objs) {
// recursively add import for mapping one type to multiple imports
Expand All @@ -1333,14 +1332,12 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
}
}

List<Object> models = (List<Object>) objs.get("models");

for (Object _mo : models) {
Map<String, Object> mo = (Map<String, Object>) _mo;
// add x-implements for serializable to all models
List<Map<String, Object>> models = (List<Map<String, Object>>) objs.get("models");
for (Map<String, Object> mo : models) {
CodegenModel cm = (CodegenModel) mo.get("model");

cm.getVendorExtensions().putIfAbsent("x-implements", new ArrayList<String>());
if (this.serializableModel) {
cm.getVendorExtensions().putIfAbsent("x-implements", new ArrayList<String>());
((ArrayList<String>) cm.getVendorExtensions().get("x-implements")).add("Serializable");
}
}
Expand Down Expand Up @@ -2054,6 +2051,18 @@ private boolean shouldBeImplicitHeader(CodegenParameter parameter) {
return StringUtils.isNotBlank(implicitHeadersRegex) && parameter.baseName.matches(implicitHeadersRegex);
}

@Override
public void addImportsToOneOfInterface(List<Map<String, String>> imports) {
if (additionalProperties.containsKey(JACKSON)) {
for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) {
Map<String, String> oneImport = new HashMap<>();
oneImport.put("import", importMapping.get(i));
if (!imports.contains(oneImport)) {
imports.add(oneImport);
}
}
}
}
@Override
public List<VendorExtension> getSupportedVendorExtensions() {
List<VendorExtension> extensions = super.getSupportedVendorExtensions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ public void processOpts() {
}

super.processOpts();
useOneOfInterfaces = true;
legacyDiscriminatorBehavior = false;

if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
LOGGER.warn("The springfox documentation provider is deprecated for removal. Use the springdoc provider instead.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import javax.annotation.Generated;
{{>enumOuterClass}}
{{/isEnum}}
{{^isEnum}}
{{>pojo}}
{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}}
{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{>additionalModelTypeAnnotations}}
{{#withXml}}
{{>xmlAnnotation}}
{{/withXml}}
{{#discriminator}}
{{>typeInfoAnnotation}}
{{/discriminator}}
{{>generatedAnnotation}}
public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
{{#discriminator}}
public {{propertyType}} {{propertyGetter}}();
{{/discriminator}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
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}} {
{{#serializableModel}}

private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
{{#jackson}}
{{#discriminator.mappedModels}}
{{#-first}}
@JsonIgnoreProperties(
value = "{{{discriminator.propertyBaseName}}}", // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization
allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true)
@JsonSubTypes({
{{#discriminator.mappedModels}}
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"),
{{/discriminator.mappedModels}}
}){{/jackson}}
{{/-first}}
{{^vendorExtensions.x-discriminator-value}}
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{mappingName}}}"){{^-last}},{{/-last}}
{{/vendorExtensions.x-discriminator-value}}
{{#vendorExtensions.x-discriminator-value}}
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{{vendorExtensions.x-discriminator-value}}}"){{^-last}},{{/-last}}
{{/vendorExtensions.x-discriminator-value}}
{{#-last}}
})
{{/-last}}
{{/discriminator.mappedModels}}
{{/jackson}}
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,44 @@ public void shouldNotAddNotNullOnReadOnlyAttributes() throws IOException {

}

@Test
public void oneOf_5381() 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/issue_5381.yaml", 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);

ClientOptInput input = new ClientOptInput();
input.openAPI(openAPI);
input.config(codegen);

DefaultGenerator generator = new DefaultGenerator();
codegen.setHateoas(true);
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
//generator.setGeneratorPropertyDefault(CodegenConstants.USE_ONEOF_DISCRIMINATOR_LOOKUP, "true");
generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false");

codegen.setUseOneOfInterfaces(true);
codegen.setLegacyDiscriminatorBehavior(false);

generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");

generator.opts(input).generate();

assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Foo.java"), "public class Foo implements FooRefOrValue");
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRef.java"), "public class FooRef implements FooRefOrValue");
assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRefOrValue.java"), "public interface FooRefOrValue");
}

@Test
public void testTypeMappings() {
final SpringCodegen codegen = new SpringCodegen();
Expand Down
134 changes: 134 additions & 0 deletions modules/openapi-generator/src/test/resources/3_0/issue_5381.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
openapi: 3.0.1
info:
title: ByRefOrValue
description: >
This tests for a oneOf interface representation
version: 0.0.1
servers:
- url: "http://localhost:8080"
tags:
- name: Foo
paths:
/foo:
get:
tags:
- Foo
summary: GET all Foos
operationId: getAllFoos
responses:
'200':
$ref: '#/components/responses/200FooArray'
post:
tags:
- Foo
summary: Create a Foo
operationId: createFoo
requestBody:
$ref: '#/components/requestBodies/Foo'
responses:
'201':
$ref: '#/components/responses/201Foo'

components:
schemas:
Entity:
type: object
allOf:
- "$ref": "#/components/schemas/Addressable"
- "$ref": "#/components/schemas/Extensible"

EntityRef:
description: Entity reference schema to be use for all entityRef class.
type: object
properties:
name:
type: string
description: Name of the related entity.
'@referredType':
type: string
description: The actual type of the target instance when needed for disambiguation.
allOf:
- $ref: '#/components/schemas/Addressable'
- "$ref": "#/components/schemas/Extensible"


Addressable:
type: object
properties:
href:
type: string
description: Hyperlink reference
id:
type: string
description: unique identifier
description: Base schema for adressable entities
Extensible:
type: object
properties:
"@schemaLocation":
type: string
description: A URI to a JSON-Schema file that defines additional attributes
and relationships
"@baseType":
type: string
description: When sub-classing, this defines the super-class
"@type":
type: string
description: When sub-classing, this defines the sub-class Extensible name
required:
- '@type'

FooRefOrValue:
type: object
oneOf:
- $ref: "#/components/schemas/Foo"
- $ref: "#/components/schemas/FooRef"
discriminator:
propertyName: "@type"
mapping:
Foo: "#/components/schemas/Foo"
FooRef: "#/components/schemas/FooRef"

Foo:
type: object
properties:
fooPropA:
type: string
fooPropB:
type: string
allOf:
- $ref: '#/components/schemas/Entity'

FooRef:
type: object
properties:
foorefPropA:
type: string
allOf:
- $ref: '#/components/schemas/EntityRef'

requestBodies:
Foo:
description: The Foo to be created
content:
application/json;charset=utf-8:
schema:
$ref: '#/components/schemas/Foo'
responses:
'204':
description: Deleted
content: { }
201Foo:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/FooRefOrValue'
200FooArray:
description: Success
content:
application/json;charset=utf-8:
schema:
type: array
items:
$ref: '#/components/schemas/FooRefOrValue'
Loading

0 comments on commit e07c7d1

Please sign in to comment.