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 fd00fc52ab9e CAMEL-25928: camel-jbang - the body of a Kamelet from its
data types (#28450)
fd00fc52ab9e is described below
commit fd00fc52ab9ea1f702119060ddc2a5c8c76a6436
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 15:25:26 2026 +0100
CAMEL-25928: camel-jbang - the body of a Kamelet from its data types
(#28450)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../user-manual/modules/ROOT/pages/body-types.adoc | 22 ++++
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 3 +-
.../dsl/jbang/core/commands/ai/CatalogDocs.java | 60 ++++++++-
.../jbang/core/commands/ai/KameletDefinitions.java | 63 +++++++++-
.../dsl/jbang/core/commands/ai/ValidatorFacts.java | 39 +++++-
.../jbang/core/commands/catalog/CatalogDoc.java | 2 +-
.../commands/catalog/KameletCatalogHelper.java | 41 +++++++
...KameletModel.java => KameletDataTypeModel.java} | 20 +--
.../jbang/core/commands/catalog/KameletModel.java | 3 +
.../core/commands/ai/KameletDataTypesTest.java | 134 +++++++++++++++++++++
.../dsl/jbang/core/commands/mcp/KameletTools.java | 37 +++++-
.../jbang/core/commands/mcp/KameletToolsTest.java | 17 +++
.../camel/dsl/yaml/validator/CatalogBodyTypes.java | 21 +++-
.../camel/dsl/yaml/validator/ProjectFacts.java | 20 ++-
.../camel/dsl/yaml/validator/TextBodyFlow.java | 81 ++++++++++++-
.../camel/dsl/yaml/validator/TextBodyFlowTest.java | 74 ++++++++++++
16 files changed, 611 insertions(+), 26 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/body-types.adoc
b/docs/user-manual/modules/ROOT/pages/body-types.adoc
index 4daf9d2e38e1..53b77f8c6824 100644
--- a/docs/user-manual/modules/ROOT/pages/body-types.adoc
+++ b/docs/user-manual/modules/ROOT/pages/body-types.adoc
@@ -240,6 +240,28 @@ writes any object as JSON bytes, and the CloudEvent
transformers of the componen
`transformDataType` takes its body from them, by the name of its `toType`:
`aws2-s3:application-cloudevents` is the
transformer `aws2-s3-application-cloudevents` of the catalog.
+=== Kamelets
+
+A Kamelet says the body it takes and gives with its data types
(`spec.dataTypes`), whose `format` names a data type
+transformer. Camel runs the transformer of the default `in` data type at the
start of the Kamelet route, and of the
+default `out` one at its end, so the body of a Kamelet is what that
transformer takes and leaves:
+
+[source,yaml]
+----
+spec:
+ dataTypes:
+ out:
+ default: binary
+ types:
+ binary:
+ format: application-octet-stream
+----
+
+The Camel CLI reads the data types of the Kamelets of the Kamelet catalog and
of the project's own `.kamelet.yaml`
+files. The YAML validator walks a route that starts with `from:
kamelet:aws-s3-source` with bytes. It reports a body
+that a Kamelet sink cannot take before a `to: kamelet:...`.
`camel_catalog_kamelet_doc` lists the data types of a
+Kamelet with the body of each. A Kamelet without data types says nothing about
its body.
+
== Tooling
=== Documentation
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 15dae476ac2a..a90afeabb2b0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -536,7 +536,8 @@ client to the next.
| `camel_catalog_kamelet_doc`
| Get detailed documentation for a specific Kamelet including all
properties/options, their types, defaults,
- examples, and the Kamelet's Maven dependencies.
+ examples, and the Kamelet's Maven dependencies. For a Kamelet with data
types, each data type with its data type
+ transformer and the body it takes or gives (see xref:body-types.adoc[Message
Body Types]).
|===
=== Example Catalog
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 6e99652f6eeb..9fee53d39d85 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -36,6 +36,7 @@ import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.catalog.EndpointValidationResult;
import org.apache.camel.catalog.impl.EditDistanceSuggestionStrategy;
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletDataTypeModel;
import org.apache.camel.tooling.model.ApiReferenceModel;
import org.apache.camel.tooling.model.BaseModel;
import org.apache.camel.tooling.model.BaseOptionModel;
@@ -46,6 +47,7 @@ import org.apache.camel.tooling.model.EipModel;
import org.apache.camel.tooling.model.JsonMapper;
import org.apache.camel.tooling.model.LanguageModel;
import org.apache.camel.tooling.model.PojoBeanModel;
+import org.apache.camel.tooling.model.TransformerModel;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
@@ -443,7 +445,7 @@ public final class CatalogDocs {
if (kind == null || "kamelet".equals(kind)) {
KameletDefinitions.Definition def =
KameletDefinitions.catalog().get(name);
if (def != null) {
- return kameletDoc(def);
+ return kameletDoc(def, catalog);
}
if (kind != null) {
JsonObject err = notFound("Kamelet", name,
@@ -553,6 +555,14 @@ public final class CatalogDocs {
* bootstrapServers), as it knows those from years of examples.
*/
public static JsonObject kameletDoc(KameletDefinitions.Definition def) {
+ return kameletDoc(def, null);
+ }
+
+ /**
+ * As {@link #kameletDoc(KameletDefinitions.Definition)}, with the body
each data type of the Kamelet takes or
+ * gives, as the body types of its data type transformer in the catalog
say (CAMEL-25928).
+ */
+ public static JsonObject kameletDoc(KameletDefinitions.Definition def,
CamelCatalog catalog) {
JsonObject result = new JsonObject();
result.put("kind", "kamelet");
result.put("name", def.name());
@@ -595,9 +605,57 @@ public final class CatalogDocs {
props.add(o);
}
result.put("properties", props);
+ if (!def.dataTypes().isEmpty()) {
+ result.put("dataTypes", kameletDataTypes(def, catalog));
+ }
return result;
}
+ /**
+ * The data types of a Kamelet: the data type transformer of each
(format), whether it is the default Camel uses,
+ * and the body it takes (in) or gives (out).
+ */
+ private static JsonArray kameletDataTypes(KameletDefinitions.Definition
def, CamelCatalog catalog) {
+ JsonArray answer = new JsonArray();
+ for (KameletDataTypeModel dt : def.dataTypes()) {
+ JsonObject o = new JsonObject();
+ o.put("direction", dt.direction);
+ o.put("name", dt.name);
+ if (dt.defaultType) {
+ o.put("default", true);
+ }
+ if (dt.format != null) {
+ o.put("format", dt.format);
+ }
+ if (dt.mediaType != null) {
+ o.put("mediaType", dt.mediaType);
+ }
+ if (dt.description != null) {
+ o.put("description", dt.description);
+ }
+ String side = "in".equals(dt.direction) ?
BodyTypeModel.TRANSFORMER_IN
+ : "out".equals(dt.direction) ?
BodyTypeModel.TRANSFORMER_OUT : null;
+ TransformerModel transformer = catalog != null && side != null &&
dt.format != null
+ ? catalog.transformerModel(transformerName(dt.format)) :
null;
+ BodyTypeModel body = transformer != null ?
transformer.getBodyType(side) : null;
+ if (body != null) {
+ o.put("body", bodyTypes(Map.of(side, body)).getString(side));
+ }
+ answer.add(o);
+ }
+ return answer;
+ }
+
+ /**
+ * The name of the data type transformer of a data type, as Camel resolves
it (TransformerResolver): without the
+ * camel scheme, and with : _ . / and \ as -, so
aws2-s3:application-cloudevents is aws2-s3-application-cloudevents.
+ */
+ static String transformerName(String dataType) {
+ String name = dataType.startsWith("camel:") ?
dataType.substring("camel:".length()) : dataType;
+ return name.replace(':', '-').replace('_', '-').replace('.',
'-').replace('/', '-').replace('\\', '-')
+ .toLowerCase(Locale.ROOT);
+ }
+
/**
* The documentation of the EIP a term such as an alias (fan-out, dedup,
rate-limit) or its whole title names, with
* the term the caller used; null when no EIP matches. A single word of
the title is not enough: retry named
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
index 7bd79e87574a..f8a30a69f9d4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
@@ -33,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletDataTypeModel;
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletModel;
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletOptionModel;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
@@ -48,9 +49,32 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
*/
public final class KameletDefinitions {
- /** A Kamelet and its properties. */
+ /** A Kamelet, its properties, and its data types (spec.dataTypes). */
public record Definition(String name, String type, String title, String
description, String source,
- List<Property> properties) {
+ List<Property> properties, List<KameletDataTypeModel> dataTypes) {
+
+ public Definition {
+ dataTypes = dataTypes != null ? List.copyOf(dataTypes) : List.of();
+ }
+
+ /** Without data types. */
+ public Definition(String name, String type, String title, String
description, String source,
+ List<Property> properties) {
+ this(name, type, title, description, source, properties,
List.of());
+ }
+
+ /**
+ * The data type transformer of the default data type of a direction
(in or out), which Camel runs at the start
+ * or the end of the Kamelet route; null when the Kamelet has none.
+ */
+ public String defaultFormat(String direction) {
+ for (KameletDataTypeModel dt : dataTypes) {
+ if (dt.direction.equals(direction) && dt.defaultType) {
+ return dt.format;
+ }
+ }
+ return null;
+ }
public Property property(String name) {
for (Property p : properties) {
@@ -133,7 +157,7 @@ public final class KameletDefinitions {
om.enumValues != null ? om.enumValues : List.of()));
}
}
- return new Definition(km.name, km.type, null, km.description, source,
props);
+ return new Definition(km.name, km.type, null, km.description, source,
props, km.dataTypes);
}
/** The project's own Kamelets: the *.kamelet.yaml files of the directory
and its subdirectories. */
@@ -228,7 +252,38 @@ public final class KameletDefinitions {
}
}
}
- return new Definition(name, type, title, description, "the project
file " + fileName, props);
+ List<KameletDataTypeModel> dataTypes = map.get("spec") instanceof
Map<?, ?> spec
+ ? dataTypes(spec.get("dataTypes")) : List.of();
+ return new Definition(name, type, title, description, "the project
file " + fileName, props, dataTypes);
+ }
+
+ /** The data types of a Kamelet file, as the Kamelet catalog reads them
(KameletCatalogHelper). */
+ private static List<KameletDataTypeModel> dataTypes(Object node) {
+ List<KameletDataTypeModel> answer = new ArrayList<>();
+ if (!(node instanceof Map<?, ?> directions)) {
+ return answer;
+ }
+ for (Map.Entry<?, ?> direction : directions.entrySet()) {
+ if (!(direction.getValue() instanceof Map<?, ?> dt) ||
!(dt.get("types") instanceof Map<?, ?> types)) {
+ continue;
+ }
+ for (Map.Entry<?, ?> type : types.entrySet()) {
+ if (!(type.getValue() instanceof Map<?, ?> t)) {
+ continue;
+ }
+ String scheme = t.get("scheme") instanceof String sc ? sc :
null;
+ String format = t.get("format") instanceof String f ? f : null;
+ KameletDataTypeModel model = new KameletDataTypeModel();
+ model.direction = String.valueOf(direction.getKey());
+ model.name = String.valueOf(type.getKey());
+ model.format = format != null && scheme != null ? scheme + ":"
+ format : format;
+ model.mediaType = t.get("mediaType") instanceof String mt ? mt
: null;
+ model.description = t.get("description") instanceof String d ?
d : null;
+ model.defaultType =
model.name.equals(String.valueOf(dt.get("default")));
+ answer.add(model);
+ }
+ }
+ return answer;
}
/**
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 735b9a15ffc9..d19e418afd03 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
@@ -19,8 +19,11 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -53,13 +56,45 @@ final class ValidatorFacts {
* the file saw at the endpoints of its routes (CAMEL-25750).
*/
static ProjectFacts read(String fileName, String content, Path directory) {
- if (content == null || directory == null) {
+ if (content == null) {
return ProjectFacts.NONE;
}
+ Map<String, ProjectFacts.KameletDataTypes> kamelets =
kameletDataTypes(content, directory);
+ if (directory == null) {
+ return new ProjectFacts(Set.of(), Map.of(), null, Map.of(),
Map.of(), kamelets);
+ }
return new ProjectFacts(
OpenApiVerbs.bodylessEndpoints(content, directory),
OpenApiVerbs.operations(content, directory),
restBindingMode(directory),
OpenApiVerbs.requestFields(content, directory),
- ObservedBodies.read(fileName, directory));
+ ObservedBodies.read(fileName, directory), kamelets);
+ }
+
+ /**
+ * The data types of the Kamelets of the catalog and of the project that
declare them (CAMEL-25928): the data type
+ * transformers of their default in and out data types, which say the body
a Kamelet takes and gives. Only read when
+ * the file uses a Kamelet.
+ */
+ static Map<String, ProjectFacts.KameletDataTypes> kameletDataTypes(String
content, Path directory) {
+ if (!content.contains("kamelet")) {
+ return Map.of();
+ }
+ Map<String, ProjectFacts.KameletDataTypes> answer = new HashMap<>();
+ List<Map<String, KameletDefinitions.Definition>> sources = new
ArrayList<>();
+ sources.add(KameletDefinitions.catalog());
+ if (directory != null) {
+ // the project's own Kamelets win over those of the catalog
+ sources.add(KameletDefinitions.projectKamelets(directory));
+ }
+ for (Map<String, KameletDefinitions.Definition> definitions : sources)
{
+ for (KameletDefinitions.Definition def : definitions.values()) {
+ String in = def.defaultFormat("in");
+ String out = def.defaultFormat("out");
+ if (in != null || out != null) {
+ answer.put(def.name(), new
ProjectFacts.KameletDataTypes(in, out));
+ }
+ }
+ }
+ return answer;
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
index 3c18872c28b8..fd1d56267327 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
@@ -298,7 +298,7 @@ public class CatalogDoc extends CamelCommand {
// the Kamelets of the version and repositories asked for, not
only the ones of the CLI
KameletModel km = KameletCatalogHelper.loadKameletModel(name,
kameletsVersion, mavenResolver.repos());
if (km != null) {
- return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the
Kamelet catalog " + kameletsVersion));
+ return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the
Kamelet catalog " + kameletsVersion), catalog);
}
}
return null;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
index 40d55c6d8c7d..492fbf3a32cd 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
@@ -51,6 +51,7 @@ public final class KameletCatalogHelper {
km.description = getDescription(kamelet);
if (all) {
km.dependencies = getDependencies(kamelet);
+ km.dataTypes = getDataTypes(kamelet);
Map<String, Object> props = getProperties(kamelet);
if (props != null) {
@@ -267,6 +268,46 @@ public final class KameletCatalogHelper {
return (Map<String, Object>) ObjectHelper.invokeMethod(m, def);
}
+ /**
+ * The data types of the Kamelet (spec.dataTypes): per direction, its
types, with the default marked. The format of
+ * a type is its scheme and format as Camel joins them to the name of the
data type transformer
+ * (KameletRoutesBuilderLoader): aws2-s3:application-cloudevents.
+ */
+ @SuppressWarnings("unchecked")
+ private static List<KameletDataTypeModel> getDataTypes(Object kamelet)
throws Exception {
+ List<KameletDataTypeModel> answer = new ArrayList<>();
+ Method m = kamelet.getClass().getMethod("getSpec");
+ Object spec = ObjectHelper.invokeMethod(m, kamelet);
+ m = spec.getClass().getMethod("getDataTypes");
+ Map<String, Object> dataTypes = (Map<String, Object>)
ObjectHelper.invokeMethod(m, spec);
+ if (dataTypes == null) {
+ return answer;
+ }
+ for (Map.Entry<String, Object> direction : dataTypes.entrySet()) {
+ Object dt = direction.getValue();
+ String defaultType = (String)
ObjectHelper.invokeMethod(dt.getClass().getMethod("get_default"), dt);
+ Map<String, Object> types
+ = (Map<String, Object>)
ObjectHelper.invokeMethod(dt.getClass().getMethod("getTypes"), dt);
+ if (types == null) {
+ continue;
+ }
+ for (Map.Entry<String, Object> type : types.entrySet()) {
+ Object t = type.getValue();
+ String scheme = (String)
ObjectHelper.invokeMethod(t.getClass().getMethod("getScheme"), t);
+ String format = (String)
ObjectHelper.invokeMethod(t.getClass().getMethod("getFormat"), t);
+ KameletDataTypeModel model = new KameletDataTypeModel();
+ model.direction = direction.getKey();
+ model.name = type.getKey();
+ model.format = format != null && scheme != null ? scheme + ":"
+ format : format;
+ model.mediaType = (String)
ObjectHelper.invokeMethod(t.getClass().getMethod("getMediaType"), t);
+ model.description = (String)
ObjectHelper.invokeMethod(t.getClass().getMethod("getDescription"), t);
+ model.defaultType = type.getKey().equals(defaultType);
+ answer.add(model);
+ }
+ }
+ return answer;
+ }
+
private static List<String> getDependencies(Object kamelet) throws
Exception {
List<String> answer = new ArrayList<>();
Method m = kamelet.getClass().getMethod("getSpec");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletDataTypeModel.java
similarity index 61%
copy from
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
copy to
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletDataTypeModel.java
index 28a4b7965260..6ceadfd9aede 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletDataTypeModel.java
@@ -16,16 +16,20 @@
*/
package org.apache.camel.dsl.jbang.core.commands.catalog;
-import java.util.List;
-import java.util.Map;
-
-public class KameletModel {
+/**
+ * A data type of a Kamelet (spec.dataTypes): its format names the data type
transformer Camel runs for it, as
+ * application-octet-stream or aws2-s3:application-cloudevents.
+ */
+public class KameletDataTypeModel {
+ /** in, out or error. */
+ public String direction;
public String name;
- public String type;
- public String supportLevel;
+ /** The data type transformer: the scheme and the format of the type, as
Camel joins them. */
+ public String format;
+ public String mediaType;
public String description;
- public Map<String, KameletOptionModel> properties;
- public List<String> dependencies;
+ /** Whether it is the default of its direction, which Camel uses unless a
Pipe chooses another. */
+ public boolean defaultType;
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
index 28a4b7965260..58ee8111eff1 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.dsl.jbang.core.commands.catalog;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -27,5 +28,7 @@ public class KameletModel {
public String description;
public Map<String, KameletOptionModel> properties;
public List<String> dependencies;
+ /** The data types of the Kamelet (spec.dataTypes), in, out and error;
empty when it has none. */
+ public List<KameletDataTypeModel> dataTypes = new ArrayList<>();
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDataTypesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDataTypesTest.java
new file mode 100644
index 000000000000..4d6b0cdfa33b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDataTypesTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The data types of a Kamelet say the body it takes and gives, as the body
types of their data type transformers in the
+ * catalog say (CAMEL-25928).
+ */
+class KameletDataTypesTest {
+
+ private static final CamelCatalog CATALOG = new DefaultCamelCatalog();
+
+ private static final String MY_SOURCE = """
+ apiVersion: camel.apache.org/v1
+ kind: Kamelet
+ metadata:
+ name: my-source
+ labels:
+ camel.apache.org/kamelet.type: source
+ spec:
+ definition:
+ title: My Source
+ dataTypes:
+ out:
+ default: binary
+ types:
+ binary:
+ format: application-octet-stream
+ mediaType: application/octet-stream
+ cloudevents:
+ scheme: aws2-s3
+ format: application-cloudevents
+ description: The CloudEvent headers
+ template:
+ from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ constant: hello
+ - to: kamelet:sink
+ """;
+
+ @BeforeAll
+ static void setUp() {
+ // no Kamelet catalog: the project's own Kamelets only
+ KameletDefinitions.setTestCatalog(Map.of());
+ }
+
+ @AfterAll
+ static void tearDown() {
+ KameletDefinitions.setTestCatalog(null);
+ }
+
+ @Test
+ void theDataTypesOfAProjectKamelet(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("my-source.kamelet.yaml"), MY_SOURCE);
+ KameletDefinitions.Definition def =
KameletDefinitions.projectKamelets(dir).get("my-source");
+ assertThat(def.dataTypes()).hasSize(2);
+
assertThat(def.defaultFormat("out")).isEqualTo("application-octet-stream");
+ assertThat(def.defaultFormat("in")).isNull();
+ // the scheme and the format name the transformer, as Camel joins them
+
assertThat(def.dataTypes().get(1).format).isEqualTo("aws2-s3:application-cloudevents");
+ assertThat(def.dataTypes().get(1).defaultType).isFalse();
+ }
+
+ @Test
+ void theDocSaysTheBodyOfEachDataType(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("my-source.kamelet.yaml"), MY_SOURCE);
+ JsonObject doc =
CatalogDocs.kameletDoc(KameletDefinitions.projectKamelets(dir).get("my-source"),
CATALOG);
+ JsonArray dataTypes = (JsonArray) doc.get("dataTypes");
+ assertThat(dataTypes).hasSize(2);
+ JsonObject binary = (JsonObject) dataTypes.get(0);
+ assertThat(binary.getString("direction")).isEqualTo("out");
+
assertThat(binary.getString("format")).isEqualTo("application-octet-stream");
+ assertThat(binary.getBoolean("default")).isTrue();
+ assertThat(binary.getString("body")).startsWith("bytes");
+ assertThat(((JsonObject)
dataTypes.get(1)).getString("body")).startsWith("unchanged");
+ // without the catalog the doc has the data types, but not their body
+ JsonObject plain =
CatalogDocs.kameletDoc(KameletDefinitions.projectKamelets(dir).get("my-source"));
+ assertThat(((JsonObject) ((JsonArray)
plain.get("dataTypes")).get(0)).get("body")).isNull();
+ }
+
+ @Test
+ void theValidatorWalksTheBodyOfTheDataType(@TempDir Path dir) throws
Exception {
+ Files.writeString(dir.resolve("my-source.kamelet.yaml"), MY_SOURCE);
+ String route = """
+ - route:
+ from:
+ uri: kamelet:my-source
+ steps:
+ - setHeader:
+ name: status
+ simple: "${body[status]}"
+ """;
+ List<String> messages = SourceValidator.validate("orders.camel.yaml",
route, CATALOG, null, dir);
+ assertThat(messages).anyMatch(m -> m.contains("simple reads a field of
the body (${body[status]})")
+ && m.contains("kamelet:my-source gives it as its out data type
application-octet-stream"));
+ assertThat(ValidatorFacts.kameletDataTypes(route, dir))
+ .containsKey("my-source")
+ .doesNotContainKey("other");
+ // a file that uses no Kamelet does not read them
+ assertThat(ValidatorFacts.kameletDataTypes("- route: {from: {uri:
timer:tick}}", dir)).isEmpty();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
index 278c581cc6fd..a3ed06b2630d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
@@ -23,14 +23,19 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.jbang.core.commands.ai.CatalogDocs;
+import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions;
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletModel;
import org.apache.camel.dsl.jbang.core.commands.catalog.KameletOptionModel;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
/**
* MCP Tools for querying the Kamelet Catalog using Quarkus MCP Server.
@@ -39,6 +44,9 @@ import org.apache.camel.dsl.jbang.core.common.VersionHelper;
@ApplicationScoped
public class KameletTools {
+ @Inject
+ CatalogService catalogService;
+
/**
* Cache of loaded Kamelet catalogs keyed by version string. Avoids
recreating a new downloader and class-loader on
* every request (CAMEL-25640).
@@ -158,13 +166,30 @@ public class KameletTools {
}
}
+ // the body each data type takes or gives, as its data type
transformer in the catalog says (CAMEL-25928)
+ List<KameletDataTypeInfo> dataTypes = new ArrayList<>();
+ if (km.dataTypes != null && !km.dataTypes.isEmpty()) {
+ JsonObject doc =
CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the Kamelet catalog " +
version),
+ catalogService != null ?
catalogService.getDefaultCatalog() : null);
+ if (doc.get("dataTypes") instanceof JsonArray array) {
+ for (Object o : array) {
+ JsonObject dt = (JsonObject) o;
+ dataTypes.add(new KameletDataTypeInfo(
+ dt.getString("direction"),
dt.getString("name"), dt.getString("format"),
+ dt.getBooleanOrDefault("default", false),
dt.getString("mediaType"),
+ dt.getString("description"),
dt.getString("body")));
+ }
+ }
+ }
+
return new KameletDetailResult(
km.name,
km.type,
km.supportLevel,
km.description,
options,
- km.dependencies);
+ km.dependencies,
+ dataTypes.isEmpty() ? null : dataTypes);
} catch (ToolCallException e) {
throw e;
} catch (Throwable e) {
@@ -183,7 +208,15 @@ public class KameletTools {
}
public record KameletDetailResult(String name, String type, String
supportLevel, String description,
- List<KameletOptionInfo> options, List<String> dependencies) {
+ List<KameletOptionInfo> options, List<String> dependencies,
List<KameletDataTypeInfo> dataTypes) {
+ }
+
+ /**
+ * A data type of the Kamelet: in, out or error, its data type transformer
(format), whether Camel uses it by
+ * default, and the body it takes (in) or gives (out).
+ */
+ public record KameletDataTypeInfo(String direction, String name, String
format, boolean defaultType,
+ String mediaType, String description, String body) {
}
public record KameletOptionInfo(String name, String description, String
type, boolean required,
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
index ad9c0830b9e8..c0224eeb1da7 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
@@ -122,4 +122,21 @@ class KameletToolsTest {
assertThat(r1.kamelets()).isEqualTo(r2.kamelets());
assertThat(r1.kameletsVersion()).isEqualTo(r2.kameletsVersion());
}
+
+ /** CAMEL-25928: the data types of a Kamelet, and the body their data type
transformer gives. */
+ @Test
+ void theDataTypesOfAKamelet() {
+ tools.catalogService = new CatalogService();
+ KameletTools.KameletDetailResult s3 =
tools.camel_catalog_kamelet_doc("aws-s3-source", null);
+ assertThat(s3.dataTypes()).isNotEmpty();
+ KameletTools.KameletDataTypeInfo binary = s3.dataTypes().stream()
+
.filter(KameletTools.KameletDataTypeInfo::defaultType).findFirst().orElseThrow();
+ assertThat(binary.direction()).isEqualTo("out");
+ assertThat(binary.format()).isEqualTo("application-octet-stream");
+ assertThat(binary.body()).startsWith("bytes");
+ assertThat(s3.dataTypes()).anyMatch(dt ->
"aws2-s3:application-cloudevents".equals(dt.format())
+ && dt.body().startsWith("unchanged"));
+ // a Kamelet without data types has none
+ assertThat(tools.camel_catalog_kamelet_doc("log-sink",
null).dataTypes()).isNull();
+ }
}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
index 264ac9807a71..122595a70cf0 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
@@ -194,9 +194,26 @@ final class CatalogBodyTypes {
return transformerSide(step, BodyTypeModel.TRANSFORMER_OUT);
}
+ /**
+ * The bodies the data type transformer of a data type can transform, such
as the in data type of a Kamelet; null
+ * when it is not known.
+ */
+ Answer transformerIn(String dataType) {
+ return transformerSide(dataType, BodyTypeModel.TRANSFORMER_IN);
+ }
+
+ /** The body after the data type transformer of a data type, such as the
out data type of a Kamelet. */
+ Answer transformerOut(String dataType) {
+ return transformerSide(dataType, BodyTypeModel.TRANSFORMER_OUT);
+ }
+
private Answer transformerSide(JsonNode step, String side) {
- String toType = step != null && step.isObject() && step.has("toType")
? step.get("toType").asText() : null;
- TransformerModel model = transformer(toType);
+ return transformerSide(step != null && step.isObject() &&
step.has("toType") ? step.get("toType").asText() : null,
+ side);
+ }
+
+ private Answer transformerSide(String dataType, String side) {
+ TransformerModel model = transformer(dataType);
return model != null ? answer(model.getBodyType(side), Map.of(),
Set.of()) : null;
}
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 01b05c33b975..e84ad2d8fab1 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
@@ -37,9 +37,12 @@ import java.util.Set;
* @param observed what a running integration of the routes saw at
their endpoints (CAMEL-25750): the body a
* consumer gave, keyed as {@link
#consumerKey(String)}, and the body a producer returned,
* keyed as {@link #producerKey(String)}: the types
the step after it was reached with
+ * @param kamelets the data types of the Kamelets the routes use, by
name (CAMEL-25928): the data type
+ * transformers Camel runs at the start and the end
of their route
*/
public record ProjectFacts(Set<String> bodylessEndpoints, Map<String,
Map<String, String>> operations,
- String restBindingMode, Map<String, Map<String, List<String>>>
requestFields, Map<String, Observed> observed) {
+ String restBindingMode, Map<String, Map<String, List<String>>>
requestFields, Map<String, Observed> observed,
+ Map<String, KameletDataTypes> kamelets) {
/** Nothing known about the project. */
public static final ProjectFacts NONE = new ProjectFacts(Set.of(),
Map.of(), null);
@@ -56,11 +59,26 @@ public record ProjectFacts(Set<String> bodylessEndpoints,
Map<String, Map<String
}
}
+ /**
+ * The data types of a Kamelet: the data type transformer of its default
in and out data type (spec.dataTypes), as
+ * application-octet-stream or aws2-s3:application-cloudevents; null for a
direction it has none for.
+ */
+ public record KameletDataTypes(String in, String out) {
+ }
+
public ProjectFacts {
bodylessEndpoints = bodylessEndpoints != null ? bodylessEndpoints :
Set.of();
operations = operations != null ? operations : Map.of();
requestFields = requestFields != null ? requestFields : Map.of();
observed = observed != null ? observed : Map.of();
+ kamelets = kamelets != null ? kamelets : Map.of();
+ }
+
+ /** Without the data types of the Kamelets. */
+ public ProjectFacts(Set<String> bodylessEndpoints, Map<String, Map<String,
String>> operations,
+ String restBindingMode, Map<String, Map<String,
List<String>>> requestFields,
+ Map<String, Observed> observed) {
+ this(bodylessEndpoints, operations, restBindingMode, requestFields,
observed, Map.of());
}
/** Without what a running integration saw. */
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 d318fbda5177..0dd20624d29a 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
@@ -175,11 +175,12 @@ final class TextBodyFlow {
*/
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, List<String>>> requestFields,
- Map<String, ProjectFacts.Observed> observed, CatalogBodyTypes
bodyTypes) {
+ Map<String, ProjectFacts.Observed> observed, CatalogBodyTypes
bodyTypes,
+ Map<String, ProjectFacts.KameletDataTypes> kamelets) {
/** The walk of a route a step calls: for its outcome alone. */
Walk called() {
- return new Walk(null, byFrom, null, seen, false, operations,
requestFields, observed, bodyTypes);
+ return new Walk(null, byFrom, null, seen, false, operations,
requestFields, observed, bodyTypes, kamelets);
}
}
@@ -193,6 +194,7 @@ final class TextBodyFlow {
Map<String, Map<String, List<String>>> requestFields = unbound ?
facts.requestFields() : Map.of();
Set<String> restReplies = unbound ? restReplies(target, facts) :
Set.of();
Map<String, ProjectFacts.Observed> observed = facts != null ?
facts.observed() : Map.of();
+ Map<String, ProjectFacts.KameletDataTypes> kamelets = facts != null ?
facts.kamelets() : Map.of();
Map<String, JsonNode> byFrom = new HashMap<>();
for (JsonNode entry : target) {
JsonNode from = from(entry);
@@ -226,14 +228,17 @@ final class TextBodyFlow {
boolean post = NO_HTTP_HEADERS.contains(consumer) &&
!setsHttpHeaders(steps);
// with variableReceive the message goes into the variable, and
the route starts with no body
boolean variable = bodyTypes.startsWithNoBody(from);
- Body start = uri != null && !variable ? consumerBody(uri, from,
bodyTypes) : null;
+ Body start = uri == null || variable ? null
+ : "kamelet".equals(consumer)
+ ? kameletBody(kameletName(uri), kamelets,
bodyTypes, "from: " + normalize(uri))
+ : consumerBody(uri, from, bodyTypes);
if (start == null && uri != null && !variable) {
// what the consumer gave in a running integration of the route
start = observed(observed, ProjectFacts.consumerKey(uri),
"from: " + normalize(uri) + " gave");
}
Body end = walk(steps, start, at, new Walk(
id, byFrom, errors, new HashSet<>(), post, operations,
- requestFields, observed, bodyTypes));
+ requestFields, observed, bodyTypes, kamelets));
// the body at the end of the route of a consumer that replies
with text (platform-http), or of the route of
// a REST call, is the reply
boolean rest = uri != null && restReplies.contains(normalize(uri));
@@ -432,8 +437,16 @@ final class TextBodyFlow {
}
return producerBody(out, "transformDataType: " +
toTypeOf(value) + " makes it");
}
+ case "kamelet" -> {
+ // what the Kamelet leaves, as the transformer of its out data
type says
+ String kamelet = kameletName(value);
+ return kamelet != null ? kameletAfter(kamelet, body, w,
"kamelet: " + kamelet) : null;
+ }
case "to", "toD" -> {
String uri = endpointOf(value);
+ if ("kamelet".equals(scheme(uri))) {
+ return kameletAfter(kameletName(uri), body, w, name + ": "
+ normalize(uri));
+ }
CatalogBodyTypes.Answer out = uri != null ?
w.bodyTypes().producerOut(uri, value) : null;
if (out != null && out.unchanged()) {
// log, a file write
@@ -683,6 +696,51 @@ final class TextBodyFlow {
private record Operation(String spec, String id) {
}
+ /**
+ * The name of the Kamelet of a kamelet: endpoint (aws-s3-source of
kamelet:aws-s3-source?bucketNameOrArn=x, or of
+ * kamelet:my/route), or of a kamelet step ({@code kamelet: my-action} or
with a name option).
+ */
+ private static String kameletName(String uri) {
+ if (uri == null || !uri.startsWith("kamelet:")) {
+ return null;
+ }
+ String name = uri.substring("kamelet:".length());
+ for (char c : new char[] { '?', '/' }) {
+ int i = name.indexOf(c);
+ if (i >= 0) {
+ name = name.substring(0, i);
+ }
+ }
+ return name.isBlank() || name.contains("{{") ? null : name;
+ }
+
+ private static String kameletName(JsonNode value) {
+ JsonNode name = value != null && value.isObject() ? value.get("name")
: value;
+ return name != null && name.isTextual() ? kameletName("kamelet:" +
name.asText()) : null;
+ }
+
+ /** The body a Kamelet gives, as the transformer of its out data type
says; null when it is not known. */
+ private static Body kameletBody(
+ String kamelet, Map<String, ProjectFacts.KameletDataTypes>
kamelets, CatalogBodyTypes bodyTypes,
+ String origin) {
+ ProjectFacts.KameletDataTypes types = kamelet != null ?
kamelets.get(kamelet) : null;
+ if (types == null || types.out() == null) {
+ return null;
+ }
+ return producerBody(bodyTypes.transformerOut(types.out()),
+ origin + " gives it as its out data type " + types.out());
+ }
+
+ /** The body after a step that runs a Kamelet: its out data type may keep
the body, as the CloudEvent ones do. */
+ private static Body kameletAfter(String kamelet, Body body, Walk w, String
origin) {
+ ProjectFacts.KameletDataTypes types = kamelet != null ?
w.kamelets().get(kamelet) : null;
+ CatalogBodyTypes.Answer out = types != null && types.out() != null ?
w.bodyTypes().transformerOut(types.out()) : null;
+ if (out != null && out.unchanged()) {
+ return body;
+ }
+ return kameletBody(kamelet, w.kamelets(), w.bodyTypes(), origin);
+ }
+
private static String toTypeOf(JsonNode value) {
return value != null && value.isObject() && value.has("toType") ?
value.get("toType").asText() : "";
}
@@ -694,6 +752,21 @@ final class TextBodyFlow {
* producer sends the body, and a REST binding may write it for
rest-openapi: they keep their own rule.
*/
private static Need needsText(String name, JsonNode value, Walk w) {
+ String kamelet = "kamelet".equals(name) ? kameletName(value)
+ : ("to".equals(name) || "toD".equals(name)) &&
"kamelet".equals(scheme(endpointOf(value)))
+ ? kameletName(endpointOf(value))
+ : null;
+ if (kamelet != null) {
+ // what the transformer of the in data type of the Kamelet takes
+ ProjectFacts.KameletDataTypes types = w.kamelets().get(kamelet);
+ CatalogBodyTypes.Answer in = types != null && types.in() != null ?
w.bodyTypes().transformerIn(types.in()) : null;
+ return in != null && in.onlyPayload()
+ ? new Need(
+ name + ": the Kamelet " + kamelet + " needs the
body as " + in.inWords() + " (its in data type "
+ + types.in() + ")",
+ "before this step")
+ : null;
+ }
if ("transformDataType".equals(name)) {
CatalogBodyTypes.Answer in = w.bodyTypes().transformerIn(value);
return in != null && in.onlyPayload()
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 5e47b7207fb5..8debd489f14a 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
@@ -2508,4 +2508,78 @@ public class TextBodyFlowTest {
.contains(
"transformDataType:
camel:application-octet-stream needs the body as text, bytes, a stream or a
file"));
}
+
+ /** The data types of the Kamelets of a project: the transformers of their
default in and out data type. */
+ private static final ProjectFacts KAMELETS = new ProjectFacts(
+ Set.of(), Map.of(), null, Map.of(), Map.of(),
+ Map.of("my-source", new ProjectFacts.KameletDataTypes(null,
"application-octet-stream"),
+ "my-sink", new
ProjectFacts.KameletDataTypes("application-octet-stream", null),
+ "my-action", new
ProjectFacts.KameletDataTypes("application-x-struct", "text-plain"),
+ "my-events", new ProjectFacts.KameletDataTypes(null,
"aws2-s3:application-cloudevents")));
+
+ @Test
+ public void testAKameletSourceGivesItsOutDataType() {
+ // application-octet-stream gives bytes: a field of the body cannot be
read
+ String yaml = """
+ - route:
+ from:
+ uri: kamelet:my-source?bucket=orders
+ steps:
+ - setHeader:
+ name: status
+ simple: "${body[status]}"
+ """;
+ assertThat(lines(yaml, KAMELETS)).singleElement()
+ .satisfies(m -> assertThat(m).contains("simple reads a field
of the body (${body[status]})"));
+ // a Kamelet without data types says nothing
+ assertThat(lines(yaml.replace("my-source", "other-source"),
KAMELETS)).isEmpty();
+ }
+
+ @Test
+ public void testAKameletSinkNeedsItsInDataType() {
+ String yaml = """
+ - route:
+ from:
+ uri: file:inbox
+ steps:
+ - unmarshal:
+ json: {}
+ - to:
+ uri: kamelet:my-sink
+ """;
+ assertThat(lines(yaml, KAMELETS)).singleElement()
+ .satisfies(m -> assertThat(m).contains(
+ "to: the Kamelet my-sink needs the body as text,
bytes, a stream or a file (its in data type"
+ + "
application-octet-stream)"));
+ }
+
+ @Test
+ public void testAKameletStepLeavesItsOutDataType() {
+ // text-plain makes the parsed data text again
+ String yaml = """
+ - route:
+ from:
+ uri: file:inbox
+ steps:
+ - unmarshal:
+ json: {}
+ - kamelet:
+ name: my-action
+ - to: file:outbox
+ """;
+ assertThat(lines(yaml, KAMELETS)).isEmpty();
+ // a CloudEvent data type sets headers only: the body is still parsed
data
+ String events = """
+ - route:
+ from:
+ uri: file:inbox
+ steps:
+ - unmarshal:
+ json: {}
+ - to: kamelet:my-events
+ - to: file:outbox
+ """;
+ assertThat(lines(events, KAMELETS)).singleElement()
+ .satisfies(m -> assertThat(m).contains("to: file needs the
body as text"));
+ }
}