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 6da4bdfdd22f CAMEL-25264: docs, camel-jbang - samples for the HTTP
rungs; the validator accepts Simple inlined in a JsonPath (#27277)
6da4bdfdd22f is described below
commit 6da4bdfdd22f97dfe4d78a2b08efa5fe80d63bce
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 14:12:02 2026 +0200
CAMEL-25264: docs, camel-jbang - samples for the HTTP rungs; the validator
accepts Simple inlined in a JsonPath (#27277)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/catalog/docs/jsonpath-language.adoc | 155 +++++++++++++++++++++
.../camel/catalog/docs/rest-openapi-component.adoc | 102 ++++++++++++++
.../src/main/docs/jsonpath-language.adoc | 155 +++++++++++++++++++++
.../src/main/docs/rest-openapi-component.adoc | 102 ++++++++++++++
docs/user-manual/modules/ROOT/pages/rest-dsl.adoc | 118 ++++++++++++++--
.../dsl/jbang/core/commands/ai/eip-samples.json | 16 ++-
.../dsl/jbang/core/commands/ai/CatalogSamples.java | 26 +++-
.../jbang/core/commands/ai/CatalogSamplesTest.java | 18 +++
.../camel/dsl/yaml/validator/YamlValidator.java | 18 ++-
.../validator/YamlValidatorPropertyHintTest.java | 40 ++++++
10 files changed, 731 insertions(+), 19 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonpath-language.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonpath-language.adoc
index cf5977c22a3e..24309d4a6c97 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonpath-language.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonpath-language.adoc
@@ -112,6 +112,33 @@ XML DSL::
</route>
----
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:start
+ steps:
+ - choice:
+ when:
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < 10)]"
+ steps:
+ - to:
+ uri: mock:cheap
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < 30)]"
+ steps:
+ - to:
+ uri: mock:average
+ otherwise:
+ steps:
+ - to:
+ uri: mock:expensive
+----
====
=== Supported message body types
@@ -227,6 +254,134 @@ XML DSL::
</route>
----
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:start
+ steps:
+ - choice:
+ when:
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < ${header.cheap})]"
+ steps:
+ - to:
+ uri: mock:cheap
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < ${header.average})]"
+ steps:
+ - to:
+ uri: mock:average
+ otherwise:
+ steps:
+ - to:
+ uri: mock:expensive
+----
+====
+
+The inlined Simple expressions also work when the result becomes the message
body, for example to filter a JSON
+array down to the elements matching a header, such as the path parameter of a
rest service. A filter returns a
+list (empty when nothing matches), so set `resultType` to `java.util.List` and
check its size. The element found is
+a `Map`, which is marshalled back to JSON:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:one-sku")
+ .setBody(constant("resource:file:stock.json"))
+ .setBody().jsonpath("$[?(@.sku == '${header.sku}')]", List.class)
+ .choice()
+ .when(simple("${body.size()} == 0"))
+ .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(404))
+ .setBody(simple("unknown SKU ${header.sku}"))
+ .otherwise()
+ .setBody(simple("${body[0]}"))
+ .marshal().json()
+ .end();
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="direct:one-sku"/>
+ <setBody>
+ <constant>resource:file:stock.json</constant>
+ </setBody>
+ <setBody>
+ <jsonpath resultType="java.util.List">$[?(@.sku ==
'${header.sku}')]</jsonpath>
+ </setBody>
+ <choice>
+ <when>
+ <simple>${body.size()} == 0</simple>
+ <setHeader name="CamelHttpResponseCode">
+ <constant>404</constant>
+ </setHeader>
+ <setBody>
+ <simple>unknown SKU ${header.sku}</simple>
+ </setBody>
+ </when>
+ <otherwise>
+ <setBody>
+ <simple>${body[0]}</simple>
+ </setBody>
+ <marshal>
+ <json/>
+ </marshal>
+ </otherwise>
+ </choice>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:one-sku
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: resource:file:stock.json
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$[?(@.sku == '${header.sku}')]"
+ resultType: java.util.List
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${body.size()} == 0"
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "404"
+ - setBody:
+ expression:
+ simple:
+ expression: "unknown SKU ${header.sku}"
+ otherwise:
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "${body[0]}"
+ - marshal:
+ json: {}
+----
====
You can turn off support for inlined Simple expression by setting the option
`allowSimple` to `false` as shown:
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc
index 1e845c121933..7e47736829fa 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc
@@ -157,6 +157,108 @@ YAML::
----
====
+=== Calling an operation of an API
+
+To call an API by its contract, send to a `rest-openapi` endpoint with the
contract and the _operation id_. Camel
+builds the HTTP request from the contract:
+
+* a path or query parameter, such as `+{petId}+` in `+/pet/{petId}+`, is taken
from the message header of the same name;
+* the message body is the request body (a JSON string for a JSON API, with a
`Content-Type` header);
+* the response body becomes the message body, as text: unmarshal it to read
its fields.
+
+The server comes from the `servers` of the contract; set `host` to call
another one (for example
+`http://localhost:8080` while developing). The HTTP call itself is done by an
HTTP component on the classpath, such
+as `camel-http`; set `componentName` when there is more than one.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("timer:pets?repeatCount=1")
+ .setHeader("petId", constant(10))
+ .to("rest-openapi:petstore-v3.json#getPetById")
+ .unmarshal().json()
+ .log("Pet ${body[name]}")
+ .setBody(constant("{\"id\": 11, \"name\": \"Rex\"}"))
+ .setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
+ .to("rest-openapi:petstore-v3.json#addPet?host={{petstore.url}}")
+ .log("Added ${body}");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="timer:pets?repeatCount=1"/>
+ <setHeader name="petId">
+ <constant>10</constant>
+ </setHeader>
+ <to uri="rest-openapi:petstore-v3.json#getPetById"/>
+ <unmarshal>
+ <json/>
+ </unmarshal>
+ <log message="Pet ${body[name]}"/>
+ <setBody>
+ <constant>{"id": 11, "name": "Rex"}</constant>
+ </setBody>
+ <setHeader name="Content-Type">
+ <constant>application/json</constant>
+ </setHeader>
+ <to uri="rest-openapi:petstore-v3.json#addPet?host={{petstore.url}}"/>
+ <log message="Added ${body}"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: timer:pets
+ parameters:
+ repeatCount: 1
+ steps:
+ - setHeader:
+ name: petId
+ expression:
+ constant:
+ expression: "10"
+ - to:
+ uri: rest-openapi
+ parameters:
+ specificationUri: petstore-v3.json
+ operationId: getPetById
+ - unmarshal:
+ json: {}
+ - log:
+ message: "Pet ${body[name]}"
+ - setBody:
+ expression:
+ constant:
+ expression: '{"id": 11, "name": "Rex"}'
+ - setHeader:
+ name: Content-Type
+ expression:
+ constant:
+ expression: application/json
+ - to:
+ uri: rest-openapi
+ parameters:
+ specificationUri: petstore-v3.json
+ operationId: addPet
+ host: "{{petstore.url}}"
+ - log:
+ message: "Added ${body}"
+----
+====
+
+An answer with an error status (a `404` or a `409`) fails the exchange with an
`HttpOperationFailedException`; handle
+it with `onException` to read `${exception.statusCode}` and
`${exception.responseBody}`.
+
=== Consumer OAuth Bearer token validation
When `rest-openapi` is used as a consumer, HTTP requests are served by the
selected
diff --git a/components/camel-jsonpath/src/main/docs/jsonpath-language.adoc
b/components/camel-jsonpath/src/main/docs/jsonpath-language.adoc
index cf5977c22a3e..24309d4a6c97 100644
--- a/components/camel-jsonpath/src/main/docs/jsonpath-language.adoc
+++ b/components/camel-jsonpath/src/main/docs/jsonpath-language.adoc
@@ -112,6 +112,33 @@ XML DSL::
</route>
----
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:start
+ steps:
+ - choice:
+ when:
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < 10)]"
+ steps:
+ - to:
+ uri: mock:cheap
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < 30)]"
+ steps:
+ - to:
+ uri: mock:average
+ otherwise:
+ steps:
+ - to:
+ uri: mock:expensive
+----
====
=== Supported message body types
@@ -227,6 +254,134 @@ XML DSL::
</route>
----
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:start
+ steps:
+ - choice:
+ when:
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < ${header.cheap})]"
+ steps:
+ - to:
+ uri: mock:cheap
+ - expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price < ${header.average})]"
+ steps:
+ - to:
+ uri: mock:average
+ otherwise:
+ steps:
+ - to:
+ uri: mock:expensive
+----
+====
+
+The inlined Simple expressions also work when the result becomes the message
body, for example to filter a JSON
+array down to the elements matching a header, such as the path parameter of a
rest service. A filter returns a
+list (empty when nothing matches), so set `resultType` to `java.util.List` and
check its size. The element found is
+a `Map`, which is marshalled back to JSON:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:one-sku")
+ .setBody(constant("resource:file:stock.json"))
+ .setBody().jsonpath("$[?(@.sku == '${header.sku}')]", List.class)
+ .choice()
+ .when(simple("${body.size()} == 0"))
+ .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(404))
+ .setBody(simple("unknown SKU ${header.sku}"))
+ .otherwise()
+ .setBody(simple("${body[0]}"))
+ .marshal().json()
+ .end();
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="direct:one-sku"/>
+ <setBody>
+ <constant>resource:file:stock.json</constant>
+ </setBody>
+ <setBody>
+ <jsonpath resultType="java.util.List">$[?(@.sku ==
'${header.sku}')]</jsonpath>
+ </setBody>
+ <choice>
+ <when>
+ <simple>${body.size()} == 0</simple>
+ <setHeader name="CamelHttpResponseCode">
+ <constant>404</constant>
+ </setHeader>
+ <setBody>
+ <simple>unknown SKU ${header.sku}</simple>
+ </setBody>
+ </when>
+ <otherwise>
+ <setBody>
+ <simple>${body[0]}</simple>
+ </setBody>
+ <marshal>
+ <json/>
+ </marshal>
+ </otherwise>
+ </choice>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:one-sku
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: resource:file:stock.json
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$[?(@.sku == '${header.sku}')]"
+ resultType: java.util.List
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${body.size()} == 0"
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "404"
+ - setBody:
+ expression:
+ simple:
+ expression: "unknown SKU ${header.sku}"
+ otherwise:
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "${body[0]}"
+ - marshal:
+ json: {}
+----
====
You can turn off support for inlined Simple expression by setting the option
`allowSimple` to `false` as shown:
diff --git
a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
index 1e845c121933..7e47736829fa 100644
--- a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
+++ b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
@@ -157,6 +157,108 @@ YAML::
----
====
+=== Calling an operation of an API
+
+To call an API by its contract, send to a `rest-openapi` endpoint with the
contract and the _operation id_. Camel
+builds the HTTP request from the contract:
+
+* a path or query parameter, such as `+{petId}+` in `+/pet/{petId}+`, is taken
from the message header of the same name;
+* the message body is the request body (a JSON string for a JSON API, with a
`Content-Type` header);
+* the response body becomes the message body, as text: unmarshal it to read
its fields.
+
+The server comes from the `servers` of the contract; set `host` to call
another one (for example
+`http://localhost:8080` while developing). The HTTP call itself is done by an
HTTP component on the classpath, such
+as `camel-http`; set `componentName` when there is more than one.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("timer:pets?repeatCount=1")
+ .setHeader("petId", constant(10))
+ .to("rest-openapi:petstore-v3.json#getPetById")
+ .unmarshal().json()
+ .log("Pet ${body[name]}")
+ .setBody(constant("{\"id\": 11, \"name\": \"Rex\"}"))
+ .setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
+ .to("rest-openapi:petstore-v3.json#addPet?host={{petstore.url}}")
+ .log("Added ${body}");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="timer:pets?repeatCount=1"/>
+ <setHeader name="petId">
+ <constant>10</constant>
+ </setHeader>
+ <to uri="rest-openapi:petstore-v3.json#getPetById"/>
+ <unmarshal>
+ <json/>
+ </unmarshal>
+ <log message="Pet ${body[name]}"/>
+ <setBody>
+ <constant>{"id": 11, "name": "Rex"}</constant>
+ </setBody>
+ <setHeader name="Content-Type">
+ <constant>application/json</constant>
+ </setHeader>
+ <to uri="rest-openapi:petstore-v3.json#addPet?host={{petstore.url}}"/>
+ <log message="Added ${body}"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: timer:pets
+ parameters:
+ repeatCount: 1
+ steps:
+ - setHeader:
+ name: petId
+ expression:
+ constant:
+ expression: "10"
+ - to:
+ uri: rest-openapi
+ parameters:
+ specificationUri: petstore-v3.json
+ operationId: getPetById
+ - unmarshal:
+ json: {}
+ - log:
+ message: "Pet ${body[name]}"
+ - setBody:
+ expression:
+ constant:
+ expression: '{"id": 11, "name": "Rex"}'
+ - setHeader:
+ name: Content-Type
+ expression:
+ constant:
+ expression: application/json
+ - to:
+ uri: rest-openapi
+ parameters:
+ specificationUri: petstore-v3.json
+ operationId: addPet
+ host: "{{petstore.url}}"
+ - log:
+ message: "Added ${body}"
+----
+====
+
+An answer with an error status (a `404` or a `409`) fails the exchange with an
`HttpOperationFailedException`; handle
+it with `onException` to read `${exception.statusCode}` and
`${exception.responseBody}`.
+
=== Consumer OAuth Bearer token validation
When `rest-openapi` is used as a consumer, HTTP requests are served by the
selected
diff --git a/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
b/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
index cf037a63c6aa..9e0e00581f93 100644
--- a/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
+++ b/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
@@ -142,6 +142,103 @@ define the services as shown below:
----
====
+== Returning an HTTP status code
+
+A rest service answers with the HTTP status `200` by default (or `204` when
there is no body). To answer with
+another status, such as a `404` for something that is not found or a `409` for
a conflict, set the
+`CamelHttpResponseCode` header (`Exchange.HTTP_RESPONSE_CODE` in Java) in the
route the service calls; the message
+body becomes the response body. A path parameter such as `+{sku}+` arrives as
a header of the same name:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+rest("/stock")
+ .get("/{sku}").to("direct:one-sku");
+
+from("direct:one-sku")
+ .choice()
+ .when(simple("${header.sku} == 'CAMEL-MUG'"))
+ .setBody(constant("{\"sku\": \"CAMEL-MUG\", \"qty\": 42}"))
+ .otherwise()
+ .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(404))
+ .setBody(simple("{\"error\": \"unknown SKU ${header.sku}\"}"))
+ .end();
+----
+
+XML::
++
+[source,xml]
+----
+<rest path="/stock">
+ <get path="/{sku}">
+ <to uri="direct:one-sku"/>
+ </get>
+</rest>
+
+<route>
+ <from uri="direct:one-sku"/>
+ <choice>
+ <when>
+ <simple>${header.sku} == 'CAMEL-MUG'</simple>
+ <setBody>
+ <constant>{"sku": "CAMEL-MUG", "qty": 42}</constant>
+ </setBody>
+ </when>
+ <otherwise>
+ <setHeader name="CamelHttpResponseCode">
+ <constant>404</constant>
+ </setHeader>
+ <setBody>
+ <simple>{"error": "unknown SKU ${header.sku}"}</simple>
+ </setBody>
+ </otherwise>
+ </choice>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- rest:
+ path: /stock
+ get:
+ - path: "/{sku}"
+ to: direct:one-sku
+- route:
+ from:
+ uri: direct:one-sku
+ steps:
+ - choice:
+ when:
+ - expression:
+ simple:
+ expression: "${header.sku} == 'CAMEL-MUG'"
+ steps:
+ - setBody:
+ expression:
+ constant:
+ expression: '{"sku": "CAMEL-MUG", "qty": 42}'
+ otherwise:
+ steps:
+ - setHeader:
+ name: CamelHttpResponseCode
+ expression:
+ constant:
+ expression: "404"
+ - setBody:
+ expression:
+ simple:
+ expression: '{"error": "unknown SKU ${header.sku}"}'
+----
+====
+
+When the route throws an exception that is not handled, the client gets a
`500`. To answer with a specific
+status for an exception, handle it in `onException` and set
`CamelHttpResponseCode` there.
+
== Using a base path
The REST DSL allows defining a base path to help applying the _"don't repeat
yourself"_ (DRY) practice.
@@ -186,9 +283,9 @@ YAML::
- rest:
path: "/customers/"
get:
- - path: "/{id}}"
+ - path: "/{id}"
to: "direct:customerDetails"
- - path: "/{id}/orders}"
+ - path: "/{id}/orders"
to: "direct:customerOrders"
post:
- path: "/neworder"
@@ -241,9 +338,9 @@ YAML::
----
- rest:
get:
- - path: "/customers/{id}}"
+ - path: "/customers/{id}"
to: "direct:customerDetails"
- - path: "/customers/{id}/orders}"
+ - path: "/customers/{id}/orders"
to: "direct:customerOrders"
post:
- path: "/customers/neworder"
@@ -290,7 +387,6 @@ YAML::
----
====
-
== Managing Rest services
Each of the rest services becomes a Camel route, so in the first example,
@@ -376,9 +472,9 @@ YAML::
inlineRoutes: true
- rest:
get:
- - path: "/customers/{id}}"
+ - path: "/customers/{id}"
to: "direct:customerDetails"
- - path: "/customers/{id}/orders}"
+ - path: "/customers/{id}/orders"
to: "direct:customerOrders"
post:
- path: "/customers/neworder"
@@ -502,9 +598,9 @@ YAML::
- rest:
path: "/customers/"
get:
- - path: "/{id}}"
+ - path: "/{id}"
to: "direct:customerDetails"
- - path: "/{id}/orders}"
+ - path: "/{id}/orders"
to: "direct:customerOrders"
post:
- path: "/neworder"
@@ -574,9 +670,9 @@ YAML::
----
- rest:
get:
- - path: "/customers/{id}}"
+ - path: "/customers/{id}"
to: "direct:customerDetails"
- - path: "/customers/{id}/orders}"
+ - path: "/customers/{id}/orders"
to: "direct:customerOrders"
disabled: "{{ordersEnabled}}"
post:
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
index 6c0cb0b6dd20..b2ba3ff34354 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
@@ -972,7 +972,7 @@
},
{
"source": "rest-dsl.adoc",
- "yaml": "- rest:\n path: \"\/customers\/\"\n get:\n - path:
\"\/{id}}\"\n to: \"direct:customerDetails\"\n - path:
\"\/{id}\/orders}\"\n to: \"direct:customerOrders\"\n post:\n -
path: \"\/neworder\"\n to: \"direct:customerNewOrder\"\n"
+ "yaml": "- rest:\n path: \/stock\n get:\n - path:
\"\/{sku}\"\n to: direct:one-sku\n- route:\n from:\n uri:
direct:one-sku\n steps:\n - choice:\n when:\n
- expression:\n simple:\n expression:
\"${header.sku} == 'CAMEL-MUG'\"\n steps:\n -
setBody:\n expression:\n
constant:\n expression: [...]
},
{
"source": "rest-dsl-openapi.adoc",
@@ -980,7 +980,7 @@
},
{
"source": "rest-dsl.adoc",
- "yaml": "- rest:\n get:\n - path: \"\/customers\/{id}}\"\n
to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders}\"\n
to: \"direct:customerOrders\"\n post:\n - path:
\"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n"
+ "yaml": "- rest:\n path: \"\/customers\/\"\n get:\n - path:
\"\/{id}\"\n to: \"direct:customerDetails\"\n - path:
\"\/{id}\/orders\"\n to: \"direct:customerOrders\"\n post:\n -
path: \"\/neworder\"\n to: \"direct:customerNewOrder\"\n"
},
{
"source": "rest-dsl-openapi.adoc",
@@ -988,7 +988,7 @@
},
{
"source": "rest-dsl.adoc",
- "yaml": "- rest:\n path: \"items\/\"\n get:\n - path:
\"{id}\/{filename}.{content-type}\"\n to: \"direct:item\"\n"
+ "yaml": "- rest:\n get:\n - path: \"\/customers\/{id}\"\n
to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders\"\n
to: \"direct:customerOrders\"\n post:\n - path:
\"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n"
},
{
"source": "rest-dsl-openapi.adoc",
@@ -996,7 +996,11 @@
},
{
"source": "rest-dsl.adoc",
- "yaml": "- restConfiguration:\n inlineRoutes: true\n- rest:\n
get:\n - path: \"\/customers\/{id}}\"\n to:
\"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders}\"\n
to: \"direct:customerOrders\"\n post:\n - path:
\"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n"
+ "yaml": "- rest:\n path: \"items\/\"\n get:\n - path:
\"{id}\/{filename}.{content-type}\"\n to: \"direct:item\"\n"
+ },
+ {
+ "source": "rest-dsl.adoc",
+ "yaml": "- restConfiguration:\n inlineRoutes: true\n- rest:\n
get:\n - path: \"\/customers\/{id}\"\n to:
\"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders\"\n
to: \"direct:customerOrders\"\n post:\n - path:
\"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n"
},
{
"source": "rest-dsl.adoc",
@@ -1004,11 +1008,11 @@
},
{
"source": "rest-dsl.adoc",
- "yaml": "- restConfiguration:\n inlineRoutes: true\n- rest:\n
path: \"\/customers\/\"\n get:\n - path: \"\/{id}}\"\n to:
\"direct:customerDetails\"\n - path: \"\/{id}\/orders}\"\n to:
\"direct:customerOrders\"\n post:\n - path: \"\/neworder\"\n to:
\"direct:customerNewOrder\"\n- route:\n from:\n uri:
direct:customerNewOrder\n steps:\n - to:\n uri:
direct:commonCustomerNewOrder\n- route:\n from:\n [...]
+ "yaml": "- restConfiguration:\n inlineRoutes: true\n- rest:\n
path: \"\/customers\/\"\n get:\n - path: \"\/{id}\"\n to:
\"direct:customerDetails\"\n - path: \"\/{id}\/orders\"\n to:
\"direct:customerOrders\"\n post:\n - path: \"\/neworder\"\n to:
\"direct:customerNewOrder\"\n- route:\n from:\n uri:
direct:customerNewOrder\n steps:\n - to:\n uri:
direct:commonCustomerNewOrder\n- route:\n from:\n u [...]
},
{
"source": "rest-dsl.adoc",
- "yaml": "- rest:\n get:\n - path: \"\/customers\/{id}}\"\n
to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders}\"\n
to: \"direct:customerOrders\"\n disabled: \"{{ordersEnabled}}\"\n
post:\n - path: \"\/customers\/neworder\"\n to:
\"direct:customerNewOrder\"\n disabled: \"true\"\n"
+ "yaml": "- rest:\n get:\n - path: \"\/customers\/{id}\"\n
to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders\"\n
to: \"direct:customerOrders\"\n disabled: \"{{ordersEnabled}}\"\n
post:\n - path: \"\/customers\/neworder\"\n to:
\"direct:customerNewOrder\"\n disabled: \"true\"\n"
}
],
"returnAddress": [
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
index bd1b71e6e402..42f9856ca1e5 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
@@ -99,11 +99,17 @@ public final class CatalogSamples {
* is here: a task (read file, call service), a wording of the outcome
(retry, batch) or a technology (json, cron).
* A protocol or a product (mqtt, s3) is what camel_catalog_find turns
into a component, so it is not here.
*/
+ private static final String[] HTTP_CALL = {
+ "http", "a to: with the http component (toD: when the URI is built
from the message, such as a path"
+ + " parameter); the response becomes the message body" };
+ private static final String[] OPENAPI_CALL = {
+ "rest-openapi", "a to: with the rest-openapi component, its
specificationUri and operationId; path and"
+ + " query parameters come from headers of the same
name, the body is the request body" };
+
static final Map<String, String> INTENTS = Map.ofEntries(
Map.entry("read file", "poll"), Map.entry("readfile", "poll"),
Map.entry("load file", "poll"),
Map.entry("read a file", "poll"), Map.entry("read", "poll"),
Map.entry("fetch", "poll"),
Map.entry("consume once", "poll"), Map.entry("poll once", "poll"),
- Map.entry("call service", "enrich"), Map.entry("call", "enrich"),
Map.entry("http call", "enrich"),
Map.entry("lookup", "enrich"),
Map.entry("batch", "aggregate"), Map.entry("collect",
"aggregate"), Map.entry("group", "aggregate"),
Map.entry("retry", "onException"), Map.entry("error handling",
"onException"),
@@ -116,6 +122,17 @@ public final class CatalogSamples {
Map.entry("convert", "convertBodyTo"), Map.entry("json",
"marshal"),
Map.entry("schedule", "from"), Map.entry("cron", "from"));
+ /**
+ * What a request is about, to the component that does it, with how.
Calling a service is a to: (or a toD: when the
+ * URI is built from the message) on the http component, not the enrich
EIP, which also merges the answer into the
+ * message with an aggregation strategy.
+ */
+ static final Map<String, String[]> COMPONENT_INTENTS = Map.ofEntries(
+ Map.entry("call service", HTTP_CALL), Map.entry("call",
HTTP_CALL), Map.entry("http call", HTTP_CALL),
+ Map.entry("call api", HTTP_CALL), Map.entry("http client",
HTTP_CALL), Map.entry("call http", HTTP_CALL),
+ Map.entry("openapi client", OPENAPI_CALL), Map.entry("call by
contract", OPENAPI_CALL),
+ Map.entry("call operation", OPENAPI_CALL));
+
private static volatile Map<String, List<Map<String, String>>> samples;
private CatalogSamples() {
@@ -408,6 +425,13 @@ public final class CatalogSamples {
int max = Math.max(1, Math.min(MAX_LIMIT, limit <= 0 ? DEFAULT_LIMIT :
limit));
boolean eips = wanted == null || wanted.equals("eip");
+ // what to do that a component does (call service)
+ String[] componentIntent =
COMPONENT_INTENTS.get(given.toLowerCase(Locale.ROOT));
+ if (componentIntent != null && catalog != null && (wanted == null ||
wanted.equals("component"))) {
+ JsonObject found = catalogAnswer(catalog, "component",
componentIntent[0], max);
+ found.put("note", "'" + given + "' is done with " +
componentIntent[1]);
+ return found;
+ }
// an EIP or a file entry by its name, a part of it, or what to do
if (eips) {
String key = resolveExact(given);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
index 6a10cc9d54e2..2c464cf51140 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
@@ -112,6 +112,24 @@ public class CatalogSamplesTest {
assertThat(CatalogSamples.sample(catalog, "lookup",
1).getString("name")).isEqualTo("enrich");
}
+ @Test
+ void callingAServiceIsTheHttpComponentNotEnrich() {
+ CamelCatalog catalog = new DefaultCamelCatalog();
+ for (String term : List.of("call service", "http call", "Call API")) {
+ JsonObject o = CatalogSamples.sample(catalog, term, 2);
+ assertThat(o.getString("name")).as(term).isEqualTo("http");
+ assertThat(o.getString("note")).as(term).contains("to:", "toD:");
+ assertThat(((JsonObject) ((JsonArray)
o.get("samples")).get(0)).getString("yaml")).contains("uri: http");
+ }
+ // calling an API by its contract: the rest-openapi producer
+ JsonObject o = CatalogSamples.sample(catalog, "openapi client", 5);
+ assertThat(o.getString("name")).isEqualTo("rest-openapi");
+ assertThat(((JsonArray) o.get("samples")).stream().map(s ->
((JsonObject) s).getString("yaml")))
+ .anyMatch(y -> y.contains("operationId: getPetById"));
+ // asked for an EIP: no component
+ assertThat(CatalogSamples.sample(catalog, "eip", "call service",
1).getString("name")).isNotEqualTo("http");
+ }
+
@Test
void unknownNameGetsSuggestions() {
JsonObject o = CatalogSamples.sample("aggregat", 2);
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
index 876015336b5a..85c19f190020 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
@@ -809,7 +809,8 @@ public class YamlValidator {
}
String outsideQuotes
= text != null ?
text.replaceAll("\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'", "\"\"") :
null;
- if (outsideQuotes != null && outsideQuotes.contains("${") &&
!name.equals("js") && !name.equals("quickjs")) {
+ if (outsideQuotes != null && outsideQuotes.contains("${") &&
!name.equals("js") && !name.equals("quickjs")
+ && !jsonPathWithInlineSimple(name, value, outsideQuotes)) {
// "${x}" inside quotes is a groovy GString (or a JS template)
and is fine; ${...} outside quotes is simple
String example;
if (name.equals("groovy") || name.equals("mvel") ||
name.equals("ognl") || name.equals("jactl")) {
@@ -835,6 +836,21 @@ public class YamlValidator {
}
}
+ /**
+ * A JsonPath with Simple inlined, such as $.store.book[?(@.price <
${header.max})]: the jsonpath language evaluates
+ * the ${...} first (allowSimple, on by default), so it is valid when the
expression has a JsonPath root of its own.
+ * A whole ${...} (jsonpath: ${body.x}) has none and is simple syntax.
+ */
+ private static boolean jsonPathWithInlineSimple(String name, JsonNode
value, String outsideQuotes) {
+ if (!name.equals("jsonpath")) {
+ return false;
+ }
+ if (value.isObject() && value.has("allowSimple") &&
"false".equals(value.get("allowSimple").asText())) {
+ return false;
+ }
+ return outsideQuotes.replaceAll("\\$\\{[^}]*}", "").contains("$");
+ }
+
/**
* to: http://host/stock/${header.sku}: the endpoint of a to: is resolved
once when the route starts, so an
* expression in its path is never evaluated - it is sent as the text it
is, url-encoded. That is what toD: is for
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
index e41a7b1d7e7a..d90380a8a37d 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
@@ -706,6 +706,46 @@ public class YamlValidatorPropertyHintTest {
.contains("or use simple: {expression: \"${body.value} <
1\"}");
}
+ @Test
+ public void testJsonPathWithInlinedSimpleIsValid() throws Exception {
+ // the jsonpath language evaluates ${...} inside a JsonPath first
(allowSimple, on by default)
+ List<Error> errors = validator.validate("""
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price <
${header.max})]"
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$[?(@.sku == '${header.sku}')]"
+ resultType: java.util.List
+ """);
+ assertThat(errors).isEmpty();
+
+ // a whole ${...} is simple syntax, and so is any ${...} when
allowSimple is off
+ errors = validator.validate("""
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "${body.price}"
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$.store.book[?(@.price <
${header.max})]"
+ allowSimple: false
+ """);
+ assertThat(errors).hasSize(2);
+ assertThat(errors.get(0).getMessage()).startsWith("jsonpath: ${...} is
simple syntax, not jsonpath");
+ }
+
@Test
public void testOnExceptionAsAListSaysMap() throws Exception {
List<Error> errors = validator.validate("""