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

spmallette pushed a commit to branch fulfill-pr-3216
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 9959310019eb7110fa22973de2a86fcbf7494893
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                 | 26 ++++++++++++-
 .../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, 175 insertions(+), 16 deletions(-)

diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
index 86a7536106..404b9ff7d4 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
@@ -462,7 +462,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 cce3db9d6f..727b83511b 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1186,6 +1186,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 9a0b44dde1..edffce1da0 100644
--- a/gremlin-go/driver/cucumber/cucumberSteps_test.go
+++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go
@@ -247,6 +247,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)
@@ -254,7 +281,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
@@ -266,7 +293,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 daed71fb1d..774c771562 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1156,6 +1156,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-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
index 753124ed22..4581abde5c 100644
--- 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/feature-steps.js
@@ -437,11 +437,33 @@ 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));
+  return splitByElement(stringList).map(x => parseValue.call(this, x));
 }
 
 function toSet(stringList) {
@@ -450,7 +472,7 @@ function toSet(stringList) {
   }
 
   const s = new Set();
-  stringList.split(',').forEach(x => s.add(parseValue.call(this, x)));
+  splitByElement(stringList).forEach(x => s.add(parseValue.call(this, x)));
   return s;
 }
 
diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
index a8ea46ca87..cf75e374a6 100644
--- 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
@@ -1187,6 +1187,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 06757934e2..7902b27c86 100644
--- a/gremlin-python/src/main/python/tests/feature/feature_steps.py
+++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py
@@ -232,6 +232,27 @@ def nothing_happening(step):
     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
@@ -242,9 +263,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 98c36e0dbc..38d68ae9e4 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1159,6 +1159,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 9707a473d8..dc070f74c4 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
@@ -131,14 +131,14 @@ 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 -> 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"));
@@ -195,14 +195,14 @@ public final class StepDefinition {
 
         add(Pair.with(Pattern.compile("l\\[\\]"), s -> 
Collections.emptyList()));
         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 -> 
Collections.emptySet()));
         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
@@ -684,6 +684,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