This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25264 in repository https://gitbox.apache.org/repos/asf/camel.git
commit cd7ed8041468547c77bf88e3c43f8b67b050eee4 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Oct 2 12:51:51 2026 +0200 CAMEL-25264: docs, camel-jbang - samples for the HTTP rungs: REST status codes, JsonPath YAML, calling a rest-openapi operation, the call-service intent; the validator accepts Simple inlined in a JsonPath Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[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..1b937e01f745 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..1b937e01f745 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..831d55dc3e2c 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("""
