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

cdeppisch 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 17175a0dc3e CAMEL-20602: Support user properties on Camel JBang bind 
command
17175a0dc3e is described below

commit 17175a0dc3e46901b136e37dbab2115272789067
Author: Christoph Deppisch <cdeppi...@redhat.com>
AuthorDate: Fri Mar 22 16:37:38 2024 +0100

    CAMEL-20602: Support user properties on Camel JBang bind command
    
    - Allow to specify properties on source, sink, step in a Pipe
    - User properties can be set as additional command options targeting either 
the source or the sink or a specific step
---
 .../apache/camel/dsl/jbang/core/commands/Bind.java | 220 ++++++--
 .../camel/dsl/jbang/core/common/YamlHelper.java    |  59 ++
 .../resources/templates/pipe-kamelet-uri.yaml.tmpl |   3 +-
 .../resources/templates/pipe-uri-kamelet.yaml.tmpl |   3 +-
 .../resources/templates/pipe-uri-uri.yaml.tmpl     |   6 +-
 .../main/resources/templates/step-uri.yaml.tmpl    |   3 +-
 .../camel/dsl/jbang/core/commands/BindTest.java    | 623 +++++++++++++++++++++
 .../jbang/core/commands/k/KubernetesHelper.java    |  24 +-
 8 files changed, 863 insertions(+), 78 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
index 3fd5d4f09f7..27b16288bc6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java
@@ -20,16 +20,22 @@ import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.nio.file.Path;
-import java.util.Iterator;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.Stack;
 
+import org.apache.camel.dsl.jbang.core.common.JSonHelper;
+import org.apache.camel.dsl.jbang.core.common.YamlHelper;
 import org.apache.camel.github.GitHubResourceResolver;
 import org.apache.camel.impl.engine.DefaultResourceResolvers;
 import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.ResourceResolver;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.URISupport;
+import org.apache.camel.util.json.Jsoner;
 import org.snakeyaml.engine.v2.api.LoadSettings;
 import org.snakeyaml.engine.v2.api.YamlUnicodeReader;
 import org.snakeyaml.engine.v2.composer.Composer;
@@ -64,21 +70,49 @@ public class Bind extends CamelCommand {
                         required = true)
     String sink;
 
+    @CommandLine.Option(names = { "--property" },
+                        description = "Adds a pipe property in the form of 
[source|sink|step-<n>].<key>=<value> where <n> is the step number starting from 
1",
+                        arity = "0")
+    String[] properties;
+
+    @CommandLine.Option(names = { "--output" },
+                        defaultValue = "file",
+                        description = "Output format generated by this command 
(supports: file, yaml or json).")
+    String output;
+
     public Bind(CamelJBangMain main) {
         super(main);
     }
 
     @Override
     public Integer doCall() throws Exception {
-
         // the pipe source and sink can either be a kamelet or an uri
         String in = "kamelet";
         String out = "kamelet";
+
+        String sourceEndpoint = source;
+        String sinkEndpoint = sink;
+        Map<String, Object> sourceUriProperties = new HashMap<>();
+        Map<String, Object> sinkUriProperties = new HashMap<>();
         if (source.contains(":")) {
             in = "uri";
+            if (source.contains("?")) {
+                sourceEndpoint = StringHelper.before(source, "?");
+                String query = StringHelper.after(source, "?");
+                if (query != null) {
+                    sourceUriProperties = URISupport.parseQuery(query, true);
+                }
+            }
         }
         if (sink.contains(":")) {
             out = "uri";
+            if (sink.contains("?")) {
+                sinkEndpoint = StringHelper.before(sink, "?");
+                String query = StringHelper.after(sink, "?");
+                if (query != null) {
+                    sinkUriProperties = URISupport.parseQuery(query, true);
+                }
+            }
         }
 
         InputStream is = 
Bind.class.getClassLoader().getResourceAsStream("templates/pipe-" + in + "-" + 
out + ".yaml.tmpl");
@@ -88,21 +122,35 @@ public class Bind extends CamelCommand {
         String stepsContext = "";
         if (steps != null) {
             StringBuilder sb = new StringBuilder("\n  steps:\n");
-            for (String step : steps) {
+            for (int i = 0; i < steps.length; i++) {
+                String step = steps[i];
                 boolean uri = step.contains(":");
                 String text;
+                String stepType;
+                Map<String, Object> stepProperties = 
getProperties("step-%d".formatted(i + 1));
                 if (uri) {
-                    is = 
Bind.class.getClassLoader().getResourceAsStream("templates/step-uri.yaml.tmpl");
-                    text = IOHelper.loadText(is);
-                    IOHelper.close(is);
-                    text = text.replaceFirst("\\{\\{ \\.Name }}", step);
+                    stepType = "uri";
+                    if (step.contains("?")) {
+                        String query = StringHelper.after(step, "?");
+                        step = StringHelper.before(step, "?");
+                        if (query != null) {
+                            stepProperties.putAll(URISupport.parseQuery(query, 
true));
+                        }
+                    }
                 } else {
-                    is = 
Bind.class.getClassLoader().getResourceAsStream("templates/step-kamelet.yaml.tmpl");
-                    text = IOHelper.loadText(is);
-                    IOHelper.close(is);
-                    text = text.replaceFirst("\\{\\{ \\.Name }}", step);
-                    String props = kameletProperties(step);
-                    text = text.replaceFirst("\\{\\{ \\.StepProperties }}", 
props);
+                    stepType = "kamelet";
+                    stepProperties = kameletProperties(step, stepProperties);
+                }
+
+                is = 
Bind.class.getClassLoader().getResourceAsStream("templates/step-%s.yaml.tmpl".formatted(stepType));
+                text = IOHelper.loadText(is);
+                IOHelper.close(is);
+                text = text.replaceFirst("\\{\\{ \\.Name }}", step);
+
+                if (i == steps.length - 1) {
+                    text = text.replaceFirst("\\{\\{ \\.StepProperties }}\n", 
asEndpointProperties(stepProperties));
+                } else {
+                    text = text.replaceFirst("\\{\\{ \\.StepProperties }}", 
asEndpointProperties(stepProperties));
                 }
                 sb.append(text);
             }
@@ -111,31 +159,117 @@ public class Bind extends CamelCommand {
 
         String name = FileUtil.onlyName(file, false);
         context = context.replaceFirst("\\{\\{ \\.Name }}", name);
-        context = context.replaceFirst("\\{\\{ \\.Source }}", source);
-        context = context.replaceFirst("\\{\\{ \\.Sink }}", sink);
+        context = context.replaceFirst("\\{\\{ \\.Source }}", sourceEndpoint);
+        context = context.replaceFirst("\\{\\{ \\.Sink }}", sinkEndpoint);
         context = context.replaceFirst("\\{\\{ \\.Steps }}", stepsContext);
 
+        Map<String, Object> sourceProperties = getProperties("source");
         if ("kamelet".equals(in)) {
-            String props = kameletProperties(source);
-            context = context.replaceFirst("\\{\\{ \\.SourceProperties }}", 
props);
+            sourceProperties = kameletProperties(sourceEndpoint, 
sourceProperties);
+        } else {
+            sourceProperties.putAll(sourceUriProperties);
         }
+        context = context.replaceFirst("\\{\\{ \\.SourceProperties }}\n", 
asEndpointProperties(sourceProperties));
+
+        Map<String, Object> sinkProperties = getProperties("sink");
         if ("kamelet".equals(out)) {
-            String props = kameletProperties(sink);
-            context = context.replaceFirst("\\{\\{ \\.SinkProperties }}", 
props);
+            sinkProperties = kameletProperties(sinkEndpoint, sinkProperties);
+        } else {
+            sinkProperties.putAll(sinkUriProperties);
         }
+        context = context.replaceFirst("\\{\\{ \\.SinkProperties }}\n", 
asEndpointProperties(sinkProperties));
 
-        IOHelper.writeText(context, new FileOutputStream(file, false));
+        switch (output) {
+            case "file":
+                if (file.endsWith(".yaml")) {
+                    IOHelper.writeText(context, new FileOutputStream(file, 
false));
+                } else if (file.endsWith(".json")) {
+                    
IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(context, 
Map.class)),
+                            new FileOutputStream(file, false));
+                } else {
+                    IOHelper.writeText(context, new FileOutputStream(file + 
".yaml", false));
+                }
+                break;
+            case "yaml":
+                printer().println(context);
+                break;
+            case "json":
+                
printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(context,
 Map.class)), 2)
+                        .replaceAll("\\\\/", "/"));
+                break;
+            default:
+                printer().printf("Unsupported output format '%s' (supported: 
file, yaml, json)%n", output);
+                return -1;
+        }
         return 0;
     }
 
-    protected String kameletProperties(String kamelet) throws Exception {
+    /**
+     * Creates YAML snippet representing the endpoint properties section.
+     *
+     * @param  props the properties to set as endpoint properties.
+     * @return
+     */
+    private String asEndpointProperties(Map<String, Object> props) {
         StringBuilder sb = new StringBuilder();
+        if (props.isEmpty()) {
+            // create a dummy placeholder, so it is easier to add new 
properties manually
+            return sb.append("#properties:\n      ").append("#key: 
\"value\"").toString();
+        }
+
+        sb.append("properties:\n");
+        for (Map.Entry<String, Object> propertyEntry : props.entrySet()) {
+            sb.append("      ").append(propertyEntry.getKey()).append(": ")
+                    .append(propertyEntry.getValue()).append("\n");
+        }
+        return sb.toString().trim();
+    }
+
+    /**
+     * Extracts properties from given property arguments. Filter properties by 
given prefix. This way each component in
+     * pipe (source, sink, step[1-n]) can have its individual properties.
+     *
+     * @param  keyPrefix
+     * @return
+     */
+    private Map<String, Object> getProperties(String keyPrefix) {
+        Map<String, Object> props = new HashMap<>();
+        if (properties != null) {
+            for (String propertyExpression : properties) {
+                if (propertyExpression.startsWith(keyPrefix + ".")) {
+                    String[] keyValue = propertyExpression.split("=", 2);
+                    if (keyValue.length != 2) {
+                        printer().printf(
+                                "property '%s' does not follow format 
[source|sink|step-<n>].<key>=<value>%n",
+                                propertyExpression);
+                        continue;
+                    }
 
+                    props.put(keyValue[0].substring(keyPrefix.length() + 1), 
keyValue[1]);
+                }
+            }
+        }
+
+        return props;
+    }
+
+    /**
+     * Get required properties from Kamelet specification and add those to the 
given user properties if not already set.
+     * In case a required property is not present in the provided user 
properties the value is either set to the example
+     * coming from the Kamelet specification or to a placeholder value for 
users to fill in manually. Property values do
+     * already have quotes when the type is String.
+     *
+     * @param  kamelet
+     * @return
+     * @throws Exception
+     */
+    protected Map<String, Object> kameletProperties(String kamelet, 
Map<String, Object> userProperties) throws Exception {
+        Map<String, Object> endpointProperties = new HashMap<>();
         InputStream is;
         String loc;
         Resource res;
 
-        // try local disk first before github
+        // try local disk first before GitHub
         ResourceResolver resolver = new 
DefaultResourceResolvers.FileResolver();
         try {
             res = resolver.resolve("file:" + kamelet + ".kamelet.yaml");
@@ -167,26 +301,23 @@ public class Bind extends CamelCommand {
                 if (root != null) {
                     Set<String> required = asStringSet(nodeAt(root, 
"/spec/definition/required"));
                     if (required != null && !required.isEmpty()) {
-                        sb.append("properties:\n");
-                        Iterator<String> it = required.iterator();
-                        while (it.hasNext()) {
-                            String req = it.next();
-                            String type = asText(nodeAt(root, 
"/spec/definition/properties/" + req + "/type"));
-                            String example = asText(nodeAt(root, 
"/spec/definition/properties/" + req + "/example"));
-                            sb.append("      ").append(req).append(": ");
-                            if (example != null) {
-                                if ("string".equals(type)) {
-                                    sb.append("\"");
-                                }
-                                sb.append(example);
-                                if ("string".equals(type)) {
-                                    sb.append("\"");
+                        for (String req : required) {
+                            if (!userProperties.containsKey(req)) {
+                                String type = asText(nodeAt(root, 
"/spec/definition/properties/" + req + "/type"));
+                                String example = asText(nodeAt(root, 
"/spec/definition/properties/" + req + "/example"));
+                                StringBuilder vb = new StringBuilder();
+                                if (example != null) {
+                                    if ("string".equals(type)) {
+                                        vb.append("\"");
+                                    }
+                                    vb.append(example);
+                                    if ("string".equals(type)) {
+                                        vb.append("\"");
+                                    }
+                                } else {
+                                    vb.append("\"value\"");
                                 }
-                            } else {
-                                sb.append("\"value\"");
-                            }
-                            if (it.hasNext()) {
-                                sb.append("\n");
+                                endpointProperties.put(req, vb.toString());
                             }
                         }
                     }
@@ -199,12 +330,9 @@ public class Bind extends CamelCommand {
             System.err.println("Kamelet not found on github: " + kamelet);
         }
 
-        // create a dummy placeholder, so it is easier to add new properties 
manually
-        if (sb.isEmpty()) {
-            sb.append("#properties:\n      #key: \"value\"");
-        }
+        endpointProperties.putAll(userProperties);
 
-        return sb.toString();
+        return endpointProperties;
     }
 
     static class FileConsumer extends ParameterConsumer<Bind> {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/YamlHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/YamlHelper.java
new file mode 100644
index 00000000000..db408c40a13
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/YamlHelper.java
@@ -0,0 +1,59 @@
+/*
+ * 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.common;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.introspector.Property;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.representer.Representer;
+
+public final class YamlHelper {
+
+    private YamlHelper() {
+    }
+
+    /**
+     * Creates new Yaml instance. The implementation provided by Snakeyaml is 
not thread-safe. It is better to create a
+     * fresh instance for every YAML stream.
+     *
+     * @return
+     */
+    public static Yaml yaml() {
+        Representer representer = new Representer(new DumperOptions()) {
+            @Override
+            protected NodeTuple representJavaBeanProperty(
+                    Object javaBean, Property property, Object propertyValue, 
Tag customTag) {
+                // if value of property is null, ignore it.
+                if (propertyValue == null || (propertyValue instanceof 
Collection && ((Collection<?>) propertyValue).isEmpty())
+                        ||
+                        (propertyValue instanceof Map && ((Map<?, ?>) 
propertyValue).isEmpty())) {
+                    return null;
+                } else {
+                    return super.representJavaBeanProperty(javaBean, property, 
propertyValue, customTag);
+                }
+            }
+        };
+        representer.getPropertyUtils().setSkipMissingProperties(true);
+        return new Yaml(representer);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl
index 7e6b69967c4..07de5540d3f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl
@@ -12,5 +12,4 @@ spec:
 {{ .Steps }}
   sink:
     uri: {{ .Sink }}
-    #properties:
-      #key: "value"
+    {{ .SinkProperties }}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl
index 06dd6b0ee67..9c7b90820a7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl
@@ -5,8 +5,7 @@ metadata:
 spec:
   source:
     uri: {{ .Source }}
-    #properties:
-      #key: "value"
+    {{ .SourceProperties }}
 {{ .Steps }}
   sink:
     ref:
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl
index c88aade2e25..b71a4e1f96a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl
@@ -5,10 +5,8 @@ metadata:
 spec:
   source:
     uri: {{ .Source }}
-    #properties:
-      #key: "value"
+    {{ .SourceProperties }}
 {{ .Steps }}
   sink:
     uri: {{ .Sink }}
-    #properties:
-      #key: "value"
+    {{ .SinkProperties }}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/step-uri.yaml.tmpl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/step-uri.yaml.tmpl
index ad279dac276..dc1c371c06a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/step-uri.yaml.tmpl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/step-uri.yaml.tmpl
@@ -1,3 +1,2 @@
   - uri: {{ .Name }}
-    #properties:
-      #key: "value"
\ No newline at end of file
+    {{ .StepProperties }}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java
new file mode 100644
index 00000000000..edf952212f9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java
@@ -0,0 +1,623 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class BindTest extends CamelCommandBaseTest {
+
+    @Test
+    public void shouldBindKameletSourceToKameletSink() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-sink
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindKameletSourceToKameletSinkWithProperties() throws 
Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "yaml";
+
+        command.properties = new String[] {
+                "source.message=Hello",
+                "source.period=5000",
+                "sink.showHeaders=true",
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: Hello
+                      period: 5000
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-sink
+                    properties:
+                      showHeaders: true
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithSteps() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-http";
+        command.source = "timer-source";
+        command.sink = "http-sink";
+        command.output = "yaml";
+
+        command.steps = new String[] {
+                "set-body-action",
+                "log-action"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-http
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  steps:
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: set-body-action
+                    properties:
+                      value: "value"
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-action
+                    #properties:
+                      #key: "value"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: http-sink
+                    properties:
+                      url: "https://my-service/path";
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithUriSteps() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-http";
+        command.source = "timer-source";
+        command.sink = "http-sink";
+        command.output = "yaml";
+
+        command.steps = new String[] {
+                "set-body-action",
+                "log:info"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-http
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  steps:
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: set-body-action
+                    properties:
+                      value: "value"
+                  - uri: log:info
+                    #properties:
+                      #key: "value"
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: http-sink
+                    properties:
+                      url: "https://my-service/path";
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithStepsAndProperties() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-http";
+        command.source = "timer-source";
+        command.sink = "http-sink";
+        command.output = "yaml";
+
+        command.steps = new String[] {
+                "set-body-action",
+                "log-action"
+        };
+
+        command.properties = new String[] {
+                "step-1.value=\"Camel rocks!\"",
+                "step-2.showHeaders=true",
+                "step-2.showExchangePattern=false"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-http
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  steps:
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: set-body-action
+                    properties:
+                      value: "Camel rocks!"
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: log-action
+                    properties:
+                      showHeaders: true
+                      showExchangePattern: false
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: http-sink
+                    properties:
+                      url: "https://my-service/path";
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithUriStepsAndProperties() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-http";
+        command.source = "timer-source";
+        command.sink = "http-sink";
+        command.output = "yaml";
+
+        command.steps = new String[] {
+                "set-body-action",
+                "log:info"
+        };
+
+        command.properties = new String[] {
+                "step-1.value=\"Camel rocks!\"",
+                "step-2.showHeaders=true"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-http
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  steps:
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: set-body-action
+                    properties:
+                      value: "Camel rocks!"
+                  - uri: log:info
+                    properties:
+                      showHeaders: true
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: http-sink
+                    properties:
+                      url: "https://my-service/path";
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindWithUriStepsAndUriProperties() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-http";
+        command.source = "timer-source";
+        command.sink = "http-sink";
+        command.output = "yaml";
+
+        command.steps = new String[] {
+                "set-body-action",
+                "log:info?showExchangePattern=false&showStreams=true"
+        };
+
+        command.properties = new String[] {
+                "step-1.value=\"Camel rocks!\"",
+                "step-2.showHeaders=true"
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-http
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  steps:
+                  - ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: set-body-action
+                    properties:
+                      value: "Camel rocks!"
+                  - uri: log:info
+                    properties:
+                      showStreams: true
+                      showHeaders: true
+                      showExchangePattern: false
+                  sink:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: http-sink
+                    properties:
+                      url: "https://my-service/path";
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindKameletSourceToUri() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log:info";
+        command.output = "yaml";
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: "hello world"
+                  sink:
+                    uri: log:info
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindKameletSourceToUriWithProperties() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log:info";
+        command.output = "yaml";
+
+        command.properties = new String[] {
+                "source.message=Hello",
+                "sink.showHeaders=true",
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: Hello
+                  sink:
+                    uri: log:info
+                    properties:
+                      showHeaders: true
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindKameletSourceToUriWithUriProperties() throws 
Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log:info?showStreams=false";
+        command.output = "yaml";
+
+        command.properties = new String[] {
+                "source.message=Hello",
+                "sink.showHeaders=true",
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    ref:
+                      kind: Kamelet
+                      apiVersion: camel.apache.org/v1
+                      name: timer-source
+                    properties:
+                      message: Hello
+                  sink:
+                    uri: log:info
+                    properties:
+                      showStreams: false
+                      showHeaders: true
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindUriToUri() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer:tick";
+        command.sink = "log:info";
+        command.output = "yaml";
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    uri: timer:tick
+                    #properties:
+                      #key: "value"
+                  sink:
+                    uri: log:info
+                    #properties:
+                      #key: "value"
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindUriToUriWithProperties() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer:tick";
+        command.sink = "log:info";
+        command.output = "yaml";
+
+        command.properties = new String[] {
+                "source.message=Hello",
+                "sink.showHeaders=true",
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    uri: timer:tick
+                    properties:
+                      message: Hello
+                  sink:
+                    uri: log:info
+                    properties:
+                      showHeaders: true
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldBindUriToUriWithUriProperties() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer:tick?period=10000";
+        command.sink = "log:info?showStreams=false";
+        command.output = "yaml";
+
+        command.properties = new String[] {
+                "source.message=Hello",
+                "sink.showHeaders=true",
+        };
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                apiVersion: camel.apache.org/v1
+                kind: Pipe
+                metadata:
+                  name: timer-to-log
+                spec:
+                  source:
+                    uri: timer:tick
+                    properties:
+                      period: 10000
+                      message: Hello
+                  sink:
+                    uri: log:info
+                    properties:
+                      showStreams: false
+                      showHeaders: true
+                """.trim(), output);
+    }
+
+    @Test
+    public void shouldSupportJsonOutput() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "json";
+
+        command.doCall();
+
+        String output = printer.getOutput();
+        Assertions.assertEquals("""
+                 {
+                  "apiVersion": "camel.apache.org/v1",
+                  "kind": "Pipe",
+                  "metadata": {
+                    "name": "timer-to-log"
+                  },
+                  "spec": {
+                    "source": {
+                      "ref": {
+                        "kind": "Kamelet",
+                        "apiVersion": "camel.apache.org/v1",
+                        "name": "timer-source"
+                      },
+                      "properties": {
+                        "message": "hello world"
+                      }
+                    },
+                    "sink": {
+                      "ref": {
+                        "kind": "Kamelet",
+                        "apiVersion": "camel.apache.org/v1",
+                        "name": "log-sink"
+                      }
+                    }
+                  }
+                }
+                 """.trim(), output);
+    }
+
+    @Test
+    public void shouldHandleUnsupportedOutputFormat() throws Exception {
+        Bind command = new Bind(new CamelJBangMain().withPrinter(printer));
+        command.file = "timer-to-log";
+        command.source = "timer-source";
+        command.sink = "log-sink";
+        command.output = "wrong";
+
+        Assertions.assertEquals(-1, command.doCall());
+
+        Assertions.assertEquals("Unsupported output format 'wrong' (supported: 
file, yaml, json)", printer.getOutput());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java
index 61944dc85b7..20b44d7a5fa 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-k/src/main/java/org/apache/camel/dsl/jbang/core/commands/k/KubernetesHelper.java
@@ -17,7 +17,6 @@
 
 package org.apache.camel.dsl.jbang.core.commands.k;
 
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -31,14 +30,10 @@ import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import io.fabric8.kubernetes.client.KubernetesClient;
 import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import org.apache.camel.dsl.jbang.core.common.YamlHelper;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.StringHelper;
-import org.yaml.snakeyaml.DumperOptions;
 import org.yaml.snakeyaml.Yaml;
-import org.yaml.snakeyaml.introspector.Property;
-import org.yaml.snakeyaml.nodes.NodeTuple;
-import org.yaml.snakeyaml.nodes.Tag;
-import org.yaml.snakeyaml.representer.Representer;
 
 /**
  * Helper class provides access to cached Kubernetes client. Also provides 
access to generic Json and Yaml mappers.
@@ -103,22 +98,7 @@ public final class KubernetesHelper {
      * @return
      */
     public static Yaml yaml() {
-        Representer representer = new Representer(new DumperOptions()) {
-            @Override
-            protected NodeTuple representJavaBeanProperty(
-                    Object javaBean, Property property, Object propertyValue, 
Tag customTag) {
-                // if value of property is null, ignore it.
-                if (propertyValue == null || (propertyValue instanceof 
Collection && ((Collection<?>) propertyValue).isEmpty())
-                        ||
-                        (propertyValue instanceof Map && ((Map<?, ?>) 
propertyValue).isEmpty())) {
-                    return null;
-                } else {
-                    return super.representJavaBeanProperty(javaBean, property, 
propertyValue, customTag);
-                }
-            }
-        };
-        representer.getPropertyUtils().setSkipMissingProperties(true);
-        return new Yaml(representer);
+        return YamlHelper.yaml();
     }
 
     public static ObjectMapper json() {


Reply via email to