This is an automated email from the ASF dual-hosted git repository. Cole-Greer pushed a commit to branch simplePDT in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 16316eb11f605d87bee4586807b1c0f00a5b8052 Author: Cole Greer <[email protected]> AuthorDate: Wed Jun 24 20:30:04 2026 -0700 Add PrimitivePDT support to gremlin-javascript GLV Implements PrimitivePDT in the JavaScript GLV, mirroring composite support and applying the review lessons from the Python GLV. - PrimitiveProviderDefinedType (name, value) in structure/graph.ts. - PrimitivePDTSerializer replaces the prior StubSerializer for DataType.PRIMITIVEPDT (0xf1): writes/reads two fully-qualified Strings. - ProviderDefinedTypeRegistry gains an explicit primitive adapter path (registerPrimitive / getPrimitiveAdapterByClass / hydratePrimitive), mirroring the composite/primitive split used in Java/Python. - gremlin-lang text serialization emits PDT("name","value") for a PrimitiveProviderDefinedType and auto-dehydrates classes with a registered primitive adapter (primitive checked before composite). This is the client-side text path that was the Python gap; covered by unit tests here. - Client/connection reuse the existing pdtRegistry option. No GraphSON g:PrimitivePdt read path is added (consistent with the JS driver's GraphBinary-based V4 response handling; nothing fabricated). Tests: unit tests (serializer round-trip incl. opaque-value fidelity, registry hydration, gremlin-lang PDT text emission) — full unit suite passing (21082 tests). Integration tests (raw/unregistered, registered de/hydration, nested-in-composite) pass against the test server: 4/4. tinkerpop-2gy.9 Assisted-by: Kiro:claude-opus-4.8 --- gremlin-js/gremlin-javascript/lib/index.ts | 1 + .../gremlin-javascript/lib/process/gremlin-lang.ts | 12 +- .../lib/structure/ProviderDefinedTypeRegistry.ts | 45 +++++- .../gremlin-javascript/lib/structure/graph.ts | 19 +++ .../lib/structure/io/binary/GraphBinary.js | 4 +- .../structure/io/binary/internals/AnySerializer.js | 1 + .../io/binary/internals/PrimitivePDTSerializer.js | 84 ++++++++++ .../test/integration/client-tests.js | 49 +++++- .../test/integration/traversal-test.js | 123 ++++++++++++++- .../graphbinary/PrimitivePDTSerializer-test.js | 172 +++++++++++++++++++++ .../test/unit/gremlin-lang-test.js | 98 +++++++++++- .../test/unit/pdt-registry-test.js | 119 +++++++++++++- 12 files changed, 719 insertions(+), 8 deletions(-) diff --git a/gremlin-js/gremlin-javascript/lib/index.ts b/gremlin-js/gremlin-javascript/lib/index.ts index ef0e7ce133..ae04f1ffb2 100644 --- a/gremlin-js/gremlin-javascript/lib/index.ts +++ b/gremlin-js/gremlin-javascript/lib/index.ts @@ -85,6 +85,7 @@ export const structure = { Graph: graph.Graph, Path: graph.Path, Property: graph.Property, + PrimitiveProviderDefinedType: graph.PrimitiveProviderDefinedType, ProviderDefinedType: graph.ProviderDefinedType, ProviderDefinedTypeRegistry, Vertex: graph.Vertex, diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts index 070a88fceb..fdc8bbfa9a 100644 --- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts +++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts @@ -20,7 +20,7 @@ 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, ProviderDefinedType } from '../structure/graph.js'; +import { Vertex, ProviderDefinedType, PrimitiveProviderDefinedType } from '../structure/graph.js'; import { ProviderDefinedTypeRegistry } from '../structure/ProviderDefinedTypeRegistry.js'; import { Buffer } from 'buffer'; @@ -131,6 +131,11 @@ export default class GremlinLang { if (typeof arg === 'function' && arg.prototype instanceof TraversalStrategy) { return arg.name; } + if (arg instanceof PrimitiveProviderDefinedType) { + const escapedName = JSON.stringify(arg.name).slice(1, -1); + const escapedValue = JSON.stringify(arg.value).slice(1, -1); + return `PDT("${escapedName}","${escapedValue}")`; + } if (arg instanceof ProviderDefinedType) { const fields = arg.fields; const keys = Object.keys(fields); @@ -180,6 +185,11 @@ export default class GremlinLang { } // Registry-based dehydration if (this.pdtRegistry && typeof arg === 'object' && arg.constructor) { + const primitiveEntry = this.pdtRegistry.getPrimitiveAdapterByClass(arg.constructor); + if (primitiveEntry) { + const value = primitiveEntry.toValue(arg); + return this._argAsString(new PrimitiveProviderDefinedType(primitiveEntry.typeName, value)); + } const entry = this.pdtRegistry.getAdapterByClass(arg.constructor); if (entry) { const fields = entry.serialize(arg); diff --git a/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts b/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts index 7c560147a6..fa57c6532a 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts +++ b/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts @@ -17,20 +17,28 @@ * under the License. */ -import { ProviderDefinedType } from './graph.js'; +import { ProviderDefinedType, PrimitiveProviderDefinedType } from './graph.js'; export interface PdtAdapter { serialize: (obj: any) => Record<string, any>; deserialize: (fields: Record<string, any>) => any; } +export interface PrimitivePdtAdapter { + toValue: (obj: any) => string; + fromValue: (value: string) => any; +} + /** * A standalone registry that allows users to register adapters for hydrating - * raw {@link ProviderDefinedType} instances into domain-specific objects. + * raw {@link ProviderDefinedType} and {@link PrimitiveProviderDefinedType} instances + * into domain-specific objects. */ export class ProviderDefinedTypeRegistry { private readonly _adapters: Map<string, PdtAdapter> = new Map(); private readonly _adaptersByClass: Map<Function, { typeName: string; adapter: PdtAdapter }> = new Map(); + private readonly _primitiveAdapters: Map<string, PrimitivePdtAdapter> = new Map(); + private readonly _primitiveAdaptersByClass: Map<Function, { typeName: string; adapter: PrimitivePdtAdapter }> = new Map(); register(typeName: string, adapter: PdtAdapter, targetClass?: Function): void { this._adapters.set(typeName, adapter); @@ -39,6 +47,13 @@ export class ProviderDefinedTypeRegistry { } } + registerPrimitive(typeName: string, adapter: PrimitivePdtAdapter, targetClass?: Function): void { + this._primitiveAdapters.set(typeName, adapter); + if (targetClass) { + this._primitiveAdaptersByClass.set(targetClass, { typeName, adapter }); + } + } + hydrate(pdt: any): any { if (!(pdt instanceof ProviderDefinedType)) return pdt; const adapter = this._adapters.get(pdt.name); @@ -49,6 +64,10 @@ export class ProviderDefinedTypeRegistry { const h = this.hydrate(v); hydratedFields[k] = h; if (h !== v) changed = true; + } else if (v instanceof PrimitiveProviderDefinedType) { + const h = this.hydratePrimitive(v); + hydratedFields[k] = h; + if (h !== v) changed = true; } else { hydratedFields[k] = v; } @@ -64,10 +83,26 @@ export class ProviderDefinedTypeRegistry { } } + hydratePrimitive(pdt: any): any { + if (!(pdt instanceof PrimitiveProviderDefinedType)) return pdt; + const adapter = this._primitiveAdapters.get(pdt.name); + if (!adapter) return pdt; + try { + return adapter.fromValue(pdt.value); + } catch (e: any) { + console.warn(`Primitive PDT hydration failed for '${pdt.name}': ${e.message}`); + return pdt; + } + } + hasAdapter(typeName: string): boolean { return this._adapters.has(typeName); } + hasPrimitiveAdapter(typeName: string): boolean { + return this._primitiveAdapters.has(typeName); + } + getSerializer(typeName: string): ((obj: any) => Record<string, any>) | null { const adapter = this._adapters.get(typeName); return adapter ? adapter.serialize : null; @@ -78,4 +113,10 @@ export class ProviderDefinedTypeRegistry { if (!entry) return null; return { typeName: entry.typeName, serialize: entry.adapter.serialize }; } + + getPrimitiveAdapterByClass(cls: Function): { typeName: string; toValue: (obj: any) => string } | null { + const entry = this._primitiveAdaptersByClass.get(cls); + if (!entry) return null; + return { typeName: entry.typeName, toValue: entry.adapter.toValue }; + } } diff --git a/gremlin-js/gremlin-javascript/lib/structure/graph.ts b/gremlin-js/gremlin-javascript/lib/structure/graph.ts index 75baa70014..17b447b3cc 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/graph.ts +++ b/gremlin-js/gremlin-javascript/lib/structure/graph.ts @@ -214,6 +214,25 @@ export class ProviderDefinedType { } } +/** + * Represents a primitive Provider Defined Type (PDT). + */ +export class PrimitiveProviderDefinedType { + readonly name: string; + readonly value: string; + + constructor(name: string, value: string) { + if (!name) throw new Error('PrimitiveProviderDefinedType name cannot be null or empty'); + if (value === null || value === undefined) throw new Error('PrimitiveProviderDefinedType value cannot be null'); + this.name = name; + this.value = value; + } + + toString() { + return `pdt[${this.name}:${this.value}]`; + } +} + function summarize(value: any) { if (value === null || value === undefined) { return value; diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js index ab3cd58b0b..188c44be7c 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js @@ -67,6 +67,7 @@ import UnspecifiedNullSerializer from './internals/UnspecifiedNullSerializer.js' import EnumSerializer from './internals/EnumSerializer.js'; import StubSerializer from './internals/StubSerializer.js'; import CompositePDTSerializer from './internals/CompositePDTSerializer.js'; +import PrimitivePDTSerializer from './internals/PrimitivePDTSerializer.js'; import NumberSerializationStrategy from './internals/NumberSerializationStrategy.js'; import AnySerializer from './internals/AnySerializer.js'; import GraphBinaryReader from './internals/GraphBinaryReader.js'; @@ -107,10 +108,10 @@ function createIoc(anySerializerOptions) { ioc.unspecifiedNullSerializer = new UnspecifiedNullSerializer(ioc); ioc.enumSerializer = new EnumSerializer(ioc); ioc.compositePDTSerializer = new CompositePDTSerializer(ioc); + ioc.primitivePDTSerializer = new PrimitivePDTSerializer(ioc); // Register stub serializers for unimplemented v4 types new StubSerializer(ioc, ioc.DataType.TREE, 'Tree'); - new StubSerializer(ioc, ioc.DataType.PRIMITIVEPDT, 'PrimitivePDT'); ioc.pdtRegistry = null; @@ -177,6 +178,7 @@ export const { unspecifiedNullSerializer, enumSerializer, compositePDTSerializer, + primitivePDTSerializer, numberSerializationStrategy, anySerializer, graphBinaryReader, diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js index 94841ad355..e4010789bc 100644 --- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/AnySerializer.js @@ -44,6 +44,7 @@ export default class AnySerializer { ioc.enumSerializer, ioc.stringSerializer, ioc.binarySerializer, + ioc.primitivePDTSerializer, ioc.compositePDTSerializer, ioc.mapSerializer, ]; diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PrimitivePDTSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PrimitivePDTSerializer.js new file mode 100644 index 0000000000..311007e5a7 --- /dev/null +++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/PrimitivePDTSerializer.js @@ -0,0 +1,84 @@ +/* + * 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. + */ + +import { Buffer } from 'buffer'; +import { PrimitiveProviderDefinedType } from '../../../graph.js'; + +export default class PrimitivePDTSerializer { + constructor(ioc) { + this.ioc = ioc; + this.ioc.serializers[ioc.DataType.PRIMITIVEPDT] = this; + } + + canBeUsedFor(value) { + return value instanceof PrimitiveProviderDefinedType; + } + + serialize(item, fullyQualifiedFormat = true) { + if (item === undefined || item === null) { + if (fullyQualifiedFormat) { + return Buffer.from([this.ioc.DataType.PRIMITIVEPDT, 0x01]); + } + const bufs = []; + bufs.push(this.ioc.stringSerializer.serialize('', false)); + bufs.push(this.ioc.stringSerializer.serialize('', false)); + return Buffer.concat(bufs); + } + + const bufs = []; + if (fullyQualifiedFormat) { + bufs.push(Buffer.from([this.ioc.DataType.PRIMITIVEPDT, 0x00])); + } + bufs.push(this.ioc.stringSerializer.serialize(item.name, true)); + bufs.push(this.ioc.stringSerializer.serialize(item.value, true)); + return Buffer.concat(bufs); + } + + async deserializeValue(reader, valueFlag, typeCode) { + const name = await this.ioc.anySerializer.deserialize(reader); + if (!name) { + throw new Error('PrimitivePDTSerializer: name cannot be null or empty'); + } + const value = await this.ioc.anySerializer.deserialize(reader); + const pdt = new PrimitiveProviderDefinedType(name, value != null ? String(value) : ''); + const pdtRegistry = reader.pdtRegistry; + if (pdtRegistry) { + const hydrated = pdtRegistry.hydratePrimitive(pdt); + if (!(hydrated instanceof PrimitiveProviderDefinedType)) { + return hydrated; + } + } + return pdt; + } + + async deserialize(reader) { + const type_code = await reader.readUInt8(); + if (type_code !== this.ioc.DataType.PRIMITIVEPDT) { + throw new Error(`PrimitivePDTSerializer: unexpected {type_code}=0x${type_code.toString(16)}`); + } + const value_flag = await reader.readUInt8(); + if (value_flag === 0x01) { + return null; + } + if (value_flag !== 0x00) { + throw new Error(`PrimitivePDTSerializer: unexpected {value_flag}=0x${value_flag.toString(16)}`); + } + return this.deserializeValue(reader, value_flag, type_code); + } +} diff --git a/gremlin-js/gremlin-javascript/test/integration/client-tests.js b/gremlin-js/gremlin-javascript/test/integration/client-tests.js index cf439aaa56..0e8f490e95 100644 --- a/gremlin-js/gremlin-javascript/test/integration/client-tests.js +++ b/gremlin-js/gremlin-javascript/test/integration/client-tests.js @@ -18,7 +18,7 @@ */ import assert from 'assert'; -import { Vertex, Edge, VertexProperty, ProviderDefinedType } from '../../lib/structure/graph.js'; +import { Vertex, Edge, VertexProperty, ProviderDefinedType, PrimitiveProviderDefinedType } from '../../lib/structure/graph.js'; import { getClient, serverUrl } from '../helper.js'; import { cardinality } from '../../lib/process/traversal.js'; import Client from '../../lib/driver/client.js'; @@ -273,4 +273,51 @@ describe('ProviderDefinedType - Client', function () { assert.strictEqual(list[1].fields.y, 4); }); }); +}); + +describe('PrimitiveProviderDefinedType - Client', function () { + let pdtClient; + before(function () { + pdtClient = getClient('gmodern'); + return pdtClient.open(); + }); + after(function () { + return pdtClient.close(); + }); + + it('should round-trip a simple primitive PDT', function () { + return pdtClient.submit('g.inject(PDT("Uint32","42"))') + .then(function (result) { + assert.strictEqual(result.length, 1); + const pdt = result.first(); + assert.ok(pdt instanceof PrimitiveProviderDefinedType); + assert.strictEqual(pdt.name, 'Uint32'); + assert.strictEqual(pdt.value, '42'); + }); + }); + + it('should round-trip a primitive PDT with leading zeros', function () { + return pdtClient.submit('g.inject(PDT("TinkerId","007"))') + .then(function (result) { + assert.strictEqual(result.length, 1); + const pdt = result.first(); + assert.ok(pdt instanceof PrimitiveProviderDefinedType); + assert.strictEqual(pdt.name, 'TinkerId'); + assert.strictEqual(pdt.value, '007'); + }); + }); + + it('should handle primitive PDTs in a collection', function () { + return pdtClient.submit('g.inject([PDT("Uint32","1"), PDT("Uint32","2")])') + .then(function (result) { + assert.strictEqual(result.length, 1); + const list = result.first(); + assert.ok(Array.isArray(list)); + assert.strictEqual(list.length, 2); + assert.ok(list[0] instanceof PrimitiveProviderDefinedType); + assert.strictEqual(list[0].value, '1'); + assert.ok(list[1] instanceof PrimitiveProviderDefinedType); + assert.strictEqual(list[1].value, '2'); + }); + }); }); \ No newline at end of file diff --git a/gremlin-js/gremlin-javascript/test/integration/traversal-test.js b/gremlin-js/gremlin-javascript/test/integration/traversal-test.js index 867080b040..98e2fedfed 100644 --- a/gremlin-js/gremlin-javascript/test/integration/traversal-test.js +++ b/gremlin-js/gremlin-javascript/test/integration/traversal-test.js @@ -23,7 +23,7 @@ import assert from 'assert'; import { AssertionError } from 'assert'; -import {Edge, Vertex, VertexProperty, ProviderDefinedType} from '../../lib/structure/graph.js'; +import {Edge, Vertex, VertexProperty, ProviderDefinedType, PrimitiveProviderDefinedType} from '../../lib/structure/graph.js'; import { ProviderDefinedTypeRegistry } from '../../lib/structure/ProviderDefinedTypeRegistry.js'; import anon from '../../lib/process/anonymous-traversal.js'; import { GraphTraversalSource, GraphTraversal, statics } from '../../lib/process/graph-traversal.js'; @@ -404,3 +404,124 @@ describe('ProviderDefinedType - Traversal API', function () { }); }); }); + +describe('PrimitiveProviderDefinedType - Traversal API', function () { + describe('raw primitive PDT round-trip via Traversal API', function () { + let pdtConnection; + + before(function () { + pdtConnection = getConnection('gmodern'); + return pdtConnection.open(); + }); + after(function () { + return pdtConnection.close(); + }); + + it('should round-trip a primitive PDT via g.inject()', async function () { + const g = anon.traversal().with_(pdtConnection); + const pdt = new PrimitiveProviderDefinedType('Uint32', '42'); + + const results = await g.inject(pdt).toList(); + + assert.strictEqual(results.length, 1); + const result = results[0]; + assert.ok(result instanceof PrimitiveProviderDefinedType); + assert.strictEqual(result.name, 'Uint32'); + assert.strictEqual(result.value, '42'); + }); + + it('should round-trip an unregistered primitive PDT (raw)', async function () { + const g = anon.traversal().with_(pdtConnection); + const pdt = new PrimitiveProviderDefinedType('UnregisteredType', 'opaque-value'); + + const results = await g.inject(pdt).toList(); + + assert.strictEqual(results.length, 1); + const result = results[0]; + assert.ok(result instanceof PrimitiveProviderDefinedType); + assert.strictEqual(result.name, 'UnregisteredType'); + assert.strictEqual(result.value, 'opaque-value'); + }); + }); + + describe('registry-based primitive round-trip via typed object', function () { + let pdtConnection; + + class Uint32 { + constructor(v) { + this.v = v; + } + } + + before(function () { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj.v), + fromValue: (value) => new Uint32(parseInt(value, 10)), + }, Uint32); + pdtConnection = new DriverRemoteConnection(serverUrl, { + traversalSource: 'gmodern', + pdtRegistry: registry, + }); + return pdtConnection.open(); + }); + after(function () { + return pdtConnection.close(); + }); + + it('should auto-dehydrate primitive on send and auto-hydrate on receive', async function () { + const g = anon.traversal().with_(pdtConnection); + const val = new Uint32(99); + + const results = await g.inject(val).toList(); + + assert.strictEqual(results.length, 1); + const result = results[0]; + assert.ok(result instanceof Uint32); + assert.strictEqual(result.v, 99); + }); + }); + + describe('nested composite containing primitive PDT', function () { + let pdtConnection; + + class Uint32 { + constructor(v) { + this.v = v; + } + } + + before(function () { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj.v), + fromValue: (value) => new Uint32(parseInt(value, 10)), + }, Uint32); + pdtConnection = new DriverRemoteConnection(serverUrl, { + traversalSource: 'gmodern', + pdtRegistry: registry, + }); + return pdtConnection.open(); + }); + after(function () { + return pdtConnection.close(); + }); + + it('should hydrate nested primitive inside composite', async function () { + const g = anon.traversal().with_(pdtConnection); + const inner = new PrimitiveProviderDefinedType('Uint32', '55'); + const outer = new ProviderDefinedType('Measurement', { unit: 'kg', amount: inner }); + + const results = await g.inject(outer).toList(); + + assert.strictEqual(results.length, 1); + const result = results[0]; + assert.ok(result instanceof ProviderDefinedType); + assert.strictEqual(result.name, 'Measurement'); + assert.strictEqual(result.fields.unit, 'kg'); + // The nested primitive PDT should be hydrated to Uint32 + assert.ok(result.fields.amount instanceof Uint32); + assert.strictEqual(result.fields.amount.v, 55); + }); + }); +}); diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/PrimitivePDTSerializer-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/PrimitivePDTSerializer-test.js new file mode 100644 index 0000000000..b3a08e038a --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/PrimitivePDTSerializer-test.js @@ -0,0 +1,172 @@ +/* + * 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. + */ + +import { assert } from 'chai'; +import { PrimitiveProviderDefinedType, ProviderDefinedType } from '../../../lib/structure/graph.js'; +import { ProviderDefinedTypeRegistry } from '../../../lib/structure/ProviderDefinedTypeRegistry.js'; +import ioc, { DataType } from '../../../lib/structure/io/binary/GraphBinary.js'; +import StreamReader from '../../../lib/structure/io/binary/internals/StreamReader.js'; + +const { anySerializer, primitivePDTSerializer } = ioc; + +async function roundTrip(value) { + const bytes = anySerializer.serialize(value); + return anySerializer.deserialize(StreamReader.fromBuffer(bytes)); +} + +describe('PrimitivePDTSerializer', () => { + describe('round-trip: simple primitive PDT', () => { + it('serializes and deserializes a simple PrimitiveProviderDefinedType', async () => { + const pdt = new PrimitiveProviderDefinedType('Uint32', '42'); + const result = await roundTrip(pdt); + assert.instanceOf(result, PrimitiveProviderDefinedType); + assert.strictEqual(result.name, 'Uint32'); + assert.strictEqual(result.value, '42'); + }); + + it('uses PRIMITIVEPDT type code', () => { + const pdt = new PrimitiveProviderDefinedType('Uint32', '123'); + const bytes = anySerializer.serialize(pdt); + assert.strictEqual(bytes[0], DataType.PRIMITIVEPDT); + }); + }); + + describe('round-trip: opaque string values', () => { + it('handles leading zeros (preserved as string)', async () => { + const pdt = new PrimitiveProviderDefinedType('TinkerId', '007'); + const result = await roundTrip(pdt); + assert.instanceOf(result, PrimitiveProviderDefinedType); + assert.strictEqual(result.value, '007'); + }); + + it('handles large numbers', async () => { + const pdt = new PrimitiveProviderDefinedType('BigNum', '99999999999999999999999999'); + const result = await roundTrip(pdt); + assert.strictEqual(result.value, '99999999999999999999999999'); + }); + + it('handles non-numeric values', async () => { + const pdt = new PrimitiveProviderDefinedType('CustomId', 'abc-def-123'); + const result = await roundTrip(pdt); + assert.strictEqual(result.value, 'abc-def-123'); + }); + + it('handles empty string value', async () => { + const pdt = new PrimitiveProviderDefinedType('Empty', ''); + const result = await roundTrip(pdt); + assert.instanceOf(result, PrimitiveProviderDefinedType); + assert.strictEqual(result.name, 'Empty'); + assert.strictEqual(result.value, ''); + }); + }); + + describe('empty name rejected', () => { + it('constructor rejects empty string name', () => { + assert.throws(() => new PrimitiveProviderDefinedType('', '42'), /name cannot be null or empty/); + }); + + it('constructor rejects null name', () => { + assert.throws(() => new PrimitiveProviderDefinedType(null, '42'), /name cannot be null or empty/); + }); + + it('constructor rejects undefined name', () => { + assert.throws(() => new PrimitiveProviderDefinedType(undefined, '42'), /name cannot be null or empty/); + }); + + it('constructor rejects null value', () => { + assert.throws(() => new PrimitiveProviderDefinedType('Uint32', null), /value cannot be null/); + }); + + it('deserializer rejects null name from wire', async () => { + const nullString = Buffer.from([DataType.STRING, 0x01]); + const valueString = Buffer.from([DataType.STRING, 0x00, 0x00, 0x00, 0x00, 0x02, 0x34, 0x32]); // "42" + const bytes = Buffer.concat([ + Buffer.from([DataType.PRIMITIVEPDT, 0x00]), + nullString, + valueString, + ]); + try { + await anySerializer.deserialize(StreamReader.fromBuffer(bytes)); + assert.fail('should have thrown'); + } catch (e) { + assert.match(e.message, /name cannot be null or empty/); + } + }); + }); + + describe('canBeUsedFor', () => { + it('returns true for PrimitiveProviderDefinedType instances', () => { + assert.isTrue(primitivePDTSerializer.canBeUsedFor(new PrimitiveProviderDefinedType('t', '1'))); + }); + + it('returns false for plain objects', () => { + assert.isFalse(primitivePDTSerializer.canBeUsedFor({ name: 'test', value: '1' })); + }); + + it('returns false for strings', () => { + assert.isFalse(primitivePDTSerializer.canBeUsedFor('test')); + }); + + it('returns false for composite ProviderDefinedType', () => { + assert.isFalse(primitivePDTSerializer.canBeUsedFor(new ProviderDefinedType('t', { a: 1 }))); + }); + }); + + describe('auto-hydration via pdtRegistry', () => { + it('auto-hydrates when pdtRegistry is set on the reader', async () => { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj), + fromValue: (value) => parseInt(value, 10), + }); + + const pdt = new PrimitiveProviderDefinedType('Uint32', '42'); + const bytes = anySerializer.serialize(pdt); + const reader = StreamReader.fromBuffer(bytes); + reader.pdtRegistry = registry; + const result = await anySerializer.deserialize(reader); + + assert.notInstanceOf(result, PrimitiveProviderDefinedType); + assert.strictEqual(result, 42); + }); + + it('returns raw primitive PDT when no pdtRegistry is set', async () => { + const pdt = new PrimitiveProviderDefinedType('Uint32', '42'); + const bytes = anySerializer.serialize(pdt); + const result = await anySerializer.deserialize(StreamReader.fromBuffer(bytes)); + + assert.instanceOf(result, PrimitiveProviderDefinedType); + assert.strictEqual(result.name, 'Uint32'); + assert.strictEqual(result.value, '42'); + }); + + it('returns raw primitive PDT when no adapter registered for that type', async () => { + const registry = new ProviderDefinedTypeRegistry(); + const pdt = new PrimitiveProviderDefinedType('Unknown', 'xyz'); + const bytes = anySerializer.serialize(pdt); + const reader = StreamReader.fromBuffer(bytes); + reader.pdtRegistry = registry; + const result = await anySerializer.deserialize(reader); + + assert.instanceOf(result, PrimitiveProviderDefinedType); + assert.strictEqual(result.name, 'Unknown'); + assert.strictEqual(result.value, 'xyz'); + }); + }); +}); 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 73ccf69e36..4009a3f72c 100644 --- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js @@ -25,10 +25,11 @@ import { P, TextP, t as T, order as Order, scope as Scope, column as Column, withOptions as WithOptions, direction } from '../../lib/process/traversal.js'; import { ReadOnlyStrategy, SubgraphStrategy, OptionsStrategy, PartitionStrategy, SeedStrategy } from '../../lib/process/traversal-strategy.js'; -import { Graph, Vertex, ProviderDefinedType } from '../../lib/structure/graph.js'; +import { Graph, Vertex, ProviderDefinedType, PrimitiveProviderDefinedType } 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 { ProviderDefinedTypeRegistry } from '../../lib/structure/ProviderDefinedTypeRegistry.js'; const g = new GraphTraversalSource(new Graph(), new TraversalStrategies()); @@ -661,4 +662,99 @@ describe('GremlinLang', function () { ); }); }); + + describe('Primitive PDT gremlin-lang tests', function () { + it('should handle basic primitive PDT', function () { + const pdt = new PrimitiveProviderDefinedType('Uint32', '42'); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("Uint32","42"))' + ); + }); + + it('should handle primitive PDT with leading zeros', function () { + const pdt = new PrimitiveProviderDefinedType('TinkerId', '007'); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("TinkerId","007"))' + ); + }); + + it('should handle primitive PDT with large number', function () { + const pdt = new PrimitiveProviderDefinedType('BigNum', '99999999999999999999'); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("BigNum","99999999999999999999"))' + ); + }); + + it('should handle primitive PDT with non-numeric value', function () { + const pdt = new PrimitiveProviderDefinedType('CustomId', 'abc-def-123'); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("CustomId","abc-def-123"))' + ); + }); + + it('should handle primitive PDT with empty value', function () { + const pdt = new PrimitiveProviderDefinedType('Empty', ''); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("Empty",""))' + ); + }); + + it('should handle primitive PDT with special chars in name', function () { + const pdt = new PrimitiveProviderDefinedType('my"type', '1'); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("my\\"type","1"))' + ); + }); + + it('should handle primitive PDT with special chars in value', function () { + const pdt = new PrimitiveProviderDefinedType('Str', 'hello"world'); + assert.strictEqual( + g.inject(pdt).getGremlinLang().getGremlin(), + 'g.inject(PDT("Str","hello\\"world"))' + ); + }); + + it('should auto-dehydrate registered primitive types', function () { + class Uint32 { + constructor(v) { this.v = v; } + } + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj.v), + fromValue: (value) => new Uint32(parseInt(value, 10)), + }, Uint32); + + const gl = new GremlinLang(); + gl.pdtRegistry = registry; + gl.addStep('inject', [new Uint32(99)]); + assert.strictEqual(gl.getGremlin(), 'g.inject(PDT("Uint32","99"))'); + }); + + it('should prefer primitive adapter over composite when both are registered', function () { + class DualType { + constructor(v) { this.v = v; } + } + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('DualType', { + toValue: (obj) => String(obj.v), + fromValue: (value) => new DualType(value), + }, DualType); + registry.register('DualType', { + serialize: (obj) => ({ v: obj.v }), + deserialize: (fields) => new DualType(fields.v), + }, DualType); + + const gl = new GremlinLang(); + gl.pdtRegistry = registry; + gl.addStep('inject', [new DualType('hello')]); + // primitive should win + assert.strictEqual(gl.getGremlin(), 'g.inject(PDT("DualType","hello"))'); + }); + }); }); diff --git a/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js b/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js index ded764b7c6..285717e882 100644 --- a/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js +++ b/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js @@ -18,7 +18,7 @@ */ import { assert } from 'chai'; -import { ProviderDefinedType } from '../../lib/structure/graph.js'; +import { ProviderDefinedType, PrimitiveProviderDefinedType } from '../../lib/structure/graph.js'; import { ProviderDefinedTypeRegistry } from '../../lib/structure/ProviderDefinedTypeRegistry.js'; import Client from '../../lib/driver/client.js'; import Connection from '../../lib/driver/connection.js'; @@ -172,3 +172,120 @@ describe('pdtRegistry wiring through Client/Connection', () => { assert.strictEqual(conn1._reader.pdtRegistry, registry); }); }); + +describe('ProviderDefinedTypeRegistry - Primitive', () => { + describe('#hydratePrimitive()', () => { + it('should return a typed value when a primitive adapter is registered', () => { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj), + fromValue: (value) => parseInt(value, 10), + }); + + const pdt = new PrimitiveProviderDefinedType('Uint32', '42'); + const result = registry.hydratePrimitive(pdt); + + assert.strictEqual(result, 42); + }); + + it('should return the raw primitive PDT when no adapter is registered', () => { + const registry = new ProviderDefinedTypeRegistry(); + const pdt = new PrimitiveProviderDefinedType('Unknown', 'xyz'); + const result = registry.hydratePrimitive(pdt); + + assert.strictEqual(result, pdt); + assert.instanceOf(result, PrimitiveProviderDefinedType); + }); + + it('should fall back gracefully when adapter throws', () => { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Broken', { + toValue: () => '', + fromValue: () => { throw new Error('adapter error'); }, + }); + + const pdt = new PrimitiveProviderDefinedType('Broken', '1'); + const warnings = []; + const origWarn = console.warn; + console.warn = (msg) => warnings.push(msg); + try { + const result = registry.hydratePrimitive(pdt); + assert.strictEqual(result, pdt); + assert.lengthOf(warnings, 1); + assert.include(warnings[0], 'adapter error'); + assert.include(warnings[0], 'Broken'); + } finally { + console.warn = origWarn; + } + }); + + it('should return non-PrimitiveProviderDefinedType values unchanged', () => { + const registry = new ProviderDefinedTypeRegistry(); + assert.strictEqual(registry.hydratePrimitive('hello'), 'hello'); + assert.strictEqual(registry.hydratePrimitive(42), 42); + assert.strictEqual(registry.hydratePrimitive(null), null); + }); + + it('should preserve leading zeros in opaque string value', () => { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('PaddedId', { + toValue: (obj) => obj.id, + fromValue: (value) => ({ id: value }), + }); + + const pdt = new PrimitiveProviderDefinedType('PaddedId', '007'); + const result = registry.hydratePrimitive(pdt); + assert.deepStrictEqual(result, { id: '007' }); + }); + }); + + describe('#hasPrimitiveAdapter()', () => { + it('should return true for registered primitive types', () => { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { toValue: () => '', fromValue: (v) => v }); + assert.isTrue(registry.hasPrimitiveAdapter('Uint32')); + assert.isFalse(registry.hasPrimitiveAdapter('Missing')); + }); + }); + + describe('#getPrimitiveAdapterByClass()', () => { + it('should return the adapter entry for registered class', () => { + const registry = new ProviderDefinedTypeRegistry(); + class Uint32 { constructor(v) { this.v = v; } } + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj.v), + fromValue: (value) => new Uint32(parseInt(value, 10)), + }, Uint32); + const entry = registry.getPrimitiveAdapterByClass(Uint32); + assert.isNotNull(entry); + assert.strictEqual(entry.typeName, 'Uint32'); + assert.strictEqual(entry.toValue(new Uint32(5)), '5'); + }); + + it('should return null for unregistered class', () => { + const registry = new ProviderDefinedTypeRegistry(); + class Unknown {} + assert.isNull(registry.getPrimitiveAdapterByClass(Unknown)); + }); + }); + + describe('composite hydrate with nested primitive PDT', () => { + it('should hydrate nested primitive PDT inside composite fields', () => { + const registry = new ProviderDefinedTypeRegistry(); + registry.registerPrimitive('Uint32', { + toValue: (obj) => String(obj), + fromValue: (value) => parseInt(value, 10), + }); + registry.register('Measurement', { + serialize: (obj) => obj, + deserialize: (fields) => ({ type: 'Measurement', unit: fields.unit, value: fields.value }), + }); + + const primPdt = new PrimitiveProviderDefinedType('Uint32', '99'); + const compPdt = new ProviderDefinedType('Measurement', { unit: 'kg', value: primPdt }); + const result = registry.hydrate(compPdt); + + assert.deepStrictEqual(result, { type: 'Measurement', unit: 'kg', value: 99 }); + }); + }); +});
