Skip to content

Commit 9c037be

Browse files
Merge branch 'main' into dependabot/maven/quarkus.version-2.8.0.Final
2 parents 98cc86c + 0c1aeaf commit 9c037be

File tree

16 files changed

+482
-37
lines changed

16 files changed

+482
-37
lines changed

.all-contributorsrc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"avatar_url": "https://avatars.githubusercontent.com/u/11776454?v=4",
2222
"profile": "http://thegreatapi.com",
2323
"contributions": [
24-
"doc"
24+
"doc",
25+
"code"
2526
]
2627
},
2728
{
@@ -51,6 +52,15 @@
5152
"contributions": [
5253
"code"
5354
]
55+
},
56+
{
57+
"login": "Orbifoldt",
58+
"name": "Orbifoldt",
59+
"avatar_url": "https://avatars.githubusercontent.com/u/30009459?v=4",
60+
"profile": "https://github.com/Orbifoldt",
61+
"contributions": [
62+
"code"
63+
]
5464
}
5565
],
5666
"contributorsPerLine": 7,

README.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Quarkus - OpenAPI Generator
22

33
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4-
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-)
4+
[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-)
55
<!-- ALL-CONTRIBUTORS-BADGE:END -->
66

77
Quarkus' extension for generation of [Rest Clients](https://quarkus.io/guides/rest-client) based on OpenAPI specification files.
@@ -276,6 +276,69 @@ org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/failureRatio=3.14
276276
org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/successThreshold=22
277277
````
278278

279+
## Sending multipart/form-data
280+
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 bodies.
281+
282+
You need to add the following additional dependency to your `pom.xml`:
283+
```xml
284+
<dependency>
285+
<groupId>io.quarkus</groupId>
286+
<artifactId>quarkus-resteasy-multipart</artifactId>
287+
</dependency>
288+
```
289+
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:
290+
- `javax.ws.rs.FormParam`, where the value parameter denotes the part name,
291+
- `org.jboss.resteasy.annotations.providers.multipart.PartType`, where the parameter is the jax-rs MediaType of the part (see below for details),
292+
- 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 Content-Disposition header of the part.
293+
294+
For example, the model for a request that requires a file, a string and some complex object will look like this:
295+
```java
296+
public class MultipartBody {
297+
298+
@FormParam("file")
299+
@PartType(MediaType.APPLICATION_OCTET_STREAM)
300+
@PartFilename("defaultFileName")
301+
public File file;
302+
303+
@FormParam("fileName")
304+
@PartType(MediaType.TEXT_PLAIN)
305+
public String fileName;
306+
307+
@FormParam("someObject")
308+
@PartType(MediaType.APPLICATION_JSON)
309+
public MyComplexObject someObject;
310+
}
311+
```
312+
313+
Then in the client the `org.jboss.resteasy.annotations.providers.multipart.MultipartForm` annotation is added in front of the multipart parameter:
314+
```java
315+
@Path("/echo")
316+
@RegisterRestClient
317+
public interface MultipartService {
318+
319+
@POST
320+
@Consumes(MediaType.MULTIPART_FORM_DATA)
321+
@Produces(MediaType.TEXT_PLAIN)
322+
String sendMultipartData(@MultipartForm MultipartBody data);
323+
324+
}
325+
```
326+
See [Quarkus - Using the REST Client with Multipart](https://quarkus.io/guides/rest-client-multipart) and the [RESTEasy JAX-RS specifications](https://docs.jboss.org/resteasy/docs/4.7.5.Final/userguide/html_single/index.html) for more details.
327+
328+
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 the `skip-form-model` property corresponding to your spec in the `application.properties` to `false`, e.g.:
329+
```properties
330+
quarkus.openapi-generator.codegen.spec."my-multipart-requests.yml".skip-form-model=false
331+
```
332+
333+
### Default content-types according to OpenAPI Specification and limitations
334+
The [OAS 3.0](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#special-considerations-for-multipart-content) specifies the following default content-types for a multipart:
335+
- If the property is a primitive, or an array of primitive values, the default Content-Type is `text/plain`
336+
- If the property is complex, or an array of complex values, the default Content-Type is `application/json`
337+
- If the property is a `type: string` with `format: binary` or `format: base64` (aka a file object), the default Content-Type is `application/octet-stream`
338+
339+
A different content-type may be defined in your api spec, but this is not yet supported in the code generation. Also, this "annotation-oriented" approach of RestEasy (i.e. using `@MultipartForm` to denote the multipart body parameter) does not seem to properly support the unmarshalling of arrays of the same type (e.g. array of files), in these cases it uses Content-Type equal to `application/json`.
340+
341+
279342
## Generating files via InputStream
280343

281344
Having the files in the `src/main/openapi` directory will generate the REST stubs by default. Alternatively, you can implement the `io.quarkiverse.openapi.generator.codegen.OpenApiSpecInputProvider`
@@ -304,10 +367,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
304367
<table>
305368
<tr>
306369
<td align="center"><a href="https://ricardozanini.medium.com/"><img src="https://avatars.githubusercontent.com/u/1538000?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ricardo Zanini</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=ricardozanini" title="Code">💻</a> <a href="#maintenance-ricardozanini" title="Maintenance">🚧</a></td>
307-
<td align="center"><a href="http://thegreatapi.com"><img src="https://avatars.githubusercontent.com/u/11776454?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Helber Belmiro</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=hbelmiro" title="Documentation">📖</a></td>
370+
<td align="center"><a href="http://thegreatapi.com"><img src="https://avatars.githubusercontent.com/u/11776454?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Helber Belmiro</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=hbelmiro" title="Documentation">📖</a> <a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=hbelmiro" title="Code">💻</a></td>
308371
<td align="center"><a href="http://gastaldi.wordpress.com"><img src="https://avatars.githubusercontent.com/u/54133?v=4?s=100" width="100px;" alt=""/><br /><sub><b>George Gastaldi</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=gastaldi" title="Code">💻</a> <a href="#infra-gastaldi" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
309372
<td align="center"><a href="https://github.com/RishiKumarRay"><img src="https://avatars.githubusercontent.com/u/87641376?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rishi Kumar Ray</b></sub></a><br /><a href="#infra-RishiKumarRay" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
310373
<td align="center"><a href="https://github.com/fjtirado"><img src="https://avatars.githubusercontent.com/u/65240126?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Francisco Javier Tirado Sarti</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=fjtirado" title="Code">💻</a></td>
374+
<td align="center"><a href="https://github.com/Orbifoldt"><img src="https://avatars.githubusercontent.com/u/30009459?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Orbifoldt</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-openapi-generator/commits?author=Orbifoldt" title="Code">💻</a></td>
311375
</tr>
312376
</table>
313377

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class SpecConfig {
1313
public static final String MODEL_PKG_SUFFIX = ".model";
1414
public static final String BUILD_TIME_SPEC_PREFIX_FORMAT = BUILD_TIME_CONFIG_PREFIX + ".spec.\"%s\"";
1515
private static final String BASE_PACKAGE_PROP_FORMAT = "%s.base-package";
16+
private static final String SKIP_FORM_MODEL_PROP_FORMAT = "%s.skip-form-model";
1617

1718
/**
1819
* Defines the base package name for the generated classes.
@@ -32,6 +33,10 @@ public static String getResolvedBasePackageProperty(final Path openApiFilePath)
3233
return String.format(BASE_PACKAGE_PROP_FORMAT, getBuildTimeSpecPropertyPrefix(openApiFilePath));
3334
}
3435

36+
public static String getSkipFormModelPropertyName(final Path openApiFilePath) {
37+
return String.format(SKIP_FORM_MODEL_PROP_FORMAT, getBuildTimeSpecPropertyPrefix(openApiFilePath));
38+
}
39+
3540
/**
3641
* Gets the config prefix for a given OpenAPI file in the path.
3742
* For example, given a path like /home/luke/projects/petstore.json, the returned value is

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.quarkiverse.openapi.generator.deployment.codegen;
22

33
import static io.quarkiverse.openapi.generator.deployment.SpecConfig.getResolvedBasePackageProperty;
4+
import static io.quarkiverse.openapi.generator.deployment.SpecConfig.getSkipFormModelPropertyName;
45

56
import java.io.IOException;
67
import java.nio.file.Files;
@@ -62,6 +63,10 @@ protected void generate(CodeGenContext context, final Path openApiFilePath, fina
6263
context.config()
6364
.getOptionalValue(getResolvedBasePackageProperty(openApiFilePath), String.class)
6465
.ifPresent(generator::withBasePackage);
66+
context.config()
67+
.getOptionalValue(getSkipFormModelPropertyName(openApiFilePath), String.class)
68+
.ifPresent(generator::withSkipFormModelConfig);
69+
6570
generator.generate();
6671
}
6772
}

deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public class QuteTemplatingEngineAdapter extends AbstractTemplatingEngineAdapter
2222
"bodyParams.qute",
2323
"enumClass.qute",
2424
"enumOuterClass.qute",
25-
"formParams.qute",
2625
"headerParams.qute",
2726
"pathParams.qute",
2827
"pojo.qute",
2928
"queryParams.qute",
3029
"auth/compositeAuthenticationProvider.qute",
31-
"auth/authConfig.qute"
30+
"auth/authConfig.qute",
31+
"multipartFormdataPojo.qute"
3232
};
3333
public final Engine engine;
3434

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ public OpenApiClientGeneratorWrapper withCircuitBreakerConfiguration(final Map<S
8080
return this;
8181
}
8282

83+
/**
84+
* Sets the global 'skipFormModel' setting. If not set this setting will default to true.
85+
*
86+
* @param skipFormModel whether to skip the generation of models for form parameters
87+
* @return this wrapper
88+
*/
89+
public OpenApiClientGeneratorWrapper withSkipFormModelConfig(final String skipFormModel) {
90+
GlobalSettings.setProperty(CodegenConstants.SKIP_FORM_MODEL, skipFormModel);
91+
return this;
92+
}
93+
8394
public List<File> generate() {
8495
this.consolidatePackageNames();
8596
return generator.opts(configurator.toClientOptInput()).generate();

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,24 @@ public interface {classname} {
5858
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
5959
{/if}{/for}
6060
{/if}{/for}
61-
public {#if op.returnType}{op.returnType}{#else}void{/if} {op.nickname}({#for p in op.allParams}{#include pathParams.qute param=p/}{#include queryParams.qute param=p/}{#include bodyParams.qute param=p/}{#include formParams.qute param=p/}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for});
61+
public {#if op.returnType}{op.returnType}{#else}void{/if} {op.nickname}(
62+
{#if op.hasFormParams}
63+
@org.jboss.resteasy.annotations.providers.multipart.MultipartForm {op.operationIdCamelCase}MultipartForm multipartForm{#if op.hasPathParams},{/if}{!
64+
!}{#for p in op.pathParams}{#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}{!
65+
!}{#for p in op.queryParams}{#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},{/if}{!
66+
!}{#for p in op.bodyParams}{#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}{!
67+
!}{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}
68+
{#else}
69+
{#for p in op.allParams}
70+
{#include pathParams.qute param=p/}{#include queryParams.qute param=p/}{#include bodyParams.qute param=p/}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}
71+
{/for}
72+
{/if}
73+
);
74+
{#if op.hasFormParams}
75+
76+
{#include multipartFormdataPojo.qute param=op/}
77+
{/if}
78+
6279

6380
{/for}
6481
}

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

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public static class {op.operationIdCamelCase}MultipartForm {
2+
{#for p in op.formParams}
3+
@FormParam("{p.baseName}")
4+
{#if p.isFile || (p.isString && p.dataFormat == 'base64')}
5+
@org.jboss.resteasy.annotations.providers.multipart.PartFilename("{p.baseName}File")
6+
@org.jboss.resteasy.annotations.providers.multipart.PartType(MediaType.APPLICATION_OCTET_STREAM)
7+
{#else if p.isPrimitiveType or (p.isArray && p.items.isPrimitiveType)}
8+
@org.jboss.resteasy.annotations.providers.multipart.PartType(MediaType.TEXT_PLAIN)
9+
{#else}
10+
@org.jboss.resteasy.annotations.providers.multipart.PartType(MediaType.APPLICATION_JSON)
11+
{/if}
12+
public {p.dataType} {p.paramName};
13+
{/for}
14+
}

0 commit comments

Comments
 (0)