This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch quick-fix/rest-contract-first-samples in repository https://gitbox.apache.org/repos/asf/camel.git
commit 29fda2350444b6402ae192837c2c4a262c41b451 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 1 21:55:57 2026 +0200 chore: camel_catalog_sample shows the contract-first rest next to the path-based one camel_catalog_sample rest returned only the path-based examples of rest-dsl.adoc; the contract-first form (rest: openApi: specification:) on rest-dsl-openapi.adoc was validated by the build but never sampled. - rest-dsl-openapi.adoc is an entry page of rest, so its "- rest:" examples are samples too - a name with samples from several pages takes them from each page in turn, so the default two show both forms (doTry now shows the try-catch-finally page second as well) - the rest-openapi component page gets a contract-first section, so camel_catalog_sample rest-openapi shows it after the consumer endpoint Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../camel/catalog/docs/rest-openapi-component.adoc | 50 ++++++++++++++++++++++ .../src/main/docs/rest-openapi-component.adoc | 50 ++++++++++++++++++++++ dsl/camel-jbang/camel-jbang-core/pom.xml | 2 + .../dsl/jbang/core/commands/ai/eip-samples.json | 24 +++++++++-- .../jbang/core/commands/ai/CatalogSamplesTest.java | 12 ++++++ .../dsl/yaml/validator/GenerateDocSamplesMojo.java | 28 +++++++++++- .../yaml/validator/GenerateDocSamplesMojoTest.java | 22 ++++++++++ 7 files changed, 182 insertions(+), 6 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc index 19e3c4824ff6..1e845c121933 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rest-openapi-component.adoc @@ -107,6 +107,56 @@ include::partial$component-endpoint-headers.adoc[] == Usage +=== Contract-first REST DSL + +To serve the operations of an OpenAPI contract, use the Rest DSL in _contract-first_ mode rather than a +`rest-openapi` consumer endpoint. Camel loads the contract at startup and routes every operation to the +route that starts from `direct:` and its _operation id_; see +xref:manual::rest-dsl-openapi.adoc[REST DSL with contract-first OpenAPI]. + +[tabs] +==== +Java:: ++ +[source,java] +---- +rest().openApi("petstore-v3.json"); + +from("direct:getPetById") + .setBody(simple("{\"id\": ${header.petId}, \"name\": \"Rex\"}")); +---- + +XML:: ++ +[source,xml] +---- +<rest> + <openApi specification="petstore-v3.json"/> +</rest> +<route> + <from uri="direct:getPetById"/> + <setBody> + <simple>{"id": ${header.petId}, "name": "Rex"}</simple> + </setBody> +</route> +---- + +YAML:: ++ +[source,yaml] +---- +- rest: + openApi: + specification: petstore-v3.json +- route: + from: + uri: direct:getPetById + steps: + - setBody: + simple: '{"id": ${header.petId}, "name": "Rex"}' +---- +==== + === Consumer OAuth Bearer token validation When `rest-openapi` is used as a consumer, HTTP requests are served by the selected diff --git a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc index 19e3c4824ff6..1e845c121933 100644 --- a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc +++ b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc @@ -107,6 +107,56 @@ include::partial$component-endpoint-headers.adoc[] == Usage +=== Contract-first REST DSL + +To serve the operations of an OpenAPI contract, use the Rest DSL in _contract-first_ mode rather than a +`rest-openapi` consumer endpoint. Camel loads the contract at startup and routes every operation to the +route that starts from `direct:` and its _operation id_; see +xref:manual::rest-dsl-openapi.adoc[REST DSL with contract-first OpenAPI]. + +[tabs] +==== +Java:: ++ +[source,java] +---- +rest().openApi("petstore-v3.json"); + +from("direct:getPetById") + .setBody(simple("{\"id\": ${header.petId}, \"name\": \"Rex\"}")); +---- + +XML:: ++ +[source,xml] +---- +<rest> + <openApi specification="petstore-v3.json"/> +</rest> +<route> + <from uri="direct:getPetById"/> + <setBody> + <simple>{"id": ${header.petId}, "name": "Rex"}</simple> + </setBody> +</route> +---- + +YAML:: ++ +[source,yaml] +---- +- rest: + openApi: + specification: petstore-v3.json +- route: + from: + uri: direct:getPetById + steps: + - setBody: + simple: '{"id": ${header.petId}, "name": "Rex"}' +---- +==== + === Consumer OAuth Bearer token validation When `rest-openapi` is used as a consumer, HTTP requests are served by the selected diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml b/dsl/camel-jbang/camel-jbang-core/pom.xml index 29fc9e7c400f..6e0765ef7b1a 100644 --- a/dsl/camel-jbang/camel-jbang-core/pom.xml +++ b/dsl/camel-jbang/camel-jbang-core/pom.xml @@ -291,6 +291,8 @@ </pages> <entryPages> <entryPage>../../camel-yaml-dsl/camel-yaml-dsl/src/main/docs/yaml-dsl.adoc=beans</entryPage> + <!-- the contract-first rest, next to the path-based examples of rest-dsl --> + <entryPage>../../../docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc=rest</entryPage> </entryPages> <!-- user manual pages that show old syntax or YAML that is not a Camel file --> <excludes> diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json index 71af218d0aae..6c0cb0b6dd20 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json @@ -292,14 +292,14 @@ "source": "doTry-eip.adoc", "yaml": "- route:\n from:\n uri: direct:start\n steps:\n - doTry:\n steps:\n - process:\n ref: processorFail\n - to:\n uri: mock:result\n - doCatch:\n exception:\n - java.io.IOException\n - java.lang.IllegalStateException\n steps:\n - to:\n uri: mock:catch\n [...] }, - { - "source": "doTry-eip.adoc", - "yaml": "- route:\n from:\n uri: direct:start\n steps:\n - doTry:\n steps:\n - process:\n ref: processorFail\n - to:\n uri: mock:result\n - doCatch:\n exception:\n - java.io.IOException\n - java.lang.IllegalStateException\n onWhen:\n expression:\n simple:\n [...] - }, { "source": "try-catch-finally.adoc", "yaml": "- route:\n from:\n uri: direct:start\n steps:\n - doTry:\n steps:\n - process:\n ref: processorFail\n - to:\n uri: mock:result\n - doCatch:\n exception:\n - java.io.IOException\n - java.lang.IllegalStateException\n steps:\n - to:\n uri: mock:catch\n [...] }, + { + "source": "doTry-eip.adoc", + "yaml": "- route:\n from:\n uri: direct:start\n steps:\n - doTry:\n steps:\n - process:\n ref: processorFail\n - to:\n uri: mock:result\n - doCatch:\n exception:\n - java.io.IOException\n - java.lang.IllegalStateException\n onWhen:\n expression:\n simple:\n [...] + }, { "source": "try-catch-finally.adoc", "yaml": "- route:\n from:\n uri: direct:start\n steps:\n - doTry:\n steps:\n - process:\n ref: processorFail\n - to:\n uri: mock:result\n - doCatch:\n exception:\n - java.io.IOException\n - java.lang.IllegalStateException\n onWhen:\n simple: \"${exception.message} contains 'Damn'\"\n [...] @@ -966,18 +966,34 @@ "source": "rest-dsl.adoc", "yaml": "- rest:\n path: \"\/say\"\n get:\n - path: \"\/hello\"\n to: \"direct:hello\"\n - path: \"\/bye\"\n consumes: \"application\/json\"\n to: \"direct:bye\"\n post:\n - path: \"\/bye\"\n to: \"direct:update\"\n- route:\n from:\n uri: direct:hello\n steps:\n - transform:\n expression:\n constant:\n expression: Hello World\n- route:\n from:\n uri: direct:bye\n [...] }, + { + "source": "rest-dsl-openapi.adoc", + "yaml": "- rest:\n openApi:\n specification: petstore-v3.json\n" + }, { "source": "rest-dsl.adoc", "yaml": "- rest:\n path: \"\/customers\/\"\n get:\n - path: \"\/{id}}\"\n to: \"direct:customerDetails\"\n - path: \"\/{id}\/orders}\"\n to: \"direct:customerOrders\"\n post:\n - path: \"\/neworder\"\n to: \"direct:customerNewOrder\"\n" }, + { + "source": "rest-dsl-openapi.adoc", + "yaml": "- rest:\n openApi:\n specification: petstore-v3.json\n- route:\n from:\n uri: direct:getUserByName\n steps:\n - log:\n message: \"do something here\"\n" + }, { "source": "rest-dsl.adoc", "yaml": "- rest:\n get:\n - path: \"\/customers\/{id}}\"\n to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders}\"\n to: \"direct:customerOrders\"\n post:\n - path: \"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n" }, + { + "source": "rest-dsl-openapi.adoc", + "yaml": "- rest:\n openApi:\n specification: petstore-v3.json\n missingOperation: ignore\n" + }, { "source": "rest-dsl.adoc", "yaml": "- rest:\n path: \"items\/\"\n get:\n - path: \"{id}\/{filename}.{content-type}\"\n to: \"direct:item\"\n" }, + { + "source": "rest-dsl-openapi.adoc", + "yaml": "- rest:\n openApi:\n specification: petstore-v3.json\n missingOperation: mock\n" + }, { "source": "rest-dsl.adoc", "yaml": "- restConfiguration:\n inlineRoutes: true\n- rest:\n get:\n - path: \"\/customers\/{id}}\"\n to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders}\"\n to: \"direct:customerOrders\"\n post:\n - path: \"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n" diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java index 716eda767050..6a10cc9d54e2 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java @@ -146,6 +146,10 @@ public class CatalogSamplesTest { assertThat(((JsonObject) ((JsonArray) CatalogSamples.sample(catalog, "rest", 1).get("samples")).get(0)) .getString("source")) .isEqualTo("rest-dsl.adoc"); + // the default two show the path-based and the contract-first rest + JsonArray rest = (JsonArray) CatalogSamples.sample(catalog, "rest", 0).get("samples"); + assertThat(((JsonObject) rest.get(1)).getString("source")).isEqualTo("rest-dsl-openapi.adoc"); + assertThat(((JsonObject) rest.get(1)).getString("yaml")).contains("openApi:"); } @Test @@ -223,6 +227,14 @@ public class CatalogSamplesTest { assertThat(CatalogSamples.sample("kafka", 1).getString("error")).contains("kafka"); } + @Test + void restOpenApiShowsTheConsumerEndpointAndTheContractFirstRest() { + CamelCatalog catalog = new DefaultCamelCatalog(); + JsonObject o = CatalogSamples.sample(catalog, "component", "rest-openapi", 2); + assertThat(yaml(o, 0)).contains("rest-openapi:"); + assertThat(yaml(o, 1)).contains("openApi:").contains("direct:getPetById"); + } + @Test void theSubPagesOfAComponentBelongToIt() { CamelCatalog catalog = new DefaultCamelCatalog(); diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java index c31b0054ca63..711819f95e78 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -54,8 +55,10 @@ import org.apache.maven.project.MavenProject; * ({@code yaml-dsl.adoc} is {@code beans})</li> * </ul> * A route example is a {@code [source,yaml]} block whose first line starts with {@code "- "}; other YAML blocks are - * fragments and are neither validated nor sampled. The output is a JSON object keyed by the sample name, each value a - * list of {@code {source, yaml}} objects, written only when its content changed. + * fragments and are neither validated nor sampled. A name with samples from several pages takes them from each page in + * turn, so the first few (what the AI tools return by default) show every page ({@code rest} is path-based from + * {@code rest-dsl.adoc} and contract-first from {@code rest-dsl-openapi.adoc}). The output is a JSON object keyed by + * the sample name, each value a list of {@code {source, yaml}} objects, written only when its content changed. */ @Mojo(name = "generate-doc-samples", threadSafe = true) public class GenerateDocSamplesMojo extends AbstractMojo { @@ -223,9 +226,30 @@ public class GenerateDocSamplesMojo extends AbstractMojo { } } + samples.replaceAll((key, list) -> interleave(list)); return samples; } + /** The samples taken from each page in turn, in page order, keeping the order of the samples of a page. */ + static List<Sample> interleave(List<Sample> list) { + Map<String, List<Sample>> bySource = new LinkedHashMap<>(); + for (Sample s : list) { + bySource.computeIfAbsent(s.source(), k -> new ArrayList<>()).add(s); + } + if (bySource.size() < 2) { + return list; + } + List<Sample> answer = new ArrayList<>(list.size()); + for (int i = 0; answer.size() < list.size(); i++) { + for (List<Sample> page : bySource.values()) { + if (i < page.size()) { + answer.add(page.get(i)); + } + } + } + return answer; + } + private static List<File> pages(File dir) { File[] files = dir.listFiles(f -> f.isFile() && f.getName().endsWith(".adoc")); if (files == null) { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/test/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojoTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/test/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojoTest.java index ea1b6cb4040e..56a4efc3c66e 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/test/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojoTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/test/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojoTest.java @@ -126,6 +126,28 @@ class GenerateDocSamplesMojoTest { assertEquals(ROUTE, first.getString("yaml")); } + @Test + void aNameWithSamplesFromSeveralPagesTakesThemFromEachPageInTurn() throws Exception { + Path eips = Files.createDirectories(tempDir.resolve("eips")); + Path manual = Files.createDirectories(tempDir.resolve("manual")); + String path = "- rest:\n path: /say\n get:\n - path: /hello\n to: direct:hello\n"; + Files.writeString(manual.resolve("rest-dsl.adoc"), block(path) + block(path) + block(path)); + Path openApi = manual.resolve("rest-dsl-openapi.adoc"); + String contract = "- rest:\n openApi:\n specification: petstore-v3.json\n"; + Files.writeString(openApi, block("- restConfiguration:\n contextPath: /api\n") + block(contract) + block(contract)); + + List<String> failures = new ArrayList<>(); + Map<String, List<GenerateDocSamplesMojo.Sample>> samples = GenerateDocSamplesMojo.generate( + VALIDATOR, eips.toFile(), manual.toFile(), Map.of("rest-dsl", "rest"), + Map.of(openApi.toFile(), "rest"), null, failures); + + assertTrue(failures.isEmpty(), failures.toString()); + assertEquals(List.of("rest-dsl.adoc", "rest-dsl-openapi.adoc", "rest-dsl.adoc", "rest-dsl-openapi.adoc", + "rest-dsl.adoc"), + samples.get("rest").stream().map(GenerateDocSamplesMojo.Sample::source).toList()); + assertEquals(contract, samples.get("rest").get(1).yaml()); + } + @Test void anExampleThatDoesNotValidateIsAFailure() throws Exception { Path eips = Files.createDirectories(tempDir.resolve("eips"));
