You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2024/03/28 14:40:29 UTC

(camel) 15/38: CAMEL-20557: Rest DSL to use openapi spec directly

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 <cl...@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()) {