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 3fd5d4f09f7f7..0375d6b431d0f 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.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 = { "-p", "--prop", "--property" }, + description = "Adds a pipe property in the form of [source|sink|step-].= where is the step number starting from 1", + arity = "0") + String[] properties; + + @CommandLine.Option(names = { "--output", "-o" }, + 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 sourceUriProperties = new HashMap<>(); + Map 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 Integer doCall() throws Exception { 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 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 Integer doCall() throws Exception { 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 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 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 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 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 getProperties(String keyPrefix) { + Map 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", + 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 kameletProperties(String kamelet, Map userProperties) throws Exception { + Map 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 @@ protected String kameletProperties(String kamelet) throws Exception { if (root != null) { Set required = asStringSet(nodeAt(root, "/spec/definition/required")); if (required != null && !required.isEmpty()) { - sb.append("properties:\n"); - Iterator 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 @@ protected String kameletProperties(String kamelet) throws Exception { 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 { 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 0000000000000..db408c40a1308 --- /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 7e6b69967c424..07de5540d3f64 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 06dd6b0ee6793..9c7b90820a72a 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 c88aade2e2574..b71a4e1f96ad4 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 ad279dac2766a..dc1c371c06a65 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 0000000000000..edf952212f988 --- /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 61944dc85b7aa..20b44d7a5fa07 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.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 static KubernetesClient getKubernetesClient(String config) { * @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() {