This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-kamelets.git


The following commit(s) were added to refs/heads/main by this push:
     new 973d9ca29 Fix #929: let a Kamelet's declared headers answer 
getKameletSupportedHeaders (#3061)
973d9ca29 is described below

commit 973d9ca291ee4aa6c1f54894f608adbedc987ade
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Sep 25 09:56:29 2026 +0200

    Fix #929: let a Kamelet's declared headers answer 
getKameletSupportedHeaders (#3061)
    
    The catalog API never consulted what a Kamelet declares. It took the Kamelet
    name, stripped the last segment, mapped that to a Camel component and 
returned
    the component's endpoint headers. So a caller asking the catalog what a 
Kamelet
    emits got what its component can emit, which is a different question.
    
    The gap is wide where it has been measured. aws-s3-source declares four 
headers
    and the component reports twenty six; azure-storage-blob-source declares 
eight
    against thirty nine; google-storage-source four against twenty one. It runs 
the
    other way too: azure-storage-datalake-source declares twenty six where the
    component reports twenty five, so the declaration carries a header the 
component
    does not know about, which is the case #929 was opened for.
    
    getKameletSupportedHeaders now returns spec.dataTypes headers when the 
Kamelet
    declares any, and falls back to the component otherwise. Fifteen Kamelets 
declare
    headers today, so the other two hundred and forty seven are unaffected.
    
    Six expectations in testSupportedHeaders move to the declared counts. Three 
more
    declaring Kamelets already agreed with their component and are unchanged. A 
new
    test states the semantics directly rather than leaving them implied by 
counts: it
    asserts the four names aws-s3-source declares, that the Kamelet's own title
    travels with the header, and that a Kamelet declaring nothing still falls 
back.
    
    This does change what the API answers for those fifteen, which is a visible
    change for tooling that reads it. It is the half of #929 that needs no 
change to
    the Kamelet spec. The other half, whether the declaration should be 
decoupled
    from dataTypes so a Kamelet that does no data type transformation has 
somewhere
    to put it, is untouched here and is what still keeps adoption at fifteen of 
two
    hundred and sixty two. The same question covers #328 for media types.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../camel/kamelets/catalog/KameletsCatalog.java    | 42 ++++++++++++++++++++++
 .../kamelets/catalog/KameletsCatalogTest.java      | 31 ++++++++++++----
 2 files changed, 67 insertions(+), 6 deletions(-)

diff --git 
a/library/camel-kamelets-catalog/src/main/java/org/apache/camel/kamelets/catalog/KameletsCatalog.java
 
b/library/camel-kamelets-catalog/src/main/java/org/apache/camel/kamelets/catalog/KameletsCatalog.java
index 845fd091b..7bb6cbc48 100644
--- 
a/library/camel-kamelets-catalog/src/main/java/org/apache/camel/kamelets/catalog/KameletsCatalog.java
+++ 
b/library/camel-kamelets-catalog/src/main/java/org/apache/camel/kamelets/catalog/KameletsCatalog.java
@@ -41,6 +41,7 @@ import 
org.apache.camel.kamelets.catalog.model.KameletTypeEnum;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.v1.Kamelet;
+import org.apache.camel.v1.kameletspec.datatypes.Headers;
 import org.apache.camel.v1.kameletspec.DataTypes;
 import org.apache.camel.v1.kameletspec.Definition;
 import org.apache.camel.v1.kameletspec.Template;
@@ -243,6 +244,13 @@ public class KameletsCatalog {
         List<ComponentModel.EndpointHeaderModel> resultingHeaders = new 
ArrayList<>();
         Kamelet local = kameletModels.get(name);
         if (ObjectHelper.isNotEmpty(local)) {
+            // What the Kamelet declares about itself wins. The component list 
describes
+            // everything the component can emit, which both over-reports 
headers this
+            // template never surfaces and misses the ones the template adds 
itself.
+            List<ComponentModel.EndpointHeaderModel> declared = 
getDeclaredHeaders(local);
+            if (!declared.isEmpty()) {
+                return declared;
+            }
             String camelType = determineCamelType(local);
             String kameletName = local.getMetadata().getName();
             int lastIndex = kameletName.lastIndexOf("-");
@@ -263,6 +271,40 @@ public class KameletsCatalog {
         return resultingHeaders;
     }
 
+    /**
+     * Headers the Kamelet declares under spec.dataTypes, which describe what 
this
+     * template actually emits or consumes rather than what its component 
supports.
+     */
+    private List<ComponentModel.EndpointHeaderModel> 
getDeclaredHeaders(Kamelet kamelet) {
+        List<ComponentModel.EndpointHeaderModel> declared = new ArrayList<>();
+        if (kamelet.getSpec() == null || kamelet.getSpec().getDataTypes() == 
null) {
+            return declared;
+        }
+        for (DataTypes dataType : kamelet.getSpec().getDataTypes().values()) {
+            if (dataType == null || dataType.getHeaders() == null) {
+                continue;
+            }
+            for (Map.Entry<String, Headers> entry : 
dataType.getHeaders().entrySet()) {
+                declared.add(toHeaderModel(entry.getKey(), entry.getValue()));
+            }
+        }
+        return declared;
+    }
+
+    private ComponentModel.EndpointHeaderModel toHeaderModel(String name, 
Headers header) {
+        ComponentModel.EndpointHeaderModel model = new 
ComponentModel.EndpointHeaderModel();
+        model.setName(name);
+        if (header != null) {
+            model.setDisplayName(header.getTitle());
+            model.setDescription(header.getDescription());
+            model.setType(header.getType());
+            model.setJavaType(header.getType());
+            model.setDefaultValue(header.get_default());
+            model.setRequired(Boolean.TRUE.equals(header.getRequired()));
+        }
+        return model;
+    }
+
     public String getKameletScheme(String prefix) {
         return enumValue(prefix);
     }
diff --git 
a/library/camel-kamelets-catalog/src/test/java/org/apache/camel/kamelets/catalog/KameletsCatalogTest.java
 
b/library/camel-kamelets-catalog/src/test/java/org/apache/camel/kamelets/catalog/KameletsCatalogTest.java
index ace8a10bc..9653d36bf 100644
--- 
a/library/camel-kamelets-catalog/src/test/java/org/apache/camel/kamelets/catalog/KameletsCatalogTest.java
+++ 
b/library/camel-kamelets-catalog/src/test/java/org/apache/camel/kamelets/catalog/KameletsCatalogTest.java
@@ -177,7 +177,7 @@ public class KameletsCatalogTest {
 
     @Test
     void testSupportedHeaders() throws Exception {
-        verifyHeaders("aws-s3-source", 26);
+        verifyHeaders("aws-s3-source", 4);
         verifyHeaders("aws-s3-sink", 51);
         verifyHeaders("aws-cloudtrail-source", 4);
         verifyHeaders("aws-redshift-source", 0);
@@ -185,10 +185,10 @@ public class KameletsCatalogTest {
         verifyHeaders("azure-eventhubs-sink", 2);
         verifyHeaders("azure-functions-sink", 8);
         verifyHeaders("azure-servicebus-source", 21);
-        verifyHeaders("azure-storage-blob-source", 39);
+        verifyHeaders("azure-storage-blob-source", 8);
         verifyHeaders("azure-storage-blob-sink", 46);
         verifyHeaders("azure-storage-blob-changefeed-source", 39);
-        verifyHeaders("azure-storage-datalake-source", 25);
+        verifyHeaders("azure-storage-datalake-source", 26);
         verifyHeaders("azure-storage-datalake-sink", 37);
         verifyHeaders("azure-storage-queue-source", 6);
         verifyHeaders("azure-storage-queue-sink", 16);
@@ -214,11 +214,11 @@ public class KameletsCatalogTest {
         verifyHeaders("google-bigquery-sink", 4);
         verifyHeaders("google-calendar-source", 1);
         verifyHeaders("google-functions-sink", 5);
-        verifyHeaders("google-mail-source", 9);
+        verifyHeaders("google-mail-source", 6);
         verifyHeaders("google-pubsub-sink", 3);
         verifyHeaders("google-pubsub-source", 6);
-        verifyHeaders("google-sheets-source", 6);
-        verifyHeaders("google-storage-source", 21);
+        verifyHeaders("google-sheets-source", 5);
+        verifyHeaders("google-storage-source", 4);
         verifyHeaders("google-storage-sink", 15);
         verifyHeaders("http-source", 5);
         verifyHeaders("http-sink", 14);
@@ -321,6 +321,25 @@ public class KameletsCatalogTest {
         return scheme != null && new 
DefaultCamelCatalog().componentModel(scheme) != null;
     }
 
+    @Test
+    void testDeclaredHeadersWinOverTheComponent() throws Exception {
+        // aws-s3-source declares four headers under spec.dataTypes. The 
aws2-s3
+        // component reports twenty six, most of which this template never 
emits,
+        // so the declaration is what callers should see.
+        List<ComponentModel.EndpointHeaderModel> headers = 
catalog.getKameletSupportedHeaders("aws-s3-source");
+        List<String> names = 
headers.stream().map(ComponentModel.EndpointHeaderModel::getName).sorted().toList();
+        assertEquals(List.of("CamelAwsS3BucketName", "CamelAwsS3ContentType", 
"CamelAwsS3ETag", "CamelAwsS3Key"), names);
+
+        // The title and description travel with it, so a consumer gets the
+        // Kamelet's own wording rather than the component's.
+        ComponentModel.EndpointHeaderModel key = headers.stream()
+                .filter(h -> 
"CamelAwsS3Key".equals(h.getName())).findFirst().orElseThrow();
+        assertEquals("S3 Key", key.getDisplayName());
+
+        // A Kamelet that declares nothing still falls back to its component.
+        
assertFalse(catalog.getKameletSupportedHeaders("timer-source").isEmpty());
+    }
+
     @Test
     void testGetKameletScheme() throws Exception {
         assertEquals("aws2-s3", catalog.getKameletScheme("aws-s3"));

Reply via email to