Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/samples-jaxrs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- samples/server/petstore/jaxrs-spec-withxml
- samples/server/petstore/jaxrs-spec-interface
- samples/server/petstore/jaxrs-spec-interface-response
- samples/server/petstore/jaxrs-spec-oneof-interface
- samples/server/petstore/jaxrs-datelib-j8
- samples/server/petstore/jaxrs-resteasy/default
- samples/server/petstore/jaxrs-resteasy/eap
Expand Down
9 changes: 9 additions & 0 deletions bin/configs/jaxrs-spec-oneof-interface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-oneof-interface
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
additionalProperties:
artifactId: jaxrs-spec-oneof-interface-petstore-server
useOneOfInterfaces: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.openapitools.codegen.model.OperationsMap;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -391,17 +393,34 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
Map<String, ModelsMap> result = super.postProcessAllModels(objs);

// Index the discriminators of the generated oneOf interfaces by classname. A child of a oneOf
// interface (useOneOfInterfaces) inherits a shared base via allOf and therefore has no Java
// parentModel carrying the discriminator - the discriminator lives on the interface it implements.
Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators = new HashMap<>();
for (ModelsMap modelsMap : result.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
if (Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))
&& model.getDiscriminator() != null) {
oneOfInterfaceDiscriminators.put(model.classname, model.getDiscriminator());
}
}
}

for (ModelsMap modelsMap : result.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
if (model.parentModel != null) {
CodegenDiscriminator discriminator = model.parentModel.getDiscriminator();
if (discriminator != null) {
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
if (mappedModel.getSchemaName().equals(model.schemaName)) {
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
break;
}
// Resolve the @JsonTypeName mapping value from the Java parent's discriminator, if any,
// otherwise from the discriminator of the oneOf interface the model implements.
CodegenDiscriminator discriminator = model.parentModel != null
? model.parentModel.getDiscriminator()
: discriminatorOfImplementedOneOfInterface(model, oneOfInterfaceDiscriminators);
if (discriminator != null) {
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
if (mappedModel.getSchemaName().equals(model.schemaName)) {
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
break;
}
}
}
Expand All @@ -410,6 +429,27 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
return result;
}

/**
* Returns the discriminator of the oneOf interface (produced by useOneOfInterfaces) that the given
* model implements, or {@code null} if the model does not implement such an interface. The interface
* name is read from the model's {@code x-implements} vendor extension.
*/
@SuppressWarnings("unchecked")
private CodegenDiscriminator discriminatorOfImplementedOneOfInterface(
CodegenModel model, Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators) {
Object implementsExtension = model.getVendorExtensions().get("x-implements");
if (!(implementsExtension instanceof Collection)) {
return null;
}
for (Object intf : (Collection<Object>) implementsExtension) {
CodegenDiscriminator discriminator = oneOfInterfaceDiscriminators.get(String.valueOf(intf));
if (discriminator != null) {
return discriminator;
}
}
return null;
}

@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{#additionalOneOfTypeAnnotations}}{{{.}}}
{{/additionalOneOfTypeAnnotations}}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ import {{javaxPackage}}.validation.Valid;
{{>enumOuterClass}}

{{/isEnum}}
{{^isEnum}}{{>pojo}}{{/isEnum}}
{{^isEnum}}{{#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,21 @@
{{>additionalOneOfTypeAnnotations}}
{{#discriminator}}
{{>typeInfoAnnotation}}

{{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
{{#interfaceModels}}
@JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}}
{{/interfaceModels}}
})
{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{/discriminator}}
{{>generatedAnnotation}}

public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
{{#discriminator}}
{{propertyType}} {{propertyGetter}}();
{{/discriminator}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2106,4 +2106,60 @@ private String generateQuarkusItemsApi(String specPath, boolean interfaceOnly, b
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/ItemsApi.java");
return Files.readString(output.toPath().resolve("src/gen/java/org/openapitools/api/ItemsApi.java"));
}

/**
* With {@code useOneOfInterfaces=true} a oneOf schema is generated as a Java interface, and the
* concrete subtypes implement (not extend) it. The discriminator property is declared only on a
* shared non-discriminator base (acyclic pattern), so the interface getter type resolves to the
* enum model (PetType) from the mapped children rather than falling back to String.
*/
@Test
public void testOneOfInterfaceGeneration() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useOneOfInterfaces", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";

// The oneOf schema becomes an interface declaring the discriminator getter with the resolved
// enum type rather than String.
assertFileContains(Paths.get(modelDir + "PetRequest.java"),
"public interface PetRequest",
"PetType getPetType();");
// an interface is not a class and must not extend a parent
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "class PetRequest");

// The concrete subtypes implement (not extend) the interface, with a matching getter return type,
// so there is no cyclical extends/implements and no return-type clash.
assertFileContains(Paths.get(modelDir + "CatRequest.java"),
"public class CatRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "extends PetRequest");

assertFileContains(Paths.get(modelDir + "DogRequest.java"),
"public class DogRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "extends PetRequest");

// The @JsonTypeName of each subtype is the discriminator mapping value (CAT/DOG), resolved from
// the oneOf interface it implements, not the class name - so polymorphic (de)serialization keys
// off the discriminator value and round-trips with the @JsonSubTypes mapping on the interface.
assertFileContains(Paths.get(modelDir + "CatRequest.java"), "@JsonTypeName(\"CAT\")");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "@JsonTypeName(\"CatRequest\")");
assertFileContains(Paths.get(modelDir + "DogRequest.java"), "@JsonTypeName(\"DOG\")");
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "@JsonTypeName(\"DogRequest\")");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
openapi: 3.0.3
info:
title: oneOf interface
description: >
A oneOf interface whose subtypes inherit shared properties from a base schema
(acyclic pattern). The discriminator property is declared on the oneOf container
and on the shared base the subtypes inherit via allOf, so the interface getter
type resolves to the enum from the container's own properties.
version: 1.0.0
paths:
/pets:
post:
operationId: createPet
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PetRequest'
responses:
'201':
description: created
components:
schemas:
PetType:
type: string
enum:
- CAT
- DOG
description: Discriminator value identifying the type of pet

PetRequest:
type: object
properties:
petType:
$ref: '#/components/schemas/PetType'
oneOf:
- $ref: '#/components/schemas/CatRequest'
- $ref: '#/components/schemas/DogRequest'
discriminator:
propertyName: petType
mapping:
CAT: '#/components/schemas/CatRequest'
DOG: '#/components/schemas/DogRequest'

PetBase:
type: object
required:
- petType
- name
properties:
petType:
$ref: '#/components/schemas/PetType'
name:
type: string

CatRequest:
type: object
allOf:
- $ref: '#/components/schemas/PetBase'
- type: object
required:
- indoor
properties:
indoor:
type: boolean

DogRequest:
type: object
allOf:
- $ref: '#/components/schemas/PetBase'
- type: object
required:
- trained
properties:
trained:
type: boolean
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator

# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs

# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux

# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux

# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
README.md
pom.xml
src/gen/java/org/openapitools/api/PetsApi.java
src/gen/java/org/openapitools/api/RestApplication.java
src/gen/java/org/openapitools/api/RestResourceRoot.java
src/gen/java/org/openapitools/model/CatRequest.java
src/gen/java/org/openapitools/model/DogRequest.java
src/gen/java/org/openapitools/model/PetBase.java
src/gen/java/org/openapitools/model/PetRequest.java
src/gen/java/org/openapitools/model/PetType.java
src/main/openapi/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7.24.0-SNAPSHOT
27 changes: 27 additions & 0 deletions samples/server/petstore/jaxrs-spec-oneof-interface/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# JAX-RS server with OpenAPI

## Overview
This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using an
[OpenAPI-Spec](https://openapis.org), you can easily generate a server stub.

This is an example of building a OpenAPI-enabled JAX-RS server.
This example uses the [JAX-RS](https://jax-rs-spec.java.net/) framework.


The JAX-RS implementation needs to be provided by the application server you are deploying on.

To run the server from the command line, you can use maven to provision and start a TomEE Server.
Please execute the following:

```
mvn -Dtomee-embedded-plugin.http=8080 package org.apache.tomee.maven:tomee-embedded-maven-plugin:7.0.5:run
```

You can then call your server endpoints under:

```
http://localhost:8080/
```

Note that if you have configured the `host` to be something other than localhost, the calls through
swagger-ui will be directed to that host and not localhost!
Loading
Loading