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

gnodet 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 8629043381d0 CAMEL-23227: Enhance camel-datasonnet with utility 
functions and standard library
8629043381d0 is described below

commit 8629043381d0245e14ca1b8f591488e2e2ce2138
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Mar 23 09:19:52 2026 +0100

    CAMEL-23227: Enhance camel-datasonnet with utility functions and standard 
library
    
    - Add null handling functions (defaultVal, isEmpty) to CML
    - Add type coercion functions (toInteger, toDecimal, toBoolean)
    - Add date/time functions (now, nowFmt, formatDate, parseDate)
    - Add utility functions (uuid, typeOf)
    - Add camel.libsonnet standard library with string, collection, and object 
helpers
    - Add comprehensive tests for all new functions
---
 .../src/main/docs/datasonnet-language.adoc         | 114 +++++++
 .../org/apache/camel/language/datasonnet/CML.java  | 250 +++++++++++++++-
 .../src/main/resources/camel.libsonnet             |  68 +++++
 .../language/datasonnet/CamelLibsonnetTest.java    | 269 +++++++++++++++++
 .../language/datasonnet/CmlFunctionsTest.java      | 333 +++++++++++++++++++++
 5 files changed, 1025 insertions(+), 9 deletions(-)

diff --git a/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc 
b/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
index 27a3f74305aa..2d8bd5cd74ae 100644
--- a/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
+++ b/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
@@ -115,6 +115,120 @@ xref:ROOT:properties-component.adoc[Properties] component 
(property placeholders
 |cml.variable |the variable name |String |Will return the exchange variable.
 |===
 
+=== Null Handling Functions
+
+[width="100%",cols="20%,30%,50%",options="header",]
+|===
+|Function |Example |Description
+
+|cml.defaultVal(value, fallback) |`cml.defaultVal(body.currency, 'USD')` 
|Returns `value` if non-null, `fallback` otherwise.
+
+|cml.isEmpty(value) |`cml.isEmpty(body.name)` |Returns `true` if the value is 
null, an empty string, or an empty array.
+|===
+
+=== Type Coercion Functions
+
+[width="100%",cols="20%,30%,50%",options="header",]
+|===
+|Function |Example |Description
+
+|cml.toInteger(value) |`cml.toInteger('42')` |Converts a string or number to 
an integer. Returns null for null input.
+
+|cml.toDecimal(value) |`cml.toDecimal('3.14')` |Converts a string to a decimal 
number. Returns null for null input.
+
+|cml.toBoolean(value) |`cml.toBoolean('true')` |Converts a string 
(`true`/`false`/`yes`/`no`/`1`/`0`) or number to boolean. Returns null for null 
input.
+|===
+
+=== Date/Time Functions
+
+[width="100%",cols="20%,30%,50%",options="header",]
+|===
+|Function |Example |Description
+
+|cml.now() |`cml.now()` |Returns the current timestamp as an ISO-8601 string.
+
+|cml.nowFmt(format) |`cml.nowFmt('yyyy-MM-dd')` |Returns the current UTC time 
formatted with the given pattern.
+
+|cml.formatDate(value, format) |`cml.formatDate('2026-03-20T10:30:00Z', 
'dd/MM/yyyy')` |Reformats an ISO-8601 date string using the given pattern. 
Returns null for null input.
+
+|cml.parseDate(value, format) |`cml.parseDate('20/03/2026', 'dd/MM/yyyy')` 
|Parses a date string into epoch milliseconds. Returns null for null input.
+|===
+
+=== Utility Functions
+
+[width="100%",cols="20%,30%,50%",options="header",]
+|===
+|Function |Example |Description
+
+|cml.uuid() |`cml.uuid()` |Generates a random UUID string.
+
+|cml.typeOf(value) |`cml.typeOf(body.x)` |Returns the type name: `string`, 
`number`, `boolean`, `object`, `array`, `null`, or `function`.
+|===
+
+=== Camel Standard Library (`camel.libsonnet`)
+
+Camel ships a standard library of helper functions that can be imported in any 
DataSonnet script:
+
+[source,java]
+----
+local c = import 'camel.libsonnet';
+{
+  total: c.sumBy(body.items, function(i) i.price * i.qty),
+  names: c.join(std.map(function(i) i.name, body.items), ', '),
+  topItem: c.first(c.sortBy(body.items, function(i) -i.price))
+}
+----
+
+==== String Helpers
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+|Function |Description
+
+|`c.capitalize(s)` |Capitalizes the first letter.
+|`c.trim(s)` |Strips whitespace from both ends.
+|`c.split(s, sep)` |Splits a string by separator.
+|`c.join(arr, sep)` |Joins an array of strings with separator.
+|`c.contains(s, sub)` |Checks if string contains substring.
+|`c.startsWith(s, prefix)` |Checks if string starts with prefix.
+|`c.endsWith(s, suffix)` |Checks if string ends with suffix.
+|`c.replace(s, old, new)` |Replaces all occurrences.
+|`c.lower(s)` / `c.upper(s)` |Case conversion.
+|===
+
+==== Collection Helpers
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+|Function |Description
+
+|`c.sum(arr)` |Sums all elements.
+|`c.sumBy(arr, f)` |Sums by applying function `f` to each element.
+|`c.first(arr)` / `c.last(arr)` |First/last element, or null if empty.
+|`c.count(arr)` |Array length.
+|`c.distinct(arr)` |Removes duplicates.
+|`c.flatMap(arr, f)` |Maps and flattens.
+|`c.sortBy(arr, f)` |Sorts by key function.
+|`c.groupBy(arr, f)` |Groups into object by key function.
+|`c.min(arr)` / `c.max(arr)` |Minimum/maximum element.
+|`c.take(arr, n)` / `c.drop(arr, n)` |First/last n elements.
+|`c.zip(a, b)` |Zips two arrays into pairs.
+|===
+
+==== Object Helpers
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+|Function |Description
+
+|`c.pick(obj, keys)` |Selects only the listed keys.
+|`c.omit(obj, keys)` |Removes the listed keys.
+|`c.merge(a, b)` |Merges two objects (b overrides a).
+|`c.keys(obj)` / `c.values(obj)` |Object keys/values as arrays.
+|`c.entries(obj)` |Converts to `[{key, value}]` array.
+|`c.fromEntries(arr)` |Converts `[{key, value}]` array back to object.
+|===
+
 Here's an example showing some of these functions in use:
 
 [tabs]
diff --git 
a/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
 
b/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
index 6c1db12711d9..50e2b2b5f9e6 100644
--- 
a/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
+++ 
b/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
@@ -16,10 +16,18 @@
  */
 package org.apache.camel.language.datasonnet;
 
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 
 import com.datasonnet.document.DefaultDocument;
 import com.datasonnet.document.Document;
@@ -60,29 +68,76 @@ public final class CML extends Library {
     @Override
     public Map<String, Val.Func> functions(DataFormatService dataFormats, 
Header header) {
         Map<String, Val.Func> answer = new HashMap<>();
+
+        // Existing Camel exchange access functions
         answer.put("properties", makeSimpleFunc(
-                Collections.singletonList("key"), //parameters list
+                Collections.singletonList("key"),
                 params -> properties(params.get(0))));
         answer.put("header", makeSimpleFunc(
-                Collections.singletonList("key"), //parameters list
+                Collections.singletonList("key"),
                 params -> header(params.get(0), dataFormats)));
         answer.put("variable", makeSimpleFunc(
-                Collections.singletonList("key"), //parameters list
+                Collections.singletonList("key"),
                 params -> variable(params.get(0), dataFormats)));
         answer.put("exchangeProperty", makeSimpleFunc(
-                Collections.singletonList("key"), //parameters list
+                Collections.singletonList("key"),
                 params -> exchangeProperty(params.get(0), dataFormats)));
 
+        // Null handling functions
+        answer.put("defaultVal", makeSimpleFunc(
+                Arrays.asList("value", "fallback"),
+                params -> defaultVal(params.get(0), params.get(1))));
+        answer.put("isEmpty", makeSimpleFunc(
+                Collections.singletonList("value"),
+                params -> isEmpty(params.get(0))));
+
+        // Type coercion functions
+        answer.put("toInteger", makeSimpleFunc(
+                Collections.singletonList("value"),
+                params -> toInteger(params.get(0))));
+        answer.put("toDecimal", makeSimpleFunc(
+                Collections.singletonList("value"),
+                params -> toDecimal(params.get(0))));
+        answer.put("toBoolean", makeSimpleFunc(
+                Collections.singletonList("value"),
+                params -> toBoolean(params.get(0))));
+
+        // Date/time functions
+        answer.put("now", makeSimpleFunc(
+                Collections.emptyList(),
+                params -> now()));
+        answer.put("nowFmt", makeSimpleFunc(
+                Collections.singletonList("format"),
+                params -> nowFmt(params.get(0))));
+        answer.put("formatDate", makeSimpleFunc(
+                Arrays.asList("value", "format"),
+                params -> formatDate(params.get(0), params.get(1))));
+        answer.put("parseDate", makeSimpleFunc(
+                Arrays.asList("value", "format"),
+                params -> parseDate(params.get(0), params.get(1))));
+
+        // Utility functions
+        answer.put("uuid", makeSimpleFunc(
+                Collections.emptyList(),
+                params -> uuid()));
+        answer.put("typeOf", makeSimpleFunc(
+                Collections.singletonList("value"),
+                params -> typeOf(params.get(0))));
+
         return answer;
     }
 
+    @Override
     public Map<String, Val.Obj> modules(DataFormatService dataFormats, Header 
header) {
         return Collections.emptyMap();
     }
 
+    // ---- Existing exchange access functions ----
+
     private Val properties(Val key) {
-        if (key instanceof Val.Str) {
-            return new 
Val.Str(exchange.get().getContext().resolvePropertyPlaceholders("{{" + 
((Val.Str) key).value() + "}}"));
+        if (key instanceof Val.Str str) {
+            return new Val.Str(
+                    
exchange.get().getContext().resolvePropertyPlaceholders("{{" + str.value() + 
"}}"));
         }
         throw new IllegalArgumentException("Expected String got: " + 
key.prettyName());
     }
@@ -108,12 +163,189 @@ public final class CML extends Library {
         throw new IllegalArgumentException("Expected String got: " + 
key.prettyName());
     }
 
+    // ---- Null handling functions ----
+
+    private Val defaultVal(Val value, Val fallback) {
+        if (isNull(value)) {
+            return fallback;
+        }
+        return value;
+    }
+
+    private Val isEmpty(Val value) {
+        if (isNull(value)) {
+            return Val.True$.MODULE$;
+        }
+        if (value instanceof Val.Str str) {
+            return str.value().isEmpty() ? Val.True$.MODULE$ : 
Val.False$.MODULE$;
+        }
+        if (value instanceof Val.Arr arr) {
+            return arr.value().isEmpty() ? Val.True$.MODULE$ : 
Val.False$.MODULE$;
+        }
+        return Val.False$.MODULE$;
+    }
+
+    // ---- Type coercion functions ----
+
+    private Val toInteger(Val value) {
+        if (isNull(value)) {
+            return Val.Null$.MODULE$;
+        }
+        if (value instanceof Val.Num num) {
+            return new Val.Num((int) num.value());
+        }
+        if (value instanceof Val.Str str) {
+            return new Val.Num(Integer.parseInt(str.value().trim()));
+        }
+        if (value instanceof Val.Bool) {
+            return new Val.Num(value == Val.True$.MODULE$ ? 1 : 0);
+        }
+        throw new IllegalArgumentException("Cannot convert " + 
value.prettyName() + " to integer");
+    }
+
+    private Val toDecimal(Val value) {
+        if (isNull(value)) {
+            return Val.Null$.MODULE$;
+        }
+        if (value instanceof Val.Num num) {
+            return value;
+        }
+        if (value instanceof Val.Str str) {
+            return new Val.Num(new 
BigDecimal(str.value().trim()).doubleValue());
+        }
+        throw new IllegalArgumentException("Cannot convert " + 
value.prettyName() + " to decimal");
+    }
+
+    private Val toBoolean(Val value) {
+        if (isNull(value)) {
+            return Val.Null$.MODULE$;
+        }
+        if (value instanceof Val.Bool) {
+            return value;
+        }
+        if (value instanceof Val.Str str) {
+            String s = str.value().trim().toLowerCase();
+            return switch (s) {
+                case "true", "1", "yes" -> Val.True$.MODULE$;
+                case "false", "0", "no" -> Val.False$.MODULE$;
+                default -> throw new IllegalArgumentException("Cannot convert 
string '" + s + "' to boolean");
+            };
+        }
+        if (value instanceof Val.Num num) {
+            return num.value() != 0 ? Val.True$.MODULE$ : Val.False$.MODULE$;
+        }
+        throw new IllegalArgumentException("Cannot convert " + 
value.prettyName() + " to boolean");
+    }
+
+    // ---- Date/time functions ----
+
+    private Val now() {
+        return new Val.Str(Instant.now().toString());
+    }
+
+    private Val nowFmt(Val format) {
+        if (!(format instanceof Val.Str str)) {
+            throw new IllegalArgumentException("Expected String format, got: " 
+ format.prettyName());
+        }
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(str.value());
+        return new 
Val.Str(ZonedDateTime.now(ZoneId.of("UTC")).format(formatter));
+    }
+
+    private Val formatDate(Val value, Val format) {
+        if (isNull(value)) {
+            return Val.Null$.MODULE$;
+        }
+        if (!(value instanceof Val.Str valStr)) {
+            throw new IllegalArgumentException("Expected String date value, 
got: " + value.prettyName());
+        }
+        if (!(format instanceof Val.Str fmtStr)) {
+            throw new IllegalArgumentException("Expected String format, got: " 
+ format.prettyName());
+        }
+        ZonedDateTime dateTime = parseToZonedDateTime(valStr.value());
+        DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(fmtStr.value());
+        return new Val.Str(dateTime.format(formatter));
+    }
+
+    private Val parseDate(Val value, Val format) {
+        if (isNull(value)) {
+            return Val.Null$.MODULE$;
+        }
+        if (!(value instanceof Val.Str valStr)) {
+            throw new IllegalArgumentException("Expected String date value, 
got: " + value.prettyName());
+        }
+        if (!(format instanceof Val.Str fmtStr)) {
+            throw new IllegalArgumentException("Expected String format, got: " 
+ format.prettyName());
+        }
+        DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(fmtStr.value()).withZone(ZoneId.of("UTC"));
+        java.time.temporal.TemporalAccessor parsed = 
formatter.parse(valStr.value());
+        Instant instant;
+        try {
+            instant = Instant.from(parsed);
+        } catch (java.time.DateTimeException e) {
+            // If the format has no time component, default to start of day
+            java.time.LocalDate date = java.time.LocalDate.from(parsed);
+            instant = date.atStartOfDay(ZoneId.of("UTC")).toInstant();
+        }
+        return new Val.Num(instant.toEpochMilli());
+    }
+
+    // ---- Utility functions ----
+
+    private Val uuid() {
+        return new Val.Str(UUID.randomUUID().toString());
+    }
+
+    private Val typeOf(Val value) {
+        if (isNull(value)) {
+            return new Val.Str("null");
+        }
+        if (value instanceof Val.Str) {
+            return new Val.Str("string");
+        }
+        if (value instanceof Val.Num) {
+            return new Val.Str("number");
+        }
+        if (value instanceof Val.Bool) {
+            return new Val.Str("boolean");
+        }
+        if (value instanceof Val.Arr) {
+            return new Val.Str("array");
+        }
+        if (value instanceof Val.Obj) {
+            return new Val.Str("object");
+        }
+        if (value instanceof Val.Func) {
+            return new Val.Str("function");
+        }
+        return new Val.Str("unknown");
+    }
+
+    // ---- Helper methods ----
+
+    private static boolean isNull(Val value) {
+        return value == null || value instanceof Val.Null$;
+    }
+
+    private static ZonedDateTime parseToZonedDateTime(String value) {
+        try {
+            return ZonedDateTime.parse(value);
+        } catch (DateTimeParseException e) {
+            // Try as instant
+            try {
+                return Instant.parse(value).atZone(ZoneId.of("UTC"));
+            } catch (DateTimeParseException e2) {
+                throw new IllegalArgumentException("Cannot parse date: " + 
value, e2);
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
     private Val valFrom(Object obj, DataFormatService dataformats) {
-        Document doc;
-        if (obj instanceof Document document) {
+        Document<?> doc;
+        if (obj instanceof Document<?> document) {
             doc = document;
         } else {
-            doc = new DefaultDocument(obj, MediaTypes.APPLICATION_JAVA);
+            doc = new DefaultDocument<>(obj, MediaTypes.APPLICATION_JAVA);
         }
 
         try {
diff --git a/components/camel-datasonnet/src/main/resources/camel.libsonnet 
b/components/camel-datasonnet/src/main/resources/camel.libsonnet
new file mode 100644
index 000000000000..9852088fd0b9
--- /dev/null
+++ b/components/camel-datasonnet/src/main/resources/camel.libsonnet
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+// Apache Camel standard library for DataSonnet
+// Auto-discovered from the classpath. Import with: local c = import 
'camel.libsonnet';
+{
+  // String helpers
+  capitalize(s):: std.asciiUpper(s[0]) + s[1:],
+  trim(s):: std.stripChars(s, " \t\n\r"),
+  split(s, sep):: std.split(s, sep),
+  join(arr, sep):: std.join(sep, arr),
+  contains(s, sub):: std.length(std.findSubstr(sub, s)) > 0,
+  startsWith(s, prefix):: std.length(s) >= std.length(prefix) && 
s[:std.length(prefix)] == prefix,
+  endsWith(s, suffix):: std.length(s) >= std.length(suffix) && s[std.length(s) 
- std.length(suffix):] == suffix,
+  replace(s, old, new):: std.strReplace(s, old, new),
+  lower(s):: std.asciiLower(s),
+  upper(s):: std.asciiUpper(s),
+
+  // Collection helpers
+  sum(arr):: std.foldl(function(acc, x) acc + x, arr, 0),
+  sumBy(arr, f):: std.foldl(function(acc, x) acc + f(x), arr, 0),
+  first(arr):: if std.length(arr) > 0 then arr[0] else null,
+  last(arr):: if std.length(arr) > 0 then arr[std.length(arr) - 1] else null,
+  count(arr):: std.length(arr),
+  distinct(arr):: std.foldl(
+    function(acc, x) if std.member(acc, x) then acc else acc + [x],
+    arr, []
+  ),
+  flatMap(arr, f):: std.flatMap(f, arr),
+  sortBy(arr, f):: std.sort(arr, keyF=f),
+  groupBy(arr, f):: std.foldl(
+    function(acc, x)
+      local k = f(x);
+      acc + (
+        if std.objectHas(acc, k)
+        then { [k]: acc[k] + [x] }
+        else { [k]: [x] }
+      ),
+    arr, {}
+  ),
+  min(arr):: std.foldl(function(acc, x) if acc == null || x < acc then x else 
acc, arr, null),
+  max(arr):: std.foldl(function(acc, x) if acc == null || x > acc then x else 
acc, arr, null),
+  zip(a, b):: std.mapWithIndex(function(i, x) [x, b[i]], a),
+  take(arr, n):: arr[:n],
+  drop(arr, n):: arr[n:],
+
+  // Object helpers
+  pick(obj, keys):: { [k]: obj[k] for k in keys if std.objectHas(obj, k) },
+  omit(obj, keys):: { [k]: obj[k] for k in std.objectFields(obj) if 
!std.member(keys, k) },
+  merge(a, b):: a + b,
+  entries(obj):: [{ key: k, value: obj[k] } for k in std.objectFields(obj)],
+  fromEntries(arr):: { [e.key]: e.value for e in arr },
+  keys(obj):: std.objectFields(obj),
+  values(obj):: [obj[k] for k in std.objectFields(obj)],
+}
diff --git 
a/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/CamelLibsonnetTest.java
 
b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/CamelLibsonnetTest.java
new file mode 100644
index 000000000000..cea1d5669e75
--- /dev/null
+++ 
b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/CamelLibsonnetTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.language.datasonnet;
+
+import com.datasonnet.document.MediaTypes;
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CamelLibsonnetTest extends CamelTestSupport {
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // String helper tests
+                from("direct:capitalize")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet'; 
c.capitalize('hello')",
+                                String.class))
+                        .to("mock:result");
+
+                from("direct:stringOps")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " { trimmed: c.trim('  hi  '),"
+                                              + "   parts: c.split('a,b,c', 
','),"
+                                              + "   joined: 
c.join(['x','y','z'], '-'),"
+                                              + "   has: c.contains('hello 
world', 'world'),"
+                                              + "   starts: 
c.startsWith('hello', 'hel'),"
+                                              + "   ends: c.endsWith('hello', 
'llo'),"
+                                              + "   replaced: c.replace('foo 
bar foo', 'foo', 'baz'),"
+                                              + "   low: c.lower('HELLO'),"
+                                              + "   up: c.upper('hello')"
+                                              + " }",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                // Collection helper tests
+                from("direct:collectionOps")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " { total: c.sum([1, 2, 3, 
4]),"
+                                              + "   totalBy: c.sumBy([{v: 10}, 
{v: 20}], function(x) x.v),"
+                                              + "   head: c.first([5, 6, 7]),"
+                                              + "   tail: c.last([5, 6, 7]),"
+                                              + "   len: c.count([1, 2, 3]),"
+                                              + "   unique: c.distinct([1, 2, 
2, 3, 3, 3]),"
+                                              + "   smallest: c.min([3, 1, 
2]),"
+                                              + "   biggest: c.max([3, 1, 2]),"
+                                              + "   taken: c.take([1,2,3,4,5], 
3),"
+                                              + "   dropped: 
c.drop([1,2,3,4,5], 2)"
+                                              + " }",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                from("direct:firstEmpty")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet'; 
c.first([])",
+                                String.class))
+                        .to("mock:result");
+
+                from("direct:groupBy")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " 
c.groupBy([{t:'a',v:1},{t:'b',v:2},{t:'a',v:3}], function(x) x.t)",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                from("direct:flatMap")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " c.flatMap([[1,2],[3,4]], 
function(x) x)",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                from("direct:sortBy")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " 
c.sortBy([{n:3},{n:1},{n:2}], function(x) x.n)",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                // Object helper tests
+                from("direct:objectOps")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " { picked: c.pick({a:1, b:2, 
c:3}, ['a','c']),"
+                                              + "   omitted: c.omit({a:1, b:2, 
c:3}, ['b']),"
+                                              + "   merged: c.merge({a:1}, 
{b:2}),"
+                                              + "   k: c.keys({x:1, y:2}),"
+                                              + "   v: c.values({x:1, y:2})"
+                                              + " }",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                from("direct:entries")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " c.entries({a:1, b:2})",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                from("direct:fromEntries")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " 
c.fromEntries([{key:'a',value:1},{key:'b',value:2}])",
+                                String.class,
+                                null, MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+
+                // Combined test with body data
+                from("direct:transformWithLib")
+                        .transform(datasonnet(
+                                "local c = import 'camel.libsonnet';"
+                                              + " { total: c.sumBy(body.items, 
function(i) i.price * i.qty),"
+                                              + "   names: 
c.join(std.map(function(i) i.name, body.items), ', '),"
+                                              + "   count: c.count(body.items)"
+                                              + " }",
+                                String.class,
+                                MediaTypes.APPLICATION_JSON_VALUE, 
MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+            }
+        };
+    }
+
+    @Test
+    public void testCapitalize() throws Exception {
+        assertEquals("Hello", sendAndGetString("direct:capitalize", ""));
+    }
+
+    @Test
+    public void testStringOps() throws Exception {
+        String result = sendAndGetString("direct:stringOps", "");
+        JSONAssert.assertEquals(
+                "{\"trimmed\":\"hi\","
+                                + "\"parts\":[\"a\",\"b\",\"c\"],"
+                                + "\"joined\":\"x-y-z\","
+                                + "\"has\":true,"
+                                + "\"starts\":true,"
+                                + "\"ends\":true,"
+                                + "\"replaced\":\"baz bar baz\","
+                                + "\"low\":\"hello\","
+                                + "\"up\":\"HELLO\"}",
+                result, false);
+    }
+
+    @Test
+    public void testCollectionOps() throws Exception {
+        String result = sendAndGetString("direct:collectionOps", "");
+        JSONAssert.assertEquals(
+                "{\"total\":10,"
+                                + "\"totalBy\":30,"
+                                + "\"head\":5,"
+                                + "\"tail\":7,"
+                                + "\"len\":3,"
+                                + "\"unique\":[1,2,3],"
+                                + "\"smallest\":1,"
+                                + "\"biggest\":3,"
+                                + "\"taken\":[1,2,3],"
+                                + "\"dropped\":[3,4,5]}",
+                result, false);
+    }
+
+    @Test
+    public void testFirstEmpty() throws Exception {
+        Object result = sendAndGetBody("direct:firstEmpty", "");
+        // null result from first([])
+        assertEquals(null, result);
+    }
+
+    @Test
+    public void testGroupBy() throws Exception {
+        String result = sendAndGetString("direct:groupBy", "");
+        JSONAssert.assertEquals(
+                
"{\"a\":[{\"t\":\"a\",\"v\":1},{\"t\":\"a\",\"v\":3}],\"b\":[{\"t\":\"b\",\"v\":2}]}",
+                result, false);
+    }
+
+    @Test
+    public void testFlatMap() throws Exception {
+        String result = sendAndGetString("direct:flatMap", "");
+        JSONAssert.assertEquals("[1,2,3,4]", result, false);
+    }
+
+    @Test
+    public void testSortBy() throws Exception {
+        String result = sendAndGetString("direct:sortBy", "");
+        JSONAssert.assertEquals("[{\"n\":1},{\"n\":2},{\"n\":3}]", result, 
false);
+    }
+
+    @Test
+    public void testObjectOps() throws Exception {
+        String result = sendAndGetString("direct:objectOps", "");
+        JSONAssert.assertEquals(
+                "{\"picked\":{\"a\":1,\"c\":3},"
+                                + "\"omitted\":{\"a\":1,\"c\":3},"
+                                + "\"merged\":{\"a\":1,\"b\":2},"
+                                + "\"k\":[\"x\",\"y\"],"
+                                + "\"v\":[1,2]}",
+                result, false);
+    }
+
+    @Test
+    public void testEntries() throws Exception {
+        String result = sendAndGetString("direct:entries", "");
+        JSONAssert.assertEquals(
+                "[{\"key\":\"a\",\"value\":1},{\"key\":\"b\",\"value\":2}]",
+                result, false);
+    }
+
+    @Test
+    public void testFromEntries() throws Exception {
+        String result = sendAndGetString("direct:fromEntries", "");
+        JSONAssert.assertEquals("{\"a\":1,\"b\":2}", result, false);
+    }
+
+    @Test
+    public void testTransformWithLib() throws Exception {
+        String payload
+                = 
"{\"items\":[{\"name\":\"Widget\",\"price\":10,\"qty\":3},{\"name\":\"Gadget\",\"price\":25,\"qty\":2}]}";
+        String result = sendAndGetString("direct:transformWithLib", payload);
+        JSONAssert.assertEquals(
+                "{\"total\":80,\"names\":\"Widget, Gadget\",\"count\":2}",
+                result, false);
+    }
+
+    private String sendAndGetString(String uri, Object body) {
+        template.sendBody(uri, body);
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        Exchange exchange = 
mock.assertExchangeReceived(mock.getReceivedCounter() - 1);
+        return exchange.getMessage().getBody(String.class);
+    }
+
+    private Object sendAndGetBody(String uri, Object body) {
+        template.sendBody(uri, body);
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        Exchange exchange = 
mock.assertExchangeReceived(mock.getReceivedCounter() - 1);
+        return exchange.getMessage().getBody();
+    }
+}
diff --git 
a/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/CmlFunctionsTest.java
 
b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/CmlFunctionsTest.java
new file mode 100644
index 000000000000..e435e35dd1aa
--- /dev/null
+++ 
b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/CmlFunctionsTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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.language.datasonnet;
+
+import com.datasonnet.document.MediaTypes;
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CmlFunctionsTest extends CamelTestSupport {
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // Null handling routes
+                from("direct:defaultVal-null")
+                        .transform(datasonnet("cml.defaultVal(null, 
'fallback')", String.class))
+                        .to("mock:result");
+                from("direct:defaultVal-present")
+                        .transform(datasonnet("cml.defaultVal('hello', 
'fallback')", String.class))
+                        .to("mock:result");
+                from("direct:isEmpty-null")
+                        .transform(datasonnet("cml.isEmpty(null)", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:isEmpty-emptyString")
+                        .transform(datasonnet("cml.isEmpty('')", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:isEmpty-nonEmpty")
+                        .transform(datasonnet("cml.isEmpty('hello')", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:isEmpty-emptyArray")
+                        .transform(datasonnet("cml.isEmpty([])", 
Boolean.class))
+                        .to("mock:result");
+
+                // Type coercion routes
+                from("direct:toInteger-string")
+                        .transform(datasonnet("cml.toInteger('42')", 
Integer.class))
+                        .to("mock:result");
+                from("direct:toInteger-num")
+                        .transform(datasonnet("cml.toInteger(3.7)", 
Integer.class))
+                        .to("mock:result");
+                from("direct:toInteger-null")
+                        .transform(datasonnet("cml.toInteger(null)", 
String.class))
+                        .to("mock:result");
+                from("direct:toDecimal-string")
+                        .transform(datasonnet("cml.toDecimal('3.14')", 
Double.class))
+                        .to("mock:result");
+                from("direct:toBoolean-true")
+                        .transform(datasonnet("cml.toBoolean('true')", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:toBoolean-yes")
+                        .transform(datasonnet("cml.toBoolean('yes')", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:toBoolean-false")
+                        .transform(datasonnet("cml.toBoolean('false')", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:toBoolean-zero")
+                        .transform(datasonnet("cml.toBoolean('0')", 
Boolean.class))
+                        .to("mock:result");
+                from("direct:toBoolean-num")
+                        .transform(datasonnet("cml.toBoolean(1)", 
Boolean.class))
+                        .to("mock:result");
+
+                // Date/time routes
+                from("direct:now")
+                        .transform(datasonnet("cml.now()", String.class))
+                        .to("mock:result");
+                from("direct:nowFmt")
+                        .transform(datasonnet("cml.nowFmt('yyyy-MM-dd')", 
String.class))
+                        .to("mock:result");
+                from("direct:formatDate")
+                        
.transform(datasonnet("cml.formatDate('2026-03-20T10:30:00Z', 'dd/MM/yyyy')", 
String.class))
+                        .to("mock:result");
+                from("direct:parseDate")
+                        .transform(datasonnet("cml.parseDate('20/03/2026', 
'dd/MM/yyyy')", Double.class))
+                        .to("mock:result");
+                from("direct:formatDate-null")
+                        .transform(datasonnet("cml.formatDate(null, 
'dd/MM/yyyy')", String.class))
+                        .to("mock:result");
+
+                // Utility routes
+                from("direct:uuid")
+                        .transform(datasonnet("cml.uuid()", String.class))
+                        .to("mock:result");
+                from("direct:typeOf-string")
+                        .transform(datasonnet("cml.typeOf('hello')", 
String.class))
+                        .to("mock:result");
+                from("direct:typeOf-number")
+                        .transform(datasonnet("cml.typeOf(42)", String.class))
+                        .to("mock:result");
+                from("direct:typeOf-boolean")
+                        .transform(datasonnet("cml.typeOf(true)", 
String.class))
+                        .to("mock:result");
+                from("direct:typeOf-null")
+                        .transform(datasonnet("cml.typeOf(null)", 
String.class))
+                        .to("mock:result");
+                from("direct:typeOf-array")
+                        .transform(datasonnet("cml.typeOf([1,2,3])", 
String.class))
+                        .to("mock:result");
+                from("direct:typeOf-object")
+                        .transform(datasonnet("cml.typeOf({a: 1})", 
String.class))
+                        .to("mock:result");
+
+                // Combined test: use body data with cml functions
+                from("direct:combined")
+                        .transform(datasonnet(
+                                "{ name: cml.defaultVal(body.name, 'unknown'), 
active: cml.toBoolean(body.active) }",
+                                String.class,
+                                MediaTypes.APPLICATION_JSON_VALUE, 
MediaTypes.APPLICATION_JSON_VALUE))
+                        .to("mock:result");
+            }
+        };
+    }
+
+    // ---- Null handling tests ----
+
+    @Test
+    public void testDefaultValNull() throws Exception {
+        Object result = sendAndGetResult("direct:defaultVal-null", "");
+        assertEquals("fallback", result);
+    }
+
+    @Test
+    public void testDefaultValPresent() throws Exception {
+        Object result = sendAndGetResult("direct:defaultVal-present", "");
+        assertEquals("hello", result);
+    }
+
+    @Test
+    public void testIsEmptyNull() throws Exception {
+        Object result = sendAndGetResult("direct:isEmpty-null", "");
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void testIsEmptyEmptyString() throws Exception {
+        Object result = sendAndGetResult("direct:isEmpty-emptyString", "");
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void testIsEmptyNonEmpty() throws Exception {
+        Object result = sendAndGetResult("direct:isEmpty-nonEmpty", "");
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void testIsEmptyEmptyArray() throws Exception {
+        Object result = sendAndGetResult("direct:isEmpty-emptyArray", "");
+        assertEquals(true, result);
+    }
+
+    // ---- Type coercion tests ----
+
+    @Test
+    public void testToIntegerString() throws Exception {
+        Object result = sendAndGetResult("direct:toInteger-string", "");
+        assertEquals(42, result);
+    }
+
+    @Test
+    public void testToIntegerNum() throws Exception {
+        Object result = sendAndGetResult("direct:toInteger-num", "");
+        assertEquals(3, result);
+    }
+
+    @Test
+    public void testToIntegerNull() throws Exception {
+        Object result = sendAndGetResult("direct:toInteger-null", "");
+        assertNull(result);
+    }
+
+    @Test
+    public void testToDecimalString() throws Exception {
+        Object result = sendAndGetResult("direct:toDecimal-string", "");
+        assertEquals(3.14, result);
+    }
+
+    @Test
+    public void testToBooleanTrue() throws Exception {
+        Object result = sendAndGetResult("direct:toBoolean-true", "");
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void testToBooleanYes() throws Exception {
+        Object result = sendAndGetResult("direct:toBoolean-yes", "");
+        assertEquals(true, result);
+    }
+
+    @Test
+    public void testToBooleanFalse() throws Exception {
+        Object result = sendAndGetResult("direct:toBoolean-false", "");
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void testToBooleanZero() throws Exception {
+        Object result = sendAndGetResult("direct:toBoolean-zero", "");
+        assertEquals(false, result);
+    }
+
+    @Test
+    public void testToBooleanNum() throws Exception {
+        Object result = sendAndGetResult("direct:toBoolean-num", "");
+        assertEquals(true, result);
+    }
+
+    // ---- Date/time tests ----
+
+    @Test
+    public void testNow() throws Exception {
+        Object result = sendAndGetResult("direct:now", "");
+        assertNotNull(result);
+        String str = result.toString();
+        // Should be ISO-8601 format
+        assertTrue(str.contains("T"), "Expected ISO-8601 format but got: " + 
str);
+    }
+
+    @Test
+    public void testNowFmt() throws Exception {
+        Object result = sendAndGetResult("direct:nowFmt", "");
+        assertNotNull(result);
+        String str = result.toString();
+        // Should match yyyy-MM-dd format
+        assertTrue(str.matches("\\d{4}-\\d{2}-\\d{2}"), "Expected date format 
but got: " + str);
+    }
+
+    @Test
+    public void testFormatDate() throws Exception {
+        Object result = sendAndGetResult("direct:formatDate", "");
+        assertEquals("20/03/2026", result);
+    }
+
+    @Test
+    public void testParseDate() throws Exception {
+        Object result = sendAndGetResult("direct:parseDate", "");
+        assertNotNull(result);
+        // 2026-03-20 00:00:00 UTC in epoch millis
+        assertTrue(((Number) result).longValue() > 0);
+    }
+
+    @Test
+    public void testFormatDateNull() throws Exception {
+        Object result = sendAndGetResult("direct:formatDate-null", "");
+        assertNull(result);
+    }
+
+    // ---- Utility tests ----
+
+    @Test
+    public void testUuid() throws Exception {
+        Object result = sendAndGetResult("direct:uuid", "");
+        assertNotNull(result);
+        String str = result.toString();
+        // UUID format: 8-4-4-4-12 hex digits
+        
assertTrue(str.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"),
+                "Expected UUID format but got: " + str);
+    }
+
+    @Test
+    public void testTypeOfString() throws Exception {
+        assertEquals("string", sendAndGetResult("direct:typeOf-string", ""));
+    }
+
+    @Test
+    public void testTypeOfNumber() throws Exception {
+        assertEquals("number", sendAndGetResult("direct:typeOf-number", ""));
+    }
+
+    @Test
+    public void testTypeOfBoolean() throws Exception {
+        assertEquals("boolean", sendAndGetResult("direct:typeOf-boolean", ""));
+    }
+
+    @Test
+    public void testTypeOfNull() throws Exception {
+        assertEquals("null", sendAndGetResult("direct:typeOf-null", ""));
+    }
+
+    @Test
+    public void testTypeOfArray() throws Exception {
+        assertEquals("array", sendAndGetResult("direct:typeOf-array", ""));
+    }
+
+    @Test
+    public void testTypeOfObject() throws Exception {
+        assertEquals("object", sendAndGetResult("direct:typeOf-object", ""));
+    }
+
+    // ---- Combined tests ----
+
+    @Test
+    public void testCombined() throws Exception {
+        template.sendBody("direct:combined", "{\"name\": \"John\", \"active\": 
\"true\"}");
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        Exchange exchange = 
mock.assertExchangeReceived(mock.getReceivedCounter() - 1);
+        String body = exchange.getMessage().getBody(String.class);
+        assertTrue(body.contains("\"name\":\"John\"") || 
body.contains("\"name\": \"John\""));
+        assertTrue(body.contains("\"active\":true") || 
body.contains("\"active\": true"));
+    }
+
+    private Object sendAndGetResult(String uri, Object body) {
+        template.sendBody(uri, body);
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        Exchange exchange = 
mock.assertExchangeReceived(mock.getReceivedCounter() - 1);
+        return exchange.getMessage().getBody();
+    }
+}


Reply via email to