This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch openapi2 in repository https://gitbox.apache.org/repos/asf/camel.git
commit c104cc1762c3ba0446bc562660082c6fa88e276b Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Mon Mar 25 10:59:32 2024 +0100 CAMEL-20557: Rest DSL to use openapi spec directly --- .../vertx/PlatformHttpRestOpenApiConsumerTest.java | 28 +++++++++++ .../rest/openapi/RestOpenApiComponent.java | 6 +-- .../rest/openapi/RestOpenApiEndpoint.java | 2 +- .../rest/openapi/RestOpenApiProcessor.java | 55 +++++++++++++++++----- .../validator/RequestValidationCustomizer.java | 1 + .../rest/openapi/validator/RequestValidator.java | 31 ++++++------ 6 files changed, 90 insertions(+), 33 deletions(-) diff --git a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java index 6c9865ecb59..1ea0a976913 100644 --- a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java +++ b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/PlatformHttpRestOpenApiConsumerTest.java @@ -110,4 +110,32 @@ public class PlatformHttpRestOpenApiConsumerTest { } } + @Test + public void testRestOpenApiValidate() throws Exception { + final CamelContext context = VertxPlatformHttpEngineTest.createCamelContext(); + + try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("rest-openapi:classpath:openapi-v3.json?requestValidationEnabled=true") + .log("dummy"); + + from("direct:updatePet") + .setBody().constant("{\"pet\": \"tony the tiger\"}"); + } + }); + + context.start(); + + given() + .when() + .put("/api/v3/pet") + .then() + .statusCode(405); // no request body + } finally { + context.stop(); + } + } + } diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java index b405b1466c5..98ea9e91fe4 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java @@ -128,8 +128,7 @@ public final class RestOpenApiComponent extends DefaultComponent implements SSLC + " configuration.", defaultValue = DEFAULT_SPECIFICATION_URI_STR, label = "producer") private URI specificationUri; - @Metadata(description = "Enable validation of requests against the configured OpenAPI specification", - defaultValue = "false") + @Metadata(description = "Enable validation of requests against the configured OpenAPI specification") private boolean requestValidationEnabled; @Metadata(description = "If request validation is enabled, this option provides the capability to customize" + " the creation of OpenApiInteractionValidator used to validate requests.", @@ -252,8 +251,7 @@ public final class RestOpenApiComponent extends DefaultComponent implements SSLC return this.requestValidationEnabled; } - public void setRequestValidationCustomizer( - RequestValidationCustomizer requestValidationCustomizer) { + public void setRequestValidationCustomizer(RequestValidationCustomizer requestValidationCustomizer) { this.requestValidationCustomizer = requestValidationCustomizer; } diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java index 034a7e3e56b..2c55358dacf 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java @@ -206,7 +206,7 @@ public final class RestOpenApiEndpoint extends DefaultEndpoint { public Consumer createConsumer(final Processor processor) throws Exception { OpenAPI doc = loadSpecificationFrom(getCamelContext(), specificationUri); String path = determineBasePath(doc); - Processor target = new RestOpenApiProcessor(doc, path, processor); + Processor target = new RestOpenApiProcessor(this, doc, path, processor); CamelContextAware.trySetCamelContext(target, getCamelContext()); return createConsumerFor(path, target); } diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java index 5e3291d82c9..b1000f26ccc 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiProcessor.java @@ -18,10 +18,16 @@ package org.apache.camel.component.rest.openapi; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; import org.apache.camel.AsyncCallback; import org.apache.camel.CamelContext; import org.apache.camel.CamelContextAware; @@ -38,14 +44,15 @@ public class RestOpenApiProcessor extends DelegateAsyncProcessor implements Came = Arrays.asList("GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "OPTIONS", "CONNECT", "PATCH"); private CamelContext camelContext; + private final RestOpenApiEndpoint endpoint; private final OpenAPI openAPI; private final String basePath; private final List<RestConsumerContextPathMatcher.ConsumerPath<Operation>> paths = new ArrayList<>(); private RestOpenapiProcessorStrategy restOpenapiProcessorStrategy; - private RequestValidator requestValidator; - public RestOpenApiProcessor(OpenAPI openAPI, String basePath, Processor processor) { + public RestOpenApiProcessor(RestOpenApiEndpoint endpoint, OpenAPI openAPI, String basePath, Processor processor) { super(processor); + this.endpoint = endpoint; this.basePath = basePath; this.openAPI = openAPI; this.restOpenapiProcessorStrategy = new DefaultRestOpenapiProcessorStrategy(); @@ -69,17 +76,8 @@ public class RestOpenApiProcessor extends DelegateAsyncProcessor implements Came this.restOpenapiProcessorStrategy = restOpenapiProcessorStrategy; } - public RequestValidator getRequestValidator() { - return requestValidator; - } - - public void setRequestValidator(RequestValidator requestValidator) { - this.requestValidator = requestValidator; - } - @Override public boolean process(Exchange exchange, AsyncCallback callback) { - // TODO: RequestValidator // TODO: binding String path = exchange.getMessage().getHeader(Exchange.HTTP_PATH, String.class); @@ -91,8 +89,39 @@ public class RestOpenApiProcessor extends DelegateAsyncProcessor implements Came RestConsumerContextPathMatcher.ConsumerPath<Operation> m = RestConsumerContextPathMatcher.matchBestPath(verb, path, paths); if (m != null) { - Operation o = m.getConsumer(); - return restOpenapiProcessorStrategy.process(o, path, exchange, callback); + Operation operation = m.getConsumer(); + + // we have found the operation to call, but if validation is enabled then we need + // to validate the incoming request first + if (endpoint.isRequestValidationEnabled()) { + Map<String, Parameter> pathParameters; + if (operation.getParameters() != null) { + pathParameters = operation.getParameters().stream() + .filter(p -> "path".equals(p.getIn())) + .collect(Collectors.toMap(Parameter::getName, Function.identity())); + } else { + pathParameters = new HashMap<>(); + } + try { + final String uriTemplate = endpoint.resolveUri(path, pathParameters); + RequestValidator validator = endpoint.configureRequestValidator(openAPI, operation, verb, uriTemplate); + Set<String> errors = validator.validate(exchange); + if (!errors.isEmpty()) { + RestOpenApiValidationException exception = new RestOpenApiValidationException(errors); + exchange.setException(exception); + // validation error should be 405 + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 405); + callback.done(true); + return true; + } + } catch (Exception e) { + exchange.setException(e); + callback.done(true); + return true; + } + } + + return restOpenapiProcessorStrategy.process(operation, path, exchange, callback); } // okay we cannot process this requires so return either 404 or 405. diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidationCustomizer.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidationCustomizer.java index 5ac2c99d286..42a5913bab6 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidationCustomizer.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidationCustomizer.java @@ -24,6 +24,7 @@ import org.apache.camel.Exchange; * An abstraction for customizing the behavior of OpenApi request validation. */ public interface RequestValidationCustomizer { + /** * Customizes the creation of a {@link OpenApiInteractionValidator}. The default implementation enables validation * of only the request body. diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidator.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidator.java index 7636f668527..56724f71d37 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidator.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/validator/RequestValidator.java @@ -39,18 +39,19 @@ import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.UnsafeUriCharactersEncoder; public class RequestValidator { + private static final Pattern REST_PATH_PARAM_PATTERN = Pattern.compile("\\{([^}]+)}"); - private final OpenApiInteractionValidator openApiInteractionValidator; - private final RestOpenApiOperation restOpenApiOperation; - private final RequestValidationCustomizer requestValidationCustomizer; + private final OpenApiInteractionValidator validator; + private final RestOpenApiOperation operation; + private final RequestValidationCustomizer customizer; - public RequestValidator(OpenApiInteractionValidator openApiInteractionValidator, - RestOpenApiOperation restOpenApiOperation, - RequestValidationCustomizer requestValidationCustomizer) { - this.openApiInteractionValidator = openApiInteractionValidator; - this.restOpenApiOperation = restOpenApiOperation; - this.requestValidationCustomizer = requestValidationCustomizer; + public RequestValidator(OpenApiInteractionValidator validator, + RestOpenApiOperation operation, + RequestValidationCustomizer customizer) { + this.validator = validator; + this.operation = operation; + this.customizer = customizer; } public Set<String> validate(Exchange exchange) { @@ -63,7 +64,7 @@ public class RequestValidator { } SimpleRequest.Builder builder - = new SimpleRequest.Builder(restOpenApiOperation.getMethod(), resolvePathParams(exchange)); + = new SimpleRequest.Builder(operation.getMethod(), resolvePathParams(exchange)); builder.withContentType(contentType); // Validate request body if available @@ -81,7 +82,7 @@ public class RequestValidator { } // Validate required operation query params - restOpenApiOperation.getQueryParams() + operation.getQueryParams() .stream() .filter(parameter -> Objects.nonNull(parameter.getRequired()) && parameter.getRequired()) .forEach(parameter -> { @@ -96,7 +97,7 @@ public class RequestValidator { }); // Validate operation required headers - restOpenApiOperation.getHeaders() + operation.getHeaders() .stream() .filter(parameter -> Objects.nonNull(parameter.getRequired()) && parameter.getRequired()) .forEach(parameter -> { @@ -111,11 +112,11 @@ public class RequestValidator { }); // Apply any extra customizations to the validation request - requestValidationCustomizer.customizeSimpleRequestBuilder(builder, restOpenApiOperation, exchange); + customizer.customizeSimpleRequestBuilder(builder, operation, exchange); // Perform validation and capture errors Set<String> validationErrors = new LinkedHashSet<>(); - openApiInteractionValidator.validateRequest(builder.build()) + validator.validateRequest(builder.build()) .getMessages() .stream() .filter(validationMessage -> validationMessage.getLevel().equals(ValidationReport.Level.ERROR)) @@ -129,7 +130,7 @@ public class RequestValidator { } protected String resolvePathParams(Exchange exchange) { - String path = restOpenApiOperation.getUriTemplate(); + String path = operation.getUriTemplate(); Matcher matcher = REST_PATH_PARAM_PATTERN.matcher(path); String pathToProcess = path; while (matcher.find()) {