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"
- ]
-}