Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .github/workflows/samples-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:
- samples/server/petstore/java-helidon-server/v3/mp/**
- samples/server/petstore/java-helidon-server/v3/se/**
- samples/server/petstore/jaxrs-spec-sealed/**
- samples/server/petstore/jaxrs-spec-records/**
pull_request:
paths:
# clients
Expand All @@ -46,6 +47,7 @@ on:
- samples/server/petstore/java-helidon-server/v3/mp/**
- samples/server/petstore/java-helidon-server/v3/se/**
- samples/server/petstore/jaxrs-spec-sealed/**
- samples/server/petstore/jaxrs-spec-records/**
jobs:
build:
name: Build with JDK17
Expand Down Expand Up @@ -75,6 +77,7 @@ jobs:
- samples/server/petstore/java-helidon-server/v3/mp/
- samples/server/petstore/java-helidon-server/v3/se
- samples/server/petstore/jaxrs-spec-sealed
- samples/server/petstore/jaxrs-spec-records
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
Expand Down
11 changes: 11 additions & 0 deletions bin/configs/jaxrs-spec-records.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-records
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-records-petstore-server
useOneOfInterfaces: "true"
useSealed: "true"
useRecords: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.openapitools.codegen.languages.features.GzipFeatures.USE_GZIP_FEATURE;

Expand All @@ -60,6 +62,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations";
public static final String USE_ENUM_CASE_INSENSITIVE = "useEnumCaseInsensitive";
public static final String USE_SEALED = "useSealed";
public static final String USE_RECORDS = "useRecords";

public static final String QUARKUS_LIBRARY = "quarkus";
public static final String THORNTAIL_LIBRARY = "thorntail";
Expand All @@ -83,6 +86,9 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
@Setter
protected boolean useSealed = false;

@Setter
protected boolean useRecords = false;

private final JakartaSecurityAnnotationProcessor jakartaSecurityAnnotationProcessor = new JakartaSecurityAnnotationProcessor();

@Getter @Setter
Expand Down Expand Up @@ -167,6 +173,7 @@ public JavaJAXRSSpecServerCodegen() {
cliOptions.add(CliOption.newBoolean(GENERATE_JSON_CREATOR, "Whether to generate @JsonCreator constructor for required properties.", generateJsonCreator));
cliOptions.add(CliOption.newBoolean(USE_ENUM_CASE_INSENSITIVE, "Use `equalsIgnoreCase` when String for enum comparison", useEnumCaseInsensitive));
cliOptions.add(CliOption.newBoolean(USE_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed));
cliOptions.add(CliOption.newBoolean(USE_RECORDS, "Whether to render the implementations of a generated oneOf interface (useOneOfInterfaces=true) as Java records instead of classes.", useRecords));
}

@Override
Expand Down Expand Up @@ -211,6 +218,7 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(GENERATE_JSON_CREATOR, this::setGenerateJsonCreator);
convertPropertyToBooleanAndWriteBack(USE_ENUM_CASE_INSENSITIVE, this::setUseEnumCaseInsensitive);
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);
convertPropertyToBooleanAndWriteBack(USE_RECORDS, this::setUseRecords);

if (additionalProperties.containsKey(OPEN_API_SPEC_FILE_LOCATION)) {
openApiSpecFileLocation = additionalProperties.get(OPEN_API_SPEC_FILE_LOCATION).toString();
Expand Down Expand Up @@ -400,16 +408,20 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
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.
// Collect, for the interfaces generated by the useOneOfInterfaces feature: their classnames (to
// identify which concrete models implement one of them and are therefore record candidates) and
// their discriminators indexed by classname (a child of a oneOf interface inherits a shared base
// via allOf and has no Java parentModel carrying the discriminator - it lives on the interface).
Set<String> oneOfInterfaceNames = new HashSet<>();
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());
if (Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))) {
oneOfInterfaceNames.add(model.classname);
if (model.getDiscriminator() != null) {
oneOfInterfaceDiscriminators.put(model.classname, model.getDiscriminator());
}
}
}
}
Expand Down Expand Up @@ -437,6 +449,9 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
if (useSealed && !Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))) {
model.permits.removeAll(model.oneOf);
}
if (useRecords && isOneOfInterfaceRecordCandidate(model, oneOfInterfaceNames)) {
model.getVendorExtensions().put("x-jaxrs-record", true);
}
}
}
return result;
Expand All @@ -463,6 +478,32 @@ private CodegenDiscriminator discriminatorOfImplementedOneOfInterface(
return null;
}

/**
* A concrete model is rendered as a record only when it implements a oneOf interface produced by the
* useOneOfInterfaces feature, and it is structurally record-compatible: it has no Java parent (records
* cannot extend) and no additional (undeclared) properties (those require a mutable map-backed pojo).
*
* @param model the concrete model that may implement a oneOf interface
* @param oneOfInterfaceNames the classnames of the generated oneOf interfaces
* @return {@code true} when the model should be rendered as a record
*/
@SuppressWarnings("unchecked")
private boolean isOneOfInterfaceRecordCandidate(CodegenModel model, Set<String> oneOfInterfaceNames) {
if (model.getParent() != null || model.getAdditionalPropertiesType() != null) {
return false;
}
Object implementsExtension = model.getVendorExtensions().get("x-implements");
if (!(implementsExtension instanceof Collection)) {
return false;
}
for (Object intf : (Collection<Object>) implementsExtension) {
if (oneOfInterfaceNames.contains(String.valueOf(intf))) {
return true;
}
}
return false;
}

@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
Expand Up @@ -16,6 +16,6 @@ import {{javaxPackage}}.validation.Valid;
{{>enumOuterClass}}

{{/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}}
{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{#vendorExtensions.x-jaxrs-record}}{{>record}}{{/vendorExtensions.x-jaxrs-record}}{{^vendorExtensions.x-jaxrs-record}}{{>pojo}}{{/vendorExtensions.x-jaxrs-record}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@

public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#discriminator}}
{{propertyType}} {{propertyGetter}}();
{{propertyType}} {{#useRecords}}{{propertyName}}{{/useRecords}}{{^useRecords}}{{propertyGetter}}{{/useRecords}}();
{{/discriminator}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
{{/openApiNullable}}
</dependencies>
<properties>
<java.version>{{#useSealed}}17{{/useSealed}}{{^useSealed}}1.8{{/useSealed}}</java.version>
<java.version>{{#useSealed}}17{{/useSealed}}{{^useSealed}}{{#useRecords}}17{{/useRecords}}{{^useRecords}}1.8{{/useRecords}}{{/useSealed}}</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{{#useSwaggerAnnotations}}
import io.swagger.annotations.*;
{{/useSwaggerAnnotations}}
{{#useSwaggerV3Annotations}}
import io.swagger.v3.oas.annotations.media.Schema;
{{/useSwaggerV3Annotations}}
{{#jackson}}
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.annotation.JsonTypeName;
{{/jackson}}
{{#withXml}}
import {{javaxPackage}}.xml.bind.annotation.XmlElement;
import {{javaxPackage}}.xml.bind.annotation.XmlRootElement;
import {{javaxPackage}}.xml.bind.annotation.XmlAccessType;
import {{javaxPackage}}.xml.bind.annotation.XmlAccessorType;
import {{javaxPackage}}.xml.bind.annotation.XmlType;
import {{javaxPackage}}.xml.bind.annotation.XmlEnum;
import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue;
{{/withXml}}

{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{#description}}/**
* {{.}}
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
**/{{/description}}
{{#isDeprecated}}
@Deprecated

{{/isDeprecated}}{{#useSwaggerAnnotations}}{{#description}}@ApiModel(description = "{{{.}}}"){{/description}}{{/useSwaggerAnnotations}}{{#useSwaggerV3Annotations}}
@Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}{{#isDeprecated}}, deprecated = true{{/isDeprecated}}){{/useSwaggerV3Annotations}}{{#useMicroProfileOpenAPIAnnotations}}
@org.eclipse.microprofile.openapi.annotations.media.Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}{{#isDeprecated}}, deprecated = true{{/isDeprecated}}){{/useMicroProfileOpenAPIAnnotations}}
{{#jackson}}
@JsonTypeName("{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}{{^vendorExtensions.x-discriminator-value}}{{name}}{{/vendorExtensions.x-discriminator-value}}")
{{/jackson}}
{{>generatedAnnotation}}{{>additionalModelTypeAnnotations}}
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public {{>sealed}}record {{classname}}(
{{#vars}}
{{#vendorExtensions.x-field-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-field-extra-annotation}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
{{#jackson}}@JsonProperty({{#required}}required = {{required}}, value = {{/required}}"{{baseName}}") {{/jackson}}{{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}}{{{datatypeWithEnum}}} {{name}}{{^-last}},{{/-last}}
{{/vars}}
) {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
{{#vars}}
{{#isEnum}}
{{^isContainer}}
{{>enumClass}}{{! prevent indent}}
{{/isContainer}}
{{#isContainer}}
{{#mostInnerItems}}
{{>enumClass}}{{! prevent indent}}
{{/mostInnerItems}}
{{/isContainer}}
{{/isEnum}}
{{/vars}}
}
Original file line number Diff line number Diff line change
@@ -1 +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}}
{{#useSealed}}{{#permits.0}}sealed {{/permits.0}}{{^permits.0}}{{^vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-jaxrs-record}}final {{/vendorExtensions.x-jaxrs-record}}{{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,63 @@ public void testWithoutUseSealedOutputIsUnchanged() throws Exception {
assertFileContains(Paths.get(outputDir + "/pom.xml"), "<java.version>1.8</java.version>");
}

/**
* With {@code useRecords=true} the concrete subtypes of a generated oneOf interface render as Java
* records rather than mutable classes. Because a record component {@code petType} exposes the
* canonical accessor {@code petType()} (not {@code getPetType()}), the interface declares the
* discriminator accessor in record style so the records satisfy it without bridge methods. The
* record components carry the {@code @JsonProperty} annotations. Combined here with
* {@code useSealed=true} so the interface is also sealed and permits the record subtypes.
*/
@Test
public void testOneOfRecordImplementationGeneration() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useOneOfInterfaces", "true");
properties.put("useSealed", "true");
properties.put("useRecords", "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();

// assertFileContains is used rather than JavaFileAssert because the latter parses the source
// with a JavaParser language level that predates sealed/record types.
String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";

// The interface is sealed, permits the record subtypes, and declares the discriminator accessor
// in record style (petType(), not getPetType()) so the records implement it via their canonical
// accessors.
assertFileContains(Paths.get(modelDir + "PetRequest.java"),
"public sealed interface PetRequest",
"permits CatRequest, DogRequest",
"PetType petType();");
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "getPetType");

// The concrete subtypes are records implementing (not extending) the interface, with @JsonProperty
// on the components and no JavaBean getters or final-class declaration.
assertFileContains(Paths.get(modelDir + "CatRequest.java"),
"public record CatRequest(",
"@JsonProperty(required = true, value = \"petType\")",
"PetType petType",
"implements PetRequest");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"),
"class CatRequest", "extends PetRequest", "public PetType getPetType()");

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

@Test
public void testGenerateJsonNullableListFieldsHelperMethodReferences_issue23251() throws Exception {
Map<String, Object> properties = new HashMap<>();
Expand Down
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-records/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