Skip to content

Commit a69d47c

Browse files
authored
Add support to RESTEasy Reactive (#262)
Signed-off-by: Helber Belmiro <[email protected]>
1 parent 0257fbf commit a69d47c

File tree

34 files changed

+520
-86
lines changed

34 files changed

+520
-86
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
@@ -122,6 +122,35 @@ public class PetResource {
122122

123123
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.
124124

125+
## RESTEasy Reactive and Classic support
126+
127+
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:
128+
129+
### RESTEasy Classic
130+
131+
```xml
132+
<dependency>
133+
<groupId>io.quarkus</groupId>
134+
<artifactId>quarkus-rest-client-jackson</artifactId>
135+
</dependency>
136+
```
137+
> **⚠️** 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!
138+
139+
### RESTEasy Reactive
140+
141+
```xml
142+
<dependency>
143+
<groupId>io.quarkus</groupId>
144+
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
145+
</dependency>
146+
```
147+
148+
For both implementations, the generated code is always blocking code.
149+
150+
When using RESTEasy Reactive:
151+
- The client must not declare multiple MIME-TYPES with `@Consumes`
152+
- You might need to implement a `ParamConverter` for each complex type
153+
125154
## Returning `Response` objects
126155

127156
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 `javax.ws.rs.core.Response` instead, you can set the `return-response` property to `true`.
@@ -278,14 +307,24 @@ The configuration suffix `quarkus.oidc-client.petstore_auth` is exclusive for th
278307

279308
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:
280309

281-
````xml
310+
RESTEasy Classic:
282311

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

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

291330
## Authorization Token Propagation
@@ -499,10 +538,11 @@ See the module [circuit-breaker](integration-tests/circuit-breaker) for an examp
499538
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
500539
bodies.
501540

502-
You need to add the following additional dependency to your `pom.xml`:
541+
> **⚠️** 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.
503542
504-
```xml
543+
If you're using RESTEasy Classic, you need to add the following additional dependency to your `pom.xml`:
505544

545+
```xml
506546
<dependency>
507547
<groupId>io.quarkus</groupId>
508548
<artifactId>quarkus-resteasy-multipart</artifactId>
@@ -512,8 +552,8 @@ You need to add the following additional dependency to your `pom.xml`:
512552
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:
513553

514554
- `javax.ws.rs.FormParam`, where the value parameter denotes the part name,
515-
- `org.jboss.resteasy.annotations.providers.multipart.PartType`, where the parameter is the jax-rs MediaType of the part (see below for details),
516-
- 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
555+
- `PartType`, where the parameter is the jax-rs MediaType of the part (see below for details),
556+
- and, if the part contains a file, `PartFilename`, with a generated default parameter that will be passed as the fileName sub-header in the
517557
Content-Disposition header of the part.
518558

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

539-
Then in the client the `org.jboss.resteasy.annotations.providers.multipart.MultipartForm` annotation is added in front of the multipart parameter:
579+
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:
540580

541581
```java
542582
@Path("/echo")
@@ -551,9 +591,26 @@ public interface MultipartService {
551591
}
552592
```
553593

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

612+
> **⚠️** `MultipartForm` is deprecated when using RESTEasy Reactive.
613+
557614
`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.
558615

559616
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
@@ -669,7 +726,6 @@ See the module [type-mapping](integration-tests/type-mapping) for an example of
669726

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

672-
- No reactive support
673729
- Only Jackson support
674730

675731
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+
@javax.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)