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


The following commit(s) were added to refs/heads/main by this push:
     new 70bd1fc8defd CAMEL-25862: camel-jbang - the validator hint for a 
rest-openapi call with parsed data offers the JSON text of the request first 
(#28142)
70bd1fc8defd is described below

commit 70bd1fc8defd922b02ef3a41bd2a53600bf03f3f
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 12:35:26 2026 +0200

    CAMEL-25862: camel-jbang - the validator hint for a rest-openapi call with 
parsed data offers the JSON text of the request first (#28142)
    
    CAMEL-25862: camel-jbang - the validator hint for a rest-openapi call with 
parsed data offers the JSON text of the request first, with the fields it needs
    
    The hint said to add marshal: json before the call. A model marshals the 
data it has, but the request of an operation
    is rarely that data: a line of an order has no orderId, which the request 
of reserveStock needs, so every call was
    answered 400. For a rest-openapi call with JSON data, the hint now offers 
to set the body to the JSON text of the
    request first, naming the fields the request needs (the required properties 
of its schema, which OpenApiVerbs reads
    from the specification, following a $ref), and marshal when the data has 
those fields.
    
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../dsl/jbang/core/commands/ai/OpenApiVerbs.java   | 106 ++++++++++++++++++---
 .../dsl/jbang/core/commands/ai/ValidatorFacts.java |   2 +-
 .../jbang/core/commands/ai/ValidatorFactsTest.java |  64 +++++++++++++
 .../camel/dsl/yaml/validator/ProjectFacts.java     |  12 ++-
 .../camel/dsl/yaml/validator/TextBodyFlow.java     |  49 ++++++++--
 .../camel/dsl/yaml/validator/TextBodyFlowTest.java |  42 +++++++-
 6 files changed, 250 insertions(+), 25 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
index 3c194461b574..1f359afb48d0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -68,17 +69,7 @@ public final class OpenApiVerbs {
         if (content == null || directory == null) {
             return answer;
         }
-        Set<String> specs = new LinkedHashSet<>();
-        for (Pattern pattern : List.of(SPECIFICATION, REST_OPENAPI_CALL, 
SPECIFICATION_URI)) {
-            Matcher m = pattern.matcher(content);
-            while (m.find()) {
-                specs.add(ProjectFacts.specName(m.group(1)));
-            }
-        }
-        if (content.contains("rest-openapi")) {
-            specs.add(ProjectFacts.specName(null));
-        }
-        for (String spec : specs) {
+        for (String spec : specs(content)) {
             Map<?, ?> paths = spec.contains("://") || spec.contains("{{") ? 
null : paths(directory, spec);
             if (paths == null) {
                 continue;
@@ -99,14 +90,97 @@ public final class OpenApiVerbs {
         return answer;
     }
 
+    /**
+     * The fields the JSON request of each operation needs: the required 
properties of its schema, following a $ref to
+     * the schemas of the components. By specification, found and keyed as 
{@link #operations} does; an operation
+     * without a JSON request, or whose schema requires nothing, is left out. 
A model that is told to marshal the data
+     * of a route sends it as it is, which an operation rarely takes.
+     */
+    public static Map<String, Map<String, List<String>>> requestFields(String 
content, Path directory) {
+        Map<String, Map<String, List<String>>> answer = new LinkedHashMap<>();
+        if (content == null || directory == null) {
+            return answer;
+        }
+        for (String spec : specs(content)) {
+            Map<?, ?> root = spec.contains("://") || spec.contains("{{") ? 
null : document(directory, spec);
+            if (root == null || !(root.get("paths") instanceof Map<?, ?> 
paths)) {
+                continue;
+            }
+            Map<String, List<String>> fields = new LinkedHashMap<>();
+            for (Object path : paths.values()) {
+                if (path instanceof Map<?, ?> operations) {
+                    for (Object operation : operations.values()) {
+                        if (operation instanceof Map<?, ?> details && 
details.get("operationId") != null) {
+                            List<String> required = required(root, 
jsonSchema(details));
+                            if (!required.isEmpty()) {
+                                
fields.put(String.valueOf(details.get("operationId")), required);
+                            }
+                        }
+                    }
+                }
+            }
+            if (!fields.isEmpty()) {
+                answer.put(spec, fields);
+            }
+        }
+        return answer;
+    }
+
+    /** The specifications a file uses, as {@link #operations} finds them. */
+    private static Set<String> specs(String content) {
+        Set<String> specs = new LinkedHashSet<>();
+        for (Pattern pattern : List.of(SPECIFICATION, REST_OPENAPI_CALL, 
SPECIFICATION_URI)) {
+            Matcher m = pattern.matcher(content);
+            while (m.find()) {
+                specs.add(ProjectFacts.specName(m.group(1)));
+            }
+        }
+        if (content.contains("rest-openapi")) {
+            specs.add(ProjectFacts.specName(null));
+        }
+        return specs;
+    }
+
+    /** The schema of the JSON request of an operation, or null. */
+    private static Object jsonSchema(Map<?, ?> operation) {
+        if (operation.get("requestBody") instanceof Map<?, ?> body && 
body.get("content") instanceof Map<?, ?> content
+                && content.get("application/json") instanceof Map<?, ?> json) {
+            return json.get("schema");
+        }
+        return null;
+    }
+
+    /** The required properties of a schema, following a $ref to the schemas 
of the components. */
+    private static List<String> required(Map<?, ?> root, Object schema) {
+        if (schema instanceof Map<?, ?> s && s.get("$ref") instanceof String 
ref
+                && ref.startsWith("#/components/schemas/")
+                && root.get("components") instanceof Map<?, ?> components
+                && components.get("schemas") instanceof Map<?, ?> schemas) {
+            schema = 
schemas.get(ref.substring("#/components/schemas/".length()));
+        }
+        List<String> answer = new ArrayList<>();
+        if (schema instanceof Map<?, ?> s && s.get("required") instanceof 
List<?> required) {
+            for (Object field : required) {
+                answer.add(String.valueOf(field));
+            }
+        }
+        return answer;
+    }
+
     /** The paths of a specification beside the route, or in 
src/main/resources; null when it cannot be read. */
     private static Map<?, ?> paths(Path directory, String file) {
+        Map<?, ?> root = document(directory, file);
+        return root != null && root.get("paths") instanceof Map<?, ?> paths ? 
paths : null;
+    }
+
+    /** A specification beside the route, or in src/main/resources; null when 
it cannot be read. */
+    private static Map<?, ?> document(Path directory, String file) {
         Path p = directory.resolve(file);
         if (!Files.isRegularFile(p)) {
             p = directory.resolve("src/main/resources").resolve(file);
         }
         try {
-            return Files.isRegularFile(p) ? paths(Files.readString(p)) : null;
+            return Files.isRegularFile(p) ? document(Files.readString(p)) : 
null;
         } catch (Exception e) {
             // an unreadable specification says nothing
             return null;
@@ -146,6 +220,12 @@ public final class OpenApiVerbs {
     }
 
     static Map<?, ?> paths(String text) {
+        Map<?, ?> root = document(text);
+        return root != null && root.get("paths") instanceof Map<?, ?> paths ? 
paths : null;
+    }
+
+    /** A specification as JSON or YAML; null when it cannot be read. */
+    private static Map<?, ?> document(String text) {
         try {
             Object document;
             if (text.stripLeading().startsWith("{")) {
@@ -153,7 +233,7 @@ public final class OpenApiVerbs {
             } else {
                 document = new Yaml(new SafeConstructor(new 
LoaderOptions())).load(text);
             }
-            return document instanceof Map<?, ?> root && root.get("paths") 
instanceof Map<?, ?> paths ? paths : null;
+            return document instanceof Map<?, ?> root ? root : null;
         } catch (Exception e) {
             // Ignore unreadable or unparseable JSON or YAML specifications.
             return null;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
index 601e7837a51a..a3ff7ef6b033 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
@@ -54,7 +54,7 @@ final class ValidatorFacts {
         }
         return new ProjectFacts(
                 OpenApiVerbs.bodylessEndpoints(content, directory), 
OpenApiVerbs.operations(content, directory),
-                restBindingMode(directory));
+                restBindingMode(directory), 
OpenApiVerbs.requestFields(content, directory));
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
index f20581ceb595..f720e453607b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
@@ -110,6 +110,70 @@ class ValidatorFactsTest {
         assertThat(OpenApiVerbs.operations("- to: direct:x\n", dir)).isEmpty();
     }
 
+    @Test
+    void theFieldsTheJsonRequestOfAnOperationNeeds(@TempDir Path dir) throws 
Exception {
+        // a $ref to the schemas of the components, and an inline schema
+        Files.writeString(dir.resolve("stock-api.json"), """
+                {
+                  "openapi": "3.0.2",
+                  "paths": {
+                    "/stock/{sku}": { "get": { "operationId": "getStock", 
"responses": { "200": { "description": "ok" } } } },
+                    "/stock/{sku}/reserve": { "post": { "operationId": 
"reserveStock",
+                      "requestBody": { "required": true, "content": { 
"application/json": {
+                        "schema": { "$ref": "#/components/schemas/Reservation" 
} } } },
+                      "responses": { "200": { "description": "ok" } } } }
+                  },
+                  "components": { "schemas": { "Reservation": { "type": 
"object", "required": ["orderId", "qty"],
+                    "properties": { "orderId": { "type": "string" }, "qty": { 
"type": "integer" } } } } }
+                }
+                """);
+        Files.writeString(dir.resolve("orders-api.yaml"), """
+                openapi: 3.0.2
+                paths:
+                  /orders:
+                    post:
+                      operationId: createOrder
+                      requestBody:
+                        content:
+                          application/json:
+                            schema:
+                              type: object
+                              required: [customer]
+                      responses:
+                        '201':
+                          description: created
+                """);
+        String content = "- to: rest-openapi:stock-api.json#reserveStock\n- 
to: rest-openapi:orders-api.yaml#createOrder\n";
+
+        assertThat(OpenApiVerbs.requestFields(content, dir)).isEqualTo(Map.of(
+                "stock-api.json", Map.of("reserveStock", List.of("orderId", 
"qty")),
+                "orders-api.yaml", Map.of("createOrder", 
List.of("customer"))));
+        // a specification without request schemas, such as STOCK_API, gives 
none
+        Files.writeString(dir.resolve("stock-api.json"), STOCK_API);
+        assertThat(OpenApiVerbs.requestFields(content, 
dir)).containsOnlyKeys("orders-api.yaml");
+    }
+
+    @Test
+    void theHintNamesTheFieldsOfTheRequest(@TempDir Path dir) throws Exception 
{
+        Files.writeString(dir.resolve("stock-api.json"), """
+                {
+                  "openapi": "3.0.2",
+                  "paths": {
+                    "/reservations": { "post": { "operationId": "reserveStock",
+                      "requestBody": { "content": { "application/json": {
+                        "schema": { "$ref": "#/components/schemas/Reservation" 
} } } },
+                      "responses": { "201": { "description": "ok" } } } }
+                  },
+                  "components": { "schemas": { "Reservation": { "required": 
["orderId", "qty"] } } }
+                }
+                """);
+
+        List<String> msgs = SourceValidator.validate("orders.camel.yaml", 
RESERVE, new DefaultCamelCatalog(), null, dir);
+        assertThat(msgs).anyMatch(m -> m.contains("set the body to the JSON 
text of the request before this step"
+                                                  + " (it needs orderId, qty), 
such as with setBody and simple:"
+                                                  + " '{\"orderId\": ..., 
\"qty\": ...}'"));
+    }
+
     @Test
     void theRestBindingModeOfTheProperties(@TempDir Path dir) throws Exception 
{
         assertThat(ValidatorFacts.restBindingMode(dir)).isEqualTo("off");
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
index 865cbfe5c378..a45603d158bb 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.yaml.validator;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -30,9 +31,11 @@ import java.util.Set;
  *                          operationId, in lower case
  * @param restBindingMode   the binding mode the project's properties set for 
REST ({@code off} when they set none), or
  *                          null when not known: then nothing is said that a 
binding could change
+ * @param requestFields     per specification, keyed as {@link #operations()} 
is: the fields the JSON request of an
+ *                          operationId needs, for the operations whose 
request schema requires some
  */
 public record ProjectFacts(Set<String> bodylessEndpoints, Map<String, 
Map<String, String>> operations,
-        String restBindingMode) {
+        String restBindingMode, Map<String, Map<String, List<String>>> 
requestFields) {
 
     /** Nothing known about the project. */
     public static final ProjectFacts NONE = new ProjectFacts(Set.of(), 
Map.of(), null);
@@ -40,6 +43,13 @@ public record ProjectFacts(Set<String> bodylessEndpoints, 
Map<String, Map<String
     public ProjectFacts {
         bodylessEndpoints = bodylessEndpoints != null ? bodylessEndpoints : 
Set.of();
         operations = operations != null ? operations : Map.of();
+        requestFields = requestFields != null ? requestFields : Map.of();
+    }
+
+    /** Without the fields of the requests. */
+    public ProjectFacts(Set<String> bodylessEndpoints, Map<String, Map<String, 
String>> operations,
+                        String restBindingMode) {
+        this(bodylessEndpoints, operations, restBindingMode, Map.of());
     }
 
     /**
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
index 241d056acf53..56a396ce1adf 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
@@ -181,14 +181,15 @@ final class TextBodyFlow {
     /**
      * What a walk knows besides the body: the route, the routes of the file, 
where reports go (null for a route a step
      * calls, which reports from its own walk), whether an http producer sends 
the body with POST, and the verbs of the
-     * OpenAPI operations a rest-openapi call can be judged by (empty when a 
REST binding may write the body).
+     * OpenAPI operations a rest-openapi call can be judged by (empty when a 
REST binding may write the body), with the
+     * fields their JSON requests need.
      */
     private record Walk(String routeId, Map<String, JsonNode> byFrom, 
List<Error> errors, Set<String> seen, boolean post,
-            Map<String, Map<String, String>> operations) {
+            Map<String, Map<String, String>> operations, Map<String, 
Map<String, List<String>>> requestFields) {
 
         /** The walk of a route a step calls: for its outcome alone. */
         Walk called() {
-            return new Walk(null, byFrom, null, seen, false, operations);
+            return new Walk(null, byFrom, null, seen, false, operations, 
requestFields);
         }
     }
 
@@ -199,6 +200,7 @@ final class TextBodyFlow {
         // a rest-openapi call and a REST route can only be judged when no 
REST binding can write the data
         boolean unbound = restUnbound(target, facts);
         Map<String, Map<String, String>> operations = unbound ? 
facts.operations() : Map.of();
+        Map<String, Map<String, List<String>>> requestFields = unbound ? 
facts.requestFields() : Map.of();
         Set<String> restReplies = unbound ? restReplies(target, facts) : 
Set.of();
         Map<String, JsonNode> byFrom = new HashMap<>();
         for (JsonNode entry : target) {
@@ -231,7 +233,9 @@ final class TextBodyFlow {
             String uri = endpointOf(from);
             String consumer = scheme(uri);
             boolean post = NO_HTTP_HEADERS.contains(consumer) && 
!setsHttpHeaders(steps);
-            Body end = walk(steps, consumerText(from), at, new Walk(id, 
byFrom, errors, new HashSet<>(), post, operations));
+            Body end = walk(steps, consumerText(from), at, new Walk(
+                    id, byFrom, errors, new HashSet<>(), post, operations,
+                    requestFields));
             // the body at the end of a platform-http route, or of the route 
of a REST call, is the reply
             boolean rest = uri != null && restReplies.contains(normalize(uri));
             if (("platform-http".equals(consumer) || rest) && end instanceof 
Parsed parsed && steps != null
@@ -565,12 +569,17 @@ final class TextBodyFlow {
     }
 
     /**
-     * What a step that needs the body as text does with it, and what to 
write; rest when a REST binding could write it.
+     * What a step that needs the body as text does with it, and what to 
write; rest when a REST binding could write it;
+     * and for the call of an OpenAPI operation, the fields its request needs 
(null for any other step).
      */
-    private record Need(String what, String fix, boolean rest) {
+    private record Need(String what, String fix, boolean rest, List<String> 
request) {
 
         Need(String what, String fix) {
-            this(what, fix, false);
+            this(what, fix, false, null);
+        }
+
+        Need(String what, String fix, boolean rest) {
+            this(what, fix, rest, null);
         }
     }
 
@@ -607,10 +616,12 @@ final class TextBodyFlow {
                     ? w.operations().getOrDefault(operation.spec(), 
Map.of()).get(operation.id())
                     : null;
             if (verb != null && 
SENDS_A_BODY.contains(verb.toUpperCase(Locale.ROOT))) {
+                List<String> fields
+                        = w.requestFields().getOrDefault(operation.spec(), 
Map.of()).getOrDefault(operation.id(), List.of());
                 return new Need(
                         name + ": rest-openapi sends the body as the request 
of the operation " + operation.id() + " ("
                                 + verb.toUpperCase(Locale.ROOT) + " in " + 
operation.spec() + ")",
-                        "before this step", true);
+                        "before this step", true, fields);
             }
         }
         return null;
@@ -899,7 +910,27 @@ final class TextBodyFlow {
                 .format(new MessageFormat("{0}"))
                 .arguments((routeId != null ? "route " + routeId + ": " : "") 
+ need.what()
                            + ", but the body here is parsed data (a Map or a 
List), not text - " + parsed.origin()
-                           + ": add marshal: " + parsed.format() + " " + 
need.fix() + restBinding(need, parsed))
+                           + ": " + fix(need, parsed))
                 .build();
     }
+
+    /**
+     * What to write for parsed data where text is needed. The request of an 
operation is rarely the data as it is, such
+     * as one line of an order that the request needs the id of the order for: 
told to marshal, a model sends the data
+     * as it is. So the JSON text of the request comes first, with the fields 
the request needs.
+     */
+    private static String fix(Need need, Parsed parsed) {
+        String marshal = "add marshal: " + parsed.format() + " " + need.fix();
+        if (need.request() == null || !"json".equals(parsed.format())) {
+            return marshal + restBinding(need, parsed);
+        }
+        List<String> fields = need.request();
+        String example = fields.isEmpty()
+                ? "{\"name\": \"${body[name]}\"}"
+                : "{" + String.join(", ", fields.stream().map(f -> "\"" + f + 
"\": ...").toList()) + "}";
+        return "set the body to the JSON text of the request " + need.fix()
+               + (fields.isEmpty() ? "" : " (it needs " + String.join(", ", 
fields) + ")")
+               + ", such as with setBody and simple: '" + example + "', or " + 
marshal
+               + " when the data has the fields of the request" + 
restBinding(need, parsed);
+    }
 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
index 09940ee1f4a3..e961585b3b6d 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
@@ -597,7 +597,11 @@ public class TextBodyFlowTest {
             assertThat(m).startsWith("Line 9: ");
             assertThat(m).contains("route reserve: to: rest-openapi sends the 
body as the request of the operation"
                                    + " reserveStock (POST in stock-api.json)");
-            assertThat(m).contains("add marshal: json before this step, or let 
the REST binding write it (bindingMode: json)");
+            // the JSON text of the request first: the data is rarely the 
request as it is
+            assertThat(m).contains("unmarshal: json turns the text into data: 
set the body to the JSON text of the request"
+                                   + " before this step, such as with setBody 
and simple: '{\"name\": \"${body[name]}\"}',"
+                                   + " or add marshal: json before this step 
when the data has the fields of the request,"
+                                   + " or let the REST binding write it 
(bindingMode: json)");
         });
         // the same operation named by options, or with the specification as a 
file: resource
         String options = """
@@ -654,6 +658,42 @@ public class TextBodyFlowTest {
         assertThat(lines(yaml.replace("expression: \"$.sku\"", "expression: 
\"$\""), STOCK_API)).isEmpty();
     }
 
+    @Test
+    public void testRestOpenApiCallNamesTheFieldsOfTheRequest() {
+        // r19 of the benchmark: told to marshal, the model sent each line as 
JSON, without the orderId of the request
+        String yaml = """
+                - route:
+                    id: reserve-order-lines
+                    from:
+                      uri: file:orders
+                      steps:
+                        - unmarshal:
+                            json: {}
+                        - split:
+                            expression:
+                              simple: "${body[lines]}"
+                            steps:
+                              - setHeader:
+                                  name: sku
+                                  expression:
+                                    simple: "${body[sku]}"
+                              - to:
+                                  uri: rest-openapi
+                                  parameters:
+                                    specificationUri: stock-api.json
+                                    operationId: reserveStock
+                """;
+        ProjectFacts facts = new ProjectFacts(
+                Set.of(), STOCK_API.operations(), "off",
+                Map.of("stock-api.json", Map.of("reserveStock", 
List.of("orderId", "qty"))));
+        assertThat(lines(yaml, facts)).singleElement().satisfies(m -> {
+            assertThat(m).contains("rest-openapi sends the body as the request 
of the operation reserveStock");
+            assertThat(m).contains("set the body to the JSON text of the 
request before this step (it needs orderId, qty),"
+                                   + " such as with setBody and simple: 
'{\"orderId\": ..., \"qty\": ...}', or add marshal:"
+                                   + " json before this step when the data has 
the fields of the request");
+        });
+    }
+
     @Test
     public void testRestOpenApiCallsThatAreFine() {
         String post = """

Reply via email to