Skip to content

Commit

Permalink
Fix #852 - Process Qute templates in a strict manner (#864) (#870)
Browse files Browse the repository at this point in the history
* 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 <1538000+ricardozanini@users.noreply.github.com>
Co-authored-by: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com>
  • Loading branch information
github-actions[bot] and ricardozanini authored Nov 25, 2024
1 parent 75f328d commit b4d52c0
Show file tree
Hide file tree
Showing 50 changed files with 6,460 additions and 160 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkiverse.openapi.generator.deployment.template;

import java.util.List;
import java.util.concurrent.ExecutionException;

import io.quarkus.qute.EvalContext;
import io.quarkus.qute.Expression;

final class ExprEvaluator {

private ExprEvaluator() {
}

@SuppressWarnings("unchecked")
public static <T> T evaluate(EvalContext context, Expression expression) throws ExecutionException, InterruptedException {
return (T) context.evaluate(expression).toCompletableFuture().get();
}

@SuppressWarnings("unchecked")
public static <T> T[] evaluate(EvalContext context, List<Expression> expressions, Class<T> type)
throws ExecutionException, InterruptedException {
T[] results = (T[]) java.lang.reflect.Array.newInstance(type, expressions.size());

for (int i = 0; i < expressions.size(); i++) {
Expression expression = expressions.get(i);
T result = type.cast(context.evaluate(expression).toCompletableFuture().get());
results[i] = result;
}

return results;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;

import org.openapitools.codegen.model.OperationMap;

import io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorOutputPaths;
import io.quarkus.qute.EvalContext;
import io.quarkus.qute.Expression;
Expand All @@ -18,9 +20,8 @@
* implement and use them.
*/
public class OpenApiNamespaceResolver implements NamespaceResolver {
private static final String GENERATE_DEPRECATED_PROP = "generateDeprecated";

static final OpenApiNamespaceResolver INSTANCE = new OpenApiNamespaceResolver();
private static final String GENERATE_DEPRECATED_PROP = "generateDeprecated";

private OpenApiNamespaceResolver() {
}
Expand Down Expand Up @@ -53,6 +54,10 @@ public String parseUri(String uri) {
return OpenApiGeneratorOutputPaths.getRelativePath(Path.of(uri)).toString().replace(File.separatorChar, '/');
}

public boolean hasAuthMethods(OperationMap operations) {
return operations != null && operations.getOperation().stream().anyMatch(operation -> operation.hasAuthMethods);
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ public QuteTemplatingEngineAdapter() {
.addDefaults()
.addValueResolver(new ReflectionValueResolver())
.addNamespaceResolver(OpenApiNamespaceResolver.INSTANCE)
.addNamespaceResolver(StrNamespaceResolver.INSTANCE)
.removeStandaloneLines(true)
.strictRendering(false)
.strictRendering(true)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.quarkiverse.openapi.generator.deployment.template;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import io.quarkus.qute.EvalContext;
import io.quarkus.qute.NamespaceResolver;

/**
* Namespace resolver to mimic the function of io.quarkus.qute.runtime.extensions.StringTemplateExtensions.
* This extension is built-in with Qute when used in a context with Quarkus DI.
* Since these extensions are built in build time by Qute we can't use them because our process also runs in build time without
* a CDI context.
* So any namespace resolver auto-generated by Quarkus won't be added to our engine.
*
* @see <a href="https://quarkus.io/guides/qute-reference#template_extension_methods">Template Extension Methods</a>
*/
public class StrNamespaceResolver implements NamespaceResolver {

static final StrNamespaceResolver INSTANCE = new StrNamespaceResolver();

private StrNamespaceResolver() {
}

@Override
public String getNamespace() {
return "str";
}

/**
* @see io.quarkus.qute.runtime.extensions.StringTemplateExtensions#fmt(String, String, Object...)
*/
public String fmt(String format, Object... args) {
return String.format(format, args);
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
switch (context.getName()) {
case "fmt":
if (context.getParams().size() < 2) {
throw new IllegalArgumentException(
"Missing required parameter for 'fmt'. Make sure that the function has at least two parameters");
}
try {
return CompletableFuture.completedFuture(
fmt(ExprEvaluator.evaluate(context, context.getParams().get(0)),
ExprEvaluator.evaluate(context, context.getParams().subList(1, context.getParams().size()),
Object.class)));
} catch (Exception e) {
throw new RuntimeException(e);
}
default:
throw new IllegalArgumentException("There's no method named '" + context.getName() + "'");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,14 @@
public abstract class OpenApiClientGeneratorWrapper {

public static final String VERBOSE = "verbose";
private static final String ONCE_LOGGER = "org.openapitools.codegen.utils.oncelogger.enabled";
/**
* Security scheme for which to apply security constraints even if the OpenAPI definition has no security definition
*/
public static final String DEFAULT_SECURITY_SCHEME = "defaultSecurityScheme";
public static final String SUPPORTS_ADDITIONAL_PROPERTIES_AS_ATTRIBUTE = "supportsAdditionalPropertiesWithComposedSchema";
private static final Map<String, String> defaultTypeMappings = Map.of(
"date", "LocalDate",
"DateTime", "OffsetDateTime");
private static final Map<String, String> defaultImportMappings = Map.of(
"LocalDate", "java.time.LocalDate",
private static final String ONCE_LOGGER = "org.openapitools.codegen.utils.oncelogger.enabled";
private static final Map<String, String> defaultTypeMappings = Map.of("date", "LocalDate", "DateTime", "OffsetDateTime");
private static final Map<String, String> defaultImportMappings = Map.of("LocalDate", "java.time.LocalDate",
"OffsetDateTime", "java.time.OffsetDateTime");
private final QuarkusCodegenConfigurator configurator;
private final DefaultGenerator generator;
Expand All @@ -53,8 +50,8 @@ public abstract class OpenApiClientGeneratorWrapper {
private String modelPackage = "";

OpenApiClientGeneratorWrapper(final QuarkusCodegenConfigurator configurator, final Path specFilePath, final Path outputDir,
final boolean verbose,
final boolean validateSpec) {
final boolean verbose, final boolean validateSpec) {

// do not generate docs nor tests
GlobalSettings.setProperty(CodegenConstants.API_DOCS, FALSE.toString());
GlobalSettings.setProperty(CodegenConstants.API_TESTS, FALSE.toString());
Expand All @@ -78,17 +75,44 @@ public abstract class OpenApiClientGeneratorWrapper {
defaultTypeMappings.forEach(this.configurator::addTypeMapping);
defaultImportMappings.forEach(this.configurator::addImportMapping);

this.generator = new DefaultGenerator();
}
this.setDefaults();

public OpenApiClientGeneratorWrapper withApiPackage(final String pkg) {
this.apiPackage = pkg;
return this;
this.generator = new DefaultGenerator();
}

public OpenApiClientGeneratorWrapper withModelPackage(final String pkg) {
this.modelPackage = pkg;
return this;
/**
* A few properties from the "with*" methods must be injected in the Qute context by default since we turned strict model
* rendering.
* This way we avoid side effects in the model such as "NOT_FOUND" strings printed everywhere.
*
* @see <a href="https://quarkus.io/guides/qute-reference#configuration-reference">Qute - Configuration Reference</a>
*/
private void setDefaults() {
// Set default values directly here
this.configurator.addAdditionalProperty("additionalApiTypeAnnotations", new String[0]);
this.configurator.addAdditionalProperty("additionalPropertiesAsAttribute", FALSE);
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMember", FALSE);
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMemberName", "");
this.configurator.addAdditionalProperty("additionalEnumTypeUnexpectedMemberStringValue", "");
this.configurator.addAdditionalProperty("additionalRequestArgs", new String[0]);
this.configurator.addAdditionalProperty("classes-codegen", new HashMap<>());
this.configurator.addAdditionalProperty("circuit-breaker", new HashMap<>());
this.configurator.addAdditionalProperty("configKey", "");
this.configurator.addAdditionalProperty("datatypeWithEnum", "");
this.configurator.addAdditionalProperty("enable-security-generation", TRUE);
this.configurator.addAdditionalProperty("generate-part-filename", FALSE);
this.configurator.addAdditionalProperty("mutiny", FALSE);
this.configurator.addAdditionalProperty("mutiny-operation-ids", new HashMap<>());
this.configurator.addAdditionalProperty("mutiny-return-response", FALSE);
this.configurator.addAdditionalProperty("part-filename-value", "");
this.configurator.addAdditionalProperty("return-response", FALSE);
this.configurator.addAdditionalProperty("skipFormModel", TRUE);
this.configurator.addAdditionalProperty("templateDir", "");
this.configurator.addAdditionalProperty("use-bean-validation", FALSE);
this.configurator.addAdditionalProperty("use-field-name-in-part-filename", FALSE);
this.configurator.addAdditionalProperty("verbose", FALSE);
// TODO: expose as properties https://github.com/quarkiverse/quarkus-openapi-generator/issues/869
this.configurator.addAdditionalProperty(CodegenConstants.SERIALIZABLE_MODEL, FALSE);
}

/**
Expand All @@ -98,30 +122,30 @@ public OpenApiClientGeneratorWrapper withModelPackage(final String pkg) {
* @return this wrapper
*/
public OpenApiClientGeneratorWrapper withCircuitBreakerConfig(final Map<String, List<String>> config) {
if (config != null) {
configurator.addAdditionalProperty("circuit-breaker", config);
}
Optional.ofNullable(config).ifPresent(cfg -> {
this.configurator.addAdditionalProperty("circuit-breaker", config);
});
return this;
}

public OpenApiClientGeneratorWrapper withClassesCodeGenConfig(final Map<String, Object> config) {
if (config != null) {
configurator.addAdditionalProperty("classes-codegen", config);
}
Optional.ofNullable(config).ifPresent(cfg -> {
this.configurator.addAdditionalProperty("classes-codegen", cfg);
});
return this;
}

public OpenApiClientGeneratorWrapper withMutiny(final Boolean config) {
if (config != null) {
configurator.addAdditionalProperty("mutiny", config);
}
Optional.ofNullable(config).ifPresent(cfg -> {
this.configurator.addAdditionalProperty("mutiny", cfg);
});
return this;
}

public OpenApiClientGeneratorWrapper withMutinyReturnResponse(final Boolean config) {
if (config != null) {
configurator.addAdditionalProperty("mutiny-return-response", config);
}
Optional.ofNullable(config).ifPresent(cfg -> {
this.configurator.addAdditionalProperty("mutiny-return-response", cfg);
});
return this;
}

Expand Down Expand Up @@ -209,16 +233,16 @@ public OpenApiClientGeneratorWrapper withAdditionalEnumTypeUnexpectedMemberStrin
* @return this wrapper
*/
public OpenApiClientGeneratorWrapper withAdditionalApiTypeAnnotationsConfig(final String additionalApiTypeAnnotations) {
if (additionalApiTypeAnnotations != null) {
Optional.ofNullable(additionalApiTypeAnnotations).ifPresent(cfg -> {
this.configurator.addAdditionalProperty("additionalApiTypeAnnotations", additionalApiTypeAnnotations.split(";"));
}
});
return this;
}

public OpenApiClientGeneratorWrapper withAdditionalRequestArgs(final String additionalRequestArgs) {
if (additionalRequestArgs != null) {
Optional.ofNullable(additionalRequestArgs).ifPresent(cfg -> {
this.configurator.addAdditionalProperty("additionalRequestArgs", additionalRequestArgs.split(";"));
}
});
return this;
}

Expand Down Expand Up @@ -261,6 +285,12 @@ public OpenApiClientGeneratorWrapper withModelNamePrefix(final String modelNameP
return this;
}

/**
* Main entrypoint, or where to generate the files based on the given base package.
*
* @param basePackage Java package name, e.g. org.acme
* @return a list of generated files
*/
public List<File> generate(final String basePackage) {
this.basePackage = basePackage;
this.consolidatePackageNames();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
* Special value if the API response contains some new value not declared in this enum.
* You should react accordingly.
*/
{additionalEnumTypeUnexpectedMemberName}({#if e.isContainer}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf("{additionalEnumTypeUnexpectedMemberStringValue}")){#if e.allowableValues},{/if}
{additionalEnumTypeUnexpectedMemberName}({#if e.isContainer.or(false)}{e.items.dataType}{#else}{e.dataType}{/if}.valueOf("{additionalEnumTypeUnexpectedMemberStringValue}")){#if e.allowableValues},{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import {imp.import};
* {#if appDescription}<p>{appDescription}</p>{/if}
*/
{/if}
@jakarta.ws.rs.Path("{#if useAnnotatedBasePath}{contextPath}{/if}{commonPath}")
@org.eclipse.microprofile.rest.client.inject.RegisterRestClient({#if defaultServerUrl}baseUri="{defaultServerUrl}",{/if} configKey="{configKey}")
@jakarta.ws.rs.Path("{#if useAnnotatedBasePath.or(false)}{contextPath}{/if}{commonPath}")
@org.eclipse.microprofile.rest.client.inject.RegisterRestClient({#if !defaultServerUrl.or('') == ''}baseUri="{defaultServerUrl}", {/if}configKey="{configKey}")
@io.quarkiverse.openapi.generator.annotations.GeneratedClass(value="{openapi:parseUri(inputSpec)}", tag = "{baseName}")
{#if enable-security-generation && hasAuthMethods}
{#if enable-security-generation && openapi:hasAuthMethods(operations) }
@org.eclipse.microprofile.rest.client.annotation.RegisterProvider({package}.auth.CompositeAuthenticationProvider.class)
@org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders({package}.auth.AuthenticationPropagationHeadersFactory.class)
{/if}
Expand Down Expand Up @@ -53,7 +53,7 @@ public interface {classname} {
@jakarta.ws.rs.Produces(\{{#for produce in op.produces}"{produce.mediaType}"{#if produce_hasNext}, {/if}{/for}\})
{/if}
@io.quarkiverse.openapi.generator.annotations.GeneratedMethod("{op.operationIdOriginal}")
{#for cbClassConfig in circuit-breaker.orEmpty}{#if cbClassConfig.key == package + classname}
{#for cbClassConfig in circuit-breaker.orEmpty}{#if cbClassConfig.key == str:fmt("%s.%s", package, classname)}
{#for cbMethod in cbClassConfig.value.orEmpty}{#if cbMethod == op.nickname}
@org.eclipse.microprofile.faulttolerance.CircuitBreaker
{/if}{/for}
Expand Down Expand Up @@ -138,13 +138,13 @@ public interface {classname} {
{#for p in op.pathParams}@jakarta.validation.Valid {#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}
{#for p in op.queryParams}@jakarta.validation.Valid {#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasCookieParams},{/if}
{#for p in op.cookieParams}@jakarta.validation.Valid {#include cookieParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}
{#for p in op.headerParams}@jakarta.validation.Valid {#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},
{#for p in op.headerParams}@jakarta.validation.Valid {#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParam},
{#for p in op.bodyParams}@jakarta.validation.Valid {#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{/if}
{#else}
{#for p in op.pathParams}{#include pathParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasQueryParams},{/if}
{#for p in op.queryParams}{#include queryParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasCookieParams},{/if}
{#for p in op.cookieParams}{#include cookieParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasHeaderParams},{/if}
{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParams},{/if}
{#for p in op.headerParams}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}{#if op.hasBodyParam},{/if}
{#for p in op.bodyParams}{#include bodyParams.qute param=p/}{#if p_hasNext}, {/if}{/for}
{/if}
{#else}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
{#if e.withXml}
@jakarta.xml.bind.annotation.XmlType(name={#if e.isEnum}"{e.items.enumName}"{#else}"{e.enumName}"{/if})
@akarta.xml.bind.annotation.XmlEnum({#if e.isEnum}{e.items.dataType}{#else}{e.dataType}{/if}.class)
{/if}
{#include additionalEnumTypeAnnotations.qute e=e /}public enum {e.enumName} {
{#if e.allowableValues}
{#if additionalEnumTypeUnexpectedMember}{#include additionalEnumTypeUnexpectedMember.qute e=e/}{/if}
{#if e.withXml}
{#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}
{#else}
{#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}
{/if}
{/if}

// caching enum access
private static final java.util.EnumSet<{e.enumName}> values = java.util.EnumSet.allOf({e.enumName}.class);
Expand Down Expand Up @@ -38,6 +30,6 @@
return b;
}
}
{#if e.useNullForUnknownEnumValue}return null;{#else if additionalEnumTypeUnexpectedMember}return {additionalEnumTypeUnexpectedMemberName};{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if}
{#if e.isNullable}return null;{#else if additionalEnumTypeUnexpectedMember}return {additionalEnumTypeUnexpectedMemberName};{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if}
}
}
Loading

0 comments on commit b4d52c0

Please sign in to comment.