Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ enum ConfigName {
GENERATE_MODEL_FOR_USAGE_AS_BEAN_PARAM("generate-model-for-usage-as-bean-param"),
EQUALS_HASHCODE("equals-hashcode"),
USE_DYNAMIC_URL("use-dynamic-url"),
METHOD_PER_MEDIA_TYPE("method-per-media-type");
METHOD_PER_MEDIA_TYPE("method-per-media-type"),
RESTEASY_REACTIVE_CLIENT_FORM("resteasy-reactive-client-form");

private final String name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ public interface CommonItemConfig {
@WithDefault("true")
Optional<Boolean> registerRestClient();

/**
* Use {@link org.jboss.resteasy.reactive.client.api.ClientMultipartForm} for multipart request bodies when generating
* RESTEasy Reactive clients. This avoids generating a multipart form POJO and lets callers build multipart payloads
* programmatically.
*/
@WithName("resteasy-reactive-client-form")
@WithDefault("false")
Optional<Boolean> resteasyReactiveClientForm();

/**
* Which CDI scope annotation (if any) should be placed on the generated API. Defaults to
* {@code @jakarta.enterprise.context.ApplicationScoped}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.REMOVE_OPERATION_ID_PREFIX;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.REMOVE_OPERATION_ID_PREFIX_COUNT;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.REMOVE_OPERATION_ID_PREFIX_DELIMITER;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.RESTEASY_REACTIVE_CLIENT_FORM;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.TEMPLATE_BASE_DIR;
import static io.quarkiverse.openapi.generator.deployment.CodegenConfig.ConfigName.VALIDATE_SPEC;

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

getValues(config, openApiFilePath, RESTEASY_REACTIVE_CLIENT_FORM, Boolean.class)
.ifPresent(generator::withResteasyReactiveClientForm);

getValues(smallRyeConfig, openApiFilePath, CodegenConfig.ConfigName.METHOD_PER_MEDIA_TYPE, Boolean.class)
.ifPresent(generator::withMethodPerMediaType);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ private void setDefaults() {
this.configurator.addAdditionalProperty("verbose", FALSE);
this.configurator.addAdditionalProperty(CodegenConstants.SERIALIZABLE_MODEL, FALSE);
this.configurator.addAdditionalProperty("equals-hashcode", TRUE);
this.configurator.addAdditionalProperty("is-resteasy-reactive-client-form", FALSE);
this.configurator.addAdditionalProperty("use-dynamic-url", FALSE);
this.configurator.addAdditionalProperty("generate-model-for-usage-as-bean-param", TRUE);
this.configurator.addAdditionalProperty("method-per-media-type", FALSE);
Expand Down Expand Up @@ -363,6 +364,11 @@ public OpenApiClientGeneratorWrapper withMethodPerMediaType(final Boolean method
return this;
}

public OpenApiClientGeneratorWrapper withResteasyReactiveClientForm(final Boolean resteasyReactiveClientForm) {
this.configurator.addAdditionalProperty("is-resteasy-reactive-client-form", resteasyReactiveClientForm);
return this;
}

/**
* Main entrypoint, or where to generate the files based on the given base package.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ public interface {classname} {
@io.quarkus.rest.client.reactive.Url String dynUrl{#if op.allParams || op.hasFormParams},{/if}
{/if}
{#if op.hasFormParams}
{#if is-resteasy-reactive}
{#if is-resteasy-reactive && is-resteasy-reactive-client-form}
org.jboss.resteasy.reactive.client.api.ClientMultipartForm dataParts{#if op.hasPathParams},{/if}
{#else if is-resteasy-reactive}
@jakarta.ws.rs.BeanParam {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
{#else}
@org.jboss.resteasy.annotations.providers.multipart.MultipartForm {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
Expand All @@ -193,7 +195,9 @@ public interface {classname} {
{/for}{/if}
);
{#if op.hasFormParams}
{#if !(is-resteasy-reactive && is-resteasy-reactive-client-form)}
{#include multipartFormdataPojo.qute param=op/}
{/if}
{/if}

{/for} {! end consume-loop !}
Expand Down Expand Up @@ -328,7 +332,9 @@ public interface {classname} {
@io.quarkus.rest.client.reactive.Url String dynUrl{#if op.allParams || op.hasFormParams},{/if}
{/if}
{#if op.hasFormParams}
{#if is-resteasy-reactive}
{#if is-resteasy-reactive && is-resteasy-reactive-client-form}
org.jboss.resteasy.reactive.client.api.ClientMultipartForm dataParts{#if op.hasPathParams},{/if}
{#else if is-resteasy-reactive}
@jakarta.ws.rs.BeanParam {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
{#else}
@org.jboss.resteasy.annotations.providers.multipart.MultipartForm {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}
Expand All @@ -353,7 +359,9 @@ public interface {classname} {
{/for}{/if}
);
{#if op.hasFormParams}
{#if !(is-resteasy-reactive && is-resteasy-reactive-client-form)}
{#include multipartFormdataPojo.qute param=op/}
{/if}
{/if}

{/if} {! end method-per-media-type else !}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,64 @@ void verifyMultipartFormAnnotationIsGeneratedForParameter() throws URISyntaxExce
assertThat(param.getAnnotationByName("MultipartForm")).isPresent();
}

@Test
void verifyReactiveMultipartFormStillUsesBeanParamByDefault() throws URISyntaxException, FileNotFoundException {
List<File> generatedFiles = createGeneratorWrapperReactive("multipart-openapi.yml").withSkipFormModelConfig("false")
.generate("org.acme");
assertThat(generatedFiles).isNotEmpty();

Optional<File> file = generatedFiles.stream().filter(f -> f.getName().endsWith("UserProfileDataApi.java")).findAny();
assertThat(file).isPresent();

CompilationUnit compilationUnit = StaticJavaParser.parse(file.orElseThrow());
List<MethodDeclaration> methodDeclarations = compilationUnit.findAll(MethodDeclaration.class);
assertThat(methodDeclarations).isNotEmpty();

Optional<MethodDeclaration> multipartPostMethod = methodDeclarations.stream()
.filter(m -> m.getNameAsString().equals("postUserProfileData")).findAny();
assertThat(multipartPostMethod).isPresent();

List<Parameter> parameters = multipartPostMethod.orElseThrow().getParameters();
assertThat(parameters).hasSize(1);

Parameter param = parameters.get(0);
assertThat(param.getTypeAsString()).isEqualTo("PostUserProfileDataMultipartForm");
assertThat(param.getNameAsString()).isEqualTo("multipartForm");
assertThat(param.getAnnotationByName("BeanParam")).isPresent();
assertThat(compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
.filter(c -> c.getNameAsString().equals("PostUserProfileDataMultipartForm")).findAny()).isNotEmpty();
}

@Test
void verifyReactiveMultipartFormCanUseClientMultipartForm() throws URISyntaxException, FileNotFoundException {
List<File> generatedFiles = createGeneratorWrapperReactive("multipart-openapi.yml")
.withSkipFormModelConfig("false")
.withResteasyReactiveClientForm(true)
.generate("org.acme");
assertThat(generatedFiles).isNotEmpty();

Optional<File> file = generatedFiles.stream().filter(f -> f.getName().endsWith("UserProfileDataApi.java")).findAny();
assertThat(file).isPresent();

CompilationUnit compilationUnit = StaticJavaParser.parse(file.orElseThrow());
List<MethodDeclaration> methodDeclarations = compilationUnit.findAll(MethodDeclaration.class);
assertThat(methodDeclarations).isNotEmpty();

Optional<MethodDeclaration> multipartPostMethod = methodDeclarations.stream()
.filter(m -> m.getNameAsString().equals("postUserProfileData")).findAny();
assertThat(multipartPostMethod).isPresent();

List<Parameter> parameters = multipartPostMethod.orElseThrow().getParameters();
assertThat(parameters).hasSize(1);

Parameter param = parameters.get(0);
assertThat(param.getTypeAsString()).isEqualTo("org.jboss.resteasy.reactive.client.api.ClientMultipartForm");
assertThat(param.getNameAsString()).isEqualTo("dataParts");
assertThat(param.getAnnotations()).isEmpty();
assertThat(compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream()
.filter(c -> c.getNameAsString().equals("PostUserProfileDataMultipartForm")).findAny()).isEmpty();
}

@Test
void verifyMultipartPojoGeneratedAndFieldsHaveAnnotations() throws URISyntaxException, FileNotFoundException {
List<File> generatedFiles = createGeneratorWrapper("multipart-openapi.yml").withSkipFormModelConfig("false")
Expand Down
93 changes: 93 additions & 0 deletions client/integration-tests/multipart-client-form/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<artifactId>quarkus-openapi-generator-integration-tests</artifactId>
<groupId>io.quarkiverse.openapi.generator</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-openapi-generator-it-multipart-client-form</artifactId>
<name>Quarkus - OpenAPI Generator - Integration Tests - Client - Multipart ClientMultipartForm</name>

<dependencies>
<dependency>
<groupId>io.quarkiverse.openapi.generator</groupId>
<artifactId>quarkus-openapi-generator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
<version>${quarkus.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
openapi: 3.0.3
info:
title: "Multipart ClientMultipartForm API"
description: An API that uses ClientMultipartForm for multipart/form-data requests
version: 1.0.0

servers:
- url: "http://my.endpoint.com/api/v1"

paths:
/multipart:
post:
tags:
- Multipart
description: Upload multipart data using ClientMultipartForm
operationId: sendMultipartData
requestBody:
required: true
description: Multipart form data
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
description: A file to upload
fileName:
type: string
description: The name of the file
metadata:
$ref: '#/components/schemas/Metadata'
responses:
"200":
description: "Data uploaded successfully"
content:
application/json:
schema:
type: object
additionalProperties:
type: string
"400":
description: "Invalid data supplied"

/simple-multipart:
post:
tags:
- Multipart
description: Upload simple multipart data
operationId: sendSimpleMultipart
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
name:
type: string
age:
type: integer
responses:
"204":
description: "Data uploaded"

components:
schemas:
Metadata:
type: object
properties:
author:
type: string
description: Author name
example: John Doe
tags:
type: array
items:
type: string
description: Tags associated with the file
example: ["document", "important"]


Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
quarkus.openapi-generator.codegen.spec.multipart_client_form_yml.base-package=org.acme.openapi.multipart.clientform
quarkus.openapi-generator.codegen.spec.multipart_client_form_yml.additional-model-type-annotations=@io.quarkus.runtime.annotations.RegisterForReflection
# Enable ClientMultipartForm for RESTEasy Reactive
quarkus.openapi-generator.codegen.spec.multipart_client_form_yml.resteasy-reactive-client-form=true

quarkus.keycloak.devservices.enabled=false

Loading
Loading