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/); + }); + }); +});
