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-jbang-examples.git


The following commit(s) were added to refs/heads/main by this push:
     new f4e4c20  CAMEL-24808: rung 5 of the ladder: connect without a service 
(#82)
f4e4c20 is described below

commit f4e4c20916844626c82492eb0025253b6ef83fee
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 18:49:24 2026 +0200

    CAMEL-24808: rung 5 of the ladder: connect without a service (#82)
    
    Three examples under connect/: stock-api (the shop's stock REST service on 
the CLI's HTTP server, answering one SKU as JSON from a file or 404), 
http-client (every order line checked against that service over HTTP, served by 
the same example, with the SKU kept in exchange properties so it does not 
travel as a header) and file-processing (invoices copied into an inbox by a 
courier route, validated, archived by month and moved to done, a bad one to 
failed, a note left alone by the inclu [...]
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .github/workflows/build.yml                        |   6 +
 .gitignore                                         |   3 +
 camel-jbang-example-catalog.json                   | 134 +++++++++++++++++++++
 connect/file-processing/README.md                  |  77 ++++++++++++
 connect/file-processing/application.properties     |  16 +++
 connect/file-processing/file-processing.camel.yaml |  56 +++++++++
 connect/file-processing/metadata.json              |  30 +++++
 connect/file-processing/samples/invoice-2001.json  |   1 +
 connect/file-processing/samples/invoice-2002.json  |   1 +
 connect/file-processing/samples/invoice-2003.json  |   1 +
 connect/file-processing/samples/invoice-2004.json  |   1 +
 connect/file-processing/samples/note-2005.txt      |   1 +
 .../test/file-processing.citrus.it.yaml            |  26 ++++
 connect/http-client/README.md                      |  76 ++++++++++++
 connect/http-client/application.properties         |  18 +++
 connect/http-client/http-client.camel.yaml         | 117 ++++++++++++++++++
 connect/http-client/metadata.json                  |  37 ++++++
 connect/http-client/orders/order-1001.json         |   2 +
 connect/http-client/orders/order-1002.json         |   2 +
 connect/http-client/orders/order-1003.json         |   2 +
 connect/http-client/stock.json                     |   5 +
 .../http-client/test/http-client.citrus.it.yaml    |  21 ++++
 connect/stock-api/README.md                        |  79 ++++++++++++
 connect/stock-api/application.properties           |  18 +++
 connect/stock-api/metadata.json                    |  33 +++++
 connect/stock-api/stock-api.camel.yaml             |  73 +++++++++++
 connect/stock-api/stock.json                       |   5 +
 connect/stock-api/test/stock-api.citrus.it.yaml    |  49 ++++++++
 28 files changed, 890 insertions(+)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ed9f2f2..16c00ec 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -62,6 +62,12 @@ jobs:
             path: fail-well/error-handling
           - name: Circuit breaker
             path: fail-well/circuit-breaker
+          - name: Stock API
+            path: connect/stock-api
+          - name: HTTP client
+            path: connect/http-client
+          - name: File processing
+            path: connect/file-processing
           - name: OpenAPI server
             path: rest/openapi-server
           - name: OpenAPI client
diff --git a/.gitignore b/.gitignore
index 6533f23..aaa583b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@ camel-runner.jar
 outbox/
 # written by the error-handling example at runtime
 parked/
+# written by the file-processing example at runtime
+inbox/
+archive/
diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json
index cf84bdc..2d873e1 100644
--- a/camel-jbang-example-catalog.json
+++ b/camel-jbang-example-catalog.json
@@ -341,6 +341,140 @@
             "http-to-aws-sqs.camel.yaml"
         ]
     },
+    {
+        "name": "connect/file-processing",
+        "title": "File processing",
+        "description": "A courier route copies five files into an inbox; the 
invoices are checked, archived under a month directory and moved to done, the 
invoice with a negative amount is rejected with a warning and moved to failed, 
and the driver's note is left alone because only .json files are picked up.",
+        "level": "connect",
+        "teaches": {
+            "components": [
+                "file",
+                "log"
+            ],
+            "eips": [
+                "validate",
+                "onException",
+                "unmarshal",
+                "marshal"
+            ],
+            "languages": [
+                "simple"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "file",
+            "inbox",
+            "archive"
+        ],
+        "bundled": false,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "file-processing.camel.yaml",
+            "samples/invoice-2001.json",
+            "samples/invoice-2002.json",
+            "samples/invoice-2003.json",
+            "samples/invoice-2004.json",
+            "samples/note-2005.txt"
+        ]
+    },
+    {
+        "name": "connect/http-client",
+        "title": "HTTP client",
+        "description": "Every line of the three orders is checked against the 
stock service over HTTP, served by the same example; the log shows each line as 
ok, or back-order when the stock is short, with the stock level from the 
response.",
+        "level": "connect",
+        "teaches": {
+            "components": [
+                "file",
+                "http",
+                "rest",
+                "platform-http",
+                "direct",
+                "log"
+            ],
+            "eips": [
+                "split",
+                "setProperty",
+                "toD",
+                "choice",
+                "unmarshal",
+                "marshal"
+            ],
+            "languages": [
+                "simple",
+                "jsonpath"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "http",
+            "rest",
+            "client"
+        ],
+        "bundled": false,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "http-client.camel.yaml",
+            "orders/order-1001.json",
+            "orders/order-1002.json",
+            "orders/order-1003.json",
+            "stock.json"
+        ]
+    },
+    {
+        "name": "connect/stock-api",
+        "title": "Stock API",
+        "description": "The shop's stock service on port 8080: GET /stock 
returns the stock file, GET /stock/{sku} returns one SKU as JSON and a 404 with 
an error message for an unknown SKU.",
+        "level": "connect",
+        "teaches": {
+            "components": [
+                "rest",
+                "platform-http",
+                "direct"
+            ],
+            "eips": [
+                "setBody",
+                "setHeader",
+                "choice",
+                "marshal"
+            ],
+            "languages": [
+                "constant",
+                "jsonpath",
+                "simple"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "rest",
+            "http",
+            "platform-http"
+        ],
+        "bundled": false,
+        "requiresDocker": false,
+        "hasCitrusTests": false,
+        "files": [
+            "README.md",
+            "application.properties",
+            "stock-api.camel.yaml",
+            "stock.json"
+        ]
+    },
     {
         "name": "database/sql",
         "title": "SQL Database",
diff --git a/connect/file-processing/README.md 
b/connect/file-processing/README.md
new file mode 100644
index 0000000..a2d8717
--- /dev/null
+++ b/connect/file-processing/README.md
@@ -0,0 +1,77 @@
+# File processing
+
+The courier's billing system drops invoices into an inbox directory. Each 
invoice is checked, archived under a
+month directory and moved to `done`; a bad one goes to `failed`; a file that 
is not an invoice is left alone.
+The first route plays the courier: it copies the five files in `samples/` into 
`inbox/`.
+
+## What you will see
+
+```text
+INFO ... file-processing.camel.yaml:47 : Invoice INV-2001 for ORD-1001: 49.4 
EUR, archived as 2026-09/invoice-2001.json
+INFO ... file-processing.camel.yaml:47 : Invoice INV-2002 for ORD-1002: 28.5 
EUR, archived as 2026-09/invoice-2002.json
+WARN ... file-processing.camel.yaml:11 : Invoice invoice-2003.json rejected: 
Validation failed for Predicate[bodyOgnl([amount]) > 0]. ...
+ERROR ... DefaultErrorHandler : Failed delivery for (MessageId: ...). 
Exhausted after delivery attempt: 1 caught: ...PredicateValidationException: ...
+WARN ... GenericFileOnCompletion : Rollback file strategy: 
...GenericFileRenameProcessStrategy
+INFO ... file-processing.camel.yaml:47 : Invoice INV-2004 for ORD-1003: 53.45 
EUR, archived as 2026-09/invoice-2004.json
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+Afterwards the directories tell the story: `inbox/done/` holds the three good 
invoices, `inbox/failed/` the
+rejected one, `archive/<year-month>/` copies of the good ones, and 
`inbox/note-2005.txt` is still where it was.
+Delete `inbox/` and `archive/` to run it again.
+
+## How it works
+
+- The `courier` route is a `file` consumer on `samples/` with `noop: true` 
writing to `file:inbox`, a copy.
+- The `invoices` route is the lesson: a `file` consumer on `inbox/` with 
`include: .*\.json`, so the driver's
+  note is never picked up; `move: done` for files that went through, 
`moveFailed: failed` for files whose
+  exchange failed. Both are directories relative to the inbox.
+- `validate` throws a `PredicateValidationException` when the amount is not 
positive; the `onException` logs a
+  warning without a stack trace and lets the exception through, which is what 
makes the consumer use `moveFailed`.
+  Had it set `handled: true`, the file would have been moved to `done` as if 
it had succeeded.
+- The archive copy uses a `fileName` expression with `${date:now:yyyy-MM}` for 
the directory and `${file:name}`
+  for the name; the file component creates directories as needed.
+
+## Build it step by step
+
+1. A `file` route on `inbox` that logs `${file:name}`; drop a file in and see 
it logged, then note the
+   `.camel` directory the consumer moved it to by default.
+2. Add `move: done` and `include: .*\.json`, drop the note in and see it 
ignored.
+3. Add the `courier` route so the samples arrive by themselves, and 
`unmarshal` and log the invoice.
+4. Add `validate` and `moveFailed: failed`, then the `onException` to tidy the 
log.
+
+## Try changing
+
+- Add `idempotent: true` with `idempotentKey: ${file:name}` and drop the same 
invoice twice.
+- Replace `move: done` with `delete: true`.
+- Point `archive` at a `sftp:` endpoint; the `ftp` example on the next rung 
shows how.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/file-processing.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/file-processing.citrus.it.yaml
+```
+
+The test starts the routes and verifies the archived and rejected lines.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/connect/file-processing/application.properties 
b/connect/file-processing/application.properties
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/connect/file-processing/application.properties
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/connect/file-processing/file-processing.camel.yaml 
b/connect/file-processing/file-processing.camel.yaml
new file mode 100644
index 0000000..fd9446c
--- /dev/null
+++ b/connect/file-processing/file-processing.camel.yaml
@@ -0,0 +1,56 @@
+# The courier's billing system drops invoices into the inbox. Each invoice
+# is checked, archived by month and moved to done; a bad one goes to failed.
+# The courier route plays the courier: it copies the samples into the inbox.
+- onException:
+    exception:
+      - org.apache.camel.support.processor.PredicateValidationException
+    redeliveryPolicy:
+      logStackTrace: false
+      logExhaustedMessageHistory: false
+    steps:
+      - log:
+          loggingLevel: WARN
+          message: "Invoice ${file:name} rejected: ${exception.message}"
+
+- route:
+    id: courier
+    from:
+      uri: file
+      parameters:
+        directoryName: samples
+        noop: true
+        sortBy: "file:name"
+      steps:
+        - to:
+            uri: file
+            parameters:
+              directoryName: inbox
+
+- route:
+    id: invoices
+    from:
+      uri: file
+      parameters:
+        directoryName: inbox
+        include: ".*\\.json"
+        move: done
+        moveFailed: failed
+        sortBy: "file:name"
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        - validate:
+            expression:
+              simple:
+                expression: "${body[amount]} > 0"
+        - log:
+            message: "Invoice ${body[invoiceId]} for ${body[orderId]}: 
${body[amount]} EUR, archived as ${date:now:yyyy-MM}/${file:name}"
+        - marshal:
+            json:
+              library: Jackson
+        - to:
+            uri: file
+            parameters:
+              directoryName: archive
+              fileName: "${date:now:yyyy-MM}/${file:name}"
diff --git a/connect/file-processing/metadata.json 
b/connect/file-processing/metadata.json
new file mode 100644
index 0000000..577d534
--- /dev/null
+++ b/connect/file-processing/metadata.json
@@ -0,0 +1,30 @@
+{
+    "title": "File processing",
+    "description": "A courier route copies five files into an inbox; the 
invoices are checked, archived under a month directory and moved to done, the 
invoice with a negative amount is rejected with a warning and moved to failed, 
and the driver's note is left alone because only .json files are picked up.",
+    "level": "connect",
+    "teaches": {
+        "components": [
+            "file",
+            "log"
+        ],
+        "eips": [
+            "validate",
+            "onException",
+            "unmarshal",
+            "marshal"
+        ],
+        "languages": [
+            "simple"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "file",
+        "inbox",
+        "archive"
+    ],
+    "bundled": false
+}
diff --git a/connect/file-processing/samples/invoice-2001.json 
b/connect/file-processing/samples/invoice-2001.json
new file mode 100644
index 0000000..51d79d5
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2001.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2001", "orderId": "ORD-1001", "amount": 49.40, "currency": 
"EUR"}
diff --git a/connect/file-processing/samples/invoice-2002.json 
b/connect/file-processing/samples/invoice-2002.json
new file mode 100644
index 0000000..0945236
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2002.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2002", "orderId": "ORD-1002", "amount": 28.50, "currency": 
"EUR"}
diff --git a/connect/file-processing/samples/invoice-2003.json 
b/connect/file-processing/samples/invoice-2003.json
new file mode 100644
index 0000000..a084e66
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2003.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2003", "orderId": "ORD-1003", "amount": -5.00, "currency": 
"EUR"}
diff --git a/connect/file-processing/samples/invoice-2004.json 
b/connect/file-processing/samples/invoice-2004.json
new file mode 100644
index 0000000..761c248
--- /dev/null
+++ b/connect/file-processing/samples/invoice-2004.json
@@ -0,0 +1 @@
+{"invoiceId": "INV-2004", "orderId": "ORD-1003", "amount": 53.45, "currency": 
"EUR"}
diff --git a/connect/file-processing/samples/note-2005.txt 
b/connect/file-processing/samples/note-2005.txt
new file mode 100644
index 0000000..e82a2d8
--- /dev/null
+++ b/connect/file-processing/samples/note-2005.txt
@@ -0,0 +1 @@
+Handwritten note from the driver: parcel for ORD-1002 left with the neighbour.
diff --git a/connect/file-processing/test/file-processing.citrus.it.yaml 
b/connect/file-processing/test/file-processing.citrus.it.yaml
new file mode 100644
index 0000000..00f2285
--- /dev/null
+++ b/connect/file-processing/test/file-processing.citrus.it.yaml
@@ -0,0 +1,26 @@
+name: file-processing-test
+description: The file-processing example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "file-processing"
+            file: "../file-processing.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "file-processing"
+          logMessage: "Invoice INV-2001 for ORD-1001: 49.4 EUR"
+  - camel:
+      jbang:
+        verify:
+          integration: "file-processing"
+          logMessage: "Invoice invoice-2003.json rejected"
+  - camel:
+      jbang:
+        verify:
+          integration: "file-processing"
+          logMessage: "Invoice INV-2004 for ORD-1003: 53.45 EUR"
diff --git a/connect/http-client/README.md b/connect/http-client/README.md
new file mode 100644
index 0000000..c24e51d
--- /dev/null
+++ b/connect/http-client/README.md
@@ -0,0 +1,76 @@
+# HTTP client
+
+Before an order goes to picking, every line is checked against the stock 
service over HTTP. The stock service
+runs inside this same example, so there is nothing else to start: the first 
two routes are a trimmed copy of
+`stock-api`, the third is the client.
+
+## What you will see
+
+```text
+INFO ... http-client.camel.yaml:112 : ORD-1001: CAMEL-TSHIRT x 2, 120 in 
stock, ok
+INFO ... http-client.camel.yaml:112 : ORD-1001: CAMEL-MUG x 1, 42 in stock, ok
+INFO ... http-client.camel.yaml:112 : ORD-1002: CAMEL-MUG x 3, 42 in stock, ok
+INFO ... http-client.camel.yaml:112 : ORD-1003: CAMEL-TSHIRT x 1, 120 in 
stock, ok
+INFO ... http-client.camel.yaml:116 : ORD-1003: CAMEL-CAP x 1, only 0 in 
stock, back-order
+INFO ... http-client.camel.yaml:112 : ORD-1003: CAMEL-MUG x 2, 42 in stock, ok
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+The three orders in `orders/` are read once each (`noop: true` leaves the 
files in place), so the run can be
+repeated. The stock levels are in `stock.json`.
+
+## How it works
+
+- The client splits each order into lines, as `order-lines` does, and calls 
`toD` with
+  `http://localhost:8080/stock/${exchangeProperty.sku}`: `toD` because the URI 
is built per message.
+- The SKU, the quantity and the order id are kept in exchange properties, not 
headers: headers travel on the
+  HTTP request, and a header named `sku` would collide with the `{sku}` path 
parameter on the server side.
+  Properties stay in the route.
+- The body is set to null before the call: a GET has no body, and the order 
line would otherwise be sent. The
+  response replaces the body, which is why the line was saved in properties 
first.
+- `throwExceptionOnFailure=false` turns a 404 into a normal response with 
`CamelHttpResponseCode` set, so the
+  `choice` can log it instead of the error handler.
+
+## Build it step by step
+
+1. Start from `stock-api` and add a `file` route on `orders` that logs each 
order.
+2. Split the lines and call `http://localhost:8080/stock/CAMEL-MUG` with a 
fixed SKU; log the response body.
+3. Build the URI from the line with `toD` and a property; notice the body is 
gone after the call.
+4. Add `throwExceptionOnFailure=false` and the `choice` on the response code 
and the quantity.
+
+## Try changing
+
+- Remove `throwExceptionOnFailure=false` and add a SKU that does not exist to 
an order: the 404 becomes an
+  `HttpOperationFailedException` handled by the error handler.
+- Call a public API instead, `https://api.github.com/repos/apache/camel` for 
example, and log `${body[stargazers_count]}`.
+- Set `camel.server.port` to another port and update the URI to match.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/http-client.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/http-client.citrus.it.yaml
+```
+
+The test starts the routes and verifies the ok and back-order lines.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/connect/http-client/application.properties 
b/connect/http-client/application.properties
new file mode 100644
index 0000000..5d54c44
--- /dev/null
+++ b/connect/http-client/application.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+camel.server.port=8080
diff --git a/connect/http-client/http-client.camel.yaml 
b/connect/http-client/http-client.camel.yaml
new file mode 100644
index 0000000..79f40db
--- /dev/null
+++ b/connect/http-client/http-client.camel.yaml
@@ -0,0 +1,117 @@
+# Before an order goes to picking, every line is checked against the stock
+# service over HTTP. The stock service runs in this same example, so there
+# is nothing else to start.
+- rest:
+    path: /stock
+    get:
+      - path: "/{sku}"
+        to:
+          uri: direct:one-sku
+
+- route:
+    id: stock-service
+    from:
+      uri: direct
+      parameters:
+        name: 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: '{"error": "unknown sku ${header.sku}"}'
+            otherwise:
+              steps:
+                - setBody:
+                    expression:
+                      simple:
+                        expression: "${body[0]}"
+                - marshal:
+                    json:
+                      library: Jackson
+        - setHeader:
+            name: Content-Type
+            expression:
+              constant:
+                expression: application/json
+
+- route:
+    id: check-stock
+    from:
+      uri: file
+      parameters:
+        directoryName: orders
+        noop: true
+        sortBy: "file:name"
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        # exchange properties stay in the route; headers would travel on the 
HTTP request
+        - setProperty:
+            name: orderId
+            expression:
+              simple:
+                expression: "${body[orderId]}"
+        - split:
+            expression:
+              simple:
+                expression: "${body[lines]}"
+            steps:
+              - setProperty:
+                  name: sku
+                  expression:
+                    simple:
+                      expression: "${body[sku]}"
+              - setProperty:
+                  name: needed
+                  expression:
+                    simple:
+                      expression: "${body[qty]}"
+              # a GET has no body, and the response replaces whatever the body 
was
+              - setBody:
+                  expression:
+                    simple:
+                      expression: "${null}"
+              - toD:
+                  uri: 
"http://localhost:8080/stock/${exchangeProperty.sku}?throwExceptionOnFailure=false";
+              - unmarshal:
+                  json:
+                    library: Jackson
+              - choice:
+                  when:
+                    - expression:
+                        simple:
+                          expression: "${header.CamelHttpResponseCode} != 200"
+                      steps:
+                        - log:
+                            message: "${exchangeProperty.orderId}: 
${body[error]} (HTTP ${header.CamelHttpResponseCode})"
+                    - expression:
+                        simple:
+                          expression: "${body[qty]} >= 
${exchangeProperty.needed}"
+                      steps:
+                        - log:
+                            message: "${exchangeProperty.orderId}: 
${exchangeProperty.sku} x ${exchangeProperty.needed}, ${body[qty]} in stock, ok"
+                  otherwise:
+                    steps:
+                      - log:
+                          message: "${exchangeProperty.orderId}: 
${exchangeProperty.sku} x ${exchangeProperty.needed}, only ${body[qty]} in 
stock, back-order"
diff --git a/connect/http-client/metadata.json 
b/connect/http-client/metadata.json
new file mode 100644
index 0000000..4b1d7be
--- /dev/null
+++ b/connect/http-client/metadata.json
@@ -0,0 +1,37 @@
+{
+    "title": "HTTP client",
+    "description": "Every line of the three orders is checked against the 
stock service over HTTP, served by the same example; the log shows each line as 
ok, or back-order when the stock is short, with the stock level from the 
response.",
+    "level": "connect",
+    "teaches": {
+        "components": [
+            "file",
+            "http",
+            "rest",
+            "platform-http",
+            "direct",
+            "log"
+        ],
+        "eips": [
+            "split",
+            "setProperty",
+            "toD",
+            "choice",
+            "unmarshal",
+            "marshal"
+        ],
+        "languages": [
+            "simple",
+            "jsonpath"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "http",
+        "rest",
+        "client"
+    ],
+    "bundled": false
+}
diff --git a/connect/http-client/orders/order-1001.json 
b/connect/http-client/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/connect/http-client/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku": 
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git a/connect/http-client/orders/order-1002.json 
b/connect/http-client/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/connect/http-client/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git a/connect/http-client/orders/order-1003.json 
b/connect/http-client/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/connect/http-client/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status": 
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku": 
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price": 
9.50}]}
diff --git a/connect/http-client/stock.json b/connect/http-client/stock.json
new file mode 100644
index 0000000..efab5de
--- /dev/null
+++ b/connect/http-client/stock.json
@@ -0,0 +1,5 @@
+[
+  {"sku": "CAMEL-TSHIRT", "qty": 120},
+  {"sku": "CAMEL-MUG", "qty": 42},
+  {"sku": "CAMEL-CAP", "qty": 0}
+]
diff --git a/connect/http-client/test/http-client.citrus.it.yaml 
b/connect/http-client/test/http-client.citrus.it.yaml
new file mode 100644
index 0000000..1d40c02
--- /dev/null
+++ b/connect/http-client/test/http-client.citrus.it.yaml
@@ -0,0 +1,21 @@
+name: http-client-test
+description: The http-client example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "http-client"
+            file: "../http-client.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "http-client"
+          logMessage: "ORD-1001: CAMEL-TSHIRT x 2, 120 in stock, ok"
+  - camel:
+      jbang:
+        verify:
+          integration: "http-client"
+          logMessage: "ORD-1003: CAMEL-CAP x 1, only 0 in stock, back-order"
diff --git a/connect/stock-api/README.md b/connect/stock-api/README.md
new file mode 100644
index 0000000..eeb0e75
--- /dev/null
+++ b/connect/stock-api/README.md
@@ -0,0 +1,79 @@
+# Stock API
+
+The shop's stock service: what the warehouse has on the shelf, per SKU. A REST 
API with two operations, served by
+the HTTP server built into the Camel CLI, answering from a JSON file.
+
+## What you will see
+
+```text
+$ curl localhost:8080/stock/CAMEL-MUG
+{"sku":"CAMEL-MUG","qty":42}
+
+$ curl -i localhost:8080/stock/CAMEL-SOCKS
+HTTP/1.1 404 Not Found
+{"error": "unknown sku CAMEL-SOCKS"}
+
+$ curl localhost:8080/stock
+[
+  {"sku": "CAMEL-TSHIRT", "qty": 120},
+  {"sku": "CAMEL-MUG", "qty": 42},
+  {"sku": "CAMEL-CAP", "qty": 0}
+]
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+Then, in another terminal, call it with `curl` as above. Stop it with `ctrl` + 
`c`.
+
+## How it works
+
+- `rest` with `path: /stock` declares the API: a `get` with no path for the 
list, a `get` with `/{sku}` for one
+  item. Each operation hands over to a `direct` route. The CLI starts the HTTP 
server on port 8080 because the
+  file uses the REST DSL; `application.properties` sets the port.
+- The list route sets the body to `resource:file:stock.json`, the file next to 
the route, and the content type.
+- The item route loads the same file and picks the item with a `jsonpath` 
filter, `$[?(@.sku == '...')]`, in which
+  the `{sku}` path parameter arrives as the header `sku`. The result is a 
list: empty means 404 with a small error
+  body, otherwise the first element is marshalled back to JSON.
+- `CamelHttpResponseCode` is the header that sets the status code; 200 is the 
default.
+
+## Build it step by step
+
+1. A `rest` with one `get` that answers `resource:file:stock.json`; run it and 
`curl localhost:8080/stock`.
+2. Add the `/{sku}` operation and log `${header.sku}` to see the path 
parameter arrive.
+3. Filter the file with `jsonpath` and return the first element; `curl` a 
known SKU.
+4. Add the `choice` for the empty result and the 404.
+
+## Try changing
+
+- Add `POST /stock/{sku}` that logs the body: `curl -X POST -d '{"qty": 5}' 
localhost:8080/stock/CAMEL-CAP`.
+- Change the port in `application.properties` and see the CLI pick it up.
+- The `http-client` example calls this service from another route.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/stock-api.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/stock-api.citrus.it.yaml
+```
+
+The test starts the service and calls both operations over HTTP, checking the 
JSON and the 404.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/connect/stock-api/application.properties 
b/connect/stock-api/application.properties
new file mode 100644
index 0000000..5d54c44
--- /dev/null
+++ b/connect/stock-api/application.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+camel.server.port=8080
diff --git a/connect/stock-api/metadata.json b/connect/stock-api/metadata.json
new file mode 100644
index 0000000..1f9704a
--- /dev/null
+++ b/connect/stock-api/metadata.json
@@ -0,0 +1,33 @@
+{
+    "title": "Stock API",
+    "description": "The shop's stock service on port 8080: GET /stock returns 
the stock file, GET /stock/{sku} returns one SKU as JSON and a 404 with an 
error message for an unknown SKU.",
+    "level": "connect",
+    "teaches": {
+        "components": [
+            "rest",
+            "platform-http",
+            "direct"
+        ],
+        "eips": [
+            "setBody",
+            "setHeader",
+            "choice",
+            "marshal"
+        ],
+        "languages": [
+            "constant",
+            "jsonpath",
+            "simple"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "rest",
+        "http",
+        "platform-http"
+    ],
+    "bundled": false
+}
diff --git a/connect/stock-api/stock-api.camel.yaml 
b/connect/stock-api/stock-api.camel.yaml
new file mode 100644
index 0000000..b5464b8
--- /dev/null
+++ b/connect/stock-api/stock-api.camel.yaml
@@ -0,0 +1,73 @@
+# The shop's stock service: what the warehouse has on the shelf, per SKU.
+# GET /stock lists everything, GET /stock/{sku} answers for one SKU or 404.
+- rest:
+    path: /stock
+    get:
+      - to:
+          uri: direct:all-stock
+      - path: "/{sku}"
+        to:
+          uri: direct:one-sku
+
+- route:
+    id: all-stock
+    from:
+      uri: direct
+      parameters:
+        name: all-stock
+      steps:
+        - setBody:
+            expression:
+              constant:
+                expression: resource:file:stock.json
+        - setHeader:
+            name: Content-Type
+            expression:
+              constant:
+                expression: application/json
+
+- route:
+    id: one-sku
+    from:
+      uri: direct
+      parameters:
+        name: 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: '{"error": "unknown sku ${header.sku}"}'
+            otherwise:
+              steps:
+                - setBody:
+                    expression:
+                      simple:
+                        expression: "${body[0]}"
+                - marshal:
+                    json:
+                      library: Jackson
+        - setHeader:
+            name: Content-Type
+            expression:
+              constant:
+                expression: application/json
diff --git a/connect/stock-api/stock.json b/connect/stock-api/stock.json
new file mode 100644
index 0000000..efab5de
--- /dev/null
+++ b/connect/stock-api/stock.json
@@ -0,0 +1,5 @@
+[
+  {"sku": "CAMEL-TSHIRT", "qty": 120},
+  {"sku": "CAMEL-MUG", "qty": 42},
+  {"sku": "CAMEL-CAP", "qty": 0}
+]
diff --git a/connect/stock-api/test/stock-api.citrus.it.yaml 
b/connect/stock-api/test/stock-api.citrus.it.yaml
new file mode 100644
index 0000000..7d653d2
--- /dev/null
+++ b/connect/stock-api/test/stock-api.citrus.it.yaml
@@ -0,0 +1,49 @@
+name: stock-api-test
+description: The stock API answers one SKU as JSON and 404 for an unknown one
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "stock-api"
+            file: "../stock-api.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "stock-api"
+          logMessage: "(stock-api) started"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/stock/CAMEL-MUG"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+          contentType: "application/json"
+        validate:
+          - jsonPath:
+              - expression: "$.sku"
+                value: "CAMEL-MUG"
+              - expression: "$.qty"
+                value: "42"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/stock/CAMEL-SOCKS"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "404"
+        validate:
+          - jsonPath:
+              - expression: "$.error"
+                value: "unknown sku CAMEL-SOCKS"

Reply via email to