Skip to content

Commit b4d52c0

Browse files
Fix #852 - Process Qute templates in a strict manner (#864) (#870)
* Fix #852 - Process Qute templates in a strict manner * Fix discriminator annotations on pojo.qute * Improve testing on discriminator annotations * Incorporate @hbelmiro review --------- Signed-off-by: Ricardo Zanini <[email protected]> Co-authored-by: Ricardo Zanini <[email protected]>
1 parent 75f328d commit b4d52c0

File tree

50 files changed

+6460
-160
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+6460
-160
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.quarkiverse.openapi.generator.deployment.template;
2+
3+
import java.util.List;
4+
import java.util.concurrent.ExecutionException;
5+
6+
import io.quarkus.qute.EvalContext;
7+
import io.quarkus.qute.Expression;
8+
9+
final class ExprEvaluator {
10+
11+
private ExprEvaluator() {
12+
}
13+
14+
@SuppressWarnings("unchecked")
15+
public static <T> T evaluate(EvalContext context, Expression expression) throws ExecutionException, InterruptedException {
16+
return (T) context.evaluate(expression).toCompletableFuture().get();
17+
}
18+
19+
@SuppressWarnings("unchecked")
20+
public static <T> T[] evaluate(EvalContext context, List<Expression> expressions, Class<T> type)
21+
throws ExecutionException, InterruptedException {
22+
T[] results = (T[]) java.lang.reflect.Array.newInstance(type, expressions.size());
23+
24+
for (int i = 0; i < expressions.size(); i++) {
25+
Expression expression = expressions.get(i);
26+
T result = type.cast(context.evaluate(expression).toCompletableFuture().get());
27+
results[i] = result;
28+
}
29+
30+
return results;
31+
}
32+
33+
}

client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/OpenApiNamespaceResolver.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.util.concurrent.CompletionStage;
88
import java.util.concurrent.ExecutionException;
99

10+
import org.openapitools.codegen.model.OperationMap;
11+
1012
import io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorOutputPaths;
1113
import io.quarkus.qute.EvalContext;
1214
import io.quarkus.qute.Expression;
@@ -18,9 +20,8 @@
1820
* implement and use them.
1921
*/
2022
public class OpenApiNamespaceResolver implements NamespaceResolver {
21-
private static final String GENERATE_DEPRECATED_PROP = "generateDeprecated";
22-
2323
static final OpenApiNamespaceResolver INSTANCE = new OpenApiNamespaceResolver();
24+
private static final String GENERATE_DEPRECATED_PROP = "generateDeprecated";
2425

2526
private OpenApiNamespaceResolver() {
2627
}
@@ -53,6 +54,10 @@ public String parseUri(String uri) {
5354
return OpenApiGeneratorOutputPaths.getRelativePath(Path.of(uri)).toString().replace(File.separatorChar, '/');
5455
}
5556

57+
public boolean hasAuthMethods(OperationMap operations) {
58+
return operations != null && operations.getOperation().stream().anyMatch(operation -> operation.hasAuthMethods);
59+
}
60+
5661
@Override
5762
public CompletionStage<Object> resolve(EvalContext context) {
5863
try {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ public QuteTemplatingEngineAdapter() {
4242
.addDefaults()
4343
.addValueResolver(new ReflectionValueResolver())
4444
.addNamespaceResolver(OpenApiNamespaceResolver.INSTANCE)
45+
.addNamespaceResolver(StrNamespaceResolver.INSTANCE)
4546
.removeStandaloneLines(true)
46-
.strictRendering(false)
47+
.strictRendering(true)
4748
.build();
4849
}
4950

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.quarkiverse.openapi.generator.deployment.template;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
import java.util.concurrent.CompletionStage;
5+
6+
import io.quarkus.qute.EvalContext;
7+
import io.quarkus.qute.NamespaceResolver;
8+
9+
/**
10+
* Namespace resolver to mimic the function of io.quarkus.qute.runtime.extensions.StringTemplateExtensions.
11+
* This extension is built-in with Qute when used in a context with Quarkus DI.
12+
* Since these extensions are built in build time by Qute we can't use them because our process also runs in build time without
13+
* a CDI context.
14+
* So any namespace resolver auto-generated by Quarkus won't be added to our engine.
15+
*
16+
* @see <a href="https://quarkus.io/guides/qute-reference#template_extension_methods">Template Extension Methods</a>
17+
*/
18+
public class StrNamespaceResolver implements NamespaceResolver {
19+
20+
static final StrNamespaceResolver INSTANCE = new StrNamespaceResolver();
21+
22+
private StrNamespaceResolver() {
23+
}
24+
25+
@Override
26+
public String getNamespace() {
27+
return "str";
28+
}
29+
30+
/**
31+
* @see io.quarkus.qute.runtime.extensions.StringTemplateExtensions#fmt(String, String, Object...)
32+
*/
33+
public String fmt(String format, Object... args) {
34+
return String.format(format, args);
35+
}
36+
37+
@Override
38+
public CompletionStage<Object> resolve(EvalContext context) {
39+
switch (context.getName()) {
40+
case "fmt":
41+
if (context.getParams().size() < 2) {
42+
throw new IllegalArgumentException(
43+
"Missing required parameter for 'fmt'. Make sure that the function has at least two parameters");
44+
}
45+
try {
46+
return CompletableFuture.completedFuture(
47+
fmt(ExprEvaluator.evaluate(context, context.getParams().get(0)),
48+
ExprEvaluator.evaluate(context, context.getParams().subList(1, context.getParams().size()),
49+
Object.class)));
50+
} catch (Exception e) {
51+
throw new RuntimeException(e);
52+
}
53+
default:
54+
throw new IllegalArgumentException("There's no method named '" + context.getName() + "'");
55+
}
56+
}
57+
58+
}

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

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,14 @@
3333
public abstract class OpenApiClientGeneratorWrapper {
3434

3535
public static final String VERBOSE = "verbose";
36-
private static final String ONCE_LOGGER = "org.openapitools.codegen.utils.oncelogger.enabled";
3736
/**
3837
* Security scheme for which to apply security constraints even if the OpenAPI definition has no security definition
3938
*/
4039
public static final String DEFAULT_SECURITY_SCHEME = "defaultSecurityScheme";
4140
public static final String SUPPORTS_ADDITIONAL_PROPERTIES_AS_ATTRIBUTE = "supportsAdditionalPropertiesWithComposedSchema";
42-
private static final Map<String, String> defaultTypeMappings = Map.of(
43-
"date", "LocalDate",
44-
"DateTime", "OffsetDateTime");
45-
private static final Map<String, String> defaultImportMappings = Map.of(
46-
"LocalDate", "java.time.LocalDate",
41+
private static final String ONCE_LOGGER = "org.openapitools.codegen.utils.oncelogger.enabled";
42+
private static final Map<String, String> defaultTypeMappings = Map.of("date", "LocalDate", "DateTime", "OffsetDateTime");
43+
private static final Map<String, String> defaultImportMappings = Map.of("LocalDate", "java.time.LocalDate",
4744
"OffsetDateTime", "java.time.OffsetDateTime");
4845
private final QuarkusCodegenConfigurator configurator;
4946
private final DefaultGenerator generator;
@@ -53,8 +50,8 @@ public abstract class OpenApiClientGeneratorWrapper {
5350
private String modelPackage = "";
5451

5552
OpenApiClientGeneratorWrapper(final QuarkusCodegenConfigurator configurator, final Path specFilePath, final Path outputDir,
56-
final boolean verbose,
57-
final boolean validateSpec) {
53+
final boolean verbose, final boolean validateSpec) {
54+
5855
// do not generate docs nor tests
5956
GlobalSettings.setProperty(CodegenConstants.API_DOCS, FALSE.toString());
6057
GlobalSettings.setProperty(CodegenConstants.API_TESTS, FALSE.toString());
@@ -78,17 +75,44 @@ public abstract class OpenApiClientGeneratorWrapper {
7875
defaultTypeMappings.forEach(this.configurator::addTypeMapping);
7976
defaultImportMappings.forEach(this.configurator::addImportMapping);
8077

81-
this.generator = new DefaultGenerator();
82-
}
78+
this.setDefaults();
8379

84-
public OpenApiClientGeneratorWrapper withApiPackage(final String pkg) {
85-
this.apiPackage = pkg;
86-
return this;
80+
this.generator = new DefaultGenerator();
8781
}
8882

89-
public OpenApiClientGeneratorWrapper withModelPackage(final String pkg) {
90-
this.modelPackage = pkg;
91-
return this;
83+
/**
84+
* A few properties from the "with*" methods must be injected in the Qute context by default since we turned strict model
85+
* rendering.
86+
* This way we avoid side effects in the model such as "NOT_FOUND" strings printed everywhere.
87+
*
88+
* @see <a href="https://quarkus.io/guides/qute-reference#configuration-reference">Qute - Configuration Reference</a>
89+
*/
90+
private void setDefaults() {
91+
// Set default values directly here
92+
this.configurator.addAdditionalProperty("additionalApiTypeAnnotations", new String[0]);
93+
this.configurator.addAdditionalProperty("additionalPropertiesAsAttribute", FALSE);
94+
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMember", FALSE);
95+
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMemberName", "");
96+
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMemberStringValue", "");
97+
this.configurator.addAdditionalProperty("additionalRequestArgs", new String[0]);
98+
this.configurator.addAdditionalProperty("classes-codegen", new HashMap<>());
99+
this.configurator.addAdditionalProperty("circuit-breaker", new HashMap<>());
100+
this.configurator.addAdditionalProperty("configKey", "");
101+
this.configurator.addAdditionalProperty("datatypeWithEnum", "");
102+
this.configurator.addAdditionalProperty("enable-security-generation", TRUE);
103+
this.configurator.addAdditionalProperty("generate-part-filename", FALSE);
104+
this.configurator.addAdditionalProperty("mutiny", FALSE);
105+
this.configurator.addAdditionalProperty("mutiny-operation-ids", new HashMap<>());
106+
this.configurator.addAdditionalProperty("mutiny-return-response", FALSE);
107+
this.configurator.addAdditionalProperty("part-filename-value", "");
108+
this.configurator.addAdditionalProperty("return-response", FALSE);
109+
this.configurator.addAdditionalProperty("skipFormModel", TRUE);
110+
this.configurator.addAdditionalProperty("templateDir", "");
111+
this.configurator.addAdditionalProperty("use-bean-validation", FALSE);
112+
this.configurator.addAdditionalProperty("use-field-name-in-part-filename", FALSE);
113+
this.configurator.addAdditionalProperty("verbose", FALSE);
114+
// TODO: expose as properties https://github.com/quarkiverse/quarkus-openapi-generator/issues/869
115+
this.configurator.addAdditionalProperty(CodegenConstants.SERIALIZABLE_MODEL, FALSE);
92116
}
93117

94118
/**
@@ -98,30 +122,30 @@ public OpenApiClientGeneratorWrapper withModelPackage(final String pkg) {
98122
* @return this wrapper
99123
*/
100124
public OpenApiClientGeneratorWrapper withCircuitBreakerConfig(final Map<String, List<String>> config) {
101-
if (config != null) {
102-
configurator.addAdditionalProperty("circuit-breaker", config);
103-
}
125+
Optional.ofNullable(config).ifPresent(cfg -> {
126+
this.configurator.addAdditionalProperty("circuit-breaker", config);
127+
});
104128
return this;
105129
}
106130

107131
public OpenApiClientGeneratorWrapper withClassesCodeGenConfig(final Map<String, Object> config) {
108-
if (config != null) {
109-
configurator.addAdditionalProperty("classes-codegen", config);
110-
}
132+
Optional.ofNullable(config).ifPresent(cfg -> {
133+
this.configurator.addAdditionalProperty("classes-codegen", cfg);
134+
});
111135
return this;
112136
}
113137

114138
public OpenApiClientGeneratorWrapper withMutiny(final Boolean config) {
115-
if (config != null) {
116-
configurator.addAdditionalProperty("mutiny", config);
117-
}
139+
Optional.ofNullable(config).ifPresent(cfg -> {
140+
this.configurator.addAdditionalProperty("mutiny", cfg);
141+
});
118142
return this;
119143
}
120144

121145
public OpenApiClientGeneratorWrapper withMutinyReturnResponse(final Boolean config) {
122-
if (config != null) {
123-
configurator.addAdditionalProperty("mutiny-return-response", config);
124-
}
146+
Optional.ofNullable(config).ifPresent(cfg -> {
147+
this.configurator.addAdditionalProperty("mutiny-return-response", cfg);
148+
});
125149
return this;
126150
}
127151

@@ -209,16 +233,16 @@ public OpenApiClientGeneratorWrapper withAdditionalEnumTypeUnexpectedMemberStrin
209233
* @return this wrapper
210234
*/
211235
public OpenApiClientGeneratorWrapper withAdditionalApiTypeAnnotationsConfig(final String additionalApiTypeAnnotations) {
212-
if (additionalApiTypeAnnotations != null) {
236+
Optional.ofNullable(additionalApiTypeAnnotations).ifPresent(cfg -> {
213237
this.configurator.addAdditionalProperty("additionalApiTypeAnnotations", additionalApiTypeAnnotations.split(";"));
214-
}
238+
});
215239
return this;
216240
}
217241

218242
public OpenApiClientGeneratorWrapper withAdditionalRequestArgs(final String additionalRequestArgs) {
219-
if (additionalRequestArgs != null) {
243+
Optional.ofNullable(additionalRequestArgs).ifPresent(cfg -> {
220244
this.configurator.addAdditionalProperty("additionalRequestArgs", additionalRequestArgs.split(";"));
221-
}
245+
});
222246
return this;
223247
}
224248

@@ -261,6 +285,12 @@ public OpenApiClientGeneratorWrapper withModelNamePrefix(final String modelNameP
261285
return this;
262286
}
263287

288+
/**
289+
* Main entrypoint, or where to generate the files based on the given base package.
290+
*
291+
* @param basePackage Java package name, e.g. org.acme
292+
* @return a list of generated files
293+
*/
264294
public List<File> generate(final String basePackage) {
265295
this.basePackage = basePackage;
266296
this.consolidatePackageNames();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
* Special value if the API response contains some new value not declared in this enum.
33
* You should react accordingly.
44
*/
5-
{additionalEnumTypeUnexpectedMemberName}({#if e.isContainer}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf("{additionalEnumTypeUnexpectedMemberStringValue}")){#if e.allowableValues},{/if}
5+
{additionalEnumTypeUnexpectedMemberName}({#if e.isContainer.or(false)}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf("{additionalEnumTypeUnexpectedMemberStringValue}")){#if e.allowableValues},{/if}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import {imp.import};
1717
* {#if appDescription}<p>{appDescription}</p>{/if}
1818
*/
1919
{/if}
20-
@jakarta.ws.rs.Path("{#if useAnnotatedBasePath}{contextPath}{/if}{commonPath}")
21-
@org.eclipse.microprofile.rest.client.inject.RegisterRestClient({#if defaultServerUrl}baseUri="{defaultServerUrl}",{/if} configKey="{configKey}")
20+
@jakarta.ws.rs.Path("{#if useAnnotatedBasePath.or(false)}{contextPath}{/if}{commonPath}")
21+
@org.eclipse.microprofile.rest.client.inject.RegisterRestClient({#if !defaultServerUrl.or('') == ''}baseUri="{defaultServerUrl}", {/if}configKey="{configKey}")
2222
@io.quarkiverse.openapi.generator.annotations.GeneratedClass(value="{openapi:parseUri(inputSpec)}", tag = "{baseName}")
23-
{#if enable-security-generation && hasAuthMethods}
23+
{#if enable-security-generation && openapi:hasAuthMethods(operations) }
2424
@org.eclipse.microprofile.rest.client.annotation.RegisterProvider({package}.auth.CompositeAuthenticationProvider.class)
2525
@org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders({package}.auth.AuthenticationPropagationHeadersFactory.class)
2626
{/if}
@@ -53,7 +53,7 @@ public interface {classname} {
5353
@jakarta.ws.rs.Produces(\{{#for produce in op.produces}"{produce.mediaType}"{#if produce_hasNext}, {/if}{/for}\})
5454
{/if}
5555
@io.quarkiverse.openapi.generator.annotations.GeneratedMethod("{op.operationIdOriginal}")
56-
{#for cbClassConfig in circuit-breaker.orEmpty}{#if cbClassConfig.key == package + classname}
56+
{#for cbClassConfig in circuit-breaker.orEmpty}{#if cbClassConfig.key == str:fmt("%s.%s", package, classname)}
5757
{#for cbMethod in cbClassConfig.value.orEmpty}{#if cbMethod == op.nickname}
5858
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
5959
{/if}{/for}
@@ -138,13 +138,13 @@ public interface {classname} {
138138
{#for p in op.pathParams}@jakarta.validation.Valid {#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}
139139
{#for p in op.queryParams}@jakarta.validation.Valid {#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasCookieParams},{/if}
140140
{#for p in op.cookieParams}@jakarta.validation.Valid {#include cookieParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}
141-
{#for p in op.headerParams}@jakarta.validation.Valid {#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},
141+
{#for p in op.headerParams}@jakarta.validation.Valid {#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParam},
142142
{#for p in op.bodyParams}@jakarta.validation.Valid {#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{/if}
143143
{#else}
144144
{#for p in op.pathParams}{#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}
145145
{#for p in op.queryParams}{#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasCookieParams},{/if}
146146
{#for p in op.cookieParams}{#include cookieParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}
147-
{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},{/if}
147+
{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParam},{/if}
148148
{#for p in op.bodyParams}{#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}
149149
{/if}
150150
{#else}

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
{#if e.withXml}
2-
@jakarta.xml.bind.annotation.XmlType(name={#if e.isEnum}"{e.items.enumName}"{#else}"{e.enumName}"{/if})
3-
@akarta.xml.bind.annotation.XmlEnum({#if e.isEnum}{e.items.dataType}{#else}{e.dataType}{/if}.class)
4-
{/if}
51
{#include additionalEnumTypeAnnotations.qute e=e /}public enum {e.enumName} {
62
{#if e.allowableValues}
73
{#if additionalEnumTypeUnexpectedMember}{#include additionalEnumTypeUnexpectedMember.qute e=e/}{/if}
8-
{#if e.withXml}
9-
{#for v in e.allowableValues.enumVars}@XmlEnumValue({#if v.isInteger || v.isDouble || v.isLong || v.isFloat}"{/if}{v.value}{#if v.isInteger || v.isDouble || v.isLong || v.isFloat}"{/if}) {v.name}({#if e.isEnum}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf({v.value})){#if v_hasNext}, {#else}; {/if}{/for}
10-
{#else}
114
{#for v in e.allowableValues.enumVars}{v.name}({#if eq e.isNumeric}{v.value}{#else}{#if e.isContainer}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf({v.value}){/if}){#if v_hasNext}, {#else};{/if}{/for}
125
{/if}
13-
{/if}
146

157
// caching enum access
168
private static final java.util.EnumSet<{e.enumName}> values = java.util.EnumSet.allOf({e.enumName}.class);
@@ -38,6 +30,6 @@
3830
return b;
3931
}
4032
}
41-
{#if e.useNullForUnknownEnumValue}return null;{#else if additionalEnumTypeUnexpectedMember}return {additionalEnumTypeUnexpectedMemberName};{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if}
33+
{#if e.isNullable}return null;{#else if additionalEnumTypeUnexpectedMember}return {additionalEnumTypeUnexpectedMemberName};{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if}
4234
}
4335
}

0 commit comments

Comments
 (0)