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

Cole-Greer pushed a commit to branch GValueFollowupTP4
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 9acd288e4c837ad9758450930fbc8c2cda2bf01f
Author: Cole Greer <[email protected]>
AuthorDate: Thu Jun 4 17:43:17 2026 -0700

    Add GValue implementation to the JavaScript GLV
    
    JavaScript was the only Gremlin Language Variant without a client-side 
GValue.
    Adds lib/process/gvalue.ts: a generic, always-named GValue<T> with fail-fast
    name validation using the shared Unicode predicate (first char a Unicode 
letter,
    remaining chars letter/digit/underscore; no keyword check), a nested-GValue
    guard, isNull(), and toString() -> "name=value". Integrates it into
    GremlinLang._argAsString (renders the variable name and stores the value in 
the
    parameters map, with duplicate-name detection via Node's 
util.isDeepStrictEqual),
    exports it from the process namespace, and adds unit tests plus the 
export-surface
    assertion. Behavior is consistent with the Python/.NET/Go GValue 
implementations.
---
 gremlin-js/gremlin-javascript/lib/index.ts         |  2 +
 .../gremlin-javascript/lib/process/gremlin-lang.ts | 13 +++
 .../gremlin-javascript/lib/process/gvalue.ts       | 47 +++++++++++
 .../gremlin-javascript/test/unit/exports-test.js   |  1 +
 .../test/unit/gremlin-lang-test.js                 | 92 +++++++++++++++++++++-
 5 files changed, 154 insertions(+), 1 deletion(-)

diff --git a/gremlin-js/gremlin-javascript/lib/index.ts 
b/gremlin-js/gremlin-javascript/lib/index.ts
index f79c31e948..b7fa87ea8e 100644
--- a/gremlin-js/gremlin-javascript/lib/index.ts
+++ b/gremlin-js/gremlin-javascript/lib/index.ts
@@ -27,6 +27,7 @@ import * as strategiesModule from 
'./process/traversal-strategy.js';
 import * as graph from './structure/graph.js';
 import * as rc from './driver/remote-connection.js';
 import GremlinLang from './process/gremlin-lang.js';
+import { GValue } from './process/gvalue.js';
 import * as utils from './utils.js';
 import DriverRemoteConnection from './driver/driver-remote-connection.js';
 import ResponseError from './driver/response-error.js';
@@ -74,6 +75,7 @@ export const process = {
   GraphTraversalSource: gt.GraphTraversalSource,
   statics: gt.statics,
   GremlinLang,
+  GValue,
   traversal: AnonymousTraversalSource.traversal,
   AnonymousTraversalSource,
   withOptions: t.withOptions,
diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts 
b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
index 2a07ad8dec..2c085a2c31 100644
--- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
@@ -21,6 +21,8 @@ import { P, TextP, EnumValue } from './traversal.js';
 import { OptionsStrategy, TraversalStrategy } from './traversal-strategy.js';
 import { Long, Int, Float, Double, Short, Byte, INT32_MIN, INT32_MAX } from 
'../utils.js';
 import { Vertex } from '../structure/graph.js';
+import { GValue } from './gvalue.js';
+import { isDeepStrictEqual } from 'node:util';
 import { Buffer } from 'buffer';
 
 export default class GremlinLang {
@@ -110,6 +112,17 @@ export default class GremlinLang {
       const escaped = JSON.stringify(arg).slice(1, -1).replace(/'/g, "\\'");
       return `'${escaped}'`;
     }
+    if (arg instanceof GValue) {
+      const key = arg.name;
+      if (this.parameters.has(key)) {
+        if (!isDeepStrictEqual(this.parameters.get(key), arg.value)) {
+          throw new Error(`Parameter with name ${key} already exists.`);
+        }
+      } else {
+        this.parameters.set(key, arg.value);
+      }
+      return key;
+    }
     if (arg instanceof P || arg instanceof TextP) {
       return this._predicateAsString(arg);
     }
diff --git a/gremlin-js/gremlin-javascript/lib/process/gvalue.ts 
b/gremlin-js/gremlin-javascript/lib/process/gvalue.ts
new file mode 100644
index 0000000000..bbbb4f52f5
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/process/gvalue.ts
@@ -0,0 +1,47 @@
+/*
+ *  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.
+ */
+
+const NAME_PATTERN = /^[\p{L}][\p{L}\p{Nd}_]*$/u;
+
+export class GValue<T = any> {
+  readonly name: string;
+  readonly value: T;
+
+  constructor(name: string, value: T) {
+    if (typeof name !== 'string' || name.length === 0) {
+      throw new Error(`Invalid GValue name: '${name}' - must be a non-empty 
string`);
+    }
+    if (!NAME_PATTERN.test(name)) {
+      throw new Error(`Invalid GValue name: '${name}' - must start with a 
Unicode letter followed by letters, digits, or underscores`);
+    }
+    if (value instanceof GValue) {
+      throw new Error('GValues cannot be nested');
+    }
+    this.name = name;
+    this.value = value;
+  }
+
+  isNull(): boolean {
+    return this.value == null;
+  }
+
+  toString(): string {
+    return `${this.name}=${this.value}`;
+  }
+}
diff --git a/gremlin-js/gremlin-javascript/test/unit/exports-test.js 
b/gremlin-js/gremlin-javascript/test/unit/exports-test.js
index f009c7074d..24cdb3d7f1 100644
--- a/gremlin-js/gremlin-javascript/test/unit/exports-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/exports-test.js
@@ -29,6 +29,7 @@ describe('API', function () {
     assert.ok(glvModule);
     assert.ok(glvModule.process);
     assert.strictEqual(typeof glvModule.process.GremlinLang, 'function');
+    assert.strictEqual(typeof glvModule.process.GValue, 'function');
     assert.strictEqual(typeof glvModule.process.EnumValue, 'function');
     assert.strictEqual(typeof glvModule.process.P, 'function');
     assert.strictEqual(typeof glvModule.process.Traversal, 'function');
diff --git a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js 
b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
index a8c4e9add0..abb86a1740 100644
--- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
@@ -29,6 +29,7 @@ import { Graph, Vertex } from '../../lib/structure/graph.js';
 import { TraversalStrategies } from '../../lib/process/traversal-strategy.js';
 import { Long, toFloat, toDouble, toShort, toByte, toInt, toLong } from 
'../../lib/utils.js';
 import GremlinLang from '../../lib/process/gremlin-lang.js';
+import { GValue } from '../../lib/process/gvalue.js';
 
 const g = new GraphTraversalSource(new Graph(), new TraversalStrategies());
 
@@ -626,4 +627,93 @@ describe('GremlinLang', function () {
       assert.ok(result.includes("'name':'marko'"));
     });
   });
-});
\ No newline at end of file
+
+  describe('GValue', function () {
+    it('should construct with name and value accessors', function () {
+      const gv = new GValue('myName', 42);
+      assert.strictEqual(gv.name, 'myName');
+      assert.strictEqual(gv.value, 42);
+    });
+
+    it('should return true for isNull() with null value', function () {
+      assert.strictEqual(new GValue('x', null).isNull(), true);
+    });
+
+    it('should return true for isNull() with undefined value', function () {
+      assert.strictEqual(new GValue('x', undefined).isNull(), true);
+    });
+
+    it('should return false for isNull() with non-null value', function () {
+      assert.strictEqual(new GValue('x', 0).isNull(), false);
+    });
+
+    it('should reject empty name', function () {
+      assert.throws(() => new GValue('', 1), /Invalid GValue name/);
+    });
+
+    it('should reject name with $ character', function () {
+      assert.throws(() => new GValue('a$b', 1), /Invalid GValue name/);
+    });
+
+    it('should reject name starting with underscore', function () {
+      assert.throws(() => new GValue('_x', 1), /Invalid GValue name/);
+    });
+
+    it('should reject numeric name', function () {
+      assert.throws(() => new GValue('1', 1), /Invalid GValue name/);
+    });
+
+    it('should reject name starting with digit', function () {
+      assert.throws(() => new GValue('1a', 1), /Invalid GValue name/);
+    });
+
+    it('should accept name with mid-string underscore', function () {
+      const gv = new GValue('a_b', 1);
+      assert.strictEqual(gv.name, 'a_b');
+    });
+
+    it('should accept Unicode letter name', function () {
+      const gv = new GValue('café', 1);
+      assert.strictEqual(gv.name, 'café');
+    });
+
+    it('should accept language keyword as name', function () {
+      const gv = new GValue('for', 1);
+      assert.strictEqual(gv.name, 'for');
+    });
+
+    it('should reject nested GValue', function () {
+      assert.throws(() => new GValue('x', new GValue('y', 1)), /GValues cannot 
be nested/);
+    });
+
+    it('should return name=value from toString()', function () {
+      const gv = new GValue('ids', 'hello');
+      assert.strictEqual(gv.toString(), 'ids=hello');
+    });
+
+    it('should render name in gremlin string and store value in parameters', 
function () {
+      const traversal = g.V(new GValue('ids', [1, 2, 3]));
+      const gl = traversal.getGremlinLang();
+      assert.strictEqual(gl.getGremlin(), 'g.V(ids)');
+      assert.deepStrictEqual(gl.getParameters().get('ids'), [1, 2, 3]);
+    });
+
+    it('should throw when duplicate name has different value', function () {
+      assert.throws(() => {
+        g.V(new GValue('x', 1)).has('name', new GValue('x', 2));
+      }, /Parameter with name x already exists/);
+    });
+
+    it('should allow reuse of same name with equal value', function () {
+      const traversal = g.V(new GValue('ids', [1, 2, 3])).has('name', new 
GValue('ids', [1, 2, 3]));
+      const gl = traversal.getGremlinLang();
+      assert.strictEqual(gl.getGremlin(), "g.V(ids).has('name',ids)");
+      assert.deepStrictEqual(gl.getParameters().get('ids'), [1, 2, 3]);
+    });
+
+    it('should reject non-string name', function () {
+      assert.throws(() => new GValue(123, 'v'), /Invalid GValue name/);
+      assert.throws(() => new GValue(null, 'v'), /Invalid GValue name/);
+    });
+  });
+});

Reply via email to