Skip to content

Commit 0dd638f

Browse files
author
Matheus Andre
committed
feat: Add support for ClientMultipartForm
1 parent 1b081bb commit 0dd638f

File tree

16 files changed

+531
-9
lines changed

16 files changed

+531
-9
lines changed

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodegenConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ enum ConfigName {
9090
GENERATE_MODEL_FOR_USAGE_AS_BEAN_PARAM("generate-model-for-usage-as-bean-param"),
9191
EQUALS_HASHCODE("equals-hashcode"),
9292
USE_DYNAMIC_URL("use-dynamic-url"),
93-
METHOD_PER_MEDIA_TYPE("method-per-media-type");
93+
METHOD_PER_MEDIA_TYPE("method-per-media-type"),
94+
RESTEASY_REACTIVE_CLIENT_FORM("resteasy-reactive-client-form");
9495

9596
private final String name;
9697

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CommonItemConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,15 @@ public interface CommonItemConfig {
187187
@WithDefault("true")
188188
Optional<Boolean> registerRestClient();
189189

190+
/**
191+
* Use {@link org.jboss.resteasy.reactive.client.api.ClientMultipartForm} for multipart request bodies when generating
192+
* RESTEasy Reactive clients. This avoids generating a multipart form POJO and lets callers build multipart payloads
193+
* programmatically.
194+
*/
195+
@WithName("resteasy-reactive-client-form")
196+
@WithDefault("false")
197+
Optional<Boolean> resteasyReactiveClientForm();
198+
190199
/**
191200
* Which CDI scope annotation (if any) should be placed on the generated API. Defaults to
192201
* {@code @jakarta.enterprise.context.ApplicationScoped}.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.REMOVE_OPERATION_ID_PREFIX;
1717
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.REMOVE_OPERATION_ID_PREFIX_COUNT;
1818
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.REMOVE_OPERATION_ID_PREFIX_DELIMITER;
19+
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.RESTEASY_REACTIVE_CLIENT_FORM;
1920
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.TEMPLATE_BASE_DIR;
2021
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.VALIDATE_SPEC;
2122

@@ -361,6 +362,9 @@ protected void generate(OpenApiGeneratorOptions options) {
361362
getValues(config, openApiFilePath, ConfigName.GENERATE_MODEL_FOR_USAGE_AS_BEAN_PARAM, Boolean.class)
362363
.ifPresent(generator::withGenerateModelForUsageAsBeanParam);
363364

365+
getValues(config, openApiFilePath, RESTEASY_REACTIVE_CLIENT_FORM, Boolean.class)
366+
.ifPresent(generator::withResteasyReactiveClientForm);
367+
364368
getValues(smallRyeConfig, openApiFilePath, CodegenConfig.ConfigName.METHOD_PER_MEDIA_TYPE, Boolean.class)
365369
.ifPresent(generator::withMethodPerMediaType);
366370

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ private void setDefaults() {
117117
this.configurator.addAdditionalProperty("verbose", FALSE);
118118
this.configurator.addAdditionalProperty(CodegenConstants.SERIALIZABLE_MODEL, FALSE);
119119
this.configurator.addAdditionalProperty("equals-hashcode", TRUE);
120+
this.configurator.addAdditionalProperty("is-resteasy-reactive-client-form", FALSE);
120121
this.configurator.addAdditionalProperty("use-dynamic-url", FALSE);
121122
this.configurator.addAdditionalProperty("generate-model-for-usage-as-bean-param", TRUE);
122123
this.configurator.addAdditionalProperty("method-per-media-type", FALSE);
@@ -363,6 +364,11 @@ public OpenApiClientGeneratorWrapper withMethodPerMediaType(final Boolean method
363364
return this;
364365
}
365366

367+
public OpenApiClientGeneratorWrapper withResteasyReactiveClientForm(final Boolean resteasyReactiveClientForm) {
368+
this.configurator.addAdditionalProperty("is-resteasy-reactive-client-form", resteasyReactiveClientForm);
369+
return this;
370+
}
371+
366372
/**
367373
* Main entrypoint, or where to generate the files based on the given base package.
368374
*

client/deployment/src/main/resources/templates/libraries/microprofile/api.qute

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ public interface {classname} {
168168
@io.quarkus.rest.client.reactive.Url String dynUrl{#if op.allParams || op.hasFormParams},{/if}
169169
{/if}
170170
{#if op.hasFormParams}
171-
{#if is-resteasy-reactive}
171+
{#if is-resteasy-reactive && is-resteasy-reactive-client-form}
172+
org.jboss.resteasy.reactive.client.api.ClientMultipartForm dataParts{#if op.hasPathParams},{/if}
173+
{#else if is-resteasy-reactive}
172174
@jakarta.ws.rs.BeanParam {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
173175
{#else}
174176
@org.jboss.resteasy.annotations.providers.multipart.MultipartForm {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
@@ -193,7 +195,9 @@ public interface {classname} {
193195
{/for}{/if}
194196
);
195197
{#if op.hasFormParams}
198+
{#if !(is-resteasy-reactive && is-resteasy-reactive-client-form)}
196199
{#include multipartFormdataPojo.qute param=op/}
200+
{/if}
197201
{/if}
198202

199203
{/for} {! end consume-loop !}
@@ -328,7 +332,9 @@ public interface {classname} {
328332
@io.quarkus.rest.client.reactive.Url String dynUrl{#if op.allParams || op.hasFormParams},{/if}
329333
{/if}
330334
{#if op.hasFormParams}
331-
{#if is-resteasy-reactive}
335+
{#if is-resteasy-reactive && is-resteasy-reactive-client-form}
336+
org.jboss.resteasy.reactive.client.api.ClientMultipartForm dataParts{#if op.hasPathParams},{/if}
337+
{#else if is-resteasy-reactive}
332338
@jakarta.ws.rs.BeanParam {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
333339
{#else}
334340
@org.jboss.resteasy.annotations.providers.multipart.MultipartForm {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
@@ -353,7 +359,9 @@ public interface {classname} {
353359
{/for}{/if}
354360
);
355361
{#if op.hasFormParams}
362+
{#if !(is-resteasy-reactive && is-resteasy-reactive-client-form)}
356363
{#include multipartFormdataPojo.qute param=op/}
364+
{/if}
357365
{/if}
358366

359367
{/if} {! end method-per-media-type else !}

client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,64 @@ void verifyMultipartFormAnnotationIsGeneratedForParameter() throws URISyntaxExce
387387
assertThat(param.getAnnotationByName("MultipartForm")).isPresent();
388388
}
389389

390+
@Test
391+
void verifyReactiveMultipartFormStillUsesBeanParamByDefault() throws URISyntaxException, FileNotFoundException {
392+
List<File> generatedFiles = createGeneratorWrapperReactive("multipart-openapi.yml").withSkipFormModelConfig("false")
393+
.generate("org.acme");
394+
assertThat(generatedFiles).isNotEmpty();
395+
396+
Optional<File> file = generatedFiles.stream().filter(f -> f.getName().endsWith("UserProfileDataApi.java")).findAny();
397+
assertThat(file).isPresent();
398+
399+
CompilationUnit compilationUnit = StaticJavaParser.parse(file.orElseThrow());
400+
List<MethodDeclaration> methodDeclarations = compilationUnit.findAll(MethodDeclaration.class);
401+
assertThat(methodDeclarations).isNotEmpty();
402+
403+
Optional<MethodDeclaration> multipartPostMethod = methodDeclarations.stream()
404+
.filter(m -> m.getNameAsString().equals("postUserProfileData")).findAny();
405+
assertThat(multipartPostMethod).isPresent();
406+
407+
List<Parameter> parameters = multipartPostMethod.orElseThrow().getParameters();
408+
assertThat(parameters).hasSize(1);
409+
410+
Parameter param = parameters.get(0);
411+
assertThat(param.getTypeAsString()).isEqualTo("PostUserProfileDataMultipartForm");
412+
assertThat(param.getNameAsString()).isEqualTo("multipartForm");
413+
assertThat(param.getAnnotationByName("BeanParam")).isPresent();
414+
assertThat(compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
415+
.filter(c -> c.getNameAsString().equals("PostUserProfileDataMultipartForm")).findAny()).isNotEmpty();
416+
}
417+
418+
@Test
419+
void verifyReactiveMultipartFormCanUseClientMultipartForm() throws URISyntaxException, FileNotFoundException {
420+
List<File> generatedFiles = createGeneratorWrapperReactive("multipart-openapi.yml")
421+
.withSkipFormModelConfig("false")
422+
.withResteasyReactiveClientForm(true)
423+
.generate("org.acme");
424+
assertThat(generatedFiles).isNotEmpty();
425+
426+
Optional<File> file = generatedFiles.stream().filter(f -> f.getName().endsWith("UserProfileDataApi.java")).findAny();
427+
assertThat(file).isPresent();
428+
429+
CompilationUnit compilationUnit = StaticJavaParser.parse(file.orElseThrow());
430+
List<MethodDeclaration> methodDeclarations = compilationUnit.findAll(MethodDeclaration.class);
431+
assertThat(methodDeclarations).isNotEmpty();
432+
433+
Optional<MethodDeclaration> multipartPostMethod = methodDeclarations.stream()
434+
.filter(m -> m.getNameAsString().equals("postUserProfileData")).findAny();
435+
assertThat(multipartPostMethod).isPresent();
436+
437+
List<Parameter> parameters = multipartPostMethod.orElseThrow().getParameters();
438+
assertThat(parameters).hasSize(1);
439+
440+
Parameter param = parameters.get(0);
441+
assertThat(param.getTypeAsString()).isEqualTo("org.jboss.resteasy.reactive.client.api.ClientMultipartForm");
442+
assertThat(param.getNameAsString()).isEqualTo("dataParts");
443+
assertThat(param.getAnnotations()).isEmpty();
444+
assertThat(compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
445+
.filter(c -> c.getNameAsString().equals("PostUserProfileDataMultipartForm")).findAny()).isEmpty();
446+
}
447+
390448
@Test
391449
void verifyMultipartPojoGeneratedAndFieldsHaveAnnotations() throws URISyntaxException, FileNotFoundException {
392450
List<File> generatedFiles = createGeneratorWrapper("multipart-openapi.yml").withSkipFormModelConfig("false")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<parent>
4+
<artifactId>quarkus-openapi-generator-integration-tests</artifactId>
5+
<groupId>io.quarkiverse.openapi.generator</groupId>
6+
<version>3.0.0-SNAPSHOT</version>
7+
</parent>
8+
<modelVersion>4.0.0</modelVersion>
9+
10+
<artifactId>quarkus-openapi-generator-it-multipart-client-form</artifactId>
11+
<name>Quarkus - OpenAPI Generator - Integration Tests - Client - Multipart ClientMultipartForm</name>
12+
13+
<dependencies>
14+
<dependency>
15+
<groupId>io.quarkiverse.openapi.generator</groupId>
16+
<artifactId>quarkus-openapi-generator</artifactId>
17+
</dependency>
18+
<dependency>
19+
<groupId>io.quarkus</groupId>
20+
<artifactId>quarkus-junit5</artifactId>
21+
<scope>test</scope>
22+
</dependency>
23+
<dependency>
24+
<groupId>org.wiremock</groupId>
25+
<artifactId>wiremock</artifactId>
26+
<scope>test</scope>
27+
</dependency>
28+
<dependency>
29+
<groupId>io.quarkus</groupId>
30+
<artifactId>quarkus-rest-client-jackson</artifactId>
31+
</dependency>
32+
</dependencies>
33+
<build>
34+
<plugins>
35+
<plugin>
36+
<groupId>io.quarkus</groupId>
37+
<artifactId>quarkus-maven-plugin</artifactId>
38+
<extensions>true</extensions>
39+
<executions>
40+
<execution>
41+
<goals>
42+
<goal>build</goal>
43+
<goal>generate-code</goal>
44+
<goal>generate-code-tests</goal>
45+
</goals>
46+
</execution>
47+
</executions>
48+
</plugin>
49+
</plugins>
50+
</build>
51+
<profiles>
52+
<profile>
53+
<id>native-image</id>
54+
<activation>
55+
<property>
56+
<name>native</name>
57+
</property>
58+
</activation>
59+
<build>
60+
<plugins>
61+
<plugin>
62+
<artifactId>maven-surefire-plugin</artifactId>
63+
<configuration>
64+
<skipTests>${native.surefire.skip}</skipTests>
65+
</configuration>
66+
</plugin>
67+
<plugin>
68+
<artifactId>maven-failsafe-plugin</artifactId>
69+
<executions>
70+
<execution>
71+
<goals>
72+
<goal>integration-test</goal>
73+
<goal>verify</goal>
74+
</goals>
75+
<configuration>
76+
<systemPropertyVariables>
77+
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
78+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
79+
<maven.home>${maven.home}</maven.home>
80+
</systemPropertyVariables>
81+
</configuration>
82+
</execution>
83+
</executions>
84+
</plugin>
85+
</plugins>
86+
</build>
87+
<properties>
88+
<quarkus.package.type>native</quarkus.package.type>
89+
</properties>
90+
</profile>
91+
</profiles>
92+
</project>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
openapi: 3.0.3
2+
info:
3+
title: "Multipart ClientMultipartForm API"
4+
description: An API that uses ClientMultipartForm for multipart/form-data requests
5+
version: 1.0.0
6+
7+
servers:
8+
- url: "http://my.endpoint.com/api/v1"
9+
10+
paths:
11+
/multipart:
12+
post:
13+
tags:
14+
- Multipart
15+
description: Upload multipart data using ClientMultipartForm
16+
operationId: sendMultipartData
17+
requestBody:
18+
required: true
19+
description: Multipart form data
20+
content:
21+
multipart/form-data:
22+
schema:
23+
type: object
24+
properties:
25+
file:
26+
type: string
27+
format: binary
28+
description: A file to upload
29+
fileName:
30+
type: string
31+
description: The name of the file
32+
metadata:
33+
$ref: '#/components/schemas/Metadata'
34+
responses:
35+
"200":
36+
description: "Data uploaded successfully"
37+
content:
38+
application/json:
39+
schema:
40+
type: object
41+
additionalProperties:
42+
type: string
43+
"400":
44+
description: "Invalid data supplied"
45+
46+
/simple-multipart:
47+
post:
48+
tags:
49+
- Multipart
50+
description: Upload simple multipart data
51+
operationId: sendSimpleMultipart
52+
requestBody:
53+
required: true
54+
content:
55+
multipart/form-data:
56+
schema:
57+
type: object
58+
properties:
59+
name:
60+
type: string
61+
age:
62+
type: integer
63+
responses:
64+
"204":
65+
description: "Data uploaded"
66+
67+
components:
68+
schemas:
69+
Metadata:
70+
type: object
71+
properties:
72+
author:
73+
type: string
74+
description: Author name
75+
example: John Doe
76+
tags:
77+
type: array
78+
items:
79+
type: string
80+
description: Tags associated with the file
81+
example: ["document", "important"]
82+
83+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
quarkus.openapi-generator.codegen.spec.multipart_client_form_yml.base-package=org.acme.openapi.multipart.clientform
2+
quarkus.openapi-generator.codegen.spec.multipart_client_form_yml.additional-model-type-annotations=@io.quarkus.runtime.annotations.RegisterForReflection
3+
# Enable ClientMultipartForm for RESTEasy Reactive
4+
quarkus.openapi-generator.codegen.spec.multipart_client_form_yml.resteasy-reactive-client-form=true
5+
6+
quarkus.keycloak.devservices.enabled=false
7+

0 commit comments

Comments
 (0)