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

spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 352312a8d30dde1fe02785c5750d212c35906231
Author: Stephen Mallette <[email protected]>
AuthorDate: Tue Apr 7 19:53:42 2026 -0400

    Add splitByElement() to Gherkin step definitions for nested collection 
parsing
    
    Naive split(",") breaks on nested collection tokens like l[1,2,3] inside
    s[l[1,2,3],l[4,5,6]]. Adds a bracket-depth-aware splitByElement() helper to
    all Gherkin step definition files across Java and every GLV, so that commas
    inside nested brackets are not treated as top-level separators.
    
    Also adds two new Gherkin scenarios to Fold.feature that exercise nested
    list-of-list results (g.inject([1,2],[3,4]).fold()), along with the
    corresponding traversal bindings in each GLV's translation map.
    
    Java implementation follows PR #3216.
    
    (tinkerpop-mxr, tinkerpop-32y, tinkerpop-ghp, tinkerpop-2ur, tinkerpop-70k, 
tinkerpop-188, tinkerpop-93a)
---
 .../Gherkin/CommonSteps.cs                         | 34 ++++++++++++++++-
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |  2 +
 gremlin-go/driver/cucumber/cucumberSteps_test.go   | 31 +++++++++++++++-
 gremlin-go/driver/cucumber/gremlin.go              |  2 +
 .../test/cucumber/feature-steps.js                 | 41 +++++++++++++++++----
 .../gremlin-javascript/test/cucumber/gremlin.js    |  2 +
 .../src/main/python/tests/feature/feature_steps.py | 25 ++++++++++++-
 .../src/main/python/tests/feature/gremlin.py       |  2 +
 .../tinkerpop/gremlin/features/StepDefinition.java | 43 ++++++++++++++++++----
 .../gremlin/test/features/map/Fold.feature         | 24 +++++++++++-
 10 files changed, 184 insertions(+), 22 deletions(-)

diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
index 1b747e1bf3..bb3cf3e671 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
@@ -492,7 +492,39 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
             {
                 return new List<object?>(0);
             }
-            return stringList.Split(',').Select(x => ParseValue(x, 
graphName)).ToList();
+            return SplitByElement(stringList).Select(x => ParseValue(x, 
graphName)).ToList();
+        }
+
+        private static List<string> SplitByElement(string s)
+        {
+            var result = new List<string>();
+            var depth = 0;
+            var current = new System.Text.StringBuilder();
+            foreach (var c in s)
+            {
+                if (c == '[')
+                {
+                    depth++;
+                    current.Append(c);
+                }
+                else if (c == ']')
+                {
+                    depth--;
+                    current.Append(c);
+                }
+                else if (c == ',' && depth == 0)
+                {
+                    result.Add(current.ToString().Trim());
+                    current.Clear();
+                }
+                else
+                {
+                    current.Append(c);
+                }
+            }
+            if (current.Length > 0)
+                result.Add(current.ToString().Trim());
+            return result;
         }
 
         private static object ToDateTime(string date, string graphName)
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 2efd471c8e..22f94c80a6 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1187,6 +1187,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_V_age_foldX0_plusX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) 
=>g.V().Values<object>("age").Fold<object>(0, Operator.Sum)}}, 
                {"g_injectXa1_b2X_foldXm_addAllX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(new Dictionary<object, object> {{ "a", 1 }}, new 
Dictionary<object, object> {{ "b", 2 }}).Fold<object>(new Dictionary<object, 
object> {}, Operator.AddAll)}}, 
                {"g_injectXa1_b2_b4X_foldXm_addAllX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(new Dictionary<object, object> {{ "a", 1 }}, new 
Dictionary<object, object> {{ "b", 2 }}, new Dictionary<object, object> {{ "b", 
4 }}).Fold<object>(new Dictionary<object, object> {}, Operator.AddAll)}}, 
+               {"g_injectXlist1_list2X_fold", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(new List<object> { 1, 2 }, new List<object> { 3, 4 
}).Fold()}}, 
+               {"g_injectXlist1_list2_list3X_fold", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(new List<object> { 1, 2 }, new List<object> { 3, 4 }, 
new List<object> { 5, 6 }).Fold()}}, 
                {"g_VX1X_formatXstrX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name", 
"marko").Format("Hello world")}}, 
                {"g_V_formatXstrX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Format("%{name} is 
%{age} years old")}}, 
                {"g_injectX1X_asXageX_V_formatXstrX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(1).As("age").V().Format("%{name} is %{age} years 
old")}}, 
diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go 
b/gremlin-go/driver/cucumber/cucumberSteps_test.go
index f0de2bd94c..87d1ecdae0 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -256,6 +256,33 @@ func toPath(stringObjects, graphName string) interface{} {
        }
 }
 
+// splitByElement splits a string on commas while respecting bracket nesting 
depth,
+// so that nested tokens like l[1,2,3] inside s[l[1,2,3],l[4,5,6]] are not 
split incorrectly.
+func splitByElement(s string) []string {
+       var result []string
+       depth := 0
+       current := strings.Builder{}
+       for _, c := range s {
+               switch {
+               case c == '[':
+                       depth++
+                       current.WriteRune(c)
+               case c == ']':
+                       depth--
+                       current.WriteRune(c)
+               case c == ',' && depth == 0:
+                       result = append(result, 
strings.TrimSpace(current.String()))
+                       current.Reset()
+               default:
+                       current.WriteRune(c)
+               }
+       }
+       if current.Len() > 0 {
+               result = append(result, strings.TrimSpace(current.String()))
+       }
+       return result
+}
+
 // Parse list.
 func toList(stringList, graphName string) interface{} {
        listVal := make([]interface{}, 0)
@@ -263,7 +290,7 @@ func toList(stringList, graphName string) interface{} {
                return listVal
        }
 
-       for _, str := range strings.Split(stringList, ",") {
+       for _, str := range splitByElement(stringList) {
                listVal = append(listVal, parseValue(str, graphName))
        }
        return listVal
@@ -275,7 +302,7 @@ func toSet(stringSet, graphName string) interface{} {
        if len(stringSet) == 0 {
                return setVal
        }
-       for _, str := range strings.Split(stringSet, ",") {
+       for _, str := range splitByElement(stringSet) {
                setVal.Add(parseValue(str, graphName))
        }
        return setVal
diff --git a/gremlin-go/driver/cucumber/gremlin.go 
b/gremlin-go/driver/cucumber/gremlin.go
index aa9d08ee30..a26f91342f 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1157,6 +1157,8 @@ var translationMap = map[string][]func(g 
*gremlingo.GraphTraversalSource, p map[
     "g_V_age_foldX0_plusX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.V().Values("age").Fold(0, gremlingo.Operator.Sum)}}, 
     "g_injectXa1_b2X_foldXm_addAllX": {func(g *gremlingo.GraphTraversalSource, 
p map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Inject(map[interface{}]interface{}{"a": 1 }, map[interface{}]interface{}{"b": 
2 }).Fold(map[interface{}]interface{}{ }, gremlingo.Operator.AddAll)}}, 
     "g_injectXa1_b2_b4X_foldXm_addAllX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.Inject(map[interface{}]interface{}{"a": 1 
}, map[interface{}]interface{}{"b": 2 }, map[interface{}]interface{}{"b": 4 
}).Fold(map[interface{}]interface{}{ }, gremlingo.Operator.AddAll)}}, 
+    "g_injectXlist1_list2X_fold": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Inject([]interface{}{1, 2}, []interface{}{3, 4}).Fold()}}, 
+    "g_injectXlist1_list2_list3X_fold": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.Inject([]interface{}{1, 2}, 
[]interface{}{3, 4}, []interface{}{5, 6}).Fold()}}, 
     "g_VX1X_formatXstrX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Has("name", 
"marko").Format("Hello world")}}, 
     "g_V_formatXstrX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Format("%{name} 
is %{age} years old")}}, 
     "g_injectX1X_asXageX_V_formatXstrX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.Inject(1).As("age").V().Format("%{name} is 
%{age} years old")}}, 
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js 
b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
index 59eeb1d76a..e8ac2d1c1c 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/feature-steps.js
@@ -448,22 +448,47 @@ function toMerge(value) {
   return merge[value];
 }
 
+function splitByElement(s) {
+  let depth = 0;
+  let current = '';
+  const results = [];
+  for (const c of s) {
+    if (c === '[') {
+      depth++;
+      current += c;
+    } else if (c === ']') {
+      depth--;
+      current += c;
+    } else if (c === ',' && depth === 0) {
+      results.push(current.trim());
+      current = '';
+    } else {
+      current += c;
+    }
+  }
+  if (current.length > 0) results.push(current.trim());
+  return results;
+}
+
 function toArray(stringList) {
   if (stringList === '') {
     return new Array(0);
   }
-  return stringList.split(',').map(x => parseValue.call(this, x));
-}
-
-function toMap(stringMap) {
-  return parseMapValue.call(this, JSON.parse(stringMap));
+  return splitByElement(stringList).map(x => parseValue.call(this, x));
 }
 
-function toSet(stringSet) {
-  if (stringSet === '') {
+function toSet(stringList) {
+  if (stringList === '') {
     return new Set();
   }
-  return new Set(stringSet.split(',').map(x => parseValue.call(this, x)));
+
+  const s = new Set();
+  splitByElement(stringList).forEach(x => s.add(parseValue.call(this, x)));
+  return s;
+}
+
+function toMap(stringMap) {
+  return parseMapValue.call(this, JSON.parse(stringMap));
 }
 
 function parseMapValue(value) {
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js 
b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 66138f47f6..fb90ed1dd4 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -1188,6 +1188,8 @@ const gremlins = {
     g_V_age_foldX0_plusX: [function({g}) { return g.V().values("age").fold(0, 
Operator.sum) }], 
     g_injectXa1_b2X_foldXm_addAllX: [function({g}) { return g.inject(new 
Map([["a", 1]]), new Map([["b", 2]])).fold(new Map([]), Operator.addAll) }], 
     g_injectXa1_b2_b4X_foldXm_addAllX: [function({g}) { return g.inject(new 
Map([["a", 1]]), new Map([["b", 2]]), new Map([["b", 4]])).fold(new Map([]), 
Operator.addAll) }], 
+    g_injectXlist1_list2X_fold: [function({g}) { return g.inject([1, 2], [3, 
4]).fold() }], 
+    g_injectXlist1_list2_list3X_fold: [function({g}) { return g.inject([1, 2], 
[3, 4], [5, 6]).fold() }], 
     g_VX1X_formatXstrX: [function({g}) { return g.V().has("name", 
"marko").format("Hello world") }], 
     g_V_formatXstrX: [function({g}) { return g.V().format("%{name} is %{age} 
years old") }], 
     g_injectX1X_asXageX_V_formatXstrX: [function({g}) { return 
g.inject(1).as("age").V().format("%{name} is %{age} years old") }], 
diff --git a/gremlin-python/src/main/python/tests/feature/feature_steps.py 
b/gremlin-python/src/main/python/tests/feature/feature_steps.py
index c074bf171a..f82b65ab55 100644
--- a/gremlin-python/src/main/python/tests/feature/feature_steps.py
+++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py
@@ -324,6 +324,27 @@ def unsupported_scenario(step, file):
     return
 
 
+def _split_by_element(s):
+    depth = 0
+    current = []
+    results = []
+    for c in s:
+        if c == '[':
+            depth += 1
+            current.append(c)
+        elif c == ']':
+            depth -= 1
+            current.append(c)
+        elif c == ',' and depth == 0:
+            results.append(''.join(current).strip())
+            current = []
+        else:
+            current.append(c)
+    if current:
+        results.append(''.join(current).strip())
+    return results
+
+
 def _convert(val, ctx):
     graph_name = ctx.graph_name
     if isinstance(val, dict):  # convert dictionary keys/values
@@ -334,9 +355,9 @@ def _convert(val, ctx):
             n[tuple(k) if isinstance(k, (set, list)) else k] = _convert(value, 
ctx)
         return n
     elif isinstance(val, str) and re.match(r"^l\[.*\]$", val):  # parse list
-        return [] if val == "l[]" else list(map((lambda x: _convert(x, ctx)), 
val[2:-1].split(",")))
+        return [] if val == "l[]" else list(map((lambda x: _convert(x, ctx)), 
_split_by_element(val[2:-1])))
     elif isinstance(val, str) and re.match(r"^s\[.*\]$", val):  # parse set
-        return set() if val == "s[]" else set(map((lambda x: _convert(x, 
ctx)), val[2:-1].split(",")))
+        return set() if val == "s[]" else set(map((lambda x: _convert(x, 
ctx)), _split_by_element(val[2:-1])))
     elif isinstance(val, str) and re.match(r"^str\[.*\]$", val):  # return 
string as is
         return val[4:-1]
     elif isinstance(val, str) and re.match(r"^dt\[.*\]$", val):  # parse 
datetime
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py 
b/gremlin-python/src/main/python/tests/feature/gremlin.py
index d1cfbf06ab..fa469b2d3b 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1160,6 +1160,8 @@ world.gremlins = {
     'g_V_age_foldX0_plusX': [(lambda g:g.V().values('age').fold(0, 
Operator.sum_))], 
     'g_injectXa1_b2X_foldXm_addAllX': [(lambda g:g.inject({ 'a': 1 }, { 'b': 2 
}).fold({  }, Operator.add_all))], 
     'g_injectXa1_b2_b4X_foldXm_addAllX': [(lambda g:g.inject({ 'a': 1 }, { 
'b': 2 }, { 'b': 4 }).fold({  }, Operator.add_all))], 
+    'g_injectXlist1_list2X_fold': [(lambda g:g.inject([1, 2], [3, 
4]).fold())], 
+    'g_injectXlist1_list2_list3X_fold': [(lambda g:g.inject([1, 2], [3, 4], 
[5, 6]).fold())], 
     'g_VX1X_formatXstrX': [(lambda g:g.V().has('name', 'marko').format_('Hello 
world'))], 
     'g_V_formatXstrX': [(lambda g:g.V().format_('%{name} is %{age} years 
old'))], 
     'g_injectX1X_asXageX_V_formatXstrX': [(lambda 
g:g.inject(1).as_('age').V().format_('%{name} is %{age} years old'))], 
diff --git 
a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
 
b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
index 1d653c8a89..09987991ac 100644
--- 
a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
+++ 
b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java
@@ -135,8 +135,8 @@ public final class StepDefinition {
         }));
         add(Pair.with(Pattern.compile("l\\[\\]"), s -> "[]"));
         add(Pair.with(Pattern.compile("l\\[(.*)\\]"), s -> {
-            final String[] items = s.split(",");
-            final String listItems = Stream.of(items).map(String::trim).map(x 
-> convertToString(x)).collect(Collectors.joining(","));
+            final List<String> items = splitByElement(s);
+            final String listItems = items.stream().map(String::trim).map(x -> 
convertToString(x)).collect(Collectors.joining(","));
             return String.format("[%s]", listItems);
         }));
         add(Pair.with(Pattern.compile("s\\[\\]"), s -> "{}"));
@@ -147,8 +147,8 @@ public final class StepDefinition {
         }));
         add(Pair.with(Pattern.compile("s\\[\\]"), s -> String.format("{}")));
         add(Pair.with(Pattern.compile("s\\[(.*)\\]"), s -> {
-            final String[] items = s.split(",");
-            final String listItems = Stream.of(items).map(String::trim).map(x 
-> convertToString(x)).collect(Collectors.joining(","));
+            final List<String> items = splitByElement(s);
+            final String listItems = items.stream().map(String::trim).map(x -> 
convertToString(x)).collect(Collectors.joining(","));
             return String.format("{%s}", listItems);
         }));
         add(Pair.with(Pattern.compile("d\\[(NaN)\\]"), s -> "NaN"));
@@ -191,14 +191,14 @@ public final class StepDefinition {
 
         add(Pair.with(Pattern.compile("l\\[\\]"), s -> new ArrayList<>()));
         add(Pair.with(Pattern.compile("l\\[(.*)\\]"), s -> {
-            final String[] items = s.split(",");
-            return Stream.of(items).map(String::trim).map(x -> 
convertToObject(x)).collect(Collectors.toList());
+            final List<String> items = splitByElement(s);
+            return items.stream().map(String::trim).map(x -> 
convertToObject(x)).collect(Collectors.toList());
         }));
 
         add(Pair.with(Pattern.compile("s\\[\\]"), s -> new HashSet<>()));
         add(Pair.with(Pattern.compile("s\\[(.*)\\]"), s -> {
-            final String[] items = s.split(",");
-            return Stream.of(items).map(String::trim).map(x -> 
convertToObject(x)).collect(Collectors.toSet());
+            final List<String> items = splitByElement(s);
+            return items.stream().map(String::trim).map(x -> 
convertToObject(x)).collect(Collectors.toSet());
         }));
 
         // return the string values as is, used to wrap results that may 
contain other regex patterns
@@ -713,6 +713,33 @@ public final class StepDefinition {
         return String.format("%s", v);
     }
 
+    /**
+     * Splits a string on commas while respecting bracket nesting, so that 
nested collection tokens
+     * like {@code l[1,2,3]} inside a set {@code s[l[1,2,3],l[4,5,6]]} are not 
incorrectly split.
+     */
+    private static List<String> splitByElement(final String s) {
+        final List<String> result = new ArrayList<>();
+        int depth = 0;
+        final StringBuilder current = new StringBuilder();
+        for (int i = 0; i < s.length(); i++) {
+            final char c = s.charAt(i);
+            if (c == '[') {
+                depth++;
+                current.append(c);
+            } else if (c == ']') {
+                depth--;
+                current.append(c);
+            } else if (c == ',' && depth == 0) {
+                result.add(current.toString());
+                current.setLength(0);
+            } else {
+                current.append(c);
+            }
+        }
+        if (current.length() > 0) result.add(current.toString());
+        return result;
+    }
+
     private static Triplet<String,String,String> getEdgeTriplet(final String 
e) {
         final Matcher m = edgeTripletPattern.matcher(e);
         if (m.matches()) {
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Fold.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Fold.feature
index a9493eae49..a721aab5b1 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Fold.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/map/Fold.feature
@@ -81,4 +81,26 @@ Feature: Step - fold()
     When iterated to list
     Then the result should be unordered
       | result |
-      | m[{"a":"d[1].i", "b":"d[4].i"}] |
\ No newline at end of file
+      | m[{"a":"d[1].i", "b":"d[4].i"}] |
+
+  Scenario: g_injectXlist1_list2X_fold
+    Given the empty graph
+    And the traversal of
+      """
+      g.inject([1, 2], [3, 4]).fold()
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | l[l[d[1].i,d[2].i],l[d[3].i,d[4].i]] |
+
+  Scenario: g_injectXlist1_list2_list3X_fold
+    Given the empty graph
+    And the traversal of
+      """
+      g.inject([1, 2], [3, 4], [5, 6]).fold()
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | l[l[d[1].i,d[2].i],l[d[3].i,d[4].i],l[d[5].i,d[6].i]] |
\ No newline at end of file

Reply via email to