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 60ba718  CAMEL-24808: rung 7 of the ladder: contracts and security 
(#84)
60ba718 is described below

commit 60ba71816b31ddb256829b473d20d9f7d493487e
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 19:47:45 2026 +0200

    CAMEL-24808: rung 7 of the ladder: contracts and security (#84)
    
    Three examples under contracts/. openapi-server is rewritten contract first 
around the shop's stock API: stock-api.json defines listStock, getStock and 
reserveStock under the base path /api, the REST DSL serves them with request 
validation, each operation is a direct route, and the answers are the 200, 404, 
409 and 400 the contract promises. openapi-client is rewritten to reserve stock 
for every order line through rest-openapi by operationId against that server, 
handling the 409 for t [...]
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .github/workflows/build.yml                        |   4 +-
 camel-jbang-example-catalog.json                   | 236 +++++++++-------
 connect-service/kafka-orders/README.md             |   2 +
 .../kafka-orders/kafka-orders.camel.yaml           |   4 +
 .../keycloak-security-rest/README.md               |  68 ++---
 .../keycloak-security-rest/application.properties  |   2 +
 contracts/keycloak-security-rest/metadata.json     |  34 +++
 .../keycloak-security-rest/rest-api.camel.yaml     |   0
 contracts/openapi-client/README.md                 |  85 ++++++
 .../openapi-client}/application.properties         |  12 +-
 contracts/openapi-client/metadata.json             |  34 +++
 contracts/openapi-client/openapi-client.camel.yaml |  64 +++++
 contracts/openapi-client/orders/order-1001.json    |   2 +
 contracts/openapi-client/orders/order-1002.json    |   2 +
 contracts/openapi-client/orders/order-1003.json    |   2 +
 contracts/openapi-client/stock-api.json            | 201 ++++++++++++++
 .../test/openapi-client.citrus.it.yaml             |  39 +++
 contracts/openapi-server/README.md                 |  90 +++++++
 .../openapi-server}/application.properties         |  12 +-
 contracts/openapi-server/metadata.json             |  37 +++
 contracts/openapi-server/openapi-server.camel.yaml | 162 +++++++++++
 contracts/openapi-server/stock-api.json            | 201 ++++++++++++++
 contracts/openapi-server/stock.json                |   5 +
 .../test/openapi-server.citrus.it.yaml             |  76 ++++++
 rest/openapi-client/README.md                      |  56 ----
 rest/openapi-client/application.properties         |  18 --
 rest/openapi-client/examples/1001.json             |  18 --
 rest/openapi-client/metadata.json                  |  26 --
 rest/openapi-client/petstore-api.json              | 299 ---------------------
 rest/openapi-client/petstore-client.camel.yaml     |  32 ---
 .../test/citrus-application.properties             |   5 -
 rest/openapi-client/test/examples/1001.json        |  18 --
 rest/openapi-client/test/petstore-api.json         | 299 ---------------------
 .../test/petstore-client.citrus.it.yaml            |  30 ---
 rest/openapi-client/test/petstoreServer.groovy     |   9 -
 rest/openapi-server/README.md                      |  80 ------
 rest/openapi-server/application.properties         |  19 --
 rest/openapi-server/examples/pet/1000.json         |  18 --
 rest/openapi-server/metadata.json                  |  22 --
 rest/openapi-server/petstore-api.json              | 299 ---------------------
 rest/openapi-server/petstore.camel.yaml            |  16 --
 .../test/citrus-application.properties             |   2 -
 rest/openapi-server/test/examples/pet/1000.json    |  18 --
 rest/openapi-server/test/petstore-api.json         | 299 ---------------------
 rest/openapi-server/test/petstore.citrus.it.yaml   |  39 ---
 security/keycloak-security-rest/metadata.json      |  27 --
 46 files changed, 1196 insertions(+), 1827 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 321f06e..edf83d2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -77,9 +77,9 @@ jobs:
           - name: File processing
             path: connect/file-processing
           - name: OpenAPI server
-            path: rest/openapi-server
+            path: contracts/openapi-server
           - name: OpenAPI client
-            path: rest/openapi-client
+            path: contracts/openapi-client
           - name: AWS SQS
             path: cloud/aws-sqs
           - name: EDI X12 AS2
diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json
index c0f7bd8..0295486 100644
--- a/camel-jbang-example-catalog.json
+++ b/camel-jbang-example-catalog.json
@@ -697,6 +697,139 @@
             "stock.json"
         ]
     },
+    {
+        "name": "contracts/keycloak-security-rest",
+        "title": "Keycloak security",
+        "description": "Two HTTP endpoints on port 8081: the public one 
answers everyone, the protected one requires a bearer token from a Keycloak 
started with camel infra whose user has the admin role, otherwise 403; the 
realm, client and users are created in the Keycloak console as the README 
describes.",
+        "level": "contracts",
+        "teaches": {
+            "components": [
+                "platform-http",
+                "keycloak",
+                "log"
+            ],
+            "eips": [
+                "policy",
+                "onException",
+                "setHeader",
+                "setBody"
+            ],
+            "languages": [
+                "simple"
+            ]
+        },
+        "tags": [
+            "security",
+            "keycloak",
+            "oauth",
+            "jwt",
+            "rest"
+        ],
+        "bundled": false,
+        "requiresDocker": true,
+        "hasCitrusTests": false,
+        "files": [
+            "README.md",
+            "application.properties",
+            "rest-api.camel.yaml"
+        ],
+        "infraServices": [
+            "keycloak"
+        ]
+    },
+    {
+        "name": "contracts/openapi-client",
+        "title": "OpenAPI client",
+        "description": "The picking desk reserves stock for every order line 
by calling the stock API by contract: rest-openapi turns the operationId 
reserveStock into the HTTP call from stock-api.json; the log shows each 
reservation and one 409 for the cap that is out of stock. Needs the 
openapi-server example running.",
+        "level": "contracts",
+        "teaches": {
+            "components": [
+                "rest-openapi",
+                "http",
+                "file",
+                "log"
+            ],
+            "eips": [
+                "split",
+                "onException",
+                "setProperty",
+                "setHeader",
+                "unmarshal"
+            ],
+            "languages": [
+                "simple"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "openapi",
+            "rest",
+            "client",
+            "contract"
+        ],
+        "bundled": false,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "openapi-client.camel.yaml",
+            "orders/order-1001.json",
+            "orders/order-1002.json",
+            "orders/order-1003.json",
+            "stock-api.json"
+        ]
+    },
+    {
+        "name": "contracts/openapi-server",
+        "title": "OpenAPI server",
+        "description": "The stock API contract first: stock-api.json is the 
OpenAPI contract, the REST DSL serves its three operations on port 8080 with 
request validation, GET /stock/{sku} answers from a file or 404, POST 
/stock/{sku}/reserve answers 200, 409 when the stock is short or 400 for a bad 
reservation, and /openapi serves the contract.",
+        "level": "contracts",
+        "teaches": {
+            "components": [
+                "rest",
+                "rest-openapi",
+                "platform-http",
+                "direct",
+                "log"
+            ],
+            "eips": [
+                "choice",
+                "validate",
+                "onException",
+                "marshal",
+                "unmarshal",
+                "setProperty"
+            ],
+            "languages": [
+                "simple",
+                "jsonpath",
+                "constant"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "openapi",
+            "rest",
+            "contract"
+        ],
+        "bundled": false,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "openapi-server.camel.yaml",
+            "stock-api.json",
+            "stock.json"
+        ]
+    },
     {
         "name": "eip/splitter",
         "title": "Splitter",
@@ -869,74 +1002,6 @@
             "orders.camel.yaml"
         ]
     },
-    {
-        "name": "rest/openapi-client",
-        "title": "OpenAPI Client",
-        "description": "Each JSON file in the examples directory is sent as an 
addPet request to the petstore service described by petstore-api.json, running 
on localhost:8080 (see openapi-server); the request and response are logged.",
-        "level": "contracts",
-        "teaches": {
-            "components": [
-                "file",
-                "kamelet",
-                "log"
-            ],
-            "eips": [
-                "setHeader",
-                "convertBodyTo"
-            ],
-            "languages": [
-                "simple",
-                "constant"
-            ]
-        },
-        "tags": [
-            "rest",
-            "openapi",
-            "client"
-        ],
-        "bundled": false,
-        "requiresDocker": false,
-        "hasCitrusTests": true,
-        "files": [
-            "README.md",
-            "application.properties",
-            "examples/1001.json",
-            "petstore-api.json",
-            "petstore-client.camel.yaml"
-        ]
-    },
-    {
-        "name": "rest/openapi-server",
-        "title": "OpenAPI Server",
-        "description": "A REST service generated from petstore-api.json 
answers under the base path /petstore on port 8080; GET /petstore/pet/1000 
returns the mocked pet from the examples directory and /openapi serves the 
specification.",
-        "level": "contracts",
-        "teaches": {
-            "components": [
-                "platform-http",
-                "direct",
-                "log"
-            ],
-            "eips": [
-                "rest",
-                "restConfiguration"
-            ]
-        },
-        "tags": [
-            "rest",
-            "openapi",
-            "server"
-        ],
-        "bundled": false,
-        "requiresDocker": false,
-        "hasCitrusTests": true,
-        "files": [
-            "README.md",
-            "application.properties",
-            "examples/pet/1000.json",
-            "petstore-api.json",
-            "petstore.camel.yaml"
-        ]
-    },
     {
         "name": "route/aggregator",
         "title": "Aggregator",
@@ -1200,41 +1265,6 @@
             "properties-and-profiles.camel.yaml"
         ]
     },
-    {
-        "name": "security/keycloak-security-rest",
-        "title": "Keycloak Security REST API",
-        "description": "A REST API on platform-http is protected by a running 
Keycloak: a request without a valid bearer token is refused with 401, a token 
with the right role reaches the route and is logged.",
-        "level": "contracts",
-        "teaches": {
-            "components": [
-                "platform-http",
-                "log"
-            ],
-            "eips": [
-                "policy",
-                "onException",
-                "setHeader",
-                "setBody"
-            ]
-        },
-        "tags": [
-            "security",
-            "keycloak",
-            "rest",
-            "oauth"
-        ],
-        "bundled": false,
-        "requiresDocker": true,
-        "hasCitrusTests": false,
-        "files": [
-            "README.md",
-            "application.properties",
-            "rest-api.camel.yaml"
-        ],
-        "infraServices": [
-            "keycloak"
-        ]
-    },
     {
         "name": "transform/csv-to-json",
         "title": "CSV to JSON",
diff --git a/connect-service/kafka-orders/README.md 
b/connect-service/kafka-orders/README.md
index ab954ab..63d1a68 100644
--- a/connect-service/kafka-orders/README.md
+++ b/connect-service/kafka-orders/README.md
@@ -57,6 +57,8 @@ Stop the example with `ctrl` + `c` and the service with 
`camel infra stop kafka`
   same partition and stay in order.
 - `dispatch` consumes `orders` and `multicast`s to two topics; `fulfilment` 
and `notification` consume those.
   `CamelKafkaPartition` and `CamelKafkaOffset` are set by the consumer on 
every message.
+- `autoOffsetReset: earliest` makes a consumer group that joins for the first 
time start at the beginning of the
+  topic; with the default, `latest`, an order published a moment before the 
consumer joined would never be seen.
 - Topics are created on first use by this broker; a production cluster usually 
has that switched off.
 
 ## Build it step by step
diff --git a/connect-service/kafka-orders/kafka-orders.camel.yaml 
b/connect-service/kafka-orders/kafka-orders.camel.yaml
index 92feebf..db228b2 100644
--- a/connect-service/kafka-orders/kafka-orders.camel.yaml
+++ b/connect-service/kafka-orders/kafka-orders.camel.yaml
@@ -63,6 +63,8 @@
       uri: kafka
       parameters:
         topic: orders
+        # start from the beginning the first time this group joins, so nothing 
published before is missed
+        autoOffsetReset: earliest
       steps:
         - log:
             message: "Dispatching ${header.orderId} from partition 
${header.CamelKafkaPartition} offset ${header.CamelKafkaOffset}"
@@ -83,6 +85,7 @@
       uri: kafka
       parameters:
         topic: fulfilment
+        autoOffsetReset: earliest
       steps:
         - unmarshal:
             json:
@@ -96,6 +99,7 @@
       uri: kafka
       parameters:
         topic: notifications
+        autoOffsetReset: earliest
       steps:
         - unmarshal:
             json:
diff --git a/security/keycloak-security-rest/README.md 
b/contracts/keycloak-security-rest/README.md
similarity index 79%
rename from security/keycloak-security-rest/README.md
rename to contracts/keycloak-security-rest/README.md
index 79af561..739ed4b 100644
--- a/security/keycloak-security-rest/README.md
+++ b/contracts/keycloak-security-rest/README.md
@@ -43,42 +43,14 @@ Which allows to run Camel CLI with `camel` as shown below.
 
 ## Running Keycloak
 
-### Option 1: Using Camel CLI Infra (Recommended)
-
-Starting from Camel CLI 4.16.0, you can easily run Keycloak using the built-in 
infrastructure support:
-
-```sh
-$ jbang -Dcamel.jbang.version=4.16.0 camel@apache/camel infra run keycloak
-```
-
-This will automatically start Keycloak configured with:
-* Admin username: `admin`
-* Admin password: `admin`
-* Port: `8080`
-
-Wait a few seconds for Keycloak to fully start before proceeding to 
configuration.
-
-To stop Keycloak later:
-
-```sh
-$ jbang -Dcamel.jbang.version=4.16.0 camel@apache/camel infra stop keycloak
-```
-
-### Option 2: Using Docker Manually
-
-Alternatively, you can run Keycloak manually with Docker:
+The Camel CLI starts Keycloak for you in a container (Docker or Podman must be 
running):
 
 ```sh
-$ docker run -d \
-  --name keycloak \
-  -p 8180:8080 \
-  -e KEYCLOAK_ADMIN=admin \
-  -e KEYCLOAK_ADMIN_PASSWORD=admin \
-  quay.io/keycloak/keycloak:latest \
-  start-dev
+$ camel infra run keycloak
 ```
 
-Wait a few seconds for Keycloak to fully start before proceeding to 
configuration.
+It prints the admin user (`admin`, password `admin`) and the URL, 
http://localhost:8080. Wait for the
+container to finish starting before the configuration below. Stop it later 
with `camel infra stop keycloak`.
 
 ## Keycloak Configuration
 
@@ -87,8 +59,7 @@ After Keycloak starts, you need to configure it:
 ### 1. Access Keycloak Admin Console
 
 Open your browser and navigate to:
-* If using Camel CLI infra: http://localhost:8080
-* If using Docker manually: http://localhost:8180
+* http://localhost:8080
 
 Login with:
 * Username: `admin`
@@ -168,20 +139,20 @@ keycloak.client.secret=<your-client-secret>
 After Keycloak is configured, start the Camel application:
 
 ```sh
-$ jbang -Dcamel.jbang.version=4.15.0-SNAPSHOT camel@apache/camel run *
+$ camel run *
 ```
 
-The application will start on port 8080 with the following endpoints:
+The application will start on port 8081 (Keycloak has 8080) with the following 
endpoints:
 
-* `http://localhost:8080/api/public` - Public endpoint (no auth required)
-* `http://localhost:8080/api/protected` - Protected endpoint (admin role 
required)
+* `http://localhost:8081/api/public` - Public endpoint (no auth required)
+* `http://localhost:8081/api/protected` - Protected endpoint (admin role 
required)
 
 ## Testing the Endpoints
 
 ### Test Public Endpoint (No Authentication)
 
 ```sh
-$ curl http://localhost:8080/api/public
+$ curl http://localhost:8081/api/public
 ```
 
 Expected response:
@@ -197,10 +168,8 @@ Expected response:
 First, try to access the protected endpoint with a regular user who doesn't 
have the admin role:
 
 ```sh
-# Set KEYCLOAK_PORT based on your setup (8080 for camel infra, 8180 for manual 
Docker)
-$ export KEYCLOAK_PORT=8080
 
-$ export ACCESS_TOKEN=$(curl -X POST 
http://localhost:${KEYCLOAK_PORT}/realms/camel/protocol/openid-connect/token \
+$ export ACCESS_TOKEN=$(curl -X POST 
http://localhost:8080/realms/camel/protocol/openid-connect/token \
   -H "Content-Type: application/x-www-form-urlencoded" \
   -d "username=testuser" \
   -d "password=password" \
@@ -210,7 +179,7 @@ $ export ACCESS_TOKEN=$(curl -X POST 
http://localhost:${KEYCLOAK_PORT}/realms/ca
   | jq -r '.access_token')
 
 $ curl -H "Authorization: Bearer $ACCESS_TOKEN" \
-  http://localhost:8080/api/protected
+  http://localhost:8081/api/protected
 ```
 
 This will return a **403 Forbidden** error because `testuser` does not have 
the `admin` role.
@@ -220,10 +189,8 @@ This will return a **403 Forbidden** error because 
`testuser` does not have the
 Now, obtain a token for the admin user and access the protected endpoint:
 
 ```sh
-# Set KEYCLOAK_PORT based on your setup (8080 for camel infra, 8180 for manual 
Docker)
-$ export KEYCLOAK_PORT=8080
 
-$ export ADMIN_TOKEN=$(curl -X POST 
http://localhost:${KEYCLOAK_PORT}/realms/camel/protocol/openid-connect/token \
+$ export ADMIN_TOKEN=$(curl -X POST 
http://localhost:8080/realms/camel/protocol/openid-connect/token \
   -H "Content-Type: application/x-www-form-urlencoded" \
   -d "username=admin-user" \
   -d "password=password" \
@@ -233,7 +200,7 @@ $ export ADMIN_TOKEN=$(curl -X POST 
http://localhost:${KEYCLOAK_PORT}/realms/cam
   | jq -r '.access_token')
 
 $ curl -H "Authorization: Bearer $ADMIN_TOKEN" \
-  http://localhost:8080/api/protected
+  http://localhost:8081/api/protected
 ```
 
 Expected response:
@@ -270,7 +237,6 @@ The policy is defined as a bean in the 
`rest-api.camel.yaml` file and requires t
 The bean references configuration properties from `application.properties`:
 
 ```properties
-# Use port 8080 if running via camel infra, or 8180 if using Docker manually
 keycloak.server.url=http://localhost:8080
 keycloak.realm=camel
 keycloak.client.id=camel-client
@@ -316,7 +282,7 @@ You can enable the developer console via `--console` flag:
 $ camel run * --console
 ```
 
-Then you can browse: http://localhost:8080/q/dev to introspect the running 
Camel application.
+Then you can browse: http://localhost:8081/q/dev to introspect the running 
Camel application.
 
 ## Stopping
 
@@ -326,7 +292,7 @@ To stop Keycloak:
 
 If you used Camel CLI infra:
 ```sh
-$ jbang -Dcamel.jbang.version=4.16.0 camel@apache/camel infra stop keycloak
+$ camel infra stop keycloak
 ```
 
 If you used Docker manually:
@@ -350,7 +316,7 @@ $ docker rm keycloak
 
 ### Connection Refused
 
-* Ensure Keycloak is running on the correct port (8080 for camel infra, 8180 
for Docker)
+* Ensure Keycloak is running on port 8080 (`camel infra ps`) and the API on 
8081
 * Verify the Keycloak server URL in `application.properties` matches your setup
 
 ### Invalid Client Credentials
diff --git a/security/keycloak-security-rest/application.properties 
b/contracts/keycloak-security-rest/application.properties
similarity index 93%
copy from security/keycloak-security-rest/application.properties
copy to contracts/keycloak-security-rest/application.properties
index 13d8d5d..efb5651 100644
--- a/security/keycloak-security-rest/application.properties
+++ b/contracts/keycloak-security-rest/application.properties
@@ -26,3 +26,5 @@ keycloak.client.secret=***********
 
 # Additional Camel configuration
 camel.main.name = KeycloakSecurityRestExample
+# Keycloak from camel infra takes port 8080, so the API listens on 8081
+camel.server.port=8081
diff --git a/contracts/keycloak-security-rest/metadata.json 
b/contracts/keycloak-security-rest/metadata.json
new file mode 100644
index 0000000..bfbd4c5
--- /dev/null
+++ b/contracts/keycloak-security-rest/metadata.json
@@ -0,0 +1,34 @@
+{
+    "title": "Keycloak security",
+    "description": "Two HTTP endpoints on port 8081: the public one answers 
everyone, the protected one requires a bearer token from a Keycloak started 
with camel infra whose user has the admin role, otherwise 403; the realm, 
client and users are created in the Keycloak console as the README describes.",
+    "level": "contracts",
+    "teaches": {
+        "components": [
+            "platform-http",
+            "keycloak",
+            "log"
+        ],
+        "eips": [
+            "policy",
+            "onException",
+            "setHeader",
+            "setBody"
+        ],
+        "languages": [
+            "simple"
+        ]
+    },
+    "tags": [
+        "security",
+        "keycloak",
+        "oauth",
+        "jwt",
+        "rest"
+    ],
+    "infraServices": [
+        "keycloak"
+    ],
+    "requiresDocker": true,
+    "bundled": false,
+    "hasCitrusTests": false
+}
diff --git a/security/keycloak-security-rest/rest-api.camel.yaml 
b/contracts/keycloak-security-rest/rest-api.camel.yaml
similarity index 100%
rename from security/keycloak-security-rest/rest-api.camel.yaml
rename to contracts/keycloak-security-rest/rest-api.camel.yaml
diff --git a/contracts/openapi-client/README.md 
b/contracts/openapi-client/README.md
new file mode 100644
index 0000000..d48c646
--- /dev/null
+++ b/contracts/openapi-client/README.md
@@ -0,0 +1,85 @@
+# OpenAPI client
+
+The picking desk reserves the stock for every order line by calling the stock 
API by contract: `stock-api.json`
+says which operations exist and how to call them, and the `rest-openapi` 
component turns an `operationId` into
+the HTTP request. The server is the `openapi-server` example, running in 
another terminal.
+
+## What you will see
+
+```text
+INFO ... openapi-client.camel.yaml:63 : ORD-1001: reserved 2 x CAMEL-TSHIRT, 
120 left on the shelf
+INFO ... openapi-client.camel.yaml:63 : ORD-1001: reserved 1 x CAMEL-MUG, 42 
left on the shelf
+INFO ... openapi-client.camel.yaml:63 : ORD-1002: reserved 3 x CAMEL-MUG, 42 
left on the shelf
+INFO ... openapi-client.camel.yaml:63 : ORD-1003: reserved 1 x CAMEL-TSHIRT, 
120 left on the shelf
+WARN ... openapi-client.camel.yaml:13 : ORD-1003: CAMEL-CAP not reserved, the 
stock API answered 409: {"error": "only 0 CAMEL-CAP in stock, 1 wanted for 
ORD-1003"}
+INFO ... openapi-client.camel.yaml:63 : ORD-1003: reserved 2 x CAMEL-MUG, 42 
left on the shelf
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+Start the server first, in the `openapi-server` directory:
+
+```shell
+camel run *
+```
+
+Then, in this directory:
+
+```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. `stock.api.url` in `application.properties` says where the 
server is.
+
+## How it works
+
+- `rest-openapi` with `specificationUri: stock-api.json` and `operationId: 
reserveStock` is the call: the
+  component reads the verb, the path and the parameters from the contract, so 
the route never spells out
+  `POST /api/stock/{sku}/reserve`. `host` is the server; `componentName: http` 
picks the HTTP client to use.
+- The path parameter `sku` is taken from the header of the same name; the 
request body is the message body,
+  built here as the JSON the contract's `Reservation` schema describes.
+- The order id and SKU are also kept in exchange properties for the log, since 
the response replaces the body
+  and headers are sent on the wire, as the `http-client` example explains.
+- A 409 or 400 from the server is an `HttpOperationFailedException`; the 
`onException` logs its status code
+  and response body and continues with the next line.
+
+## Build it step by step
+
+1. A timer route that calls `rest-openapi` with `operationId: getStock` and a 
fixed `sku` header; log the body.
+2. Read the orders, split the lines, and call `reserveStock` with the 
reservation as the body.
+3. Keep the order id and SKU in properties and log the result.
+4. Add the `onException` and see the cap's 409 handled instead of failing the 
order.
+
+## Try changing
+
+- Set `requestValidationEnabled: true` on the endpoint and send a reservation 
without `qty`: the client
+  refuses it before any HTTP call.
+- Point `stock.api.url` at a server that is not running and watch the 
connection error reach the error handler.
+- Replace the contract's server with a public one, 
`https://petstore3.swagger.io/api/v3/openapi.json`, and
+  call `getPetById`.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/openapi-client.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/openapi-client.citrus.it.yaml
+```
+
+The test starts the `openapi-server` example and this client, and verifies the 
reservations and the 409.
+
+## 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/security/keycloak-security-rest/application.properties 
b/contracts/openapi-client/application.properties
similarity index 61%
copy from security/keycloak-security-rest/application.properties
copy to contracts/openapi-client/application.properties
index 13d8d5d..cfc23ac 100644
--- a/security/keycloak-security-rest/application.properties
+++ b/contracts/openapi-client/application.properties
@@ -15,14 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Keycloak Server Configuration
-# These properties are referenced by the security policy beans defined in 
rest-api.camel.yaml
-# Port 8080 is used when running Keycloak via: camel infra run keycloak
-# Port 8180 is used when running Keycloak manually via Docker (see README.adoc)
-keycloak.server.url=http://localhost:8080
-keycloak.realm=camel
-keycloak.client.id=camel-client
-keycloak.client.secret=***********
-
-# Additional Camel configuration
-camel.main.name = KeycloakSecurityRestExample
+stock.api.url=http://localhost:8080
diff --git a/contracts/openapi-client/metadata.json 
b/contracts/openapi-client/metadata.json
new file mode 100644
index 0000000..0c487c7
--- /dev/null
+++ b/contracts/openapi-client/metadata.json
@@ -0,0 +1,34 @@
+{
+    "title": "OpenAPI client",
+    "description": "The picking desk reserves stock for every order line by 
calling the stock API by contract: rest-openapi turns the operationId 
reserveStock into the HTTP call from stock-api.json; the log shows each 
reservation and one 409 for the cap that is out of stock. Needs the 
openapi-server example running.",
+    "level": "contracts",
+    "teaches": {
+        "components": [
+            "rest-openapi",
+            "http",
+            "file",
+            "log"
+        ],
+        "eips": [
+            "split",
+            "onException",
+            "setProperty",
+            "setHeader",
+            "unmarshal"
+        ],
+        "languages": [
+            "simple"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "openapi",
+        "rest",
+        "client",
+        "contract"
+    ],
+    "bundled": false
+}
diff --git a/contracts/openapi-client/openapi-client.camel.yaml 
b/contracts/openapi-client/openapi-client.camel.yaml
new file mode 100644
index 0000000..f45644f
--- /dev/null
+++ b/contracts/openapi-client/openapi-client.camel.yaml
@@ -0,0 +1,64 @@
+# The picking desk reserves the stock for every order line by calling the
+# stock API by contract: stock-api.json says which operations exist and how
+# to call them, and rest-openapi turns an operationId into the HTTP call.
+# The server is the openapi-server example, running in another terminal.
+- onException:
+    exception:
+      - org.apache.camel.http.base.HttpOperationFailedException
+    handled:
+      constant:
+        expression: "true"
+    steps:
+      - log:
+          loggingLevel: WARN
+          message: "${exchangeProperty.orderId}: ${exchangeProperty.sku} not 
reserved, the stock API answered ${exception.statusCode}: 
${exception.responseBody}"
+
+- route:
+    id: reserve-order-lines
+    from:
+      uri: file
+      parameters:
+        directoryName: orders
+        noop: true
+        sortBy: "file:name"
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        - setProperty:
+            name: orderId
+            expression:
+              simple:
+                expression: "${body[orderId]}"
+        - split:
+            expression:
+              simple:
+                expression: "${body[lines]}"
+            steps:
+              - setProperty:
+                  name: sku
+                  expression:
+                    simple:
+                      expression: "${body[sku]}"
+              # the path parameter comes from the header of the same name
+              - setHeader:
+                  name: sku
+                  expression:
+                    simple:
+                      expression: "${body[sku]}"
+              - setBody:
+                  expression:
+                    simple:
+                      expression: '{"orderId": "${exchangeProperty.orderId}", 
"qty": ${body[qty]}}'
+              - to:
+                  uri: rest-openapi
+                  parameters:
+                    specificationUri: stock-api.json
+                    operationId: reserveStock
+                    host: "{{stock.api.url}}"
+                    componentName: http
+              - unmarshal:
+                  json:
+                    library: Jackson
+              - log:
+                  message: "${exchangeProperty.orderId}: reserved 
${body[reserved]} x ${body[sku]}, ${body[remaining]} left on the shelf"
diff --git a/contracts/openapi-client/orders/order-1001.json 
b/contracts/openapi-client/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/contracts/openapi-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/contracts/openapi-client/orders/order-1002.json 
b/contracts/openapi-client/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/contracts/openapi-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/contracts/openapi-client/orders/order-1003.json 
b/contracts/openapi-client/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/contracts/openapi-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/contracts/openapi-client/stock-api.json 
b/contracts/openapi-client/stock-api.json
new file mode 100644
index 0000000..279b681
--- /dev/null
+++ b/contracts/openapi-client/stock-api.json
@@ -0,0 +1,201 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "Stock API",
+    "description": "What the warehouse has on the shelf, per SKU, and 
reservations against it.",
+    "version": "1.0.0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost:8080/api";
+    }
+  ],
+  "paths": {
+    "/stock": {
+      "get": {
+        "operationId": "listStock",
+        "summary": "All stock levels",
+        "responses": {
+          "200": {
+            "description": "The stock levels",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/StockItem"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/stock/{sku}": {
+      "get": {
+        "operationId": "getStock",
+        "summary": "Stock level of one SKU",
+        "parameters": [
+          {
+            "name": "sku",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "The stock level",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/StockItem"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Unknown SKU",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/stock/{sku}/reserve": {
+      "post": {
+        "operationId": "reserveStock",
+        "summary": "Reserve a quantity of one SKU for an order",
+        "parameters": [
+          {
+            "name": "sku",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Reservation"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Reserved",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ReservationResult"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "The request does not match the contract"
+          },
+          "404": {
+            "description": "Unknown SKU",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          },
+          "409": {
+            "description": "Not enough stock",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "StockItem": {
+        "type": "object",
+        "required": [
+          "sku",
+          "qty"
+        ],
+        "properties": {
+          "sku": {
+            "type": "string"
+          },
+          "qty": {
+            "type": "integer",
+            "minimum": 0
+          }
+        }
+      },
+      "Reservation": {
+        "type": "object",
+        "required": [
+          "orderId",
+          "qty"
+        ],
+        "properties": {
+          "orderId": {
+            "type": "string"
+          },
+          "qty": {
+            "type": "integer",
+            "minimum": 1
+          }
+        }
+      },
+      "ReservationResult": {
+        "type": "object",
+        "required": [
+          "sku",
+          "reserved",
+          "remaining"
+        ],
+        "properties": {
+          "sku": {
+            "type": "string"
+          },
+          "reserved": {
+            "type": "integer"
+          },
+          "remaining": {
+            "type": "integer"
+          }
+        }
+      },
+      "Error": {
+        "type": "object",
+        "required": [
+          "error"
+        ],
+        "properties": {
+          "error": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/contracts/openapi-client/test/openapi-client.citrus.it.yaml 
b/contracts/openapi-client/test/openapi-client.citrus.it.yaml
new file mode 100644
index 0000000..69fdb64
--- /dev/null
+++ b/contracts/openapi-client/test/openapi-client.citrus.it.yaml
@@ -0,0 +1,39 @@
+name: openapi-client-test
+description: The client reserves stock by contract against the openapi-server 
example
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "openapi-server"
+            file: "../../openapi-server/openapi-server.camel.yaml"
+            systemProperties:
+              file: "../../openapi-server/application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-server"
+          logMessage: "(openapi-server) started"
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "openapi-client"
+            file: "../openapi-client.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock-api.json"
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-client"
+          logMessage: "ORD-1002: reserved 3 x CAMEL-MUG, 42 left on the shelf"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-client"
+          logMessage: "ORD-1003: CAMEL-CAP not reserved, the stock API 
answered 409"
diff --git a/contracts/openapi-server/README.md 
b/contracts/openapi-server/README.md
new file mode 100644
index 0000000..3e19b99
--- /dev/null
+++ b/contracts/openapi-server/README.md
@@ -0,0 +1,90 @@
+# OpenAPI server
+
+The stock API, contract first. `stock-api.json` is the OpenAPI contract: three 
operations, their parameters,
+request and response schemas and status codes. The REST DSL serves every 
operation in it, validates requests
+against it and hands each operation to a `direct` route named after its 
`operationId`.
+
+## What you will see
+
+```text
+$ curl localhost:8080/api/stock/CAMEL-MUG
+{"sku":"CAMEL-MUG","qty":42}
+
+$ curl -X POST -H 'Content-Type: application/json' -d '{"orderId": "ORD-1001", 
"qty": 2}' localhost:8080/api/stock/CAMEL-MUG/reserve
+{"sku": "CAMEL-MUG", "reserved": 2, "remaining": 42}
+
+$ curl -i -X POST -H 'Content-Type: application/json' -d '{"orderId": 
"ORD-1003", "qty": 1}' localhost:8080/api/stock/CAMEL-CAP/reserve
+HTTP/1.1 409 Conflict
+{"error": "only 0 CAMEL-CAP in stock, 1 wanted for ORD-1003"}
+
+$ curl -i -X POST -H 'Content-Type: application/json' 
localhost:8080/api/stock/CAMEL-MUG/reserve
+HTTP/1.1 400 Bad Request
+
+INFO ... openapi-server.camel.yaml:123 : Reserved 2 x CAMEL-MUG for ORD-1001
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+The contract's `servers` entry gives the base path, `/api`, so the operations 
are under http://localhost:8080/api/stock
+and the contract itself is served at http://localhost:8080/openapi. Call the
+operations with `curl` as above from another terminal, or run the 
`openapi-client` example against it.
+Stop it with `ctrl` + `c`.
+
+## How it works
+
+- `rest` with `openApi: specification: stock-api.json` is the whole API 
definition: paths, verbs and parameters
+  come from the contract, not from the route file, and the base path `/api` 
from its `servers` entry, which
+  must have a path. `restConfiguration` switches on `clientRequestValidation`,
+  which rejects a request without the body or content type the contract 
requires with 400, and
+  `apiContextPath: openapi` serves the contract.
+- Each operation is a `direct` route named after its `operationId`: 
`listStock`, `getStock`, `reserveStock`.
+  Path parameters arrive as headers, `sku` here; the request body is the 
message body.
+- `lookup` is a helper route the two SKU operations share: it filters the 
stock file with `jsonpath` and
+  leaves the item, or null, as the body.
+- The contract check does not look at the values, so `reserveStock` runs 
`validate` on them and an
+  `onException` turns the failed predicate into a 400 with an error body, the 
status the contract promises.
+- `CamelHttpResponseCode` sets 404 and 409; the bodies are the JSON the 
contract's `Error` schema describes.
+
+## Build it step by step
+
+1. Write `stock-api.json` with only `listStock`, add the `rest` block and a 
`listStock` route that returns
+   `resource:file:stock.json`; run it and open http://localhost:8080/openapi.
+2. Add `getStock` to the contract and the route, with the `lookup` helper and 
the 404.
+3. Add `reserveStock` with its request schema; POST without a body and see the 
400 from the contract check.
+4. Add `validate` and the `onException` for the values, then the 409 for short 
stock.
+
+## Try changing
+
+- Add a fourth operation to the contract and start the example: the missing 
route is reported at startup.
+- Set `missingOperation: mock` in `restConfiguration` and put an example 
response under `examples/`: the
+  operation is served from the example without a route.
+- Change a response schema in the contract and see what the client example 
makes of it.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/openapi-server.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/openapi-server.citrus.it.yaml
+```
+
+The test starts the API and calls the three operations over HTTP, checking the 
200, 409 and 400 answers.
+
+## 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/security/keycloak-security-rest/application.properties 
b/contracts/openapi-server/application.properties
similarity index 61%
rename from security/keycloak-security-rest/application.properties
rename to contracts/openapi-server/application.properties
index 13d8d5d..5d54c44 100644
--- a/security/keycloak-security-rest/application.properties
+++ b/contracts/openapi-server/application.properties
@@ -15,14 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Keycloak Server Configuration
-# These properties are referenced by the security policy beans defined in 
rest-api.camel.yaml
-# Port 8080 is used when running Keycloak via: camel infra run keycloak
-# Port 8180 is used when running Keycloak manually via Docker (see README.adoc)
-keycloak.server.url=http://localhost:8080
-keycloak.realm=camel
-keycloak.client.id=camel-client
-keycloak.client.secret=***********
-
-# Additional Camel configuration
-camel.main.name = KeycloakSecurityRestExample
+camel.server.port=8080
diff --git a/contracts/openapi-server/metadata.json 
b/contracts/openapi-server/metadata.json
new file mode 100644
index 0000000..4dd05a1
--- /dev/null
+++ b/contracts/openapi-server/metadata.json
@@ -0,0 +1,37 @@
+{
+    "title": "OpenAPI server",
+    "description": "The stock API contract first: stock-api.json is the 
OpenAPI contract, the REST DSL serves its three operations on port 8080 with 
request validation, GET /stock/{sku} answers from a file or 404, POST 
/stock/{sku}/reserve answers 200, 409 when the stock is short or 400 for a bad 
reservation, and /openapi serves the contract.",
+    "level": "contracts",
+    "teaches": {
+        "components": [
+            "rest",
+            "rest-openapi",
+            "platform-http",
+            "direct",
+            "log"
+        ],
+        "eips": [
+            "choice",
+            "validate",
+            "onException",
+            "marshal",
+            "unmarshal",
+            "setProperty"
+        ],
+        "languages": [
+            "simple",
+            "jsonpath",
+            "constant"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "openapi",
+        "rest",
+        "contract"
+    ],
+    "bundled": false
+}
diff --git a/contracts/openapi-server/openapi-server.camel.yaml 
b/contracts/openapi-server/openapi-server.camel.yaml
new file mode 100644
index 0000000..23d364e
--- /dev/null
+++ b/contracts/openapi-server/openapi-server.camel.yaml
@@ -0,0 +1,162 @@
+# The stock API, contract first: stock-api.json is the OpenAPI contract, the
+# REST DSL serves every operation in it, requests are validated against it,
+# and each operation is a direct route named after its operationId.
+- restConfiguration:
+    clientRequestValidation: true
+    apiContextPath: openapi
+
+# The contract check rejects a missing body or a wrong content type; the 
values are the route's job
+- onException:
+    exception:
+      - org.apache.camel.support.processor.PredicateValidationException
+    handled:
+      constant:
+        expression: "true"
+    steps:
+      - setHeader:
+          name: CamelHttpResponseCode
+          expression:
+            constant:
+              expression: "400"
+      - setBody:
+          expression:
+            simple:
+              expression: '{"error": "orderId and a positive qty are 
required"}'
+
+- rest:
+    openApi:
+      specification: stock-api.json
+
+- route:
+    id: listStock
+    from:
+      uri: direct
+      parameters:
+        name: listStock
+      steps:
+        - setBody:
+            expression:
+              constant:
+                expression: resource:file:stock.json
+
+- route:
+    id: getStock
+    from:
+      uri: direct
+      parameters:
+        name: getStock
+      steps:
+        - to:
+            uri: direct:lookup
+        - choice:
+            when:
+              - expression:
+                  simple:
+                    expression: "${body} == null"
+                steps:
+                  - setHeader:
+                      name: CamelHttpResponseCode
+                      expression:
+                        constant:
+                          expression: "404"
+                  - setBody:
+                      expression:
+                        simple:
+                          expression: '{"error": "unknown sku ${header.sku}"}'
+            otherwise:
+              steps:
+                - marshal:
+                    json:
+                      library: Jackson
+
+- route:
+    id: reserveStock
+    from:
+      uri: direct
+      parameters:
+        name: reserveStock
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        - validate:
+            expression:
+              simple:
+                expression: "${body[orderId]} != null && ${body[qty]} > 0"
+        - setProperty:
+            name: reservation
+            expression:
+              simple:
+                expression: "${body}"
+        - to:
+            uri: direct:lookup
+        - choice:
+            when:
+              - expression:
+                  simple:
+                    expression: "${body} == null"
+                steps:
+                  - setHeader:
+                      name: CamelHttpResponseCode
+                      expression:
+                        constant:
+                          expression: "404"
+                  - setBody:
+                      expression:
+                        simple:
+                          expression: '{"error": "unknown sku ${header.sku}"}'
+              - expression:
+                  simple:
+                    expression: "${body[qty]} < 
${exchangeProperty.reservation[qty]}"
+                steps:
+                  - setHeader:
+                      name: CamelHttpResponseCode
+                      expression:
+                        constant:
+                          expression: "409"
+                  - setBody:
+                      expression:
+                        simple:
+                          expression: '{"error": "only ${body[qty]} 
${header.sku} in stock, ${exchangeProperty.reservation[qty]} wanted for 
${exchangeProperty.reservation[orderId]}"}'
+            otherwise:
+              steps:
+                - log:
+                    message: "Reserved ${exchangeProperty.reservation[qty]} x 
${header.sku} for ${exchangeProperty.reservation[orderId]}"
+                - setBody:
+                    expression:
+                      simple:
+                        expression: '{"sku": "${header.sku}", "reserved": 
${exchangeProperty.reservation[qty]}, "remaining": ${body[qty]}}'
+
+# Finds one SKU in the stock file: the item as a map, or null
+- route:
+    id: lookup
+    from:
+      uri: direct
+      parameters:
+        name: lookup
+      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:
+                  - setBody:
+                      expression:
+                        simple:
+                          expression: "${null}"
+            otherwise:
+              steps:
+                - setBody:
+                    expression:
+                      simple:
+                        expression: "${body[0]}"
diff --git a/contracts/openapi-server/stock-api.json 
b/contracts/openapi-server/stock-api.json
new file mode 100644
index 0000000..279b681
--- /dev/null
+++ b/contracts/openapi-server/stock-api.json
@@ -0,0 +1,201 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "Stock API",
+    "description": "What the warehouse has on the shelf, per SKU, and 
reservations against it.",
+    "version": "1.0.0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost:8080/api";
+    }
+  ],
+  "paths": {
+    "/stock": {
+      "get": {
+        "operationId": "listStock",
+        "summary": "All stock levels",
+        "responses": {
+          "200": {
+            "description": "The stock levels",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/StockItem"
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/stock/{sku}": {
+      "get": {
+        "operationId": "getStock",
+        "summary": "Stock level of one SKU",
+        "parameters": [
+          {
+            "name": "sku",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "The stock level",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/StockItem"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Unknown SKU",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/stock/{sku}/reserve": {
+      "post": {
+        "operationId": "reserveStock",
+        "summary": "Reserve a quantity of one SKU for an order",
+        "parameters": [
+          {
+            "name": "sku",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Reservation"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Reserved",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ReservationResult"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "The request does not match the contract"
+          },
+          "404": {
+            "description": "Unknown SKU",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          },
+          "409": {
+            "description": "Not enough stock",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Error"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "StockItem": {
+        "type": "object",
+        "required": [
+          "sku",
+          "qty"
+        ],
+        "properties": {
+          "sku": {
+            "type": "string"
+          },
+          "qty": {
+            "type": "integer",
+            "minimum": 0
+          }
+        }
+      },
+      "Reservation": {
+        "type": "object",
+        "required": [
+          "orderId",
+          "qty"
+        ],
+        "properties": {
+          "orderId": {
+            "type": "string"
+          },
+          "qty": {
+            "type": "integer",
+            "minimum": 1
+          }
+        }
+      },
+      "ReservationResult": {
+        "type": "object",
+        "required": [
+          "sku",
+          "reserved",
+          "remaining"
+        ],
+        "properties": {
+          "sku": {
+            "type": "string"
+          },
+          "reserved": {
+            "type": "integer"
+          },
+          "remaining": {
+            "type": "integer"
+          }
+        }
+      },
+      "Error": {
+        "type": "object",
+        "required": [
+          "error"
+        ],
+        "properties": {
+          "error": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/contracts/openapi-server/stock.json 
b/contracts/openapi-server/stock.json
new file mode 100644
index 0000000..efab5de
--- /dev/null
+++ b/contracts/openapi-server/stock.json
@@ -0,0 +1,5 @@
+[
+  {"sku": "CAMEL-TSHIRT", "qty": 120},
+  {"sku": "CAMEL-MUG", "qty": 42},
+  {"sku": "CAMEL-CAP", "qty": 0}
+]
diff --git a/contracts/openapi-server/test/openapi-server.citrus.it.yaml 
b/contracts/openapi-server/test/openapi-server.citrus.it.yaml
new file mode 100644
index 0000000..6c3449e
--- /dev/null
+++ b/contracts/openapi-server/test/openapi-server.citrus.it.yaml
@@ -0,0 +1,76 @@
+name: openapi-server-test
+description: The stock API answers by contract
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "openapi-server"
+            file: "../openapi-server.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock-api.json"
+            - "stock.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-server"
+          logMessage: "(openapi-server) started"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/api/stock/CAMEL-MUG"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        validate:
+          - jsonPath:
+              - expression: "$.qty"
+                value: "42"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/api/stock/CAMEL-MUG/reserve"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1001", "qty": 2}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        validate:
+          - jsonPath:
+              - expression: "$.reserved"
+                value: "2"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/api/stock/CAMEL-CAP/reserve"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1003", "qty": 1}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "409"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/api/stock/CAMEL-MUG/reserve"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1001"}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "400"
diff --git a/rest/openapi-client/README.md b/rest/openapi-client/README.md
deleted file mode 100644
index 8cc804e..0000000
--- a/rest/openapi-client/README.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Open API client
-
-This example uses the Open API specification as a base for a Camel REST client.
-The Camel route reads files from a local directory and sends the content in 
the form of an Open API REST request to a service.
-
-## Install Camel CLI
-
-<!-- see installation instructions in ../../install.adoc -->
-
-## How to run
-
-First of all we need a Http REST service that is able to handle the client 
requests.
-You may use the Open API server example in [../server](../server/README.md).
-
-Just navigate to the server directory in a separate terminal window and start 
the Camel Open API server example with:
-
-```shell
-camel run *
-```
-
-You should see the Camel integration starting and listening on 
`http://localhost:8080` for incoming requests.
-Then you can run the Camel REST client integration using:
-
-```shell
-camel run petstore-api.json application.properties petstore-client.camel.yaml
-```
-
-You may also run this even more concise with:
-
-```shell
-camel run *
-```
-
-Now you should see the client invoking the REST service using the OpenAPI 
specification rules.
-
-## Integration testing
-
-The example provides an automated integration test 
(`petstore-client.citrus.it.yaml`) in the 
[Citrus](https://citrusframework.org/) YAML DSL, which the Camel CLI runs (the 
test plugin installs on first use).
-
-You can run the test with:
-
-```shell
-citrus test/run petstore-client.citrus.it.yaml
-```
-
-The test prepares the complete infrastructure and starts the Camel route 
automatically.
-
-## 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/rest/openapi-client/application.properties 
b/rest/openapi-client/application.properties
deleted file mode 100644
index d43a6e4..0000000
--- a/rest/openapi-client/application.properties
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# 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.
-#
-
-openapi.specification.url=petstore-api.json
diff --git a/rest/openapi-client/examples/1001.json 
b/rest/openapi-client/examples/1001.json
deleted file mode 100644
index a96a119..0000000
--- a/rest/openapi-client/examples/1001.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "id": 1001,
-  "name": "marvin",
-  "category": {
-    "id": 1001,
-    "name": "cat"
-  },
-  "photoUrls": [
-    "petstore/v3/photos/1001"
-  ],
-  "tags": [
-    {
-      "id": 1000,
-      "name": "generated"
-    }
-  ],
-  "status": "available"
-}
diff --git a/rest/openapi-client/metadata.json 
b/rest/openapi-client/metadata.json
deleted file mode 100644
index 436b670..0000000
--- a/rest/openapi-client/metadata.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-    "title": "OpenAPI Client",
-    "description": "Each JSON file in the examples directory is sent as an 
addPet request to the petstore service described by petstore-api.json, running 
on localhost:8080 (see openapi-server); the request and response are logged.",
-    "level": "contracts",
-    "teaches": {
-        "components": [
-            "file",
-            "kamelet",
-            "log"
-        ],
-        "eips": [
-            "setHeader",
-            "convertBodyTo"
-        ],
-        "languages": [
-            "simple",
-            "constant"
-        ]
-    },
-    "tags": [
-        "rest",
-        "openapi",
-        "client"
-    ],
-    "bundled": false
-}
diff --git a/rest/openapi-client/petstore-api.json 
b/rest/openapi-client/petstore-api.json
deleted file mode 100644
index 89f9c02..0000000
--- a/rest/openapi-client/petstore-api.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
-  "openapi": "3.0.2",
-  "info": {
-    "title": "Swagger Petstore",
-    "version": "1.0.1",
-    "description": "This is a sample server Petstore server.",
-    "license": {
-      "name": "Apache 2.0",
-      "url": "http://www.apache.org/licenses/LICENSE-2.0.html";
-    }
-  },
-  "servers": [
-    {
-      "url": "{scheme}://{host}/{basePath}",
-      "variables": {
-        "scheme": {
-          "enum": [
-            "https",
-            "http"
-          ],
-          "default": "http"
-        },
-        "host": {
-          "default": "localhost:8080"
-        },
-        "basePath": {
-          "default": "/petstore"
-        }
-      }
-    }
-  ],
-  "paths": {
-    "/pet": {
-      "put": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          },
-          "405": {
-            "description": "Validation exception"
-          }
-        },
-        "operationId": "updatePet",
-        "summary": "Update an existing pet",
-        "description": ""
-      },
-      "post": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "201": {
-            "description": "Created"
-          },
-          "405": {
-            "description": "Invalid input"
-          }
-        },
-        "operationId": "addPet",
-        "summary": "Add a new pet to the store",
-        "description": ""
-      }
-    },
-    "/pet/{petId}": {
-      "get": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "ID of pet to return",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          },
-          {
-            "name": "verbose",
-            "description": "Output details",
-            "schema": {
-              "type": "boolean"
-            },
-            "in": "query",
-            "required": false
-          }
-        ],
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              },
-              "application/xml": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              }
-            },
-            "description": "successful operation"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "getPetById",
-        "summary": "Find pet by ID",
-        "description": "Returns a single pet"
-      },
-      "delete": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "Pet id to delete",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          }
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "deletePet",
-        "summary": "Deletes a pet",
-        "description": ""
-      }
-    }
-  },
-  "components": {
-    "schemas": {
-      "Category": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Category"
-        }
-      },
-      "Tag": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Tag"
-        }
-      },
-      "Pet": {
-        "required": [
-          "category",
-          "name",
-          "status"
-        ],
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "category": {
-            "$ref": "#/components/schemas/Category"
-          },
-          "name": {
-            "type": "string",
-            "example": "doggie"
-          },
-          "photoUrls": {
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "xml": {
-              "name": "photoUrl",
-              "wrapped": true
-            }
-          },
-          "tags": {
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/Tag"
-            },
-            "xml": {
-              "name": "tag",
-              "wrapped": true
-            }
-          },
-          "status": {
-            "description": "pet status in the store",
-            "enum": [
-              "available",
-              "pending",
-              "sold"
-            ],
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Pet"
-        }
-      },
-      "ApiResponse": {
-        "type": "object",
-        "properties": {
-          "code": {
-            "format": "int32",
-            "type": "integer"
-          },
-          "type": {
-            "type": "string"
-          },
-          "message": {
-            "type": "string"
-          }
-        }
-      }
-    }
-  },
-  "tags": [
-    {
-      "name": "pet",
-      "description": "Everything about your Pets"
-    }
-  ]
-}
diff --git a/rest/openapi-client/petstore-client.camel.yaml 
b/rest/openapi-client/petstore-client.camel.yaml
deleted file mode 100644
index f152552..0000000
--- a/rest/openapi-client/petstore-client.camel.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-- route:
-    from:
-      uri: file
-      parameters:
-        directoryName: examples/
-        noop: true
-      steps:
-        - convertBodyTo:
-            type: java.lang.String
-            charset: UTF8
-        - setHeader:
-            name: petId
-            expression:
-              simple:
-                expression: "${file:onlyname.noext}"
-        - setHeader:
-            name: Content-Type
-            expression:
-              constant:
-                expression: application/json
-        - to:
-            uri: log
-            parameters:
-              loggerName: info
-              showHeaders: true
-        - to:
-            uri: kamelet
-            parameters:
-              templateId: rest-openapi-sink
-              operation: addPet
-              specification: "{{openapi.specification.url}}"
-
diff --git a/rest/openapi-client/test/citrus-application.properties 
b/rest/openapi-client/test/citrus-application.properties
deleted file mode 100644
index 2c9a8da..0000000
--- a/rest/openapi-client/test/citrus-application.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-# Enable dump of Camel JBang integration output
-citrus.camel.jbang.dump.integration.output=true
-
-# Disable extended OpenAPI request validation
-citrus.openapi.validation.enabled.request=false
diff --git a/rest/openapi-client/test/examples/1001.json 
b/rest/openapi-client/test/examples/1001.json
deleted file mode 100644
index a96a119..0000000
--- a/rest/openapi-client/test/examples/1001.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "id": 1001,
-  "name": "marvin",
-  "category": {
-    "id": 1001,
-    "name": "cat"
-  },
-  "photoUrls": [
-    "petstore/v3/photos/1001"
-  ],
-  "tags": [
-    {
-      "id": 1000,
-      "name": "generated"
-    }
-  ],
-  "status": "available"
-}
diff --git a/rest/openapi-client/test/petstore-api.json 
b/rest/openapi-client/test/petstore-api.json
deleted file mode 100644
index 0476702..0000000
--- a/rest/openapi-client/test/petstore-api.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
-  "openapi": "3.0.2",
-  "info": {
-    "title": "Swagger Petstore",
-    "version": "1.0.1",
-    "description": "This is a sample server Petstore server.",
-    "license": {
-      "name": "Apache 2.0",
-      "url": "http://www.apache.org/licenses/LICENSE-2.0.html";
-    }
-  },
-  "servers": [
-    {
-      "url": "{scheme}://{host}/{basePath}",
-      "variables": {
-        "scheme": {
-          "enum": [
-            "https",
-            "http"
-          ],
-          "default": "http"
-        },
-        "host": {
-          "default": "localhost:8080"
-        },
-        "basePath": {
-          "default": "/"
-        }
-      }
-    }
-  ],
-  "paths": {
-    "/petstore/pet": {
-      "put": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          },
-          "405": {
-            "description": "Validation exception"
-          }
-        },
-        "operationId": "updatePet",
-        "summary": "Update an existing pet",
-        "description": ""
-      },
-      "post": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "201": {
-            "description": "Created"
-          },
-          "405": {
-            "description": "Invalid input"
-          }
-        },
-        "operationId": "addPet",
-        "summary": "Add a new pet to the store",
-        "description": ""
-      }
-    },
-    "/petstore/pet/{petId}": {
-      "get": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "ID of pet to return",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          },
-          {
-            "name": "verbose",
-            "description": "Output details",
-            "schema": {
-              "type": "boolean"
-            },
-            "in": "query",
-            "required": false
-          }
-        ],
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              },
-              "application/xml": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              }
-            },
-            "description": "successful operation"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "getPetById",
-        "summary": "Find pet by ID",
-        "description": "Returns a single pet"
-      },
-      "delete": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "Pet id to delete",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          }
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "deletePet",
-        "summary": "Deletes a pet",
-        "description": ""
-      }
-    }
-  },
-  "components": {
-    "schemas": {
-      "Category": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Category"
-        }
-      },
-      "Tag": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Tag"
-        }
-      },
-      "Pet": {
-        "required": [
-          "category",
-          "name",
-          "status"
-        ],
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "category": {
-            "$ref": "#/components/schemas/Category"
-          },
-          "name": {
-            "type": "string",
-            "example": "doggie"
-          },
-          "photoUrls": {
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "xml": {
-              "name": "photoUrl",
-              "wrapped": true
-            }
-          },
-          "tags": {
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/Tag"
-            },
-            "xml": {
-              "name": "tag",
-              "wrapped": true
-            }
-          },
-          "status": {
-            "description": "pet status in the store",
-            "enum": [
-              "available",
-              "pending",
-              "sold"
-            ],
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Pet"
-        }
-      },
-      "ApiResponse": {
-        "type": "object",
-        "properties": {
-          "code": {
-            "format": "int32",
-            "type": "integer"
-          },
-          "type": {
-            "type": "string"
-          },
-          "message": {
-            "type": "string"
-          }
-        }
-      }
-    }
-  },
-  "tags": [
-    {
-      "name": "pet",
-      "description": "Everything about your Pets"
-    }
-  ]
-}
diff --git a/rest/openapi-client/test/petstore-client.citrus.it.yaml 
b/rest/openapi-client/test/petstore-client.citrus.it.yaml
deleted file mode 100644
index 956420a..0000000
--- a/rest/openapi-client/test/petstore-client.citrus.it.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: petstore-client-test
-description: Sample test in YAML
-variables:
-  - name: petId
-    value: 1001
-actions:
-  - groovy:
-      endpoints:
-        file: "petstoreServer.groovy"
-  - camel:
-      jbang:
-        run:
-          integration:
-            name: "petstore-client"
-            file: "../petstore-client.camel.yaml"
-            systemProperties:
-              file: "../application.properties"
-          resources:
-            - "petstore-api.json"
-  - openapi:
-      specification: "petstore-api.json"
-      server: "petstoreServer"
-      receiveRequest:
-        operation: addPet
-  - openapi:
-      specification: "petstore-api.json"
-      server: "petstoreServer"
-      sendResponse:
-        operation: addPet
-        status: 201
diff --git a/rest/openapi-client/test/petstoreServer.groovy 
b/rest/openapi-client/test/petstoreServer.groovy
deleted file mode 100644
index f649e42..0000000
--- a/rest/openapi-client/test/petstoreServer.groovy
+++ /dev/null
@@ -1,9 +0,0 @@
-import org.springframework.http.HttpStatus
-
-endpoints {
-    http('petstoreServer')
-            .server()
-            .port(8080)
-            .defaultStatus(HttpStatus.CREATED)
-            .autoStart(true)
-}
diff --git a/rest/openapi-server/README.md b/rest/openapi-server/README.md
deleted file mode 100644
index dda56f9..0000000
--- a/rest/openapi-server/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# Open API server
-
-This example uses an Open API specification as a base for creating a REST 
service in Camel.
-The Camel route is configured to implement the operations declared in the Open 
API specification.
-
-## Install Camel CLI
-
-<!-- see installation instructions in ../../install.adoc -->
-
-## How to run
-
-Then you can run the Camel integration using:
-
-```shell
-camel run petstore-api.json application.properties petstore.camel.yaml
-```
-
-You may also run this even more concise with:
-
-```shell
-camel run *
-```
-
-And then from another terminal (or run the integration with `--background` 
option),
-then send a message to the REST service.
-
-Just call the petstore REST API to retrieve a pet like this:
-
-```shell
-$ curl -i http://localhost:8080/petstore/pet/1000
-HTTP/1.1 200 OK
-petId: 1000
-transfer-encoding: chunked
-Content-Type: application/json
-
-{
-  "id": 1000,
-  "name": "fluffy",
-  "category": {
-    "id": 1000,
-    "name": "dog"
-  },
-  "photoUrls": [
-    "petstore/v3/photos/1000"
-  ],
-  "tags": [
-    {
-      "id": 1000,
-      "name": "generated"
-    }
-  ],
-  "status": "available"
-}
-
-```
-
-The REST service in Camel is configured to load example response data from a 
directory (`camel.component.rest-openapi.mockIncludePattern = 
file:examples/**,classpath:examples/**`).
-
-## Integration testing
-
-The example provides an automated integration test (`petstore.citrus.it.yaml`) 
in the [Citrus](https://citrusframework.org/) YAML DSL, which the Camel CLI 
runs (the test plugin installs on first use).
-
-You can run the test with:
-
-```shell
-camel test run test/petstore.citrus.it.yaml
-```
-
-The test prepares the complete infrastructure and starts the Camel route 
automatically.
-The Citrus test loads the Open API specification from the Camel service and 
uses the rules in that specification to verify the response data.
-
-## 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/rest/openapi-server/application.properties 
b/rest/openapi-server/application.properties
deleted file mode 100644
index 0081528..0000000
--- a/rest/openapi-server/application.properties
+++ /dev/null
@@ -1,19 +0,0 @@
-#
-# 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.component.rest-openapi.mockIncludePattern = 
file:examples/**,classpath:examples/**
-camel.server.enabled=true
diff --git a/rest/openapi-server/examples/pet/1000.json 
b/rest/openapi-server/examples/pet/1000.json
deleted file mode 100644
index eee6fad..0000000
--- a/rest/openapi-server/examples/pet/1000.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "id": 1000,
-  "name": "fluffy",
-  "category": {
-    "id": 1000,
-    "name": "dog"
-  },
-  "photoUrls": [
-    "petstore/v3/photos/1000"
-  ],
-  "tags": [
-    {
-      "id": 1000,
-      "name": "generated"
-    }
-  ],
-  "status": "available"
-}
diff --git a/rest/openapi-server/metadata.json 
b/rest/openapi-server/metadata.json
deleted file mode 100644
index 98132fa..0000000
--- a/rest/openapi-server/metadata.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-    "title": "OpenAPI Server",
-    "description": "A REST service generated from petstore-api.json answers 
under the base path /petstore on port 8080; GET /petstore/pet/1000 returns the 
mocked pet from the examples directory and /openapi serves the specification.",
-    "level": "contracts",
-    "teaches": {
-        "components": [
-            "platform-http",
-            "direct",
-            "log"
-        ],
-        "eips": [
-            "rest",
-            "restConfiguration"
-        ]
-    },
-    "tags": [
-        "rest",
-        "openapi",
-        "server"
-    ],
-    "bundled": false
-}
diff --git a/rest/openapi-server/petstore-api.json 
b/rest/openapi-server/petstore-api.json
deleted file mode 100644
index 89f9c02..0000000
--- a/rest/openapi-server/petstore-api.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
-  "openapi": "3.0.2",
-  "info": {
-    "title": "Swagger Petstore",
-    "version": "1.0.1",
-    "description": "This is a sample server Petstore server.",
-    "license": {
-      "name": "Apache 2.0",
-      "url": "http://www.apache.org/licenses/LICENSE-2.0.html";
-    }
-  },
-  "servers": [
-    {
-      "url": "{scheme}://{host}/{basePath}",
-      "variables": {
-        "scheme": {
-          "enum": [
-            "https",
-            "http"
-          ],
-          "default": "http"
-        },
-        "host": {
-          "default": "localhost:8080"
-        },
-        "basePath": {
-          "default": "/petstore"
-        }
-      }
-    }
-  ],
-  "paths": {
-    "/pet": {
-      "put": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          },
-          "405": {
-            "description": "Validation exception"
-          }
-        },
-        "operationId": "updatePet",
-        "summary": "Update an existing pet",
-        "description": ""
-      },
-      "post": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "201": {
-            "description": "Created"
-          },
-          "405": {
-            "description": "Invalid input"
-          }
-        },
-        "operationId": "addPet",
-        "summary": "Add a new pet to the store",
-        "description": ""
-      }
-    },
-    "/pet/{petId}": {
-      "get": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "ID of pet to return",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          },
-          {
-            "name": "verbose",
-            "description": "Output details",
-            "schema": {
-              "type": "boolean"
-            },
-            "in": "query",
-            "required": false
-          }
-        ],
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              },
-              "application/xml": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              }
-            },
-            "description": "successful operation"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "getPetById",
-        "summary": "Find pet by ID",
-        "description": "Returns a single pet"
-      },
-      "delete": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "Pet id to delete",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          }
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "deletePet",
-        "summary": "Deletes a pet",
-        "description": ""
-      }
-    }
-  },
-  "components": {
-    "schemas": {
-      "Category": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Category"
-        }
-      },
-      "Tag": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Tag"
-        }
-      },
-      "Pet": {
-        "required": [
-          "category",
-          "name",
-          "status"
-        ],
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "category": {
-            "$ref": "#/components/schemas/Category"
-          },
-          "name": {
-            "type": "string",
-            "example": "doggie"
-          },
-          "photoUrls": {
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "xml": {
-              "name": "photoUrl",
-              "wrapped": true
-            }
-          },
-          "tags": {
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/Tag"
-            },
-            "xml": {
-              "name": "tag",
-              "wrapped": true
-            }
-          },
-          "status": {
-            "description": "pet status in the store",
-            "enum": [
-              "available",
-              "pending",
-              "sold"
-            ],
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Pet"
-        }
-      },
-      "ApiResponse": {
-        "type": "object",
-        "properties": {
-          "code": {
-            "format": "int32",
-            "type": "integer"
-          },
-          "type": {
-            "type": "string"
-          },
-          "message": {
-            "type": "string"
-          }
-        }
-      }
-    }
-  },
-  "tags": [
-    {
-      "name": "pet",
-      "description": "Everything about your Pets"
-    }
-  ]
-}
diff --git a/rest/openapi-server/petstore.camel.yaml 
b/rest/openapi-server/petstore.camel.yaml
deleted file mode 100644
index 78afa9e..0000000
--- a/rest/openapi-server/petstore.camel.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-- restConfiguration:
-    clientRequestValidation: true
-    apiContextPath: openapi
-- rest:
-    openApi:
-      specification: petstore-api.json
-- route:
-    from:
-      uri: direct
-      parameters:
-        name: addPet
-      steps:
-        - to:
-            uri: log
-            parameters:
-              loggerName: info
diff --git a/rest/openapi-server/test/citrus-application.properties 
b/rest/openapi-server/test/citrus-application.properties
deleted file mode 100644
index 67a108f..0000000
--- a/rest/openapi-server/test/citrus-application.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-# Enable dump of Camel JBang integration output
-citrus.camel.jbang.dump.integration.output=true
diff --git a/rest/openapi-server/test/examples/pet/1000.json 
b/rest/openapi-server/test/examples/pet/1000.json
deleted file mode 100644
index eee6fad..0000000
--- a/rest/openapi-server/test/examples/pet/1000.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "id": 1000,
-  "name": "fluffy",
-  "category": {
-    "id": 1000,
-    "name": "dog"
-  },
-  "photoUrls": [
-    "petstore/v3/photos/1000"
-  ],
-  "tags": [
-    {
-      "id": 1000,
-      "name": "generated"
-    }
-  ],
-  "status": "available"
-}
diff --git a/rest/openapi-server/test/petstore-api.json 
b/rest/openapi-server/test/petstore-api.json
deleted file mode 100644
index 89f9c02..0000000
--- a/rest/openapi-server/test/petstore-api.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
-  "openapi": "3.0.2",
-  "info": {
-    "title": "Swagger Petstore",
-    "version": "1.0.1",
-    "description": "This is a sample server Petstore server.",
-    "license": {
-      "name": "Apache 2.0",
-      "url": "http://www.apache.org/licenses/LICENSE-2.0.html";
-    }
-  },
-  "servers": [
-    {
-      "url": "{scheme}://{host}/{basePath}",
-      "variables": {
-        "scheme": {
-          "enum": [
-            "https",
-            "http"
-          ],
-          "default": "http"
-        },
-        "host": {
-          "default": "localhost:8080"
-        },
-        "basePath": {
-          "default": "/petstore"
-        }
-      }
-    }
-  ],
-  "paths": {
-    "/pet": {
-      "put": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          },
-          "405": {
-            "description": "Validation exception"
-          }
-        },
-        "operationId": "updatePet",
-        "summary": "Update an existing pet",
-        "description": ""
-      },
-      "post": {
-        "requestBody": {
-          "description": "Pet object that needs to be added to the store",
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            },
-            "application/xml": {
-              "schema": {
-                "$ref": "#/components/schemas/Pet"
-              }
-            }
-          },
-          "required": true
-        },
-        "tags": [
-          "pet"
-        ],
-        "responses": {
-          "201": {
-            "description": "Created"
-          },
-          "405": {
-            "description": "Invalid input"
-          }
-        },
-        "operationId": "addPet",
-        "summary": "Add a new pet to the store",
-        "description": ""
-      }
-    },
-    "/pet/{petId}": {
-      "get": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "ID of pet to return",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          },
-          {
-            "name": "verbose",
-            "description": "Output details",
-            "schema": {
-              "type": "boolean"
-            },
-            "in": "query",
-            "required": false
-          }
-        ],
-        "responses": {
-          "200": {
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              },
-              "application/xml": {
-                "schema": {
-                  "$ref": "#/components/schemas/Pet"
-                }
-              }
-            },
-            "description": "successful operation"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "getPetById",
-        "summary": "Find pet by ID",
-        "description": "Returns a single pet"
-      },
-      "delete": {
-        "tags": [
-          "pet"
-        ],
-        "parameters": [
-          {
-            "name": "petId",
-            "description": "Pet id to delete",
-            "schema": {
-              "format": "int64",
-              "type": "integer"
-            },
-            "in": "path",
-            "required": true
-          }
-        ],
-        "responses": {
-          "204": {
-            "description": "No content"
-          },
-          "400": {
-            "description": "Invalid ID supplied"
-          },
-          "404": {
-            "description": "Pet not found"
-          }
-        },
-        "operationId": "deletePet",
-        "summary": "Deletes a pet",
-        "description": ""
-      }
-    }
-  },
-  "components": {
-    "schemas": {
-      "Category": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Category"
-        }
-      },
-      "Tag": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Tag"
-        }
-      },
-      "Pet": {
-        "required": [
-          "category",
-          "name",
-          "status"
-        ],
-        "type": "object",
-        "properties": {
-          "id": {
-            "format": "int64",
-            "type": "integer"
-          },
-          "category": {
-            "$ref": "#/components/schemas/Category"
-          },
-          "name": {
-            "type": "string",
-            "example": "doggie"
-          },
-          "photoUrls": {
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "xml": {
-              "name": "photoUrl",
-              "wrapped": true
-            }
-          },
-          "tags": {
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/Tag"
-            },
-            "xml": {
-              "name": "tag",
-              "wrapped": true
-            }
-          },
-          "status": {
-            "description": "pet status in the store",
-            "enum": [
-              "available",
-              "pending",
-              "sold"
-            ],
-            "type": "string"
-          }
-        },
-        "xml": {
-          "name": "Pet"
-        }
-      },
-      "ApiResponse": {
-        "type": "object",
-        "properties": {
-          "code": {
-            "format": "int32",
-            "type": "integer"
-          },
-          "type": {
-            "type": "string"
-          },
-          "message": {
-            "type": "string"
-          }
-        }
-      }
-    }
-  },
-  "tags": [
-    {
-      "name": "pet",
-      "description": "Everything about your Pets"
-    }
-  ]
-}
diff --git a/rest/openapi-server/test/petstore.citrus.it.yaml 
b/rest/openapi-server/test/petstore.citrus.it.yaml
deleted file mode 100644
index b8fe98f..0000000
--- a/rest/openapi-server/test/petstore.citrus.it.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: petstore-test
-description: Sample test in YAML
-variables:
-  - name: petId
-    value: 1000
-actions:
-  - camel:
-      jbang:
-        run:
-          integration:
-            name: "petstore"
-            file: "../petstore.camel.yaml"
-            systemProperties:
-              file: "../application.properties"
-          resources:
-            - "petstore-api.json"
-  - openapi:
-      specification: "http://localhost:8080/openapi";
-      client: "http://localhost:8080";
-      sendRequest:
-        operation: getPetById
-  - openapi:
-      specification: "http://localhost:8080/openapi";
-      client: "http://localhost:8080";
-      receiveResponse:
-        operation: getPetById
-        status: 200
-
-  - openapi:
-      specification: "http://localhost:8080/openapi";
-      client: "http://localhost:8080";
-      sendRequest:
-        operation: addPet
-  - openapi:
-      specification: "http://localhost:8080/openapi";
-      client: "http://localhost:8080";
-      receiveResponse:
-        operation: addPet
-        status: 200
diff --git a/security/keycloak-security-rest/metadata.json 
b/security/keycloak-security-rest/metadata.json
deleted file mode 100644
index 1424289..0000000
--- a/security/keycloak-security-rest/metadata.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-    "title": "Keycloak Security REST API",
-    "description": "A REST API on platform-http is protected by a running 
Keycloak: a request without a valid bearer token is refused with 401, a token 
with the right role reaches the route and is logged.",
-    "level": "contracts",
-    "teaches": {
-        "components": [
-            "platform-http",
-            "log"
-        ],
-        "eips": [
-            "policy",
-            "onException",
-            "setHeader",
-            "setBody"
-        ]
-    },
-    "tags": [
-        "security",
-        "keycloak",
-        "rest",
-        "oauth"
-    ],
-    "bundled": false,
-    "infraServices": [
-        "keycloak"
-    ]
-}

Reply via email to