Skip to content

Commit 3feaad5

Browse files
authored
Merge pull request #45 from quarkiverse/issue-38
Fix #38 - Add option to skip generation of deprecated attributes
2 parents b7bd2ca + 6c8aaed commit 3feaad5

File tree

19 files changed

+604
-70
lines changed

19 files changed

+604
-70
lines changed

README.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,21 +277,29 @@ org.acme.openapi.simple.api.DefaultApi/byeGet/CircuitBreaker/successThreshold=22
277277
````
278278

279279
## 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.
280+
281+
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
282+
bodies.
281283

282284
You need to add the following additional dependency to your `pom.xml`:
285+
283286
```xml
287+
284288
<dependency>
285289
<groupId>io.quarkus</groupId>
286290
<artifactId>quarkus-resteasy-multipart</artifactId>
287291
</dependency>
288292
```
293+
289294
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,
295+
296+
- `javax.ws.rs.FormParam`, where the value parameter denotes the part name,
291297
- `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.
298+
- 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
299+
Content-Disposition header of the part.
300+
301+
For example, the model for a request that requires a file, a string and some complex object will look like this:
293302

294-
For example, the model for a request that requires a file, a string and some complex object will look like this:
295303
```java
296304
public class MultipartBody {
297305

@@ -311,33 +319,41 @@ public class MultipartBody {
311319
```
312320

313321
Then in the client the `org.jboss.resteasy.annotations.providers.multipart.MultipartForm` annotation is added in front of the multipart parameter:
322+
314323
```java
315324
@Path("/echo")
316325
@RegisterRestClient
317326
public interface MultipartService {
318-
327+
319328
@POST
320329
@Consumes(MediaType.MULTIPART_FORM_DATA)
321330
@Produces(MediaType.TEXT_PLAIN)
322331
String sendMultipartData(@MultipartForm MultipartBody data);
323-
332+
324333
}
325334
```
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.
327335

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.:
336+
See [Quarkus - Using the REST Client with Multipart](https://quarkus.io/guides/rest-client-multipart) and
337+
the [RESTEasy JAX-RS specifications](https://docs.jboss.org/resteasy/docs/4.7.5.Final/userguide/html_single/index.html) for more details.
338+
339+
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
340+
the `skip-form-model` property corresponding to your spec in the `application.properties` to `false`, e.g.:
341+
329342
```properties
330343
quarkus.openapi-generator.codegen.spec."my-multipart-requests.yml".skip-form-model=false
331344
```
332345

333346
### Default content-types according to OpenAPI Specification and limitations
347+
334348
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:
349+
335350
- If the property is a primitive, or an array of primitive values, the default Content-Type is `text/plain`
336351
- If the property is complex, or an array of complex values, the default Content-Type is `application/json`
337352
- 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`
338353

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-
354+
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
355+
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
356+
to `application/json`.
341357

342358
## Generating files via InputStream
343359

@@ -347,6 +363,13 @@ saved locally in your project.
347363

348364
See the example implementation [here](test-utils/src/main/java/io/quarkiverse/openapi/generator/testutils/codegen/ClassPathPetstoreOpenApiSpecInputProvider.java)
349365

366+
## Skip Deprecated Attributes in Model classes
367+
368+
The domain objects are classes generated in the `model` package. These classes might have [deprecated attributes](https://spec.openapis.org/oas/v3.1.0#fixed-fields-9) in the Open API specification
369+
file. By default, these attributes are generated. You can fine tune this behavior if the deprecated attributes should not be generated.
370+
371+
Use the property key `<base_package>.model.MyClass.generateDeprecated=false` to disable the deprecated attributes in the given model. For example `org.acme.weather.Country.generatedDeprecated=false`.
372+
350373
## Known Limitations
351374

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

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ public class SpecConfig {
1111
private static final String BASE_PACKAGE_PROP_FORMAT = "%s.base-package";
1212
private static final String SKIP_FORM_MODEL_PROP_FORMAT = "%s.skip-form-model";
1313

14+
public static String resolveApiPackage(final String basePackage) {
15+
return String.format("%s%s", basePackage, API_PKG_SUFFIX);
16+
}
17+
18+
public static String resolveModelPackage(final String basePackage) {
19+
return String.format("%s%s", basePackage, MODEL_PKG_SUFFIX);
20+
}
21+
1422
public static String getResolvedBasePackageProperty(final Path openApiFilePath) {
1523
return String.format(BASE_PACKAGE_PROP_FORMAT, getBuildTimeSpecPropertyPrefix(openApiFilePath));
1624
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.quarkiverse.openapi.generator.deployment.codegen;
2+
3+
import static io.quarkiverse.openapi.generator.deployment.SpecConfig.resolveModelPackage;
4+
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.stream.Collectors;
9+
import java.util.stream.StreamSupport;
10+
11+
import org.eclipse.microprofile.config.Config;
12+
13+
/**
14+
* Extracts the Model codegen properties from a given {@link Config} reference.
15+
* These properties are then injected in the OpenAPI generator to tweak the code generation properties.
16+
*/
17+
public final class ModelCodegenConfigParser {
18+
19+
public static Map<String, Object> parse(final Config config, final String basePackage) {
20+
final List<String> modelProperties = filterModelPropertyNames(config.getPropertyNames(),
21+
resolveModelPackage(basePackage));
22+
final Map<String, Object> modelConfig = new HashMap<>();
23+
modelProperties.forEach(m -> {
24+
modelConfig.put(m, config.getValue(m, String.class));
25+
});
26+
return modelConfig;
27+
}
28+
29+
private static List<String> filterModelPropertyNames(final Iterable<String> propertyNames, final String modelPackage) {
30+
return StreamSupport.stream(propertyNames.spliterator(), false)
31+
.filter(propertyName -> propertyName.startsWith(modelPackage))
32+
.collect(Collectors.toList());
33+
}
34+
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,16 @@ public boolean trigger(CodeGenContext context) throws CodeGenException {
5555
}
5656

5757
protected void generate(CodeGenContext context, final Path openApiFilePath, final Path outDir) {
58-
// TODO: do not generate with the output dir has generated files and the openapi file has the same checksum of the previous runz
58+
// TODO: do not generate if the output dir has generated files and the openapi file has the same checksum of the previous run
59+
final String basePackage = getResolvedBasePackageProperty(openApiFilePath);
60+
5961
final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(
6062
openApiFilePath.normalize(), outDir)
63+
.withModelCodeGenConfiguration(ModelCodegenConfigParser.parse(context.config(), basePackage))
6164
.withCircuitBreakerConfiguration(CircuitBreakerConfigurationParser.parse(
6265
context.config()));
6366
context.config()
64-
.getOptionalValue(getResolvedBasePackageProperty(openApiFilePath), String.class)
67+
.getOptionalValue(basePackage, String.class)
6568
.ifPresent(generator::withBasePackage);
6669
context.config()
6770
.getOptionalValue(getSkipFormModelPropertyName(openApiFilePath), String.class)
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.quarkiverse.openapi.generator.deployment.template;
22

33
import java.nio.file.Path;
4+
import java.util.HashMap;
45
import java.util.concurrent.CompletableFuture;
56
import java.util.concurrent.CompletionStage;
67
import java.util.concurrent.ExecutionException;
@@ -9,11 +10,29 @@
910
import io.quarkus.qute.Expression;
1011
import io.quarkus.qute.NamespaceResolver;
1112

12-
public class QuteTemplatingExtension implements NamespaceResolver {
13+
/**
14+
* Collection of OpenAPI specific methods to use in the templates.
15+
* It can be enhanced to have even more methods. See the usage of the method #genDeprecatedModelAttr to understand how to
16+
* implement and use them.
17+
*/
18+
public class OpenApiNamespaceResolver implements NamespaceResolver {
19+
private static final String GENERATE_DEPRECATED_PROP = "generateDeprecated";
1320

14-
static final QuteTemplatingExtension INSTANCE = new QuteTemplatingExtension();
21+
static final OpenApiNamespaceResolver INSTANCE = new OpenApiNamespaceResolver();
1522

16-
private QuteTemplatingExtension() {
23+
private OpenApiNamespaceResolver() {
24+
}
25+
26+
/**
27+
* @param pkg name of the given package
28+
* @param classname name of the Model class
29+
* @param codegenConfig Map with the model codegen properties
30+
* @return true if the given model class should generate the deprecated attributes
31+
*/
32+
public boolean genDeprecatedModelAttr(final String pkg, final String classname,
33+
final HashMap<String, Object> codegenConfig) {
34+
final String key = String.format("%s.%s.%s", pkg, classname, GENERATE_DEPRECATED_PROP);
35+
return Boolean.parseBoolean(codegenConfig.getOrDefault(key, "true").toString());
1736
}
1837

1938
public String parseUri(String uri) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public QuteTemplatingEngineAdapter() {
3636
this.engine = Engine.builder()
3737
.addDefaults()
3838
.addValueResolver(new ReflectionValueResolver())
39-
.addNamespaceResolver(QuteTemplatingExtension.INSTANCE)
39+
.addNamespaceResolver(OpenApiNamespaceResolver.INSTANCE)
4040
.removeStandaloneLines(true)
4141
.strictRendering(false)
4242
.build();

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

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

3+
import static io.quarkiverse.openapi.generator.deployment.SpecConfig.resolveApiPackage;
4+
import static io.quarkiverse.openapi.generator.deployment.SpecConfig.resolveModelPackage;
35
import static java.lang.Boolean.FALSE;
46
import static java.lang.Boolean.TRUE;
57

@@ -12,8 +14,6 @@
1214
import org.openapitools.codegen.DefaultGenerator;
1315
import org.openapitools.codegen.config.GlobalSettings;
1416

15-
import io.quarkiverse.openapi.generator.deployment.SpecConfig;
16-
1717
/**
1818
* Wrapper for the OpenAPIGen tool.
1919
* This is the same as calling the Maven plugin or the CLI.
@@ -76,7 +76,16 @@ public OpenApiClientGeneratorWrapper withBasePackage(final String pkg) {
7676
* @return this wrapper
7777
*/
7878
public OpenApiClientGeneratorWrapper withCircuitBreakerConfiguration(final Map<String, List<String>> config) {
79-
configurator.addAdditionalProperty("circuit-breaker", config);
79+
if (config != null) {
80+
configurator.addAdditionalProperty("circuit-breaker", config);
81+
}
82+
return this;
83+
}
84+
85+
public OpenApiClientGeneratorWrapper withModelCodeGenConfiguration(final Map<String, Object> config) {
86+
if (config != null) {
87+
configurator.addAdditionalProperty("model-codegen", config);
88+
}
8089
return this;
8190
}
8291

@@ -101,10 +110,10 @@ private void consolidatePackageNames() {
101110
basePackage = DEFAULT_PACKAGE;
102111
}
103112
if (apiPackage.isEmpty()) {
104-
this.apiPackage = basePackage.concat(SpecConfig.API_PKG_SUFFIX);
113+
this.apiPackage = resolveApiPackage(basePackage);
105114
}
106115
if (modelPackage.isEmpty()) {
107-
this.modelPackage = basePackage.concat(SpecConfig.MODEL_PKG_SUFFIX);
116+
this.modelPackage = resolveModelPackage(basePackage);
108117
}
109118
this.configurator.setPackageName(basePackage);
110119
this.configurator.setApiPackage(apiPackage);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ import javax.validation.Valid;
1515
{/if}
1616
{#for m in models}
1717
{#if m.model.isEnum}{#include enumOuterClass.qute e=m.model/}
18-
{#else}{#include pojo.qute m=m.model withXml=withXml/}{/if}
18+
{#else}{#include pojo.qute m=m.model withXml=withXml codegen=model-codegen package=modelPackage/}{/if}
1919
{/for}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
3232
public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if m.serializableModel} implements Serializable{/if} {
3333

3434
{#for v in m.vars}
35+
{#if !v.deprecated || openapi:genDeprecatedModelAttr(package, m.classname, codegen)}
3536
{#if v.isEnum}
3637
{#if v.isContainer && v.mostInnerItems}
3738
{#include enumClass.qute e=v/}
@@ -52,9 +53,11 @@ public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if m.serializa
5253
{#else}
5354
private {v.datatypeWithEnum} {v.name}{#if v.defaultValue} = {v.defaultValue}{/if};
5455
{/if}
56+
{/if}
5557
{/for}
5658

5759
{#for v in m.vars}
60+
{#if !v.deprecated || openapi:genDeprecatedModelAttr(package, m.classname, codegen)}
5861
/**
5962
{#if v.description}
6063
* {v.description}
@@ -112,6 +115,7 @@ public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if m.serializa
112115
{/if}
113116
{/if}
114117

118+
{/if}
115119
{/for}
116120
/**
117121
* Create a string representation of this pojo.
@@ -123,7 +127,9 @@ public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if m.serializa
123127
{#if m.parent}
124128
sb.append(" ").append(toIndentedString(super.toString())).append("\n");{/if}
125129
{#for v in m.vars}
130+
{#if !v.deprecated || openapi:genDeprecatedModelAttr(package, m.classname, codegen)}
126131
sb.append(" {v.name}: ").append(toIndentedString({v.name})).append("\n");
132+
{/if}
127133
{/for}
128134
sb.append("}");
129135
return sb.toString();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.quarkiverse.openapi.generator.deployment;
2+
3+
import java.io.IOException;
4+
import java.io.UncheckedIOException;
5+
import java.net.URL;
6+
import java.util.Objects;
7+
8+
import org.eclipse.microprofile.config.Config;
9+
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
10+
11+
import io.smallrye.config.PropertiesConfigSource;
12+
13+
public final class MockConfigUtils {
14+
15+
private MockConfigUtils() {
16+
}
17+
18+
public static Config getTestConfig(String propertiesFile) {
19+
PropertiesConfigSource configSource;
20+
try {
21+
configSource = new PropertiesConfigSource(getResource(propertiesFile));
22+
} catch (IOException e) {
23+
throw new UncheckedIOException(e);
24+
}
25+
26+
return ConfigProviderResolver
27+
.instance()
28+
.getBuilder()
29+
.withSources(configSource)
30+
.build();
31+
}
32+
33+
private static URL getResource(String resourcePath) {
34+
return Objects.requireNonNull(MockConfigUtils.class.getResource(resourcePath));
35+
}
36+
37+
}

0 commit comments

Comments
 (0)