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 <[email protected]>
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.