This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new fcacbcbeb7f CAMEL-20652: camel-rest - Contract First - Make it possible to build … (#14007) fcacbcbeb7f is described below commit fcacbcbeb7fedc9b0e45f96bed58749f0e2d134a Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Wed May 1 11:52:41 2024 +0200 CAMEL-20652: camel-rest - Contract First - Make it possible to build … (#14007) * CAMEL-20652: camel-rest - Contract First - Make it possible to build response from example in the openapi spec file * CAMEL-20652: camel-rest - Contract First - Make it possible to build response from example in the openapi spec file --- .../DefaultRestOpenapiProcessorStrategy.java | 82 ++++++++++++++++++++-- .../rest/openapi/RestOpenApiProcessor.java | 2 +- .../rest/openapi/RestOpenapiProcessorStrategy.java | 3 +- .../modules/ROOT/pages/rest-dsl-openapi.adoc | 48 ++++++++++++- 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenapiProcessorStrategy.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenapiProcessorStrategy.java index 360bc123b45..e0bff0f9672 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenapiProcessorStrategy.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/DefaultRestOpenapiProcessorStrategy.java @@ -25,7 +25,10 @@ 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.examples.Example; import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.responses.ApiResponse; import org.apache.camel.AsyncCallback; import org.apache.camel.AsyncProducer; import org.apache.camel.CamelContext; @@ -48,6 +51,7 @@ import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.support.service.ServiceSupport; import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; +import org.apache.camel.util.StringHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +63,8 @@ public class DefaultRestOpenapiProcessorStrategy extends ServiceSupport private static final Logger LOG = LoggerFactory.getLogger(DefaultRestOpenapiProcessorStrategy.class); + private static final String BODY_VERBS = "DELETE,PUT,POST,PATCH"; + private CamelContext camelContext; private ProducerCache producerCache; private String component = "direct"; @@ -164,7 +170,7 @@ public class DefaultRestOpenapiProcessorStrategy extends ServiceSupport @Override public boolean process( - Operation operation, String path, + Operation operation, String verb, String path, RestBindingAdvice binding, Exchange exchange, AsyncCallback callback) { @@ -174,7 +180,7 @@ public class DefaultRestOpenapiProcessorStrategy extends ServiceSupport if (e == null) { if ("mock".equalsIgnoreCase(missingOperation)) { // no route then try to load mock data as the answer - loadMockData(operation, path, exchange); + loadMockData(operation, verb, path, exchange); } callback.done(true); return true; @@ -205,7 +211,7 @@ public class DefaultRestOpenapiProcessorStrategy extends ServiceSupport }); } - private void loadMockData(Operation operation, String path, Exchange exchange) { + private void loadMockData(Operation operation, String verb, String path, Exchange exchange) { final PackageScanResourceResolver resolver = PluginHelper.getPackageScanResourceResolver(camelContext); final String[] includes = mockIncludePattern != null ? mockIncludePattern.split(",") : null; @@ -256,9 +262,73 @@ public class DefaultRestOpenapiProcessorStrategy extends ServiceSupport // ignore } } else { - // no mock data, so return an empty response - exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 204); - exchange.getMessage().setBody(""); + // no mock data, so return data as-is for PUT,POST,DELETE,PATCH + if (BODY_VERBS.contains(verb)) { + // return input data as-is + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 200); + } else { + // no mock data (such as for GET) + // then try to see if there is an example in the openapi spec response we can use, + // otherwise use an empty body + Object body = ""; + + String contentType = ExchangeHelper.getContentType(exchange); + String accept = exchange.getMessage().getHeader("Accept", String.class); + if (operation.getResponses() != null) { + ApiResponse a = operation.getResponses().get("200"); + Content c = a.getContent(); + if (c != null && !c.isEmpty()) { + // prefer media-type that is the same as the incoming content-type + // if none found, then find first matching content-type from the HTTP Accept header + MediaType mt = contentType != null ? c.get(contentType) : null; + if (mt == null && accept != null) { + // find best match accept + for (String acc : accept.split(",")) { + acc = StringHelper.before(acc, ";", acc); + acc = acc.trim(); + mt = c.get(acc); + if (mt != null) { + // update content-type + contentType = acc; + break; + } + } + // fallback to grab json or xml if we accept anything + if (mt == null && "*/*".equals(accept)) { + mt = c.get("application/json"); + if (mt != null) { + contentType = "application/json"; + } + } + // fallback to grab json or xml if we accept anything + if (mt == null && "*/*".equals(accept)) { + mt = c.get("application/xml"); + if (mt != null) { + contentType = "application/xml"; + } + } + } + if (mt != null) { + if (mt.getExample() != null) { + body = mt.getExample(); + } else if (mt.getExamples() != null) { + // grab first example + Example ex = mt.getExamples().values().iterator().next(); + body = ex.getValue(); + } + } + } + } + boolean empty = body == null || body.toString().isBlank(); + if (empty) { + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 204); + exchange.getMessage().setBody(""); + } else { + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 200); + exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, contentType); + exchange.getMessage().setBody(body); + } + } } } 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 8b556c0beb0..d3c5c1de632 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 @@ -140,7 +140,7 @@ public class RestOpenApiProcessor extends DelegateAsyncProcessor implements Came } // process the incoming request - return restOpenapiProcessorStrategy.process(o, uri, rcp.getBinding(), exchange, callback); + return restOpenapiProcessorStrategy.process(o, verb, uri, rcp.getBinding(), exchange, callback); } // is it the api-context path diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenapiProcessorStrategy.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenapiProcessorStrategy.java index bff1e714e42..52c71a7c671 100644 --- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenapiProcessorStrategy.java +++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenapiProcessorStrategy.java @@ -67,6 +67,7 @@ public interface RestOpenapiProcessorStrategy { * Strategy for processing the Rest DSL operation * * @param operation the rest operation + * @param verb the HTTP verb (GET, POST etc.) * @param path the context-path * @param binding binding advice * @param exchange the exchange @@ -77,7 +78,7 @@ public interface RestOpenapiProcessorStrategy { * asynchronously */ boolean process( - Operation operation, String path, + Operation operation, String verb, String path, RestBindingAdvice binding, Exchange exchange, AsyncCallback callback); diff --git a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc index 88bfad8408e..62ae96d5cfa 100644 --- a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc +++ b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc @@ -149,8 +149,14 @@ This is similar to ignoring missing API operations, as you can tell Camel to moc rest().openApi("petstore-v3.json").missingOperation("mock"); ---- -When using _mock_ then Camel will (for missing operations) simulate a successful response, by attempting to load -canned responses from file system. This allows you to have a set of files that you can use for development and testing purposes. +When using _mock_ then Camel will (for missing operations) simulate a successful response: + +1. attempting to load canned responses from file system. +2. for GET verbs then attempt to use example inlined in the OpenAPI `response` section. +3. for other verbs (DELETE, PUT, POST, PATCH) then return the input body as response. +4. if none of above then return empty body. + +This allows you to have a set of files that you can use for development and testing purposes. The files should be stored in `camel-mock` when using Camel JBang, and `src/main/resources/camel-mock` for Maven/Gradle based projects. @@ -187,6 +193,44 @@ $ curl http://0.0.0.0:8080/api/v3/pet/123 } ---- +If no file is found, then Camel will attempt to find an example from the _response_ section in the OpenAPI specification. + +In the response section below, then for success GET response (200) then for the `application/json` content-type, we have +an inlined example. Note if there are multiple examples for the same content-type, then Camel will pick the first example, +so make sure it's the best example you want to let Camel use as mocked response body. + +[source,json] +---- +"responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + }, + "examples": { + "success": { + "summary": "A cat", + "value": "{\"pet\": \"Jack the cat\"}" + } + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } +---- + === Binding to POJO classes _contract first_ Rest DSL with OpenAPI also support binding mode to JSon and XML.