Skip to content

Commit 6154450

Browse files
authored
Added support to RESTEasy Reactive (#262) (#274)
Signed-off-by: Helber Belmiro <[email protected]>
1 parent e7e9366 commit 6154450

File tree

34 files changed

+514
-84
lines changed

34 files changed

+514
-84
lines changed

.github/workflows/build.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ on:
2424

2525
jobs:
2626
build:
27+
name: Build - RESTEasy Classic
2728
runs-on: ${{ matrix.os }}
2829
strategy:
2930
matrix:
@@ -43,3 +44,25 @@ jobs:
4344

4445
- name: Build with Maven
4546
run: mvn '-Dorg.slf4j.simpleLogger.log.org.openapitools=off' -B formatter:validate verify --file pom.xml
47+
48+
build_reactive:
49+
name: Build - RESTEasy Reactive
50+
runs-on: ${{ matrix.os }}
51+
strategy:
52+
matrix:
53+
os: [ubuntu-latest, windows-latest]
54+
steps:
55+
- name: Prepare git
56+
run: git config --global core.autocrlf false
57+
if: startsWith(matrix.os, 'windows')
58+
59+
- uses: actions/checkout@v2
60+
- name: Set up JDK 11
61+
uses: actions/setup-java@v2
62+
with:
63+
distribution: temurin
64+
java-version: 11
65+
cache: 'maven'
66+
67+
- name: Build with Maven
68+
run: mvn -Presteasy-reactive '-Dorg.slf4j.simpleLogger.log.org.openapitools=off' -B verify --file pom.xml

README.md

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,35 @@ public class PetResource {
121121

122122
See the [integration-tests](integration-tests) module for more information of how to use this extension. Please be advised that the extension is on experimental, early development stage.
123123

124+
## RESTEasy Reactive and Classic support
125+
126+
You can use the `quarkus-openapi-generator` with REST Client Classic or REST Client Reactive respectively. To do so add either the classic or reactive jackson dependency to your project's `pom.xml` file:
127+
128+
### RESTEasy Classic
129+
130+
```xml
131+
<dependency>
132+
<groupId>io.quarkus</groupId>
133+
<artifactId>quarkus-rest-client-jackson</artifactId>
134+
</dependency>
135+
```
136+
> **⚠️** After Version 1.2.1 / 2.1.1 you need to declare the above dependency explicitly! Even if you stay with the REST Client Classic implementation!
137+
138+
### RESTEasy Reactive
139+
140+
```xml
141+
<dependency>
142+
<groupId>io.quarkus</groupId>
143+
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
144+
</dependency>
145+
```
146+
147+
For both implementations, the generated code is always blocking code.
148+
149+
When using RESTEasy Reactive:
150+
- The client must not declare multiple MIME-TYPES with `@Consumes`
151+
- You might need to implement a `ParamConverter` for each complex type
152+
124153
## Returning `Response` objects
125154

126155
By default, this extension generates the methods according to their returning models based on the [OpenAPI specification Schema Object](https://spec.openapis.org/oas/v3.1.0#schema-object). If you want to return `jakarta.ws.rs.core.Response` instead, you can set the `return-response` property to `true`.
@@ -277,14 +306,24 @@ The configuration suffix `quarkus.oidc-client.petstore_auth` is exclusive for th
277306

278307
For this to work you **must** add [Quarkus OIDC Client Filter Extension](https://quarkus.io/guides/security-openid-connect-client#oidc-client-filter) to your project:
279308

280-
````xml
309+
RESTEasy Classic:
281310

311+
````xml
282312
<dependency>
283313
<groupId>io.quarkus</groupId>
284314
<artifactId>quarkus-oidc-client-filter</artifactId>
285315
</dependency>
286316
````
287317

318+
RESTEasy Reactive:
319+
320+
```xml
321+
<dependency>
322+
<groupId>io.quarkus</groupId>
323+
<artifactId>quarkus-oidc-client-reactive-filter</artifactId>
324+
</dependency>
325+
```
326+
288327
See the module [generation-tests](integration-tests/generation-tests) for an example of how to use this feature.
289328

290329
## Authorization Token Propagation
@@ -498,10 +537,11 @@ See the module [circuit-breaker](integration-tests/circuit-breaker) for an examp
498537
The rest client also supports request with mime-type multipart/form-data and, if the schema of the request body is known in advance, we can also automatically generate the models of the request
499538
bodies.
500539

501-
You need to add the following additional dependency to your `pom.xml`:
540+
> **⚠️** Tip: RESTEasy Reactive supports multipart/form-data [out of the box](https://quarkus.io/guides/rest-client-reactive#multipart). Thus, no additional dependency is required.
502541
503-
```xml
542+
If you're using RESTEasy Classic, you need to add the following additional dependency to your `pom.xml`:
504543

544+
```xml
505545
<dependency>
506546
<groupId>io.quarkus</groupId>
507547
<artifactId>quarkus-resteasy-multipart</artifactId>
@@ -511,8 +551,8 @@ You need to add the following additional dependency to your `pom.xml`:
511551
For any multipart/form-data operation a model for the request body will be generated. Each part of the multipart is a field in this model that is annotated with the following annotations:
512552

513553
- `jakarta.ws.rs.FormParam`, where the value parameter denotes the part name,
514-
- `org.jboss.resteasy.annotations.providers.multipart.PartType`, where the parameter is the jax-rs MediaType of the part (see below for details),
515-
- and, if the part contains a file, `org.jboss.resteasy.annotations.providers.multipart.PartFilename`, with a generated default parameter that will be passed as the fileName sub-header in the
554+
- `PartType`, where the parameter is the jax-rs MediaType of the part (see below for details),
555+
- and, if the part contains a file, `PartFilename`, with a generated default parameter that will be passed as the fileName sub-header in the
516556
Content-Disposition header of the part.
517557

518558
For example, the model for a request that requires a file, a string and some complex object will look like this:
@@ -535,7 +575,7 @@ public class MultipartBody {
535575
}
536576
```
537577

538-
Then in the client the `org.jboss.resteasy.annotations.providers.multipart.MultipartForm` annotation is added in front of the multipart parameter:
578+
Then in the client, when using RESTEasy Classic, the `org.jboss.resteasy.annotations.providers.multipart.MultipartForm` annotation is added in front of the multipart parameter:
539579

540580
```java
541581
@Path("/echo")
@@ -550,9 +590,26 @@ public interface MultipartService {
550590
}
551591
```
552592

593+
When using RESTEasy Reactive, the `jakarta.ws.rs.BeanParam` annotation is added in front of the multipart parameter:
594+
595+
```java
596+
@Path("/echo")
597+
@RegisterRestClient(baseUri="http://my.endpoint.com/api/v1", configKey="multipart-requests_yml")
598+
public interface MultipartService {
599+
600+
@POST
601+
@Consumes(MediaType.MULTIPART_FORM_DATA)
602+
@Produces(MediaType.TEXT_PLAIN)
603+
String sendMultipartData(@jakarta.ws.rs.BeanParam MultipartBody data);
604+
605+
}
606+
```
607+
553608
See [Quarkus - Using the REST Client with Multipart](https://quarkus.io/guides/rest-client-multipart) and
554609
the [RESTEasy JAX-RS specifications](https://docs.jboss.org/resteasy/docs/4.7.5.Final/userguide/html_single/index.html) for more details.
555610

611+
> **⚠️** `MultipartForm` is deprecated when using RESTEasy Reactive.
612+
556613
`baseURI` value of `RegisterRestClient` annotation is extracted from the `servers` section of the file, if present. If not, it will be left empty and it is expected you set up the uri to be used in your configuration.
557614

558615
Importantly, if some multipart request bodies contain complex objects (i.e. non-primitives) you need to explicitly tell the Open API generator to create models for these objects by setting
@@ -668,7 +725,6 @@ See the module [type-mapping](integration-tests/type-mapping) for an example of
668725

669726
These are the known limitations of this pre-release version:
670727

671-
- No reactive support
672728
- Only Jackson support
673729

674730
We will work in the next few releases to address these use cases, until there please provide feedback for the current state of this extension. We also love contributions :heart:

deployment/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@
2121
<groupId>io.quarkus</groupId>
2222
<artifactId>quarkus-core-deployment</artifactId>
2323
</dependency>
24-
<dependency>
25-
<groupId>io.quarkus</groupId>
26-
<artifactId>quarkus-rest-client-jackson-deployment</artifactId>
27-
</dependency>
2824
<dependency>
2925
<groupId>io.quarkus</groupId>
3026
<artifactId>quarkus-devtools-utilities</artifactId>

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@
2626
import io.quarkiverse.openapi.generator.OpenApiGeneratorException;
2727
import io.quarkiverse.openapi.generator.deployment.CodegenConfig;
2828
import io.quarkiverse.openapi.generator.deployment.circuitbreaker.CircuitBreakerConfigurationParser;
29+
import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiClassicClientGeneratorWrapper;
2930
import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiClientGeneratorWrapper;
31+
import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiReactiveClientGeneratorWrapper;
3032
import io.quarkus.bootstrap.prebuild.CodeGenException;
33+
import io.quarkus.deployment.Capability;
3134
import io.quarkus.deployment.CodeGenContext;
3235
import io.quarkus.deployment.CodeGenProvider;
36+
import io.quarkus.maven.dependency.ResolvedDependency;
3337
import io.smallrye.config.SmallRyeConfig;
3438

3539
/**
@@ -70,6 +74,12 @@ public boolean shouldRun(Path sourceDir, Config config) {
7074
return inputBaseDir != null || Files.isDirectory(sourceDir);
7175
}
7276

77+
protected boolean isRestEasyReactive(CodeGenContext context) {
78+
return context.applicationModel().getExtensionCapabilities().stream()
79+
.flatMap(extensionCapability -> extensionCapability.getProvidesCapabilities().stream())
80+
.anyMatch(Capability.REST_CLIENT_REACTIVE::equals);
81+
}
82+
7383
@Override
7484
public boolean trigger(CodeGenContext context) throws CodeGenException {
7585
final Path outDir = context.outDir();
@@ -79,6 +89,17 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
7989
final List<String> filesToExclude = context.config().getOptionalValues(EXCLUDE_FILES, String.class).orElse(List.of());
8090

8191
if (Files.isDirectory(openApiDir)) {
92+
final boolean isRestEasyReactive = isRestEasyReactive(context);
93+
94+
if (isRestEasyReactive) {
95+
if (!isJacksonReactiveClientPresent(context)) {
96+
throw new CodeGenException(
97+
"You need to add io.quarkus:quarkus-rest-client-reactive-jackson to your dependencies.");
98+
}
99+
} else if (!isJacksonClassicClientPresent(context)) {
100+
throw new CodeGenException("You need to add io.quarkus:quarkus-rest-client-jackson to your dependencies.");
101+
}
102+
82103
try (Stream<Path> openApiFilesPaths = Files.walk(openApiDir)) {
83104
openApiFilesPaths
84105
.filter(Files::isRegularFile)
@@ -88,7 +109,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
88109
&& !filesToExclude.contains(fileName)
89110
&& (filesToInclude.isEmpty() || filesToInclude.contains(fileName));
90111
})
91-
.forEach(openApiFilePath -> generate(context.config(), openApiFilePath, outDir));
112+
.forEach(openApiFilePath -> generate(context.config(), openApiFilePath, outDir, isRestEasyReactive));
92113
} catch (IOException e) {
93114
throw new CodeGenException("Failed to generate java files from OpenApi files in " + openApiDir.toAbsolutePath(),
94115
e);
@@ -98,20 +119,34 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
98119
return false;
99120
}
100121

122+
private boolean isJacksonReactiveClientPresent(CodeGenContext context) {
123+
return context.applicationModel().getDependencies().stream()
124+
.anyMatch(this::isJacksonReactiveClient);
125+
}
126+
127+
private boolean isJacksonClassicClientPresent(CodeGenContext context) {
128+
return context.applicationModel().getExtensionCapabilities().stream()
129+
.flatMap(extensionCapability -> extensionCapability.getProvidesCapabilities().stream())
130+
.anyMatch(Capability.RESTEASY_JSON_JACKSON_CLIENT::equals);
131+
}
132+
133+
private boolean isJacksonReactiveClient(ResolvedDependency resolvedDependency) {
134+
return "quarkus-rest-client-reactive-jackson".equals(resolvedDependency.getArtifactId())
135+
&& "io.quarkus".equals(resolvedDependency.getGroupId());
136+
}
137+
101138
// TODO: do not generate if the output dir has generated files and the openapi file has the same checksum of the previous run
102-
protected void generate(final Config config, final Path openApiFilePath, final Path outDir) {
139+
protected void generate(final Config config, final Path openApiFilePath, final Path outDir, boolean isRestEasyReactive) {
103140
final String basePackage = getBasePackage(config, openApiFilePath);
104141
final Boolean verbose = config.getOptionalValue(VERBOSE_PROPERTY_NAME, Boolean.class).orElse(false);
105142
final Boolean validateSpec = config.getOptionalValue(VALIDATE_SPEC_PROPERTY_NAME, Boolean.class).orElse(true);
106143
GlobalSettings.setProperty(OpenApiClientGeneratorWrapper.DEFAULT_SECURITY_SCHEME,
107144
config.getOptionalValue(CodegenConfig.DEFAULT_SECURITY_SCHEME, String.class).orElse(""));
108145

109-
final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(
110-
openApiFilePath.normalize(),
111-
outDir,
112-
verbose,
113-
validateSpec)
114-
.withClassesCodeGenConfig(ClassCodegenConfigParser.parse(config, basePackage))
146+
final OpenApiClientGeneratorWrapper generator = createGeneratorWrapper(openApiFilePath, outDir, isRestEasyReactive,
147+
verbose, validateSpec);
148+
149+
generator.withClassesCodeGenConfig(ClassCodegenConfigParser.parse(config, basePackage))
115150
.withCircuitBreakerConfig(CircuitBreakerConfigurationParser.parse(
116151
config));
117152

@@ -137,6 +172,23 @@ protected void generate(final Config config, final Path openApiFilePath, final P
137172
generator.generate(basePackage);
138173
}
139174

175+
private static OpenApiClientGeneratorWrapper createGeneratorWrapper(Path openApiFilePath, Path outDir,
176+
boolean isRestEasyReactive, Boolean verbose, Boolean validateSpec) {
177+
if (isRestEasyReactive) {
178+
return new OpenApiReactiveClientGeneratorWrapper(
179+
openApiFilePath.normalize(),
180+
outDir,
181+
verbose,
182+
validateSpec);
183+
} else {
184+
return new OpenApiClassicClientGeneratorWrapper(
185+
openApiFilePath.normalize(),
186+
outDir,
187+
verbose,
188+
validateSpec);
189+
}
190+
}
191+
140192
private String getBasePackage(final Config config, final Path openApiFilePath) {
141193
return config
142194
.getOptionalValue(getBasePackagePropertyName(openApiFilePath), String.class)

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorStreamCodeGen.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
5858

5959
boolean generated = false;
6060

61+
boolean isRestEasyReactive = isRestEasyReactive(context);
62+
6163
for (final OpenApiSpecInputProvider provider : this.providers) {
6264
for (SpecInputModel inputModel : provider.read(context)) {
6365
LOGGER.debug("Processing OpenAPI spec input model {}", inputModel);
@@ -72,7 +74,7 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
7274
StandardOpenOption.CREATE)) {
7375
outChannel.transferFrom(inChannel, 0, Integer.MAX_VALUE);
7476
LOGGER.debug("Saved OpenAPI spec input model in {}", openApiFilePath);
75-
this.generate(this.mergeConfig(context, inputModel), openApiFilePath, outDir);
77+
this.generate(this.mergeConfig(context, inputModel), openApiFilePath, outDir, isRestEasyReactive);
7678
generated = true;
7779
}
7880
} catch (IOException e) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.quarkiverse.openapi.generator.deployment.wrapper;
2+
3+
import java.nio.file.Path;
4+
5+
public class OpenApiClassicClientGeneratorWrapper extends OpenApiClientGeneratorWrapper {
6+
7+
public OpenApiClassicClientGeneratorWrapper(Path specFilePath, Path outputDir, boolean verbose, boolean validateSpec) {
8+
super(createConfigurator(), specFilePath, outputDir, verbose, validateSpec);
9+
}
10+
11+
private static QuarkusCodegenConfigurator createConfigurator() {
12+
QuarkusCodegenConfigurator configurator = new QuarkusCodegenConfigurator();
13+
configurator.addAdditionalProperty("is-resteasy-reactive", false);
14+
return configurator;
15+
}
16+
}

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
*
2727
* @see <a href="https://openapi-generator.tech/docs/generators/java">OpenAPI Generator Client for Java</a>
2828
*/
29-
public class OpenApiClientGeneratorWrapper {
29+
public abstract class OpenApiClientGeneratorWrapper {
3030

3131
public static final String VERBOSE = "verbose";
3232
private static final String ONCE_LOGGER = "org.openapitools.codegen.utils.oncelogger.enabled";
@@ -47,7 +47,8 @@ public class OpenApiClientGeneratorWrapper {
4747
private String apiPackage = "";
4848
private String modelPackage = "";
4949

50-
public OpenApiClientGeneratorWrapper(final Path specFilePath, final Path outputDir, final boolean verbose,
50+
OpenApiClientGeneratorWrapper(final QuarkusCodegenConfigurator configurator, final Path specFilePath, final Path outputDir,
51+
final boolean verbose,
5152
final boolean validateSpec) {
5253
// do not generate docs nor tests
5354
GlobalSettings.setProperty(CodegenConstants.API_DOCS, FALSE.toString());
@@ -62,7 +63,7 @@ public OpenApiClientGeneratorWrapper(final Path specFilePath, final Path outputD
6263
GlobalSettings.setProperty(VERBOSE, String.valueOf(verbose));
6364
GlobalSettings.setProperty(ONCE_LOGGER, verbose ? FALSE.toString() : TRUE.toString());
6465

65-
this.configurator = new QuarkusCodegenConfigurator();
66+
this.configurator = configurator;
6667
this.configurator.setInputSpec(specFilePath.toString());
6768
this.configurator.setOutputDir(outputDir.toString());
6869
this.configurator.addAdditionalProperty(QUARKUS_GENERATOR_NAME,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.quarkiverse.openapi.generator.deployment.wrapper;
2+
3+
import java.nio.file.Path;
4+
5+
public class OpenApiReactiveClientGeneratorWrapper extends OpenApiClientGeneratorWrapper {
6+
7+
public OpenApiReactiveClientGeneratorWrapper(Path specFilePath, Path outputDir, boolean verbose, boolean validateSpec) {
8+
super(createConfigurator(), specFilePath, outputDir, verbose, validateSpec);
9+
}
10+
11+
private static QuarkusCodegenConfigurator createConfigurator() {
12+
QuarkusCodegenConfigurator configurator = new QuarkusCodegenConfigurator();
13+
configurator.addAdditionalProperty("is-resteasy-reactive", true);
14+
return configurator;
15+
}
16+
}

deployment/src/main/resources/templates/api.qute

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,19 @@ public interface {classname} {
8080
public {#if op.returnType}{op.returnType}{#else}void{/if} {op.nickname}(
8181
{/if}
8282
{#if op.hasFormParams}
83+
{#if is-resteasy-reactive}
84+
@jakarta.ws.rs.BeanParam {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}{!
85+
!}{#for p in op.pathParams}{#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}{!
86+
!}{#for p in op.queryParams}{#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},{/if}{!
87+
!}{#for p in op.bodyParams}{#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}{!
88+
!}{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}
89+
{#else}
8390
@org.jboss.resteasy.annotations.providers.multipart.MultipartForm {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}{!
8491
!}{#for p in op.pathParams}{#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}{!
8592
!}{#for p in op.queryParams}{#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},{/if}{!
8693
!}{#for p in op.bodyParams}{#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}{!
8794
!}{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}
95+
{/if}
8896
{#else}
8997
{#for p in op.allParams}
9098
{#include pathParams.qute param=p/}{#include queryParams.qute param=p/}{#include bodyParams.qute param=p/}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}

0 commit comments

Comments
 (0)