You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by co...@apache.org on 2026/06/11 00:22:27 UTC
(tinkerpop) 02/15: Add GValue implementation to the JavaScript GLV
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 <co...@colegreer.ca>
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/);
+ });
+ });
+});