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/05/25 23:13:04 UTC
(tinkerpop) branch PDT created (now 4680f62725)
This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a change to branch PDT
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
at 4680f62725 Implement Provider Defined Types (PDT) for TinkerPop 4
This branch includes the following new commits:
new 4680f62725 Implement Provider Defined Types (PDT) for TinkerPop 4
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(tinkerpop) 01/01: Implement Provider Defined Types (PDT) for TinkerPop 4
Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch PDT
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 4680f62725833a7dbe1a9dad02482496b1dad467
Author: Cole Greer <co...@colegreer.ca>
AuthorDate: Mon May 25 16:12:38 2026 -0700
Implement Provider Defined Types (PDT) for TinkerPop 4
Add CompositePDT (0xF0) support enabling graph providers to define
custom types that serialize/deserialize seamlessly across all GLVs
without driver-side configuration. Replaces the TP3 CustomTypeSerializer
mechanism.
Core (gremlin-core):
- Add @ProviderDefined annotation and immutable ProviderDefinedType POJO
- Add ProviderDefinedTypeSerializer for GraphBinary with wire format:
fully-qualified type string + fully-qualified fields map
- Add PdtGraphSONSerializersV4 with g:CompositePdt type tag
- Add ProviderDefinedTypeAdapter<T> and ProviderDefinedTypeRegistry
with ServiceLoader discovery, recursive hydration, and graceful
degradation on adapter failure
- Integrate auto-hydration into GraphBinaryReader and GraphSONMapper
- Add GraphBinaryWriter auto-conversion for @ProviderDefined objects
- Cache reflection metadata per class for performance
- Support inherited fields via superclass walking
- Remove legacy CUSTOM(0x00) type mechanism entirely
Gremlin-lang:
- Add PDT("name", ["key":value]) literal to ANTLR grammar
- Server-side parser constructs ProviderDefinedType from PDT literals
- All GLV translators emit PDT literal syntax
- Registry-based and annotation-based auto-dehydration in translators
- All TranslateVisitors handle PDT for cross-language translation
GLV support (all languages):
- Python: ProviderDefinedType, serializer, registry with
@provider_defined decorator, entry_points auto-discovery,
registry wired through Client/DriverRemoteConnection
- JavaScript: ProviderDefinedType, CompositePDTSerializer, registry
with explicit function pair registration, client options wiring
- Go: ProviderDefinedType struct, serializer/deserializer, PDTRegistry
with struct tags, RegisterFuncs, PDTProvider interface, client wiring
- .NET: ProviderDefinedType, CompositePDTSerializer, registry with
IProviderDefinedTypeAdapter<T>, [ProviderDefined] attribute with
IncludedFields/ExcludedFields, assembly scanning, IMessageSerializer
.SetPdtRegistry() interface method, client/connection wiring
Server and testing:
- PDT flows end-to-end through gremlin-server with TinkerGraph storing
original objects and conversion at serialization boundary
- Test-jar with Point, Address, Person test types for Docker server
- Integration tests in all GLVs using gremlin-lang PDT literals
- Traversal API tests covering raw PDT, registry hydration, and
annotation-based auto-dehydration round-trips
Also: documentation (docs/src/dev/provider/pdt.asciidoc), CHANGELOG
entry, .dockerignore update for test-jar.
---
.dockerignore | 1 +
CHANGELOG.asciidoc | 1 +
docker/gremlin-test-server/Dockerfile | 1 +
docs/src/dev/provider/pdt.asciidoc | 295 +++++++++++++++++++++
.../grammar/DefaultGremlinBaseVisitor.java | 4 +
.../language/grammar/GenericLiteralVisitor.java | 17 ++
.../translator/AnonymizedTranslatorVisitor.java | 6 +
.../translator/DotNetTranslateVisitor.java | 10 +
.../language/translator/GoTranslateVisitor.java | 10 +
.../translator/GroovyTranslateVisitor.java | 10 +
.../language/translator/JavaTranslateVisitor.java | 10 +
.../translator/JavascriptTranslateVisitor.java | 10 +
.../translator/PythonTranslateVisitor.java | 10 +
.../language/translator/TranslateVisitor.java | 6 +
.../gremlin/process/remote/RemoteConnection.java | 9 +
.../gremlin/process/traversal/GremlinLang.java | 43 +++
.../traversal/dsl/graph/GraphTraversalSource.java | 3 +
.../gremlin/structure/io/binary/DataType.java | 3 +-
.../structure/io/binary/GraphBinaryReader.java | 21 +-
.../structure/io/binary/GraphBinaryWriter.java | 20 +-
.../io/binary/TypeSerializerRegistry.java | 94 ++-----
.../types/ProviderDefinedTypeSerializer.java | 56 ++++
.../structure/io/graphson/GraphSONMapper.java | 17 ++
.../structure/io/graphson/GraphSONModule.java | 12 +
.../io/graphson/PdtGraphSONSerializersV4.java | 116 ++++++++
.../ProviderDefined.java} | 23 +-
.../structure/io/pdt/ProviderDefinedType.java | 164 ++++++++++++
.../ProviderDefinedTypeAdapter.java} | 17 +-
.../io/pdt/ProviderDefinedTypeRegistry.java | 130 +++++++++
.../grammar/GeneralLiteralVisitorTest.java | 41 +++
.../language/translator/GremlinTranslatorTest.java | 9 +
.../gremlin/process/traversal/GremlinLangTest.java | 14 +
.../io/binary/GraphBinaryWriterPdtTest.java | 88 ++++++
.../gremlin/structure/io/binary/HeapBuffer.java | 230 ++++++++++++++++
.../types/ProviderDefinedTypeSerializerTest.java | 229 ++++++++++++++++
.../io/graphson/PdtGraphSONSerializersV4Test.java | 230 ++++++++++++++++
.../io/pdt/ProviderDefinedTypeRegistryTest.java | 293 ++++++++++++++++++++
.../structure/io/pdt/ProviderDefinedTypeTest.java | 227 ++++++++++++++++
.../src/Gremlin.Net/Driver/GremlinClient.cs | 23 +-
.../src/Gremlin.Net/Driver/IMessageSerializer.cs | 8 +
.../Driver/Remote/DriverRemoteConnection.cs | 18 +-
.../Process/Remote/IRemoteConnection.cs | 6 +
.../Process/Traversal/AnonymousTraversalSource.cs | 2 +-
.../Gremlin.Net/Process/Traversal/GremlinLang.cs | 90 ++++++-
.../Structure/IO/GraphBinary4/DataType.cs | 2 +-
.../GraphBinary4/GraphBinary4MessageSerializer.cs | 21 +-
.../Structure/IO/GraphBinary4/GraphBinaryReader.cs | 19 +-
.../IO/GraphBinary4/TypeSerializerRegistry.cs | 2 +
.../GraphBinary4/Types/CompositePDTSerializer.cs | 78 ++++++
.../Structure/IProviderDefinedTypeAdapter.cs | 49 ++++
.../Structure/ProviderDefinedAttribute.cs | 81 ++++++
.../Gremlin.Net/Structure/ProviderDefinedType.cs | 70 +++++
.../Structure/ProviderDefinedTypeRegistry.cs | 131 +++++++++
.../Driver/DriverRemoteConnectionTests.cs | 100 +++++++
.../Driver/GremlinClientTests.cs | 68 +++++
.../Process/Traversal/GremlinLangTests.cs | 86 ++++++
.../IO/GraphBinary4/ProviderDefinedTypeTests.cs | 137 ++++++++++
.../Structure/ProviderDefinedTypeRegistryTests.cs | 196 ++++++++++++++
.../src/main/java/examples/Connections.java | 7 +-
.../driver/remote/DriverRemoteConnection.java | 14 +
gremlin-examples/gremlin-java/Connections.java | 7 +-
gremlin-go/driver/client.go | 4 +
gremlin-go/driver/client_test.go | 95 +++++++
gremlin-go/driver/connection.go | 8 +-
gremlin-go/driver/driverRemoteConnection.go | 4 +
gremlin-go/driver/error_codes.go | 1 -
gremlin-go/driver/graphBinaryDeserializer.go | 54 +++-
gremlin-go/driver/graphBinarySerializer.go | 4 +-
gremlin-go/driver/graphBinarySerializer_test.go | 101 +++++++
gremlin-go/driver/graphTraversalSource.go | 3 +
gremlin-go/driver/gremlinlang.go | 48 +++-
gremlin-go/driver/gremlinlang_test.go | 86 ++++++
gremlin-go/driver/pdtRegistry.go | 104 ++++++++
gremlin-go/driver/pdtRegistry_test.go | 92 +++++++
gremlin-go/driver/providerDefinedType.go | 87 ++++++
gremlin-go/driver/providerDefinedType_test.go | 36 +++
gremlin-go/driver/serializer.go | 26 +-
gremlin-go/driver/traversal_test.go | 107 ++++++++
.../gremlin-javascript/lib/driver/connection.ts | 4 +
.../lib/driver/remote-connection.ts | 2 +-
gremlin-js/gremlin-javascript/lib/index.ts | 3 +
.../lib/process/anonymous-traversal.ts | 6 +-
.../gremlin-javascript/lib/process/gremlin-lang.ts | 31 ++-
.../lib/structure/ProviderDefinedTypeRegistry.ts | 72 +++++
.../gremlin-javascript/lib/structure/graph.ts | 24 ++
.../lib/structure/io/binary/GraphBinary.js | 6 +-
.../structure/io/binary/internals/AnySerializer.js | 1 +
.../io/binary/internals/CompositePDTSerializer.js | 89 +++++++
.../test/integration/client-tests.js | 69 ++++-
.../test/integration/traversal-test.js | 112 +++++++-
.../graphbinary/CompositePDTSerializer-test.js | 153 +++++++++++
.../test/unit/gremlin-lang-test.js | 54 +++-
.../test/unit/pdt-registry-test.js | 157 +++++++++++
gremlin-language/src/main/antlr4/Gremlin.g4 | 7 +
.../main/python/gremlin_python/driver/client.py | 6 +-
.../driver/driver_remote_connection.py | 3 +-
.../python/gremlin_python/driver/serializer.py | 11 +-
.../gremlin_python/process/graph_traversal.py | 2 +
.../python/gremlin_python/process/traversal.py | 26 +-
.../main/python/gremlin_python/structure/graph.py | 111 ++++++++
.../gremlin_python/structure/io/graphbinaryV4.py | 60 ++++-
.../src/main/python/tests/integration/conftest.py | 23 ++
.../python/tests/integration/driver/test_client.py | 58 ++++
.../driver/test_driver_remote_connection.py | 35 ++-
.../python/tests/unit/process/test_gremlin_lang.py | 35 +++
.../tests/unit/structure/io/test_graphbinaryV4.py | 24 +-
.../structure/io/test_provider_defined_type.py | 220 +++++++++++++++
gremlin-server/pom.xml | 10 +
.../tinkerpop/gremlin/server/pdt/Address.java | 24 +-
.../tinkerpop/gremlin/server/pdt/Person.java | 32 +--
.../apache/tinkerpop/gremlin/server/pdt/Point.java | 21 +-
.../pdt/ProviderDefinedTypeIntegrateTest.java | 286 ++++++++++++++++++++
.../util/ser/GraphBinaryMessageSerializerV4.java | 64 +----
.../ser/binary/TypeSerializerRegistryTest.java | 10 +-
.../types/sample/SamplePersonSerializer.java | 117 --------
.../types/sample/SamplePersonSerializerTest.java | 126 ---------
116 files changed, 6104 insertions(+), 545 deletions(-)
diff --git a/.dockerignore b/.dockerignore
index 50d6801832..400575544c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,6 +1,7 @@
**/*.log
**/target
!gremlin-server/target/apache-tinkerpop-gremlin-server-*
+!gremlin-server/target/gremlin-server-*-tests.jar
!gremlin-console/target/apache-tinkerpop-gremlin-console-*
*.iml
.idea
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 284bbcca26..9aa6b0e6a9 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -26,6 +26,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
=== TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)
* Added `NextN(n)` to `Traversal` in `gremlin-go` for batched result iteration, providing API parity with `next(n)` in the Java, Python, and .NET GLVs, and updated the Go translators in `gremlin-core` and `gremlin-javascript` to emit `NextN(n)` for the batched form.
+* Added Provider Defined Types (PDT) support — graph providers can define custom types via `@ProviderDefined` annotation that serialize/deserialize seamlessly across all GLVs without driver-side configuration. Replaces TP3 custom type mechanism.
* Added Gremlator, a single page web application, that translates Gremlin into various programming languages like Javascript and Python.
* Removed `uuid` dependency from `gremlin-javascript` in favor of the built-in `globalThis.crypto.randomUUID()`.
* Added streaming HTTP response support to `gremlin-driver` for incremental result deserialization over GraphBinary.
diff --git a/docker/gremlin-test-server/Dockerfile b/docker/gremlin-test-server/Dockerfile
index 8ac853c1d1..3740499dcc 100644
--- a/docker/gremlin-test-server/Dockerfile
+++ b/docker/gremlin-test-server/Dockerfile
@@ -24,6 +24,7 @@ USER root
RUN mkdir -p /opt
WORKDIR /opt
COPY gremlin-server/src/test /opt/test/
+COPY gremlin-server/target/gremlin-server-*-tests.jar /opt/gremlin-server/lib/
COPY docker/gremlin-server/docker-entrypoint.sh docker/gremlin-server/*.yaml docker/gremlin-server/*.conf /opt/
RUN chmod 755 /opt/docker-entrypoint.sh
diff --git a/docs/src/dev/provider/pdt.asciidoc b/docs/src/dev/provider/pdt.asciidoc
new file mode 100644
index 0000000000..e61feb21b9
--- /dev/null
+++ b/docs/src/dev/provider/pdt.asciidoc
@@ -0,0 +1,295 @@
+////
+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.
+////
+
+[[provider-defined-types]]
+== Provider Defined Types (PDT)
+
+Provider Defined Types allow graph providers to expose custom types that drivers can serialize and deserialize without
+manual configuration on the client side. A provider annotates a class (or registers an adapter for a class it doesn't
+own), and the type flows through the wire protocol automatically. Clients receive PDT values as structured objects they
+can use directly or hydrate into language-native types.
+
+[[pdt-for-graph-providers]]
+=== For Graph Providers
+
+==== Basic Usage
+
+Annotate a class with `@ProviderDefined` from the `org.apache.tinkerpop.gremlin.structure.io.pdt` package:
+
+[source,java]
+----
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
+
+@ProviderDefined(name = "x:Point")
+public class Point {
+ private final double x;
+ private final double y;
+
+ public Point(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public double getX() { return x; }
+ public double getY() { return y; }
+}
+----
+
+The `name` attribute is a unique identifier for the type, typically namespaced (e.g. `"x:Point"`). By default, all
+fields are included. Use `includedFields` or `excludedFields` to control which fields are serialized:
+
+[source,java]
+----
+@ProviderDefined(name = "x:Point", includedFields = {"x", "y"})
+public class Point { ... }
+
+// or exclude specific fields
+@ProviderDefined(name = "x:Person", excludedFields = {"internalId"})
+public class Person { ... }
+----
+
+==== Nested Types
+
+PDT supports nested custom types. Each nested type must also be annotated:
+
+[source,java]
+----
+@ProviderDefined(name = "x:Address")
+public class Address {
+ private final String street;
+ private final String city;
+
+ public Address(String street, String city) {
+ this.street = street;
+ this.city = city;
+ }
+
+ public String getStreet() { return street; }
+ public String getCity() { return city; }
+}
+
+@ProviderDefined(name = "x:Person")
+public class Person {
+ private final String name;
+ private final Address address;
+
+ public Person(String name, Address address) {
+ this.name = name;
+ this.address = address;
+ }
+
+ public String getName() { return name; }
+ public Address getAddress() { return address; }
+}
+----
+
+When serialized, the `address` field is itself encoded as a PDT value.
+
+==== Adapter for Types You Don't Own
+
+For classes you cannot annotate (e.g. `java.awt.Color`), implement `ProviderDefinedTypeAdapter<T>`:
+
+[source,java]
+----
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter;
+
+public class ColorAdapter implements ProviderDefinedTypeAdapter<java.awt.Color> {
+
+ @Override
+ public String typeName() {
+ return "x:Color";
+ }
+
+ @Override
+ public Class<java.awt.Color> targetClass() {
+ return java.awt.Color.class;
+ }
+
+ @Override
+ public Map<String, Object> toProperties(java.awt.Color color) {
+ return Map.of("r", color.getRed(), "g", color.getGreen(),
+ "b", color.getBlue(), "a", color.getAlpha());
+ }
+
+ @Override
+ public java.awt.Color fromProperties(Map<String, Object> fields) {
+ return new java.awt.Color((int) fields.get("r"), (int) fields.get("g"),
+ (int) fields.get("b"), (int) fields.get("a"));
+ }
+}
+----
+
+==== ServiceLoader Registration
+
+Register adapters for auto-discovery by adding a file at:
+
+----
+META-INF/services/org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter
+----
+
+with the fully qualified class name of each adapter:
+
+----
+com.example.graph.ColorAdapter
+----
+
+Annotated classes (`@ProviderDefined`) are discovered automatically via classpath scanning and do not require
+ServiceLoader registration.
+
+[[pdt-for-driver-users]]
+=== For Driver Users
+
+==== Receiving PDT Values
+
+PDT values arrive on the client as `ProviderDefinedType` objects containing a `name` (String) and a `properties` map.
+No configuration is needed to receive them — any driver that supports the TinkerPop 4 wire format will deserialize
+PDTs automatically.
+
+[source,java]
+----
+// Java - values arrive as ProviderDefinedType by default
+ProviderDefinedType pdt = (ProviderDefinedType) vertex.value("location");
+String typeName = pdt.getName(); // "x:Point"
+Map<String, Object> props = pdt.getProperties(); // {x: 1.0, y: 2.0}
+----
+
+==== Optional Hydration
+
+To convert `ProviderDefinedType` into language-native objects, register adapters with a
+`ProviderDefinedTypeRegistry`. This is optional — basic consumption works without it.
+
+===== Java
+
+Implement `ProviderDefinedTypeAdapter<T>` and register via ServiceLoader (same mechanism as the provider side). The
+driver discovers adapters on the classpath and hydrates automatically.
+
+===== Python (planned)
+
+[source,python]
+----
+from gremlin_python.structure.io.pdt import provider_defined
+
+@provider_defined(name="x:Point")
+class Point:
+ def __init__(self, x: float, y: float):
+ self.x = x
+ self.y = y
+----
+
+Registration via `entry_points` in `setup.cfg` or `pyproject.toml`. This feature is designed but not yet implemented.
+
+===== .NET (planned)
+
+[source,csharp]
+----
+public class PointAdapter : IProviderDefinedTypeAdapter<Point>
+{
+ public string Name => "x:Point";
+
+ public Point FromFields(IDictionary<string, object> fields) =>
+ new Point((double)fields["x"], (double)fields["y"]);
+
+ public IDictionary<string, object> ToFields(Point value) =>
+ new Dictionary<string, object> { ["x"] = value.X, ["y"] = value.Y };
+}
+----
+
+Discovered via assembly scanning. This feature is designed but not yet implemented.
+
+===== JavaScript
+
+[source,javascript]
+----
+const { registry } = require('gremlin');
+
+registry.register('x:Point', {
+ serialize: (point) => ({ x: point.x, y: point.y }),
+ deserialize: (fields) => new Point(fields.x, fields.y)
+});
+----
+
+===== Go
+
+[source,go]
+----
+type Point struct {
+ X float64 `pdt:"x"`
+ Y float64 `pdt:"y"`
+}
+
+func init() {
+ pdt.RegisterFuncs("x:Point",
+ func(p Point) map[string]any { return map[string]any{"x": p.X, "y": p.Y} },
+ func(m map[string]any) Point { return Point{X: m["x"].(float64), Y: m["y"].(float64)} },
+ )
+}
+----
+
+[[pdt-wire-format-graphbinary]]
+=== Wire Format (GraphBinary V4)
+
+PDT uses type code `0xF0`. The binary layout is:
+
+----
+{type_code: 0xF0}{value_flag}{type_string}{fields_map}
+----
+
+* `type_string` — a fully-qualified GraphBinary String containing the PDT name (e.g. `"x:Point"`)
+* `fields_map` — a fully-qualified GraphBinary Map where keys are field name strings and values are fully-typed
+
+[[pdt-wire-format-graphson]]
+=== Wire Format (GraphSON V4)
+
+In typed GraphSON, a PDT value is represented as:
+
+[source,json]
+----
+{
+ "@type": "g:CompositePdt",
+ "@value": {
+ "type": "x:Point",
+ "fields": {
+ "@type": "g:Map",
+ "@value": ["x", {"@type": "g:Double", "@value": 1.0},
+ "y", {"@type": "g:Double", "@value": 2.0}]
+ }
+ }
+}
+----
+
+The `fields` value is a typed map following standard GraphSON map encoding.
+
+[[pdt-migration-from-tp3]]
+=== Migrating from TP3 Custom Types
+
+TinkerPop 4 removes `CustomTypeSerializer`. The replacement is:
+
+[cols="1,1",options="header"]
+|===
+|TP3 |TP4
+|`CustomTypeSerializer` on provider side |`@ProviderDefined` annotation or `ProviderDefinedTypeAdapter<T>`
+|`CustomTypeSerializer` on driver side |Not needed for basic consumption; optional `ProviderDefinedTypeAdapter` for hydration
+|Manual serializer registration per driver |Automatic via ServiceLoader / classpath discovery
+|===
+
+For providers: replace your `CustomTypeSerializer` implementations with `@ProviderDefined` annotations on your types,
+or `ProviderDefinedTypeAdapter<T>` for types you don't own.
+
+For driver users: PDT values are consumable out of the box as `ProviderDefinedType(name, properties)`. Register an
+adapter only if you want automatic hydration to a specific class.
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
index 550774cd27..760da5be7a 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/DefaultGremlinBaseVisitor.java
@@ -1355,6 +1355,10 @@ public class DefaultGremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> im
* {@inheritDoc}
*/
@Override public T visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) { notImplemented(ctx); return null; }
+ /**
+ * {@inheritDoc}
+ */
+ @Override public T visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) { notImplemented(ctx); return null; }
/**
* {@inheritDoc}
*/
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
index 67dcd47665..9af6b7a3d9 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java
@@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import org.apache.tinkerpop.gremlin.util.DatetimeHelper;
import java.math.BigDecimal;
@@ -578,6 +579,22 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> {
}
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ final String name = (String) visitStringLiteral(ctx.stringLiteral());
+ final Map<String, Object> properties = new LinkedHashMap<>();
+ final Map<?, ?> rawMap = (Map<?, ?>) visitGenericMapLiteral(ctx.genericMapLiteral());
+ for (final Map.Entry<?, ?> entry : rawMap.entrySet()) {
+ if (!(entry.getKey() instanceof String))
+ throw new IllegalArgumentException("PDT properties map must have String keys, found: " + entry.getKey().getClass().getName());
+ properties.put((String) entry.getKey(), entry.getValue());
+ }
+ return new ProviderDefinedType(name, properties);
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java
index 23b70526bc..40f869dac3 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/AnonymizedTranslatorVisitor.java
@@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.language.translator;
import org.antlr.v4.runtime.ParserRuleContext;
import org.apache.tinkerpop.gremlin.language.grammar.GremlinParser;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -204,4 +205,9 @@ public class AnonymizedTranslatorVisitor extends TranslateVisitor {
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
return anonymize(ctx, ByteBuffer.class);
}
+
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ return anonymize(ctx, ProviderDefinedType.class);
+ }
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java
index 11546c6ca2..7c296572cf 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java
@@ -1207,6 +1207,16 @@ public class DotNetTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append("new ProviderDefinedType(");
+ sb.append(ctx.stringLiteral().getText());
+ sb.append(", ");
+ visitGenericMapLiteral(ctx.genericMapLiteral());
+ sb.append(")");
+ return null;
+ }
+
@Override
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
sb.append("Convert.FromBase64String(");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java
index fa3633800d..48422f675e 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java
@@ -376,6 +376,16 @@ public class GoTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append("&gremlingo.ProviderDefinedType{Name: ");
+ visitStringLiteral(ctx.stringLiteral());
+ sb.append(", Properties: ");
+ visitGenericMapLiteral(ctx.genericMapLiteral());
+ sb.append("}");
+ return null;
+ }
+
@Override
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
final String base64Str = removeFirstAndLastCharacters(ctx.stringLiteral().getText());
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java
index 009233c9eb..2fd655a506 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java
@@ -152,6 +152,16 @@ public class GroovyTranslateVisitor extends TranslateVisitor {
return null;
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append("new ProviderDefinedType(");
+ sb.append(ctx.stringLiteral().getText());
+ sb.append(", ");
+ visitGenericMapLiteral(ctx.genericMapLiteral());
+ sb.append(")");
+ return null;
+ }
+
@Override
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
sb.append("ByteBuffer.wrap(Base64.getDecoder().decode(");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java
index ee03430118..6f28494526 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java
@@ -270,6 +270,16 @@ public class JavaTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append("new ProviderDefinedType(");
+ sb.append(ctx.stringLiteral().getText());
+ sb.append(", ");
+ visitGenericMapLiteral(ctx.genericMapLiteral());
+ sb.append(")");
+ return null;
+ }
+
@Override
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
sb.append("ByteBuffer.wrap(Base64.getDecoder().decode(");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java
index 2a8e0ed07f..cf6284d275 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavascriptTranslateVisitor.java
@@ -240,6 +240,16 @@ public class JavascriptTranslateVisitor extends AbstractTranslateVisitor {
throw new TranslatorException("Duration literals are not supported in JavaScript");
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append("new ProviderDefinedType(");
+ visitStringLiteral(ctx.stringLiteral());
+ sb.append(", ");
+ visitGenericMapLiteral(ctx.genericMapLiteral());
+ sb.append(")");
+ return null;
+ }
+
@Override
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
sb.append("Buffer.from(");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java
index e6297dc539..694d6b4238 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java
@@ -317,6 +317,16 @@ public class PythonTranslateVisitor extends AbstractTranslateVisitor {
return null;
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append("ProviderDefinedType(");
+ visitStringLiteral(ctx.stringLiteral());
+ sb.append(", ");
+ visitGenericMapLiteral(ctx.genericMapLiteral());
+ sb.append(")");
+ return null;
+ }
+
@Override
public Void visitBinaryLiteral(final GremlinParser.BinaryLiteralContext ctx) {
sb.append("base64.b64decode(");
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java
index a8d51f6b28..7e39b31482 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/TranslateVisitor.java
@@ -470,6 +470,12 @@ public class TranslateVisitor extends GremlinBaseVisitor<Void> {
return null;
}
+ @Override
+ public Void visitPdtLiteral(final GremlinParser.PdtLiteralContext ctx) {
+ sb.append(ctx.getText());
+ return null;
+ }
+
@Override
public Void visitVariable(final GremlinParser.VariableContext ctx) {
final String var = ctx.getText();
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/remote/RemoteConnection.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/remote/RemoteConnection.java
index 15be3637ca..607c2b480f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/remote/RemoteConnection.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/remote/RemoteConnection.java
@@ -23,6 +23,7 @@ import org.apache.tinkerpop.gremlin.process.remote.traversal.RemoteTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.GremlinLang;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.structure.Transaction;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import java.lang.reflect.Constructor;
import java.util.Iterator;
@@ -55,6 +56,14 @@ public interface RemoteConnection extends AutoCloseable {
*/
public <E> CompletableFuture<RemoteTraversal<?, E>> submitAsync(final GremlinLang gremlinLang) throws RemoteConnectionException;
+ /**
+ * Returns the {@link ProviderDefinedTypeRegistry} associated with this connection, or {@code null} if none.
+ * Used by the gremlin-lang translator for registry-based dehydration of unknown types.
+ */
+ public default ProviderDefinedTypeRegistry getPdtRegistry() {
+ return null;
+ }
+
/**
* Create a {@link RemoteConnection} from a {@code Configuration} object. The configuration must contain a
* {@code gremlin.remote.remoteConnectionClass} key which is the fully qualified class name of a
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
index 08f124015c..14d153e7ee 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
@@ -30,6 +30,10 @@ import org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversal;
import org.apache.tinkerpop.gremlin.structure.Column;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import org.apache.tinkerpop.gremlin.util.NumberHelper;
import javax.lang.model.SourceVersion;
@@ -48,6 +52,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.Base64;
@@ -67,10 +72,15 @@ public class GremlinLang implements Cloneable, Serializable {
private Map<String, Object> parameters = new HashMap<>();
private String unsupportedType = "";
private List<OptionsStrategy> optionsStrategies = new ArrayList<>();
+ private ProviderDefinedTypeRegistry pdtRegistry;
public GremlinLang() {
}
+ public GremlinLang(final ProviderDefinedTypeRegistry pdtRegistry) {
+ this.pdtRegistry = pdtRegistry;
+ }
+
public GremlinLang(final String sourceName, final Object... arguments) {
addToGremlin(sourceName, arguments);
}
@@ -179,6 +189,11 @@ public class GremlinLang implements Cloneable, Serializable {
return String.format("Binary(\"%s\")", Base64.getEncoder().encodeToString((byte[]) arg));
}
+ if (arg instanceof ProviderDefinedType) {
+ final ProviderDefinedType pdt = (ProviderDefinedType) arg;
+ return "PDT(" + argAsString(pdt.getName()) + "," + asString((Map<?, ?>) pdt.getProperties()) + ")";
+ }
+
if (arg instanceof Enum) {
// special handling for enums with additional interfaces
if (arg instanceof T)
@@ -252,6 +267,19 @@ public class GremlinLang implements Cloneable, Serializable {
return ((Class) arg).getSimpleName();
}
+ if (pdtRegistry != null) {
+ final Optional<ProviderDefinedTypeAdapter<?>> adapter = pdtRegistry.getAdapterByClass(arg.getClass());
+ if (adapter.isPresent()) {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> props = ((ProviderDefinedTypeAdapter) adapter.get()).toProperties(arg);
+ return argAsString(new ProviderDefinedType(adapter.get().typeName(), props));
+ }
+ }
+
+ if (arg.getClass().isAnnotationPresent(ProviderDefined.class)) {
+ return argAsString(ProviderDefinedType.from(arg));
+ }
+
unsupportedType = arg.getClass().getSimpleName();
return arg.toString();
}
@@ -532,6 +560,20 @@ public class GremlinLang implements Cloneable, Serializable {
return optionsStrategies;
}
+ /**
+ * Sets the {@link ProviderDefinedTypeRegistry} used for registry-based dehydration of unknown types.
+ */
+ public void setPdtRegistry(final ProviderDefinedTypeRegistry pdtRegistry) {
+ this.pdtRegistry = pdtRegistry;
+ }
+
+ /**
+ * Gets the {@link ProviderDefinedTypeRegistry} used for registry-based dehydration.
+ */
+ public ProviderDefinedTypeRegistry getPdtRegistry() {
+ return this.pdtRegistry;
+ }
+
public boolean isEmpty() {
return this.gremlin.length() == 0;
}
@@ -565,6 +607,7 @@ public class GremlinLang implements Cloneable, Serializable {
clone.gremlin.append(gremlin);
clone.optionsStrategies = new ArrayList<>(this.optionsStrategies);
clone.unsupportedType = this.unsupportedType;
+ clone.pdtRegistry = this.pdtRegistry;
return clone;
} catch (final CloneNotSupportedException e) {
throw new IllegalStateException(e.getMessage(), e);
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
index 684d9e8609..aec1d6fadd 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.java
@@ -102,6 +102,9 @@ public class GraphTraversalSource implements TraversalSource {
this(EmptyGraph.instance(), TraversalStrategies.GlobalCache.getStrategies(EmptyGraph.class).clone());
this.connection = connection;
this.strategies.addStrategies(new RemoteStrategy(connection));
+ if (connection.getPdtRegistry() != null) {
+ this.gremlinLang.setPdtRegistry(connection.getPdtRegistry());
+ }
}
@Override
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/DataType.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/DataType.java
index 7c2e347f6e..6ca99db840 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/DataType.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/DataType.java
@@ -58,7 +58,8 @@ public enum DataType {
CHAR(0X80),
DURATION(0X81),
- CUSTOM(0),
+ COMPOSITE_PDT(0xF0),
+
MARKER(0XFD),
UNSPECIFIED_NULL(0XFE);
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryReader.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryReader.java
index fb3ef553eb..5f3cf240c6 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryReader.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryReader.java
@@ -19,6 +19,8 @@
package org.apache.tinkerpop.gremlin.structure.io.binary;
import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import java.io.IOException;
@@ -48,13 +50,19 @@ import java.io.IOException;
*/
public class GraphBinaryReader {
private final TypeSerializerRegistry registry;
+ private final ProviderDefinedTypeRegistry pdtRegistry;
public GraphBinaryReader() {
this(TypeSerializerRegistry.INSTANCE);
}
public GraphBinaryReader(final TypeSerializerRegistry registry) {
+ this(registry, null);
+ }
+
+ public GraphBinaryReader(final TypeSerializerRegistry registry, final ProviderDefinedTypeRegistry pdtRegistry) {
this.registry = registry;
+ this.pdtRegistry = pdtRegistry;
}
/**
@@ -95,14 +103,11 @@ public class GraphBinaryReader {
return null;
}
- TypeSerializer<T> serializer;
- if (type != DataType.CUSTOM) {
- serializer = registry.getSerializer(type);
- } else {
- final String customTypeName = this.readValue(buffer, String.class, false);
- serializer = registry.getSerializerForCustomType(customTypeName);
+ final TypeSerializer<T> serializer = registry.getSerializer(type);
+ final T result = serializer.read(buffer, this);
+ if (pdtRegistry != null && result instanceof ProviderDefinedType) {
+ return (T) pdtRegistry.hydrate((ProviderDefinedType) result);
}
-
- return serializer.read(buffer, this);
+ return result;
}
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriter.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriter.java
index 5719015388..ef4c07ccda 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriter.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriter.java
@@ -18,8 +18,9 @@
*/
package org.apache.tinkerpop.gremlin.structure.io.binary;
-import org.apache.tinkerpop.gremlin.structure.io.binary.types.CustomTypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.types.ProviderDefinedTypeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.TransformSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import org.apache.tinkerpop.gremlin.structure.io.Buffer;
import java.io.IOException;
@@ -50,7 +51,6 @@ public class GraphBinaryWriter {
public final static byte VERSION_BYTE = (byte)0x84;
public final static byte BULKED_BYTE = (byte)0x01;
private final static byte[] unspecifiedNullBytes = new byte[] { DataType.UNSPECIFIED_NULL.getCodeByte(), 0x01};
- private final static byte[] customTypeCodeBytes = new byte[] { DataType.CUSTOM.getCodeByte() };
public GraphBinaryWriter() {
this(TypeSerializerRegistry.INSTANCE);
@@ -76,6 +76,10 @@ public class GraphBinaryWriter {
final Class<?> objectClass = value.getClass();
final TypeSerializer<T> serializer = (TypeSerializer<T>) registry.getSerializer(objectClass);
+ if (serializer instanceof ProviderDefinedTypeSerializer && !(value instanceof ProviderDefinedType)) {
+ serializer.writeValue((T) ProviderDefinedType.from(value), buffer, this, nullable);
+ return;
+ }
serializer.writeValue(value, buffer, this, nullable);
}
@@ -92,13 +96,11 @@ public class GraphBinaryWriter {
final Class<?> objectClass = value.getClass();
final TypeSerializer<T> serializer = (TypeSerializer<T>) registry.getSerializer(objectClass);
- if (serializer instanceof CustomTypeSerializer) {
- // It's a custom type
- CustomTypeSerializer customTypeSerializer = (CustomTypeSerializer) serializer;
-
- buffer.writeBytes(customTypeCodeBytes);
- writeValue(customTypeSerializer.getTypeName(), buffer, false);
- customTypeSerializer.write(value, buffer, this);
+ if (serializer instanceof ProviderDefinedTypeSerializer && !(value instanceof ProviderDefinedType)) {
+ // Convert @ProviderDefined-annotated object to ProviderDefinedType, then re-enter write().
+ // On re-entry, ProviderDefinedType.class is directly registered in the registry,
+ // and the instanceof guard prevents double-wrapping.
+ write((T) ProviderDefinedType.from(value), buffer);
return;
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/TypeSerializerRegistry.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/TypeSerializerRegistry.java
index f3d9921252..c0a3234aac 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/TypeSerializerRegistry.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/TypeSerializerRegistry.java
@@ -31,13 +31,13 @@ import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
-import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.BigDecimalSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.BigIntegerSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.BulkSetSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.BinarySerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.CharSerializer;
-import org.apache.tinkerpop.gremlin.structure.io.binary.types.CustomTypeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.DurationSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.EdgeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.EnumSerializer;
@@ -48,6 +48,7 @@ import org.apache.tinkerpop.gremlin.structure.io.binary.types.MapSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.DateTimeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.PathSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.PropertySerializer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.types.ProviderDefinedTypeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.SetSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.SingleTypeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.StringSerializer;
@@ -57,7 +58,6 @@ import org.apache.tinkerpop.gremlin.structure.io.binary.types.TreeSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.UUIDSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.VertexPropertySerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.types.VertexSerializer;
-import org.javatuples.Pair;
import java.io.IOException;
import java.lang.reflect.Modifier;
@@ -120,7 +120,8 @@ public class TypeSerializerRegistry {
new RegistryEntry<>(Character.class, new CharSerializer()),
new RegistryEntry<>(Duration.class, new DurationSerializer()),
- new RegistryEntry<>(OffsetDateTime.class, new DateTimeSerializer())
+ new RegistryEntry<>(OffsetDateTime.class, new DateTimeSerializer()),
+ new RegistryEntry<>(ProviderDefinedType.class, new ProviderDefinedTypeSerializer())
};
public static final TypeSerializerRegistry INSTANCE = build().create();
@@ -140,39 +141,10 @@ public class TypeSerializerRegistry {
* </p>
*/
public <DT> Builder add(final Class<DT> type, final TypeSerializer<DT> serializer) {
- if (serializer.getDataType() == DataType.CUSTOM) {
- throw new IllegalArgumentException("DataType can not be CUSTOM, use addCustomType() method instead");
- }
-
if (serializer.getDataType() == DataType.UNSPECIFIED_NULL) {
throw new IllegalArgumentException("Adding a serializer for a UNSPECIFIED_NULL is not permitted");
}
- if (serializer instanceof CustomTypeSerializer) {
- throw new IllegalArgumentException(
- "CustomTypeSerializer implementations are reserved for customtypes");
- }
-
- list.add(new RegistryEntry<>(type, serializer));
- return this;
- }
-
- /**
- * Adds a serializer for a custom type.
- */
- public <DT> Builder addCustomType(final Class<DT> type, final CustomTypeSerializer<DT> serializer) {
- if (serializer == null) {
- throw new NullPointerException("serializer can not be null");
- }
-
- if (serializer.getDataType() != DataType.CUSTOM) {
- throw new IllegalArgumentException("Custom serializer must use CUSTOM data type");
- }
-
- if (serializer.getTypeName() == null) {
- throw new NullPointerException("serializer custom type name can not be null");
- }
-
list.add(new RegistryEntry<>(type, serializer));
return this;
}
@@ -185,21 +157,6 @@ public class TypeSerializerRegistry {
return this;
}
- /**
- * Add {@link CustomTypeSerializer} by way of an {@link IoRegistry}. The registry entries should be bound to
- * {@link GraphBinaryIo}.
- */
- public Builder addRegistry(final IoRegistry registry) {
- if (null == registry) throw new IllegalArgumentException("The registry cannot be null");
-
- final List<Pair<Class, CustomTypeSerializer>> classSerializers = registry.find(GraphBinaryIo.class, CustomTypeSerializer.class);
- for (Pair<Class,CustomTypeSerializer> cs : classSerializers) {
- addCustomType(cs.getValue0(), cs.getValue1());
- }
-
- return this;
- }
-
/**
* Creates a new {@link TypeSerializerRegistry} instance based on the serializers added.
*/
@@ -225,15 +182,6 @@ public class TypeSerializerRegistry {
return typeSerializer.getDataType();
}
- public String getCustomTypeName() {
- if (getDataType() != DataType.CUSTOM) {
- return null;
- }
-
- final CustomTypeSerializer customTypeSerializer = (CustomTypeSerializer) typeSerializer;
- return customTypeSerializer.getTypeName();
- }
-
public TypeSerializer<DT> getTypeSerializer() {
return typeSerializer;
}
@@ -242,7 +190,6 @@ public class TypeSerializerRegistry {
private final Map<Class<?>, TypeSerializer<?>> serializers = new HashMap<>();
private final Map<Class<?>, TypeSerializer<?>> serializersByInterface = new LinkedHashMap<>();
private final Map<DataType, TypeSerializer<?>> serializersByDataType = new HashMap<>();
- private final Map<String, CustomTypeSerializer> serializersByCustomTypeName = new HashMap<>();
private Function<Class<?>, TypeSerializer<?>> fallbackResolver;
/**
@@ -291,9 +238,7 @@ public class TypeSerializerRegistry {
serializersByInterface.put(type, serializer);
}
- if (serializer.getDataType() == DataType.CUSTOM) {
- serializersByCustomTypeName.put(entry.getCustomTypeName(), (CustomTypeSerializer) serializer);
- } else if (serializer.getDataType() != null) {
+ if (serializer.getDataType() != null) {
serializersByDataType.put(serializer.getDataType(), serializer);
}
}
@@ -333,7 +278,15 @@ public class TypeSerializerRegistry {
serializer = fallbackResolver.apply(type);
}
- validateInstance(serializer, type.getTypeName());
+ if (null == serializer && type.isAnnotationPresent(ProviderDefined.class)) {
+ serializer = serializersByDataType.get(DataType.COMPOSITE_PDT);
+ }
+
+ if (serializer == null) {
+ throw new IOException(String.format(
+ "Serializer not found for type %s. If this is a provider-defined type, annotate the class with @ProviderDefined.",
+ type.getTypeName()));
+ }
// Store the lookup match to avoid looking it up in the future
serializersByImplementation.put(type, serializer);
@@ -342,26 +295,9 @@ public class TypeSerializerRegistry {
}
public <DT> TypeSerializer<DT> getSerializer(final DataType dataType) throws IOException {
- if (dataType == DataType.CUSTOM) {
- throw new IllegalArgumentException("Custom type serializers can not be retrieved using this method");
- }
-
return validateInstance(serializersByDataType.get(dataType), dataType.toString());
}
- /**
- * Gets the serializer for a given custom type name.
- */
- public <DT> CustomTypeSerializer<DT> getSerializerForCustomType(final String name) throws IOException {
- final CustomTypeSerializer serializer = serializersByCustomTypeName.get(name);
-
- if (serializer == null) {
- throw new IOException(String.format("Serializer for custom type '%s' not found", name));
- }
-
- return serializer;
- }
-
private static TypeSerializer validateInstance(final TypeSerializer serializer, final String typeName) throws IOException {
if (serializer == null) {
throw new IOException(String.format("Serializer for type %s not found", typeName));
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ProviderDefinedTypeSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ProviderDefinedTypeSerializer.java
new file mode 100644
index 0000000000..d45b4cf2a9
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ProviderDefinedTypeSerializer.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class ProviderDefinedTypeSerializer extends SimpleTypeSerializer<ProviderDefinedType> {
+
+ public ProviderDefinedTypeSerializer() {
+ super(DataType.COMPOSITE_PDT);
+ }
+
+ @Override
+ protected ProviderDefinedType readValue(final Buffer buffer, final GraphBinaryReader context) throws IOException {
+ final String name = context.read(buffer);
+ if (name == null || name.isEmpty())
+ throw new IOException("ProviderDefinedType name cannot be null or empty");
+ final Map<?, ?> properties = context.read(buffer);
+ for (final Object key : properties.keySet()) {
+ if (!(key instanceof String))
+ throw new IOException("ProviderDefinedType properties map must have String keys, found: " + key.getClass().getName());
+ }
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> typedProperties = (Map<String, Object>) (Map<?, ?>) properties;
+ return new ProviderDefinedType(name, typedProperties);
+ }
+
+ @Override
+ protected void writeValue(final ProviderDefinedType value, final Buffer buffer, final GraphBinaryWriter context) throws IOException {
+ context.write(value.getName(), buffer);
+ context.write(value.getProperties(), buffer);
+ }
+}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java
index 3da5c4e367..6877b67c69 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapper.java
@@ -21,6 +21,7 @@ package org.apache.tinkerpop.gremlin.structure.io.graphson;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.Mapper;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import org.apache.tinkerpop.shaded.jackson.annotation.JsonTypeInfo;
import org.apache.tinkerpop.shaded.jackson.core.JsonFactory;
import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator;
@@ -71,6 +72,7 @@ public class GraphSONMapper implements Mapper<ObjectMapper> {
private final GraphSONVersion version;
private final TypeInfo typeInfo;
private final StreamReadConstraints streamReadConstraints;
+ private final ProviderDefinedTypeRegistry pdtRegistry;
private GraphSONMapper(final Builder builder) {
this.customModules = builder.customModules;
@@ -79,6 +81,7 @@ public class GraphSONMapper implements Mapper<ObjectMapper> {
this.version = builder.version;
this.streamReadConstraints = builder.streamReadConstraintsBuilder.build();
this.typeInfo = builder.typeInfo;
+ this.pdtRegistry = builder.pdtRegistry;
}
@Override
@@ -89,6 +92,9 @@ public class GraphSONMapper implements Mapper<ObjectMapper> {
}
final GraphSONModule graphSONModule = version.getBuilder().create(normalize, typeInfo);
+ if (pdtRegistry != null && graphSONModule instanceof GraphSONModule.GraphSONModuleV4) {
+ ((GraphSONModule.GraphSONModuleV4) graphSONModule).setPdtRegistry(pdtRegistry);
+ }
om.registerModule(graphSONModule);
customModules.forEach(om::registerModule);
@@ -186,6 +192,7 @@ public class GraphSONMapper implements Mapper<ObjectMapper> {
builder.loadCustomModules = mapper.loadCustomSerializers;
builder.normalize = mapper.normalize;
builder.typeInfo = mapper.typeInfo;
+ builder.pdtRegistry = mapper.pdtRegistry;
builder.streamReadConstraintsBuilder = mapper.streamReadConstraints.rebuild();
return builder;
@@ -217,6 +224,7 @@ public class GraphSONMapper implements Mapper<ObjectMapper> {
private StreamReadConstraints.Builder streamReadConstraintsBuilder = StreamReadConstraints.builder()
.maxNumberLength(DEFAULT_MAX_NUMBER_LENGTH);
private TypeInfo typeInfo = null;
+ private ProviderDefinedTypeRegistry pdtRegistry = null;
private Builder() {
}
@@ -301,6 +309,15 @@ public class GraphSONMapper implements Mapper<ObjectMapper> {
return this;
}
+ /**
+ * Set the {@link ProviderDefinedTypeRegistry} to enable automatic hydration of
+ * {@link org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType} values during deserialization.
+ */
+ public Builder pdtRegistry(final ProviderDefinedTypeRegistry pdtRegistry) {
+ this.pdtRegistry = pdtRegistry;
+ return this;
+ }
+
public Builder maxNumberLength(final int maxNumLength) {
this.streamReadConstraintsBuilder.maxNumberLength(maxNumLength);
return this;
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
index 6e98642d7b..5cdb5cb1c3 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
@@ -81,6 +81,8 @@ import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import org.apache.tinkerpop.gremlin.structure.util.star.DirectionalStarGraph;
import org.apache.tinkerpop.gremlin.structure.util.star.StarGraphGraphSONSerializerV1;
import org.apache.tinkerpop.gremlin.structure.util.star.StarGraphGraphSONSerializerV2;
@@ -156,12 +158,15 @@ abstract class GraphSONModule extends TinkerPopJacksonModule {
put(Path.class, "Path");
put(VertexProperty.class, "VertexProperty");
put(Tree.class, "Tree");
+ put(ProviderDefinedType.class, "CompositePdt");
Stream.of(
Direction.class,
Merge.class,
T.class).forEach(e -> put(e, e.getSimpleName()));
}});
+ private final PdtGraphSONSerializersV4.ProviderDefinedTypeJacksonDeserializer pdtDeserializer;
+
/**
* Constructs a new object.
*/
@@ -178,6 +183,7 @@ abstract class GraphSONModule extends TinkerPopJacksonModule {
addSerializer(Path.class, new GraphSONSerializersV4.PathJacksonSerializer());
addSerializer(DirectionalStarGraph.class, new StarGraphGraphSONSerializerV4(normalize));
addSerializer(Tree.class, new GraphSONSerializersV4.TreeJacksonSerializer());
+ addSerializer(ProviderDefinedType.class, new PdtGraphSONSerializersV4.ProviderDefinedTypeJacksonSerializer());
// java.util - use the standard jackson serializers for collections when types aren't embedded
if (typeInfo != TypeInfo.NO_TYPES) {
@@ -208,6 +214,8 @@ abstract class GraphSONModule extends TinkerPopJacksonModule {
addDeserializer(Path.class, new GraphSONSerializersV4.PathJacksonDeserializer());
addDeserializer(VertexProperty.class, new GraphSONSerializersV4.VertexPropertyJacksonDeserializer());
addDeserializer(Tree.class, new GraphSONSerializersV4.TreeJacksonDeserializer());
+ pdtDeserializer = new PdtGraphSONSerializersV4.ProviderDefinedTypeJacksonDeserializer();
+ addDeserializer(ProviderDefinedType.class, pdtDeserializer);
// java.util - use the standard jackson serializers for collections when types aren't embedded
if (typeInfo != TypeInfo.NO_TYPES) {
@@ -232,6 +240,10 @@ abstract class GraphSONModule extends TinkerPopJacksonModule {
return new Builder();
}
+ void setPdtRegistry(final ProviderDefinedTypeRegistry registry) {
+ pdtDeserializer.setRegistry(registry);
+ }
+
@Override
public Map<Class, String> getTypeDefinitions() {
return TYPE_DEFINITIONS;
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java
new file mode 100644
index 0000000000..fb4cd848a3
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.graphson;
+
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
+import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator;
+import org.apache.tinkerpop.shaded.jackson.core.JsonParser;
+import org.apache.tinkerpop.shaded.jackson.core.JsonToken;
+import org.apache.tinkerpop.shaded.jackson.databind.DeserializationContext;
+import org.apache.tinkerpop.shaded.jackson.databind.SerializerProvider;
+import org.apache.tinkerpop.shaded.jackson.databind.deser.std.StdDeserializer;
+import org.apache.tinkerpop.shaded.jackson.databind.ser.std.StdScalarSerializer;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * GraphSON V4 serializers for {@link ProviderDefinedType}.
+ */
+final class PdtGraphSONSerializersV4 {
+
+ private PdtGraphSONSerializersV4() {
+ }
+
+ final static class ProviderDefinedTypeJacksonSerializer extends StdScalarSerializer<ProviderDefinedType> {
+
+ public ProviderDefinedTypeJacksonSerializer() {
+ super(ProviderDefinedType.class);
+ }
+
+ @Override
+ public void serialize(final ProviderDefinedType pdt, final JsonGenerator jsonGenerator,
+ final SerializerProvider serializerProvider) throws IOException {
+ jsonGenerator.writeStartObject();
+ jsonGenerator.writeStringField("type", pdt.getName());
+ jsonGenerator.writeFieldName("fields");
+ jsonGenerator.writeStartObject();
+ for (final Map.Entry<String, Object> entry : pdt.getProperties().entrySet()) {
+ jsonGenerator.writeFieldName(entry.getKey());
+ jsonGenerator.writeObject(entry.getValue());
+ }
+ jsonGenerator.writeEndObject();
+ jsonGenerator.writeEndObject();
+ }
+ }
+
+ static class ProviderDefinedTypeJacksonDeserializer extends StdDeserializer<ProviderDefinedType> {
+
+ private ProviderDefinedTypeRegistry registry;
+
+ public ProviderDefinedTypeJacksonDeserializer() {
+ super(ProviderDefinedType.class);
+ }
+
+ void setRegistry(final ProviderDefinedTypeRegistry registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public ProviderDefinedType deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext)
+ throws IOException {
+ String typeName = null;
+ Map<String, Object> fields = new LinkedHashMap<>();
+
+ while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+ final String fieldName = jsonParser.getCurrentName();
+ if ("type".equals(fieldName)) {
+ jsonParser.nextToken();
+ typeName = jsonParser.getText();
+ } else if ("fields".equals(fieldName)) {
+ jsonParser.nextToken();
+ while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+ final String key = jsonParser.getCurrentName();
+ jsonParser.nextToken();
+ final Object value = deserializationContext.readValue(jsonParser, Object.class);
+ fields.put(key, value);
+ }
+ }
+ }
+
+ final ProviderDefinedType pdt = new ProviderDefinedType(typeName, fields);
+ if (registry != null) {
+ final Object hydrated = registry.hydrate(pdt);
+ if (hydrated instanceof ProviderDefinedType)
+ return (ProviderDefinedType) hydrated;
+ // Store hydrated object back as a single-entry PDT so the typed result is accessible.
+ // This preserves the return type contract while enabling hydration.
+ return pdt.withHydrated(hydrated);
+ }
+ return pdt;
+ }
+
+ @Override
+ public boolean isCachable() {
+ return true;
+ }
+ }
+}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefined.java
similarity index 58%
copy from gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
copy to gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefined.java
index a54580cab0..5161192376 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefined.java
@@ -16,17 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+package org.apache.tinkerpop.gremlin.structure.io.pdt;
-import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
/**
- * Represents a serializer for a custom (provider specific) serializer.
- * @param <T>
+ * Marks a class as a provider-defined type for serialization via {@link ProviderDefinedType}.
*/
-public interface CustomTypeSerializer<T> extends TypeSerializer<T> {
- /**
- * Gets the custom type name.
- */
- String getTypeName();
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ProviderDefined {
+ String name() default "";
+ String[] includedFields() default {};
+ String[] excludedFields() default {};
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedType.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedType.java
new file mode 100644
index 0000000000..74e24529d0
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedType.java
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.pdt;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An immutable representation of a provider-defined type consisting of a name and a map of properties.
+ */
+public final class ProviderDefinedType {
+
+ private static final ConcurrentHashMap<Class<?>, FieldCache> FIELD_CACHE = new ConcurrentHashMap<>();
+
+ private final String name;
+ private final Map<String, Object> properties;
+ private Object hydrated;
+
+ public ProviderDefinedType(final String name, final Map<String, Object> properties) {
+ if (name == null || name.isEmpty())
+ throw new IllegalArgumentException("name cannot be null or empty");
+ if (properties == null)
+ throw new IllegalArgumentException("properties cannot be null");
+ this.name = name;
+ this.properties = Collections.unmodifiableMap(new LinkedHashMap<>(properties));
+ }
+
+ /**
+ * Creates a {@code ProviderDefinedType} from an object annotated with {@link ProviderDefined}.
+ */
+ public static ProviderDefinedType from(final Object obj) {
+ if (obj == null)
+ throw new IllegalArgumentException("obj cannot be null");
+
+ final Class<?> clazz = obj.getClass();
+ final FieldCache cache = FIELD_CACHE.computeIfAbsent(clazz, ProviderDefinedType::buildCache);
+
+ final Map<String, Object> props = new LinkedHashMap<>();
+ for (final Field field : cache.fields) {
+ try {
+ props.put(field.getName(), field.get(obj));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to read field '" + field.getName() + "' from " + clazz.getName(), e);
+ }
+ }
+
+ return new ProviderDefinedType(cache.name, props);
+ }
+
+ private static FieldCache buildCache(final Class<?> clazz) {
+ final ProviderDefined annotation = clazz.getAnnotation(ProviderDefined.class);
+ if (annotation == null)
+ throw new IllegalArgumentException(clazz.getName() + " is not annotated with @ProviderDefined");
+
+ final String typeName = annotation.name().isEmpty() ? clazz.getSimpleName() : annotation.name();
+ final String[] included = annotation.includedFields();
+ final String[] excluded = annotation.excludedFields();
+
+ if (included.length > 0 && excluded.length > 0)
+ throw new IllegalArgumentException("@ProviderDefined cannot specify both includedFields and excludedFields");
+
+ final Set<String> includedSet = included.length > 0 ? new HashSet<>(Arrays.asList(included)) : null;
+ final Set<String> excludedSet = excluded.length > 0 ? new HashSet<>(Arrays.asList(excluded)) : Collections.emptySet();
+
+ final Field[] allFields = getAllFields(clazz).toArray(new Field[0]);
+ final Field[] filtered = Arrays.stream(allFields)
+ .filter(f -> !f.isSynthetic())
+ .filter(f -> {
+ if (includedSet != null) return includedSet.contains(f.getName());
+ return !excludedSet.contains(f.getName());
+ })
+ .peek(f -> f.setAccessible(true))
+ .toArray(Field[]::new);
+
+ return new FieldCache(typeName, filtered);
+ }
+
+ private static List<Field> getAllFields(final Class<?> clazz) {
+ final List<Field> fields = new ArrayList<>();
+ Class<?> current = clazz;
+ while (current != null && current != Object.class) {
+ fields.addAll(Arrays.asList(current.getDeclaredFields()));
+ current = current.getSuperclass();
+ }
+ return fields;
+ }
+
+ private static class FieldCache {
+ final String name;
+ final Field[] fields;
+
+ FieldCache(final String name, final Field[] fields) {
+ this.name = name;
+ this.fields = fields;
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map<String, Object> getProperties() {
+ return properties;
+ }
+
+ /**
+ * Returns a copy of this PDT with the hydrated object attached.
+ */
+ public ProviderDefinedType withHydrated(final Object hydrated) {
+ final ProviderDefinedType copy = new ProviderDefinedType(this.name, this.properties);
+ copy.hydrated = hydrated;
+ return copy;
+ }
+
+ /**
+ * Returns the hydrated object if this PDT was hydrated by a {@link ProviderDefinedTypeRegistry}, or {@code null}.
+ */
+ public Object getHydrated() {
+ return hydrated;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ProviderDefinedType)) return false;
+ final ProviderDefinedType that = (ProviderDefinedType) o;
+ return name.equals(that.name) && properties.equals(that.properties);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, properties);
+ }
+
+ @Override
+ public String toString() {
+ return "pdt[" + name + "]" + properties;
+ }
+}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java
similarity index 68%
copy from gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
copy to gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java
index a54580cab0..86880a0584 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java
@@ -16,17 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+package org.apache.tinkerpop.gremlin.structure.io.pdt;
-import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
+import java.util.Map;
/**
- * Represents a serializer for a custom (provider specific) serializer.
- * @param <T>
+ * Adapter for converting between a typed object and a {@link ProviderDefinedType} property map.
*/
-public interface CustomTypeSerializer<T> extends TypeSerializer<T> {
- /**
- * Gets the custom type name.
- */
- String getTypeName();
+public interface ProviderDefinedTypeAdapter<T> {
+ String typeName();
+ Class<T> targetClass();
+ Map<String, Object> toProperties(T obj);
+ T fromProperties(Map<String, Object> properties);
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeRegistry.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeRegistry.java
new file mode 100644
index 0000000000..f0a756ad9f
--- /dev/null
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeRegistry.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.pdt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Registry for {@link ProviderDefinedTypeAdapter} instances that supports hydration of
+ * {@link ProviderDefinedType} values into typed objects.
+ */
+public final class ProviderDefinedTypeRegistry {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProviderDefinedTypeRegistry.class);
+
+ private final Map<String, ProviderDefinedTypeAdapter<?>> adaptersByName = new ConcurrentHashMap<>();
+ private final Map<Class<?>, ProviderDefinedTypeAdapter<?>> adaptersByClass = new ConcurrentHashMap<>();
+
+ private ProviderDefinedTypeRegistry() {}
+
+ /**
+ * Creates a registry populated via {@link ServiceLoader} discovery.
+ */
+ @SuppressWarnings("rawtypes")
+ public static ProviderDefinedTypeRegistry build() {
+ final ProviderDefinedTypeRegistry registry = new ProviderDefinedTypeRegistry();
+ for (final ProviderDefinedTypeAdapter adapter : ServiceLoader.load(ProviderDefinedTypeAdapter.class)) {
+ registry.register(adapter);
+ }
+ return registry;
+ }
+
+ /**
+ * Creates an empty registry for manual registration.
+ */
+ public static ProviderDefinedTypeRegistry empty() {
+ return new ProviderDefinedTypeRegistry();
+ }
+
+ public void register(final ProviderDefinedTypeAdapter<?> adapter) {
+ adaptersByName.put(adapter.typeName(), adapter);
+ adaptersByClass.put(adapter.targetClass(), adapter);
+ }
+
+ public Optional<ProviderDefinedTypeAdapter<?>> getAdapterByName(final String name) {
+ return Optional.ofNullable(adaptersByName.get(name));
+ }
+
+ public Optional<ProviderDefinedTypeAdapter<?>> getAdapterByClass(final Class<?> clazz) {
+ return Optional.ofNullable(adaptersByClass.get(clazz));
+ }
+
+ /**
+ * Attempts to hydrate a {@link ProviderDefinedType} into a typed object using a registered adapter.
+ * Recursively hydrates nested PDT values in the properties map (including those inside Lists, Sets,
+ * and Maps) before calling the adapter.
+ * Returns the original PDT if no adapter is found or if the adapter throws an exception.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public Object hydrate(final ProviderDefinedType pdt) {
+ final ProviderDefinedTypeAdapter adapter = adaptersByName.get(pdt.getName());
+ if (adapter == null)
+ return pdt;
+
+ // recursively hydrate nested PDTs in the properties map
+ final Map<String, Object> hydrated = new LinkedHashMap<>();
+ for (final Map.Entry<String, Object> entry : pdt.getProperties().entrySet()) {
+ hydrated.put(entry.getKey(), hydrateValue(entry.getValue()));
+ }
+
+ try {
+ return adapter.fromProperties(hydrated);
+ } catch (final Exception e) {
+ logger.warn("Failed to hydrate ProviderDefinedType '{}', returning raw PDT: {}",
+ pdt.getName(), e.getMessage());
+ return pdt;
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private Object hydrateValue(final Object value) {
+ if (value instanceof ProviderDefinedType)
+ return hydrate((ProviderDefinedType) value);
+ if (value instanceof List) {
+ final List<Object> result = new ArrayList<>();
+ for (final Object item : (List<?>) value)
+ result.add(hydrateValue(item));
+ return result;
+ }
+ if (value instanceof Set) {
+ final Set<Object> result = new LinkedHashSet<>();
+ for (final Object item : (Set<?>) value)
+ result.add(hydrateValue(item));
+ return result;
+ }
+ if (value instanceof Map) {
+ final Map<Object, Object> result = new LinkedHashMap<>();
+ for (final Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+ result.put(entry.getKey(), hydrateValue(entry.getValue()));
+ return result;
+ }
+ return value;
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
index 71f4acce19..953565679b 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java
@@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.GType;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
@@ -1049,4 +1050,44 @@ public class GeneralLiteralVisitorTest {
}
}
}
+
+ public static class ValidPdtLiteralTest {
+ @Test
+ public void shouldParsePdtLiteral() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("PDT(\"MyType\",[\"x\":1,\"y\":\"hello\"])"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.PdtLiteralContext ctx = parser.pdtLiteral();
+ final Object result = new GenericLiteralVisitor(new GremlinAntlrToJava()).visitPdtLiteral(ctx);
+ assertThat(result, instanceOf(ProviderDefinedType.class));
+ final ProviderDefinedType pdt = (ProviderDefinedType) result;
+ assertEquals("MyType", pdt.getName());
+ assertEquals(1, pdt.getProperties().get("x"));
+ assertEquals("hello", pdt.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldParsePdtLiteralWithEmptyMap() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("PDT(\"Empty\",[:])"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.PdtLiteralContext ctx = parser.pdtLiteral();
+ final Object result = new GenericLiteralVisitor(new GremlinAntlrToJava()).visitPdtLiteral(ctx);
+ assertThat(result, instanceOf(ProviderDefinedType.class));
+ final ProviderDefinedType pdt = (ProviderDefinedType) result;
+ assertEquals("Empty", pdt.getName());
+ assertTrue(pdt.getProperties().isEmpty());
+ }
+
+ @Test
+ public void shouldRejectNonStringMapKey() {
+ final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("PDT(\"Bad\",[1:\"value\"])"));
+ final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer));
+ final GremlinParser.PdtLiteralContext ctx = parser.pdtLiteral();
+ try {
+ new GenericLiteralVisitor(new GremlinAntlrToJava()).visitPdtLiteral(ctx);
+ fail("Expected IllegalArgumentException for non-String map key");
+ } catch (final IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("PDT properties map must have String keys, found: java.lang.Integer"));
+ }
+ }
+ }
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java
index 8e13823450..42ee0aaaab 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java
@@ -1480,6 +1480,15 @@ public class GremlinTranslatorTest {
"g.inject(ByteBuffer.wrap(Base64.getDecoder().decode(\"AQID\")))",
"g.inject(Buffer.from(\"AQID\",'base64'))",
"g.inject(base64.b64decode('AQID'))"},
+ {"g.inject(PDT(\"Point\",[\"x\":1,\"y\":2]))",
+ null,
+ "g.inject(providerdefinedtype0)",
+ "g.Inject<object>(new ProviderDefinedType(\"Point\", new Dictionary<object, object> {{ \"x\", 1 }, { \"y\", 2 }}))",
+ "g.Inject(&gremlingo.ProviderDefinedType{Name: \"Point\", Properties: map[interface{}]interface{}{\"x\": 1, \"y\": 2 }})",
+ "g.inject(new ProviderDefinedType(\"Point\", [\"x\":1, \"y\":2]))",
+ "g.inject(new ProviderDefinedType(\"Point\", new LinkedHashMap<Object, Object>() {{ put(\"x\", 1); put(\"y\", 2); }}))",
+ "g.inject(new ProviderDefinedType(\"Point\", new Map([[\"x\", 1], [\"y\", 2]])))",
+ "g.inject(ProviderDefinedType('Point', { 'x': 1, 'y': 2 }))"},
});
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
index 57649b697d..e181393de7 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java
@@ -28,6 +28,7 @@ import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge;
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex;
import org.apache.tinkerpop.gremlin.util.DatetimeHelper;
@@ -140,6 +141,19 @@ public class GremlinLangTest {
{g.inject(new byte[]{1, 2, 3}), "g.inject(Binary(\"AQID\"))"},
{g.inject(new byte[]{}), "g.inject(Binary(\"\"))"},
{g.inject(new byte[]{0}), "g.inject(Binary(\"AA==\"))"},
+ // PDT
+ {g.inject(new ProviderDefinedType("MyType", asMap("x", 1, "y", "hello"))),
+ "g.inject(PDT(\"MyType\",[\"x\":1,\"y\":\"hello\"]))"},
+ {g.inject(new ProviderDefinedType("Empty", Collections.emptyMap())),
+ "g.inject(PDT(\"Empty\",[:]))"},
+ // PDT with special characters in name
+ {g.inject(new ProviderDefinedType("say\"hello\"", asMap("v", 1))),
+ "g.inject(PDT(\"say\\\"hello\\\"\",[\"v\":1]))"},
+ {g.inject(new ProviderDefinedType("back\\slash", asMap("v", 1))),
+ "g.inject(PDT(\"back\\\\slash\",[\"v\":1]))"},
+ // Nested PDT
+ {g.inject(new ProviderDefinedType("Outer", asMap("inner", new ProviderDefinedType("Inner", asMap("v", 1))))),
+ "g.inject(PDT(\"Outer\",[\"inner\":PDT(\"Inner\",[\"v\":1])]))"},
});
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriterPdtTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriterPdtTest.java
new file mode 100644
index 0000000000..863e2559da
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/GraphBinaryWriterPdtTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.binary;
+
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class GraphBinaryWriterPdtTest {
+
+ private static final GraphBinaryReader reader = new GraphBinaryReader();
+ private static final GraphBinaryWriter writer = new GraphBinaryWriter();
+
+ @ProviderDefined
+ static class TestPoint {
+ int x;
+ int y;
+
+ TestPoint(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ static class UnannotatedType {
+ int value = 1;
+ }
+
+ @Test
+ public void shouldAutoConvertAnnotatedObjectToPdt() throws IOException {
+ final Buffer buffer = HeapBuffer.allocate(1024);
+ writer.write(new TestPoint(1, 2), buffer);
+ buffer.readerIndex(0);
+
+ final ProviderDefinedType result = reader.read(buffer);
+ assertEquals("TestPoint", result.getName());
+ assertEquals(1, result.getProperties().get("x"));
+ assertEquals(2, result.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldThrowActionableMessageForUnannotatedType() {
+ final Buffer buffer = HeapBuffer.allocate(1024);
+ final IOException ex = assertThrows(IOException.class, () -> writer.write(new UnannotatedType(), buffer));
+ assertTrue(ex.getMessage().contains("@ProviderDefined"));
+ assertTrue(ex.getMessage().contains("UnannotatedType"));
+ }
+
+ @Test
+ public void shouldNotDoubleWrapProviderDefinedType() throws IOException {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ props.put("y", 2);
+ final ProviderDefinedType pdt = new ProviderDefinedType("TestPoint", props);
+
+ final Buffer buffer = HeapBuffer.allocate(1024);
+ writer.write(pdt, buffer);
+ buffer.readerIndex(0);
+
+ final ProviderDefinedType result = reader.read(buffer);
+ assertEquals(pdt, result);
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/HeapBuffer.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/HeapBuffer.java
new file mode 100644
index 0000000000..e7f090cd1b
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/HeapBuffer.java
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.binary;
+
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * A simple heap-based {@link Buffer} implementation for unit testing in gremlin-core.
+ */
+public class HeapBuffer implements Buffer {
+ private byte[] data;
+ private int readerIndex;
+ private int writerIndex;
+ private int markedWriterIndex;
+
+ public HeapBuffer(final int initialCapacity) {
+ this.data = new byte[initialCapacity];
+ }
+
+ public static HeapBuffer allocate(final int capacity) {
+ return new HeapBuffer(capacity);
+ }
+
+ private void ensureCapacity(final int needed) {
+ if (writerIndex + needed > data.length) {
+ data = Arrays.copyOf(data, Math.max(data.length * 2, writerIndex + needed));
+ }
+ }
+
+ @Override public int readableBytes() { return writerIndex - readerIndex; }
+ @Override public int readerIndex() { return readerIndex; }
+ @Override public Buffer readerIndex(final int readerIndex) { this.readerIndex = readerIndex; return this; }
+ @Override public int writerIndex() { return writerIndex; }
+ @Override public Buffer writerIndex(final int writerIndex) { this.writerIndex = writerIndex; return this; }
+ @Override public Buffer markWriterIndex() { this.markedWriterIndex = writerIndex; return this; }
+ @Override public Buffer resetWriterIndex() { this.writerIndex = markedWriterIndex; return this; }
+ @Override public int capacity() { return data.length; }
+ @Override public boolean isDirect() { return false; }
+
+ @Override
+ public boolean readBoolean() { return readByte() != 0; }
+
+ @Override
+ public byte readByte() { return data[readerIndex++]; }
+
+ @Override
+ public short readShort() {
+ short v = (short) ((data[readerIndex] & 0xFF) << 8 | (data[readerIndex + 1] & 0xFF));
+ readerIndex += 2;
+ return v;
+ }
+
+ @Override
+ public int readInt() {
+ int v = (data[readerIndex] & 0xFF) << 24 | (data[readerIndex + 1] & 0xFF) << 16 |
+ (data[readerIndex + 2] & 0xFF) << 8 | (data[readerIndex + 3] & 0xFF);
+ readerIndex += 4;
+ return v;
+ }
+
+ @Override
+ public long readLong() {
+ long v = ((long)(data[readerIndex] & 0xFF) << 56) | ((long)(data[readerIndex+1] & 0xFF) << 48) |
+ ((long)(data[readerIndex+2] & 0xFF) << 40) | ((long)(data[readerIndex+3] & 0xFF) << 32) |
+ ((long)(data[readerIndex+4] & 0xFF) << 24) | ((long)(data[readerIndex+5] & 0xFF) << 16) |
+ ((long)(data[readerIndex+6] & 0xFF) << 8) | ((long)(data[readerIndex+7] & 0xFF));
+ readerIndex += 8;
+ return v;
+ }
+
+ @Override
+ public float readFloat() { return Float.intBitsToFloat(readInt()); }
+
+ @Override
+ public double readDouble() { return Double.longBitsToDouble(readLong()); }
+
+ @Override
+ public Buffer readBytes(final byte[] destination) {
+ System.arraycopy(data, readerIndex, destination, 0, destination.length);
+ readerIndex += destination.length;
+ return this;
+ }
+
+ @Override
+ public Buffer readBytes(final byte[] destination, final int dstIndex, final int length) {
+ System.arraycopy(data, readerIndex, destination, dstIndex, length);
+ readerIndex += length;
+ return this;
+ }
+
+ @Override
+ public Buffer readBytes(final ByteBuffer dst) {
+ int len = dst.remaining();
+ dst.put(data, readerIndex, len);
+ readerIndex += len;
+ return this;
+ }
+
+ @Override
+ public Buffer readBytes(final OutputStream out, final int length) throws IOException {
+ out.write(data, readerIndex, length);
+ readerIndex += length;
+ return this;
+ }
+
+ @Override
+ public Buffer writeBoolean(final boolean value) { return writeByte(value ? 1 : 0); }
+
+ @Override
+ public Buffer writeByte(final int value) {
+ ensureCapacity(1);
+ data[writerIndex++] = (byte) value;
+ return this;
+ }
+
+ @Override
+ public Buffer writeShort(final int value) {
+ ensureCapacity(2);
+ data[writerIndex++] = (byte) (value >>> 8);
+ data[writerIndex++] = (byte) value;
+ return this;
+ }
+
+ @Override
+ public Buffer writeInt(final int value) {
+ ensureCapacity(4);
+ data[writerIndex++] = (byte) (value >>> 24);
+ data[writerIndex++] = (byte) (value >>> 16);
+ data[writerIndex++] = (byte) (value >>> 8);
+ data[writerIndex++] = (byte) value;
+ return this;
+ }
+
+ @Override
+ public Buffer writeLong(final long value) {
+ ensureCapacity(8);
+ data[writerIndex++] = (byte) (value >>> 56);
+ data[writerIndex++] = (byte) (value >>> 48);
+ data[writerIndex++] = (byte) (value >>> 40);
+ data[writerIndex++] = (byte) (value >>> 32);
+ data[writerIndex++] = (byte) (value >>> 24);
+ data[writerIndex++] = (byte) (value >>> 16);
+ data[writerIndex++] = (byte) (value >>> 8);
+ data[writerIndex++] = (byte) value;
+ return this;
+ }
+
+ @Override
+ public Buffer writeFloat(final float value) { return writeInt(Float.floatToIntBits(value)); }
+
+ @Override
+ public Buffer writeDouble(final double value) { return writeLong(Double.doubleToLongBits(value)); }
+
+ @Override
+ public Buffer writeBytes(final byte[] src) {
+ ensureCapacity(src.length);
+ System.arraycopy(src, 0, data, writerIndex, src.length);
+ writerIndex += src.length;
+ return this;
+ }
+
+ @Override
+ public Buffer writeBytes(final ByteBuffer src) {
+ int len = src.remaining();
+ ensureCapacity(len);
+ src.get(data, writerIndex, len);
+ writerIndex += len;
+ return this;
+ }
+
+ @Override
+ public Buffer writeBytes(final byte[] src, final int srcIndex, final int length) {
+ ensureCapacity(length);
+ System.arraycopy(src, srcIndex, data, writerIndex, length);
+ writerIndex += length;
+ return this;
+ }
+
+ @Override public boolean release() { return true; }
+ @Override public Buffer retain() { return this; }
+ @Override public int referenceCount() { return 1; }
+ @Override public int nioBufferCount() { return 1; }
+
+ @Override
+ public ByteBuffer[] nioBuffers() {
+ return new ByteBuffer[] { nioBuffer() };
+ }
+
+ @Override
+ public ByteBuffer[] nioBuffers(final int index, final int length) {
+ return new ByteBuffer[] { nioBuffer(index, length) };
+ }
+
+ @Override
+ public ByteBuffer nioBuffer() {
+ return ByteBuffer.wrap(data, readerIndex, readableBytes()).slice();
+ }
+
+ @Override
+ public ByteBuffer nioBuffer(final int index, final int length) {
+ return ByteBuffer.wrap(data, index, length).slice();
+ }
+
+ @Override
+ public Buffer getBytes(final int index, final byte[] dst) {
+ System.arraycopy(data, index, dst, 0, dst.length);
+ return this;
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ProviderDefinedTypeSerializerTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ProviderDefinedTypeSerializerTest.java
new file mode 100644
index 0000000000..76f373609d
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ProviderDefinedTypeSerializerTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.structure.io.binary.HeapBuffer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class ProviderDefinedTypeSerializerTest {
+
+ private static final GraphBinaryReader reader = new GraphBinaryReader();
+ private static final GraphBinaryWriter writer = new GraphBinaryWriter();
+
+ private Buffer writeAndRead(final Object value) throws IOException {
+ final Buffer buffer = HeapBuffer.allocate(1024);
+ writer.write(value, buffer);
+ buffer.readerIndex(0);
+ return buffer;
+ }
+
+ @Test
+ public void shouldRoundTripSimplePdt() throws IOException {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ props.put("y", "hello");
+ final ProviderDefinedType pdt = new ProviderDefinedType("com.example.Point", props);
+
+ final Buffer buffer = writeAndRead(pdt);
+ final ProviderDefinedType result = reader.read(buffer);
+
+ assertEquals(pdt, result);
+ }
+
+ @Test
+ public void shouldRoundTripPdtWithNullPropertyValue() throws IOException {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("name", "test");
+ props.put("value", null);
+ final ProviderDefinedType pdt = new ProviderDefinedType("com.example.Nullable", props);
+
+ final Buffer buffer = writeAndRead(pdt);
+ final ProviderDefinedType result = reader.read(buffer);
+
+ assertEquals(pdt, result);
+ }
+
+ @Test
+ public void shouldRoundTripNestedPdt() throws IOException {
+ final Map<String, Object> innerProps = new LinkedHashMap<>();
+ innerProps.put("street", "123 Main");
+ final ProviderDefinedType inner = new ProviderDefinedType("com.example.Address", innerProps);
+
+ final Map<String, Object> outerProps = new LinkedHashMap<>();
+ outerProps.put("name", "Alice");
+ outerProps.put("address", inner);
+ final ProviderDefinedType outer = new ProviderDefinedType("com.example.Person", outerProps);
+
+ final Buffer buffer = writeAndRead(outer);
+ final ProviderDefinedType result = reader.read(buffer);
+
+ assertEquals(outer, result);
+ }
+
+ @Test
+ public void shouldRoundTripPdtInsideList() throws IOException {
+ final Map<String, Object> props = Collections.singletonMap("id", 42);
+ final ProviderDefinedType pdt = new ProviderDefinedType("com.example.Item", props);
+ final List<Object> list = Arrays.asList(pdt, "other");
+
+ final Buffer buffer = writeAndRead(list);
+ final List<Object> result = reader.read(buffer);
+
+ assertEquals(list, result);
+ }
+
+ @Test
+ public void shouldRoundTripPdtInsideMapValue() throws IOException {
+ final Map<String, Object> props = Collections.singletonMap("val", 99L);
+ final ProviderDefinedType pdt = new ProviderDefinedType("com.example.Wrapper", props);
+ final Map<String, Object> map = new HashMap<>();
+ map.put("key", pdt);
+
+ final Buffer buffer = writeAndRead(map);
+ final Map<String, Object> result = reader.read(buffer);
+
+ assertEquals(map, result);
+ }
+
+ @Test(expected = IOException.class)
+ public void shouldThrowOnEmptyNameDuringRead() throws IOException {
+ // Manually write a PDT with empty name to trigger the validation
+ final Buffer buffer = HeapBuffer.allocate(256);
+ // Write type code for COMPOSITE_PDT
+ buffer.writeByte(0xF0);
+ // Write value_flag = 0 (not null)
+ buffer.writeByte(0x00);
+ // Write a fully-qualified empty string: type_code STRING (0x03), value_flag 0, length 0
+ buffer.writeByte(0x03);
+ buffer.writeByte(0x00);
+ buffer.writeInt(0);
+ // Write a fully-qualified map: type_code MAP (0x0A), value_flag 0, length 0
+ buffer.writeByte(0x0A);
+ buffer.writeByte(0x00);
+ buffer.writeInt(0);
+
+ buffer.readerIndex(0);
+ reader.read(buffer);
+ }
+
+ @Test(expected = IOException.class)
+ public void shouldThrowOnNonStringKeyInPropertiesMap() throws IOException {
+ final Buffer buffer = HeapBuffer.allocate(256);
+ // Write type code for COMPOSITE_PDT (0xF0), value_flag 0
+ buffer.writeByte(0xF0);
+ buffer.writeByte(0x00);
+ // Write fully-qualified String name: type STRING (0x03), flag 0, length 4, "test"
+ buffer.writeByte(0x03);
+ buffer.writeByte(0x00);
+ buffer.writeInt(4);
+ buffer.writeBytes(new byte[]{'t', 'e', 's', 't'});
+ // Write fully-qualified Map: type MAP (0x0A), flag 0, length 1 (one entry)
+ buffer.writeByte(0x0A);
+ buffer.writeByte(0x00);
+ buffer.writeInt(1);
+ // Key: INT type (0x01), flag 0, value 42
+ buffer.writeByte(0x01);
+ buffer.writeByte(0x00);
+ buffer.writeInt(42);
+ // Value: STRING type (0x03), flag 0, length 3, "val"
+ buffer.writeByte(0x03);
+ buffer.writeByte(0x00);
+ buffer.writeInt(3);
+ buffer.writeBytes(new byte[]{'v', 'a', 'l'});
+
+ buffer.readerIndex(0);
+ reader.read(buffer);
+ }
+
+ @Test
+ public void shouldHandleNullPdt() throws IOException {
+ final Buffer buffer = HeapBuffer.allocate(64);
+ writer.write(null, buffer);
+ buffer.readerIndex(0);
+ final Object result = reader.read(buffer);
+ assertNull(result);
+ }
+
+ @Test
+ public void shouldAutoHydrateWhenRegistryConfigured() throws IOException {
+ final ProviderDefinedTypeRegistry pdtRegistry = ProviderDefinedTypeRegistry.empty();
+ pdtRegistry.register(new ProviderDefinedTypeAdapter<Map<String, Object>>() {
+ @Override
+ public String typeName() { return "com.example.Point"; }
+
+ @Override
+ public Class<Map<String, Object>> targetClass() { return (Class) Map.class; }
+
+ @Override
+ public Map<String, Object> fromProperties(final Map<String, Object> properties) {
+ final Map<String, Object> result = new LinkedHashMap<>(properties);
+ result.put("hydrated", true);
+ return result;
+ }
+
+ @Override
+ public Map<String, Object> toProperties(final Map<String, Object> value) { return value; }
+ });
+
+ final GraphBinaryReader hydratingReader = new GraphBinaryReader(
+ org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry.INSTANCE, pdtRegistry);
+
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ props.put("y", 2);
+ final ProviderDefinedType pdt = new ProviderDefinedType("com.example.Point", props);
+
+ final Buffer buffer = writeAndRead(pdt);
+ final Object result = hydratingReader.read(buffer);
+
+ // Should be the hydrated map, not a ProviderDefinedType
+ assertEquals(true, ((Map<?, ?>) result).get("hydrated"));
+ assertEquals(1, ((Map<?, ?>) result).get("x"));
+ assertEquals(2, ((Map<?, ?>) result).get("y"));
+ }
+
+ @Test
+ public void shouldNotHydrateWhenNoRegistryConfigured() throws IOException {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("com.example.Point", props);
+
+ final Buffer buffer = writeAndRead(pdt);
+ final ProviderDefinedType result = reader.read(buffer);
+
+ assertEquals(pdt, result);
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java
new file mode 100644
index 0000000000..67786dbcd8
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializersV4Test.java
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.graphson;
+
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
+import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
+import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link PdtGraphSONSerializersV4}.
+ */
+public class PdtGraphSONSerializersV4Test extends AbstractGraphSONTest {
+
+ private ObjectMapper mapper;
+ private ObjectMapper plainMapper;
+
+ @Before
+ public void setUp() {
+ mapper = GraphSONMapper.build()
+ .version(GraphSONVersion.V4_0)
+ .addCustomModule(GraphSONXModuleV4.build())
+ .typeInfo(TypeInfo.PARTIAL_TYPES)
+ .create().createMapper();
+ plainMapper = new ObjectMapper();
+ }
+
+ @Test
+ public void shouldSerializeSimplePdt() throws Exception {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ props.put("y", 2);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+
+ final String json = mapper.writeValueAsString(pdt);
+ final JsonNode node = plainMapper.readTree(json);
+
+ assertEquals("g:CompositePdt", node.get("@type").asText());
+ final JsonNode value = node.get("@value");
+ assertEquals("Point", value.get("type").asText());
+
+ final JsonNode fields = value.get("fields");
+ assertEquals("g:Int32", fields.get("x").get("@type").asText());
+ assertEquals(1, fields.get("x").get("@value").asInt());
+ assertEquals("g:Int32", fields.get("y").get("@type").asText());
+ assertEquals(2, fields.get("y").get("@value").asInt());
+ }
+
+ @Test
+ public void shouldDeserializeValidJson() throws Exception {
+ final String json = "{\"@type\":\"g:CompositePdt\",\"@value\":{\"type\":\"Point\",\"fields\":{\"x\":{\"@type\":\"g:Int32\",\"@value\":1},\"y\":{\"@type\":\"g:Int32\",\"@value\":2}}}}";
+ final ProviderDefinedType pdt = mapper.readValue(json, ProviderDefinedType.class);
+
+ assertEquals("Point", pdt.getName());
+ assertEquals(2, pdt.getProperties().size());
+ assertEquals(1, pdt.getProperties().get("x"));
+ assertEquals(2, pdt.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldRoundTrip() throws Exception {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ props.put("y", 2);
+ final ProviderDefinedType original = new ProviderDefinedType("Point", props);
+
+ final ProviderDefinedType result = serializeDeserialize(mapper, original, ProviderDefinedType.class);
+
+ assertEquals(original.getName(), result.getName());
+ assertEquals(original.getProperties(), result.getProperties());
+ }
+
+ @Test
+ public void shouldSerializeNestedPdt() throws Exception {
+ final Map<String, Object> innerProps = new LinkedHashMap<>();
+ innerProps.put("x", 10);
+ innerProps.put("y", 20);
+ final ProviderDefinedType inner = new ProviderDefinedType("Point", innerProps);
+
+ final Map<String, Object> outerProps = new LinkedHashMap<>();
+ outerProps.put("name", "origin");
+ outerProps.put("location", inner);
+ final ProviderDefinedType outer = new ProviderDefinedType("NamedPoint", outerProps);
+
+ final String json = mapper.writeValueAsString(outer);
+ final JsonNode node = plainMapper.readTree(json);
+
+ assertEquals("g:CompositePdt", node.get("@type").asText());
+ final JsonNode fields = node.get("@value").get("fields");
+ final JsonNode locationNode = fields.get("location");
+ assertEquals("g:CompositePdt", locationNode.get("@type").asText());
+ assertEquals("Point", locationNode.get("@value").get("type").asText());
+
+ // round-trip nested
+ final ProviderDefinedType result = serializeDeserialize(mapper, outer, ProviderDefinedType.class);
+ assertEquals("NamedPoint", result.getName());
+ assertTrue(result.getProperties().get("location") instanceof ProviderDefinedType);
+ final ProviderDefinedType nestedResult = (ProviderDefinedType) result.getProperties().get("location");
+ assertEquals("Point", nestedResult.getName());
+ assertEquals(10, nestedResult.getProperties().get("x"));
+ assertEquals(20, nestedResult.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldHandleNullFieldValues() throws Exception {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("name", "test");
+ props.put("value", null);
+ final ProviderDefinedType pdt = new ProviderDefinedType("NullableType", props);
+
+ final ProviderDefinedType result = serializeDeserialize(mapper, pdt, ProviderDefinedType.class);
+
+ assertEquals("NullableType", result.getName());
+ assertEquals("test", result.getProperties().get("name"));
+ assertNull(result.getProperties().get("value"));
+ assertTrue(result.getProperties().containsKey("value"));
+ }
+
+ // --- Hydration tests ---
+
+ static class Point {
+ final int x;
+ final int y;
+ Point(int x, int y) { this.x = x; this.y = y; }
+ }
+
+ static class PointAdapter implements ProviderDefinedTypeAdapter<Point> {
+ @Override public String typeName() { return "Point"; }
+ @Override public Class<Point> targetClass() { return Point.class; }
+ @Override public Map<String, Object> toProperties(Point obj) {
+ final Map<String, Object> m = new HashMap<>();
+ m.put("x", obj.x);
+ m.put("y", obj.y);
+ return m;
+ }
+ @Override public Point fromProperties(Map<String, Object> properties) {
+ return new Point((int) properties.get("x"), (int) properties.get("y"));
+ }
+ }
+
+ @Test
+ public void shouldHydrateWhenRegistryConfigured() throws Exception {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new PointAdapter());
+
+ final ObjectMapper hydratingMapper = GraphSONMapper.build()
+ .version(GraphSONVersion.V4_0)
+ .addCustomModule(GraphSONXModuleV4.build())
+ .typeInfo(TypeInfo.PARTIAL_TYPES)
+ .pdtRegistry(registry)
+ .create().createMapper();
+
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 3);
+ props.put("y", 7);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+
+ final ProviderDefinedType result = serializeDeserialize(hydratingMapper, pdt, ProviderDefinedType.class);
+
+ assertNotNull(result.getHydrated());
+ assertTrue(result.getHydrated() instanceof Point);
+ assertEquals(3, ((Point) result.getHydrated()).x);
+ assertEquals(7, ((Point) result.getHydrated()).y);
+ }
+
+ @Test
+ public void shouldNotHydrateWhenNoRegistryConfigured() throws Exception {
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("x", 1);
+ props.put("y", 2);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+
+ final ProviderDefinedType result = serializeDeserialize(mapper, pdt, ProviderDefinedType.class);
+
+ assertNull(result.getHydrated());
+ assertEquals("Point", result.getName());
+ assertEquals(1, result.getProperties().get("x"));
+ }
+
+ @Test
+ public void shouldReturnRawPdtWhenTypeNotRegistered() throws Exception {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ // No adapter registered for "Unknown"
+
+ final ObjectMapper hydratingMapper = GraphSONMapper.build()
+ .version(GraphSONVersion.V4_0)
+ .addCustomModule(GraphSONXModuleV4.build())
+ .typeInfo(TypeInfo.PARTIAL_TYPES)
+ .pdtRegistry(registry)
+ .create().createMapper();
+
+ final Map<String, Object> props = new LinkedHashMap<>();
+ props.put("a", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Unknown", props);
+
+ final ProviderDefinedType result = serializeDeserialize(hydratingMapper, pdt, ProviderDefinedType.class);
+
+ assertNull(result.getHydrated());
+ assertEquals("Unknown", result.getName());
+ assertEquals(1, result.getProperties().get("a"));
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeRegistryTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeRegistryTest.java
new file mode 100644
index 0000000000..1cad00557a
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeRegistryTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.pdt;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class ProviderDefinedTypeRegistryTest {
+
+ // Simple test type
+ static class Point {
+ final int x;
+ final int y;
+ Point(int x, int y) { this.x = x; this.y = y; }
+ }
+
+ static class PointAdapter implements ProviderDefinedTypeAdapter<Point> {
+ @Override public String typeName() { return "Point"; }
+ @Override public Class<Point> targetClass() { return Point.class; }
+ @Override public Map<String, Object> toProperties(Point obj) {
+ final Map<String, Object> m = new HashMap<>();
+ m.put("x", obj.x);
+ m.put("y", obj.y);
+ return m;
+ }
+ @Override public Point fromProperties(Map<String, Object> properties) {
+ return new Point((int) properties.get("x"), (int) properties.get("y"));
+ }
+ }
+
+ // Nested test type
+ static class Line {
+ final Point start;
+ final Point end;
+ Line(Point start, Point end) { this.start = start; this.end = end; }
+ }
+
+ static class LineAdapter implements ProviderDefinedTypeAdapter<Line> {
+ @Override public String typeName() { return "Line"; }
+ @Override public Class<Line> targetClass() { return Line.class; }
+ @Override public Map<String, Object> toProperties(Line obj) {
+ final Map<String, Object> m = new HashMap<>();
+ m.put("start", obj.start);
+ m.put("end", obj.end);
+ return m;
+ }
+ @Override public Line fromProperties(Map<String, Object> properties) {
+ return new Line((Point) properties.get("start"), (Point) properties.get("end"));
+ }
+ }
+
+ // Adapter that always throws
+ static class FailingAdapter implements ProviderDefinedTypeAdapter<Point> {
+ @Override public String typeName() { return "Failing"; }
+ @Override public Class<Point> targetClass() { return Point.class; }
+ @Override public Map<String, Object> toProperties(Point obj) { return new HashMap<>(); }
+ @Override public Point fromProperties(Map<String, Object> properties) {
+ throw new RuntimeException("intentional failure");
+ }
+ }
+
+ @Test
+ public void shouldHydrateSimplePdt() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new PointAdapter());
+
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 3);
+ props.put("y", 7);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+
+ final Object result = registry.hydrate(pdt);
+ assertTrue(result instanceof Point);
+ assertEquals(3, ((Point) result).x);
+ assertEquals(7, ((Point) result).y);
+ }
+
+ @Test
+ public void shouldReturnRawPdtWhenNoAdapterRegistered() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Unknown", props);
+
+ final Object result = registry.hydrate(pdt);
+ assertSame(pdt, result);
+ }
+
+ @Test
+ public void shouldHydrateNestedPdts() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new PointAdapter());
+ registry.register(new LineAdapter());
+
+ final Map<String, Object> startProps = new HashMap<>();
+ startProps.put("x", 0);
+ startProps.put("y", 0);
+ final Map<String, Object> endProps = new HashMap<>();
+ endProps.put("x", 5);
+ endProps.put("y", 5);
+
+ final Map<String, Object> lineProps = new HashMap<>();
+ lineProps.put("start", new ProviderDefinedType("Point", startProps));
+ lineProps.put("end", new ProviderDefinedType("Point", endProps));
+ final ProviderDefinedType linePdt = new ProviderDefinedType("Line", lineProps);
+
+ final Object result = registry.hydrate(linePdt);
+ assertTrue(result instanceof Line);
+ final Line line = (Line) result;
+ assertEquals(0, line.start.x);
+ assertEquals(0, line.start.y);
+ assertEquals(5, line.end.x);
+ assertEquals(5, line.end.y);
+ }
+
+ @Test
+ public void shouldPartiallyHydrateWhenInnerAdapterMissing() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new LineAdapter());
+ // Point adapter NOT registered
+
+ final Map<String, Object> startProps = new HashMap<>();
+ startProps.put("x", 1);
+ startProps.put("y", 2);
+ final ProviderDefinedType startPdt = new ProviderDefinedType("Point", startProps);
+
+ final Map<String, Object> endProps = new HashMap<>();
+ endProps.put("x", 3);
+ endProps.put("y", 4);
+ final ProviderDefinedType endPdt = new ProviderDefinedType("Point", endProps);
+
+ final Map<String, Object> lineProps = new HashMap<>();
+ lineProps.put("start", startPdt);
+ lineProps.put("end", endPdt);
+ final ProviderDefinedType linePdt = new ProviderDefinedType("Line", lineProps);
+
+ // Line adapter will receive ProviderDefinedType values for start/end since Point is not registered.
+ // The LineAdapter.fromProperties casts to Point which will throw ClassCastException,
+ // so hydrate should fall back to returning the raw PDT.
+ final Object result = registry.hydrate(linePdt);
+ assertSame(linePdt, result);
+ }
+
+ @Test
+ public void shouldFallBackWhenAdapterThrows() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new FailingAdapter());
+
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Failing", props);
+
+ // should not throw, should return raw PDT
+ final Object result = registry.hydrate(pdt);
+ assertSame(pdt, result);
+ }
+
+ @Test
+ public void shouldLookUpAdapterByClass() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ final PointAdapter adapter = new PointAdapter();
+ registry.register(adapter);
+
+ final Optional<ProviderDefinedTypeAdapter<?>> found = registry.getAdapterByClass(Point.class);
+ assertTrue(found.isPresent());
+ assertEquals("Point", found.get().typeName());
+ }
+
+ // Collection test type
+ static class Polygon {
+ final List<Point> vertices;
+ Polygon(List<Point> vertices) { this.vertices = vertices; }
+ }
+
+ static class PolygonAdapter implements ProviderDefinedTypeAdapter<Polygon> {
+ @Override public String typeName() { return "Polygon"; }
+ @Override public Class<Polygon> targetClass() { return Polygon.class; }
+ @Override public Map<String, Object> toProperties(Polygon obj) {
+ final Map<String, Object> m = new HashMap<>();
+ m.put("vertices", obj.vertices);
+ return m;
+ }
+ @SuppressWarnings("unchecked")
+ @Override public Polygon fromProperties(Map<String, Object> properties) {
+ return new Polygon((List<Point>) properties.get("vertices"));
+ }
+ }
+
+ @Test
+ public void shouldHydratePdtsInsideList() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new PointAdapter());
+ registry.register(new PolygonAdapter());
+
+ final Map<String, Object> p1 = new HashMap<>();
+ p1.put("x", 1); p1.put("y", 2);
+ final Map<String, Object> p2 = new HashMap<>();
+ p2.put("x", 3); p2.put("y", 4);
+
+ final Map<String, Object> polyProps = new HashMap<>();
+ polyProps.put("vertices", Arrays.asList(
+ new ProviderDefinedType("Point", p1),
+ new ProviderDefinedType("Point", p2)));
+ final ProviderDefinedType polyPdt = new ProviderDefinedType("Polygon", polyProps);
+
+ final Object result = registry.hydrate(polyPdt);
+ assertTrue(result instanceof Polygon);
+ final Polygon polygon = (Polygon) result;
+ assertEquals(2, polygon.vertices.size());
+ assertEquals(1, polygon.vertices.get(0).x);
+ assertEquals(2, polygon.vertices.get(0).y);
+ assertEquals(3, polygon.vertices.get(1).x);
+ assertEquals(4, polygon.vertices.get(1).y);
+ }
+
+ @Test
+ public void shouldHydratePdtsInsideMapValues() {
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new PointAdapter());
+
+ // A simple adapter that receives a map of named points
+ registry.register(new ProviderDefinedTypeAdapter<Map>() {
+ @Override public String typeName() { return "PointMap"; }
+ @Override public Class<Map> targetClass() { return Map.class; }
+ @Override public Map<String, Object> toProperties(Map obj) { return new HashMap<>(); }
+ @SuppressWarnings("unchecked")
+ @Override public Map fromProperties(Map<String, Object> properties) {
+ return (Map<String, Object>) properties.get("points");
+ }
+ });
+
+ final Map<String, Object> p1 = new HashMap<>();
+ p1.put("x", 10); p1.put("y", 20);
+ final Map<String, Object> p2 = new HashMap<>();
+ p2.put("x", 30); p2.put("y", 40);
+
+ final Map<String, Object> innerMap = new HashMap<>();
+ innerMap.put("origin", new ProviderDefinedType("Point", p1));
+ innerMap.put("target", new ProviderDefinedType("Point", p2));
+
+ final Map<String, Object> props = new HashMap<>();
+ props.put("points", innerMap);
+ final ProviderDefinedType pdt = new ProviderDefinedType("PointMap", props);
+
+ final Object result = registry.hydrate(pdt);
+ assertTrue(result instanceof Map);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> resultMap = (Map<String, Object>) result;
+ assertTrue(resultMap.get("origin") instanceof Point);
+ assertTrue(resultMap.get("target") instanceof Point);
+ assertEquals(10, ((Point) resultMap.get("origin")).x);
+ assertEquals(40, ((Point) resultMap.get("target")).y);
+ }
+
+ @Test
+ public void shouldBuildViaServiceLoader() {
+ // ServiceLoader.load will find adapters on the classpath. With no META-INF/services file
+ // in test scope, this should produce an empty registry that still functions.
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.build();
+
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Unregistered", props);
+ final Object result = registry.hydrate(pdt);
+ assertSame(pdt, result);
+ }
+}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeTest.java
new file mode 100644
index 0000000000..2c153700c8
--- /dev/null
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.structure.io.pdt;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+
+public class ProviderDefinedTypeTest {
+
+ @ProviderDefined
+ static class Point {
+ int x = 1;
+ int y = 2;
+ }
+
+ @ProviderDefined(name = "GeoPoint")
+ static class NamedPoint {
+ double lat = 45.0;
+ double lon = -93.0;
+ }
+
+ @ProviderDefined(includedFields = {"x"})
+ static class IncludedFieldsPoint {
+ int x = 10;
+ int y = 20;
+ int z = 30;
+ }
+
+ @ProviderDefined(excludedFields = {"z"})
+ static class ExcludedFieldsPoint {
+ int x = 10;
+ int y = 20;
+ int z = 30;
+ }
+
+ @ProviderDefined(includedFields = {"x"}, excludedFields = {"z"})
+ static class ConflictingFieldsPoint {
+ int x = 10;
+ int y = 20;
+ int z = 30;
+ }
+
+ @ProviderDefined
+ static class NullFieldPoint {
+ String label = null;
+ int x = 5;
+ }
+
+ static class NotAnnotated {
+ int value = 1;
+ }
+
+ static class BasePoint {
+ int x = 1;
+ int y = 2;
+ }
+
+ @ProviderDefined(name = "GeoPoint")
+ static class InheritedPoint extends BasePoint {
+ String label = "origin";
+ }
+
+ @ProviderDefined(excludedFields = {"y"})
+ static class InheritedExcluded extends BasePoint {
+ String label = "test";
+ }
+
+ @ProviderDefined(includedFields = {"x", "label"})
+ static class InheritedIncluded extends BasePoint {
+ String label = "included";
+ }
+
+ @Test
+ public void shouldConstructDirectly() {
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ props.put("y", 2);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+ assertEquals("Point", pdt.getName());
+ assertEquals(props, pdt.getProperties());
+ }
+
+ @Test
+ public void shouldBeImmutableFromInputMap() {
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+ props.put("y", 2);
+ assertEquals(1, pdt.getProperties().size());
+ }
+
+ @Test
+ public void shouldReturnUnmodifiableProperties() {
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType pdt = new ProviderDefinedType("Point", props);
+ assertThrows(UnsupportedOperationException.class, () -> pdt.getProperties().put("y", 2));
+ }
+
+ @Test
+ public void shouldCreateFromAnnotatedObject() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new Point());
+ assertEquals("Point", pdt.getName());
+ assertEquals(1, pdt.getProperties().get("x"));
+ assertEquals(2, pdt.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldUseCustomNameFromAnnotation() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new NamedPoint());
+ assertEquals("GeoPoint", pdt.getName());
+ }
+
+ @Test
+ public void shouldFilterWithIncludedFields() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new IncludedFieldsPoint());
+ assertEquals(1, pdt.getProperties().size());
+ assertEquals(10, pdt.getProperties().get("x"));
+ }
+
+ @Test
+ public void shouldFilterWithExcludedFields() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new ExcludedFieldsPoint());
+ assertEquals(2, pdt.getProperties().size());
+ assertEquals(10, pdt.getProperties().get("x"));
+ assertEquals(20, pdt.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldThrowOnNullObject() {
+ assertThrows(IllegalArgumentException.class, () -> ProviderDefinedType.from(null));
+ }
+
+ @Test
+ public void shouldThrowOnNonAnnotatedObject() {
+ assertThrows(IllegalArgumentException.class, () -> ProviderDefinedType.from(new NotAnnotated()));
+ }
+
+ @Test
+ public void shouldHaveCorrectEqualsAndHashCode() {
+ final Map<String, Object> props = new HashMap<>();
+ props.put("x", 1);
+ final ProviderDefinedType a = new ProviderDefinedType("Point", props);
+ final ProviderDefinedType b = new ProviderDefinedType("Point", props);
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+
+ final ProviderDefinedType c = new ProviderDefinedType("Other", props);
+ assertNotEquals(a, c);
+ }
+
+ @Test
+ public void shouldThrowOnNullName() {
+ assertThrows(IllegalArgumentException.class, () -> new ProviderDefinedType(null, new HashMap<>()));
+ }
+
+ @Test
+ public void shouldThrowOnEmptyName() {
+ assertThrows(IllegalArgumentException.class, () -> new ProviderDefinedType("", new HashMap<>()));
+ }
+
+ @Test
+ public void shouldThrowOnNullProperties() {
+ assertThrows(IllegalArgumentException.class, () -> new ProviderDefinedType("Point", null));
+ }
+
+ @Test
+ public void shouldPreserveNullFieldValues() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new NullFieldPoint());
+ assertEquals(2, pdt.getProperties().size());
+ assertEquals(null, pdt.getProperties().get("label"));
+ assertEquals(5, pdt.getProperties().get("x"));
+ }
+
+ @Test
+ public void shouldThrowOnConflictingIncludedAndExcludedFields() {
+ assertThrows(IllegalArgumentException.class, () -> ProviderDefinedType.from(new ConflictingFieldsPoint()));
+ }
+
+ @Test
+ public void shouldIncludeInheritedFields() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new InheritedPoint());
+ assertEquals("GeoPoint", pdt.getName());
+ assertEquals(3, pdt.getProperties().size());
+ assertEquals("origin", pdt.getProperties().get("label"));
+ assertEquals(1, pdt.getProperties().get("x"));
+ assertEquals(2, pdt.getProperties().get("y"));
+ }
+
+ @Test
+ public void shouldExcludeInheritedFields() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new InheritedExcluded());
+ assertEquals(2, pdt.getProperties().size());
+ assertEquals("test", pdt.getProperties().get("label"));
+ assertEquals(1, pdt.getProperties().get("x"));
+ }
+
+ @Test
+ public void shouldIncludeOnlySpecifiedFieldsAcrossHierarchy() {
+ final ProviderDefinedType pdt = ProviderDefinedType.from(new InheritedIncluded());
+ assertEquals(2, pdt.getProperties().size());
+ assertEquals("included", pdt.getProperties().get("label"));
+ assertEquals(1, pdt.getProperties().get("x"));
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
index 50dcbcbd27..4d65eead89 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
@@ -26,6 +26,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Gremlin.Net.Driver.Messages;
+using Gremlin.Net.Structure;
using Gremlin.Net.Structure.IO.GraphBinary4;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -63,15 +64,26 @@ namespace Gremlin.Net.Driver
/// <see cref="HttpRequestContext" /> and can modify headers, body, URI, and method
/// before the request is sent.
/// </param>
+ /// <param name="pdtRegistry">
+ /// An optional <see cref="ProviderDefinedTypeRegistry"/> for automatic hydration of
+ /// provider-defined types.
+ /// </param>
public GremlinClient(GremlinServer gremlinServer, IMessageSerializer? requestSerializer,
IMessageSerializer responseSerializer,
ConnectionSettings? connectionSettings = null,
ILoggerFactory? loggerFactory = null,
- IReadOnlyList<Func<HttpRequestContext, Task>>? interceptors = null)
+ IReadOnlyList<Func<HttpRequestContext, Task>>? interceptors = null,
+ ProviderDefinedTypeRegistry? pdtRegistry = null)
{
connectionSettings ??= new ConnectionSettings();
LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
+ if (pdtRegistry != null)
+ {
+ requestSerializer?.SetPdtRegistry(pdtRegistry);
+ responseSerializer.SetPdtRegistry(pdtRegistry);
+ }
+
_connection = new Connection(
gremlinServer.Uri,
requestSerializer,
@@ -98,14 +110,19 @@ namespace Gremlin.Net.Driver
/// <param name="interceptors">
/// An optional list of request interceptors.
/// </param>
+ /// <param name="pdtRegistry">
+ /// An optional <see cref="ProviderDefinedTypeRegistry"/> for automatic hydration of
+ /// provider-defined types.
+ /// </param>
public GremlinClient(GremlinServer gremlinServer, IMessageSerializer? messageSerializer = null,
ConnectionSettings? connectionSettings = null,
ILoggerFactory? loggerFactory = null,
- IReadOnlyList<Func<HttpRequestContext, Task>>? interceptors = null)
+ IReadOnlyList<Func<HttpRequestContext, Task>>? interceptors = null,
+ ProviderDefinedTypeRegistry? pdtRegistry = null)
: this(gremlinServer,
messageSerializer ?? new GraphBinary4MessageSerializer(),
messageSerializer ?? new GraphBinary4MessageSerializer(),
- connectionSettings, loggerFactory, interceptors)
+ connectionSettings, loggerFactory, interceptors, pdtRegistry)
{
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/IMessageSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/IMessageSerializer.cs
index 9abb59dc66..831a8ee379 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/IMessageSerializer.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/IMessageSerializer.cs
@@ -26,6 +26,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Gremlin.Net.Driver.Messages;
+using Gremlin.Net.Structure;
namespace Gremlin.Net.Driver
{
@@ -61,5 +62,12 @@ namespace Gremlin.Net.Driver
/// <returns>An async sequence of deserialized result objects.</returns>
IAsyncEnumerable<object> DeserializeMessageAsync(Stream stream,
CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Sets the <see cref="ProviderDefinedTypeRegistry"/> for automatic hydration
+ /// of provider-defined types during deserialization. The default implementation
+ /// is a no-op for serializers that do not support PDT hydration.
+ /// </summary>
+ void SetPdtRegistry(ProviderDefinedTypeRegistry pdtRegistry) { }
}
}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
index 3340730c1b..32d7ce01a7 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Remote/DriverRemoteConnection.cs
@@ -29,6 +29,7 @@ using Gremlin.Net.Driver.Messages;
using Gremlin.Net.Process.Remote;
using Gremlin.Net.Process.Traversal;
using Gremlin.Net.Process.Traversal.Strategy.Decoration;
+using Gremlin.Net.Structure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -43,6 +44,11 @@ namespace Gremlin.Net.Driver.Remote
private readonly string _traversalSource;
private readonly ILogger<DriverRemoteConnection> _logger;
+ /// <summary>
+ /// Gets or sets the <see cref="ProviderDefinedTypeRegistry"/> for registry-based dehydration.
+ /// </summary>
+ public ProviderDefinedTypeRegistry? PdtRegistry { get; set; }
+
// All OptionsStrategy keys are passed through to the request fields.
// The server filters out options that don't apply, and this allows
// providers to use custom request fields via the Client directly or DRC.
@@ -61,14 +67,17 @@ namespace Gremlin.Net.Driver.Remote
/// An optional list of request interceptors forwarded to the underlying
/// <see cref="GremlinClient" />.
/// </param>
+ /// <param name="pdtRegistry">An optional registry for PDT hydration.</param>
/// <exception cref="ArgumentNullException">Thrown when client is null.</exception>
public DriverRemoteConnection(string host, int port, string traversalSource = "g",
ILoggerFactory? loggerFactory = null,
- IReadOnlyList<Func<HttpRequestContext, Task>>? interceptors = null) : this(
- new GremlinClient(new GremlinServer(host, port), loggerFactory: loggerFactory, interceptors: interceptors),
+ IReadOnlyList<Func<HttpRequestContext, Task>>? interceptors = null,
+ ProviderDefinedTypeRegistry? pdtRegistry = null) : this(
+ new GremlinClient(new GremlinServer(host, port), loggerFactory: loggerFactory, interceptors: interceptors, pdtRegistry: pdtRegistry),
traversalSource,
logger: loggerFactory?.CreateLogger<DriverRemoteConnection>() ?? NullLogger<DriverRemoteConnection>.Instance)
{
+ PdtRegistry = pdtRegistry;
}
/// <summary>
@@ -76,10 +85,13 @@ namespace Gremlin.Net.Driver.Remote
/// </summary>
/// <param name="client">The <see cref="IGremlinClient" /> that will be used for the connection.</param>
/// <param name="traversalSource">The name of the traversal source on the server to bind to.</param>
+ /// <param name="pdtRegistry">An optional registry for PDT hydration.</param>
/// <exception cref="ArgumentNullException">Thrown when client or the traversalSource is null.</exception>
- public DriverRemoteConnection(IGremlinClient client, string traversalSource = "g")
+ public DriverRemoteConnection(IGremlinClient client, string traversalSource = "g",
+ ProviderDefinedTypeRegistry? pdtRegistry = null)
: this(client, traversalSource, logger: null)
{
+ PdtRegistry = pdtRegistry;
}
private DriverRemoteConnection(IGremlinClient client, string traversalSource,
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
index a571c9546b..89b37c35d8 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Remote/IRemoteConnection.cs
@@ -24,6 +24,7 @@
using System.Threading;
using System.Threading.Tasks;
using Gremlin.Net.Process.Traversal;
+using Gremlin.Net.Structure;
namespace Gremlin.Net.Process.Remote
{
@@ -56,5 +57,10 @@ namespace Gremlin.Net.Process.Remote
/// Determines if the connection is bound to a session.
/// </summary>
bool IsSessionBound { get; }
+
+ /// <summary>
+ /// Gets the <see cref="ProviderDefinedTypeRegistry"/> for registry-based dehydration, or null.
+ /// </summary>
+ ProviderDefinedTypeRegistry? PdtRegistry => null;
}
}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/AnonymousTraversalSource.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/AnonymousTraversalSource.cs
index aff2d4c2d1..21392bb5c0 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/AnonymousTraversalSource.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/AnonymousTraversalSource.cs
@@ -58,7 +58,7 @@ namespace Gremlin.Net.Process.Traversal
/// <returns>A <see cref="GraphTraversalSource" /> configured to use the provided <see cref="IRemoteConnection" />.</returns>
public GraphTraversalSource With(IRemoteConnection remoteConnection) =>
new GraphTraversalSource(new List<ITraversalStrategy>(),
- new GremlinLang(), remoteConnection);
+ new GremlinLang { PdtRegistry = remoteConnection.PdtRegistry }, remoteConnection);
}
}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
index 21115a8f8f..fff3dcff88 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
@@ -23,9 +23,11 @@
using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
+using System.Reflection;
using System.Text;
using Gremlin.Net.Process.Traversal.Strategy;
using Gremlin.Net.Process.Traversal.Strategy.Decoration;
@@ -40,11 +42,17 @@ namespace Gremlin.Net.Process.Traversal
public class GremlinLang : ICloneable, IEquatable<GremlinLang>
{
private static readonly object[] EmptyArray = Array.Empty<object>();
+ private static readonly ConcurrentDictionary<Type, (string name, PropertyInfo[] props)?> _pdtCache = new();
private StringBuilder _gremlin = new();
private Dictionary<string, object> _parameters = new();
private List<OptionsStrategy> _optionsStrategies = new();
+ /// <summary>
+ /// Gets or sets the <see cref="ProviderDefinedTypeRegistry"/> for registry-based dehydration.
+ /// </summary>
+ public ProviderDefinedTypeRegistry? PdtRegistry { get; set; }
+
/// <summary>
/// Initializes a new instance of the <see cref="GremlinLang" /> class.
/// </summary>
@@ -329,6 +337,25 @@ namespace Gremlin.Net.Process.Traversal
if (arg is CardinalityValue cv)
return $"Cardinality.{cv.Cardinality!.EnumValue}({ArgAsString(cv.Value)})";
+ if (arg is ProviderDefinedType pdt)
+ {
+ var sb2 = new StringBuilder("[");
+ var count = pdt.Properties.Count;
+ if (count == 0)
+ {
+ sb2.Append(':');
+ }
+ else
+ {
+ foreach (var kvp in pdt.Properties)
+ {
+ sb2.Append(ArgAsString(kvp.Key)).Append(':').Append(ArgAsString(kvp.Value));
+ if (--count > 0) sb2.Append(',');
+ }
+ }
+ sb2.Append(']');
+ return $"PDT(\"{EscapeJava(pdt.Name)}\",{sb2})";
+ }
if (arg is IDictionary dict)
return AsString(dict);
@@ -348,6 +375,33 @@ namespace Gremlin.Net.Process.Traversal
if (arg is Type type)
return type.Name;
+ // Registry-based dehydration
+ if (PdtRegistry != null)
+ {
+ var adapterInfo = PdtRegistry.GetAdapterByType(arg.GetType());
+ if (adapterInfo != null)
+ {
+ var (adapterTypeName, toProperties) = adapterInfo.Value;
+ var props = toProperties(arg);
+ return ArgAsString(new ProviderDefinedType(adapterTypeName,
+ new Dictionary<string, object?>(props)));
+ }
+ }
+
+ // Auto-dehydrate objects annotated with [ProviderDefined]
+ var cached = GetPdtInfo(arg.GetType());
+ if (cached != null)
+ {
+ var (typeName, fields) = cached.Value;
+ ProviderDefinedAttribute.RegisteredTypes.TryAdd(typeName, arg.GetType());
+ var props = new Dictionary<string, object?>();
+ foreach (var field in fields)
+ {
+ props[field.Name] = field.GetValue(arg);
+ }
+ return ArgAsString(new ProviderDefinedType(typeName, props));
+ }
+
throw new ArgumentException(
$"GremlinLang contains at least one type [{arg.GetType().Name}] that cannot be represented as text.");
}
@@ -728,6 +782,39 @@ namespace Gremlin.Net.Process.Traversal
return true;
}
+ private static (string name, PropertyInfo[] props)? GetPdtInfo(Type type)
+ {
+ return _pdtCache.GetOrAdd(type, t =>
+ {
+ var attrs = t.GetCustomAttributes(typeof(ProviderDefinedAttribute), false);
+ if (attrs.Length == 0) return null;
+
+ var attr = (ProviderDefinedAttribute)attrs[0];
+ var typeName = string.IsNullOrEmpty(attr.Name) ? t.Name : attr.Name;
+
+ var included = attr.IncludedFields;
+ var excluded = attr.ExcludedFields;
+ if (included is { Length: > 0 } && excluded is { Length: > 0 })
+ {
+ throw new ArgumentException(
+ "[ProviderDefined] cannot specify both IncludedFields and ExcludedFields");
+ }
+
+ var includedSet = included is { Length: > 0 } ? new HashSet<string>(included) : null;
+ var excludedSet = excluded is { Length: > 0 } ? new HashSet<string>(excluded) : null;
+
+ var allProps = t.GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ var filtered = new List<PropertyInfo>();
+ foreach (var p in allProps)
+ {
+ if (includedSet != null && !includedSet.Contains(p.Name)) continue;
+ if (excludedSet != null && excludedSet.Contains(p.Name)) continue;
+ filtered.Add(p);
+ }
+ return (typeName, filtered.ToArray());
+ });
+ }
+
/// <summary>
/// Creates a deep copy of this <see cref="GremlinLang" /> instance.
/// </summary>
@@ -738,7 +825,8 @@ namespace Gremlin.Net.Process.Traversal
{
_gremlin = new StringBuilder(_gremlin.ToString()),
_parameters = new Dictionary<string, object>(_parameters),
- _optionsStrategies = new List<OptionsStrategy>(_optionsStrategies)
+ _optionsStrategies = new List<OptionsStrategy>(_optionsStrategies),
+ PdtRegistry = PdtRegistry
};
return clone;
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
index 9490a34302..de4445e1e1 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
@@ -59,8 +59,8 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
// Not yet implemented
// public static readonly DataType Tree = new DataType(0x2B);
public static readonly DataType Merge = new DataType(0x2E);
+ public static readonly DataType CompositePDT = new DataType(0xF0);
// Not yet implemented
- // public static readonly DataType CompositePDT = new DataType(0xF0);
// public static readonly DataType PrimitivePDT = new DataType(0xF1);
public static readonly DataType Char = new DataType(0x80);
public static readonly DataType Duration = new DataType(0x81);
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinary4MessageSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinary4MessageSerializer.cs
index 706ab47dd0..d336ebfab8 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinary4MessageSerializer.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinary4MessageSerializer.cs
@@ -36,7 +36,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
/// </summary>
public class GraphBinary4MessageSerializer : IMessageSerializer
{
- private readonly GraphBinaryReader _reader;
+ private GraphBinaryReader _reader;
private readonly GraphBinaryWriter _writer;
private readonly RequestMessageSerializer _requestSerializer = new RequestMessageSerializer();
private readonly ResponseSerializer _responseSerializer = new ResponseSerializer();
@@ -54,6 +54,25 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
_writer = new GraphBinaryWriter();
}
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GraphBinary4MessageSerializer" /> class
+ /// with a <see cref="ProviderDefinedTypeRegistry"/> for automatic hydration.
+ /// </summary>
+ public GraphBinary4MessageSerializer(ProviderDefinedTypeRegistry pdtRegistry)
+ {
+ _reader = new GraphBinaryReader(pdtRegistry: pdtRegistry);
+ _writer = new GraphBinaryWriter();
+ }
+
+ /// <summary>
+ /// Sets the <see cref="ProviderDefinedTypeRegistry"/> on this serializer's reader
+ /// for automatic hydration of provider-defined types.
+ /// </summary>
+ public void SetPdtRegistry(ProviderDefinedTypeRegistry pdtRegistry)
+ {
+ _reader = new GraphBinaryReader(pdtRegistry: pdtRegistry);
+ }
+
/// <inheritdoc />
public async Task<byte[]> SerializeMessageAsync(RequestMessage requestMessage,
CancellationToken cancellationToken = default)
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
index ffe9f08f97..97f0dd76fe 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
@@ -24,6 +24,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Gremlin.Net.Structure;
namespace Gremlin.Net.Structure.IO.GraphBinary4
{
@@ -33,14 +34,17 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
public class GraphBinaryReader
{
private readonly TypeSerializerRegistry _registry;
+ private readonly ProviderDefinedTypeRegistry? _pdtRegistry;
/// <summary>
/// Initializes a new instance of the <see cref="GraphBinaryReader" /> class.
/// </summary>
/// <param name="registry">The <see cref="TypeSerializerRegistry"/> to use for deserialization.</param>
- public GraphBinaryReader(TypeSerializerRegistry? registry = null)
+ /// <param name="pdtRegistry">Optional <see cref="ProviderDefinedTypeRegistry"/> for automatic hydration.</param>
+ public GraphBinaryReader(TypeSerializerRegistry? registry = null, ProviderDefinedTypeRegistry? pdtRegistry = null)
{
_registry = registry ?? TypeSerializerRegistry.Instance;
+ _pdtRegistry = pdtRegistry;
}
/// <summary>
@@ -90,7 +94,18 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
}
var typeSerializer = _registry.GetSerializerFor(type);
- return await typeSerializer.ReadAsync(stream, this, cancellationToken).ConfigureAwait(false);
+ var result = await typeSerializer.ReadAsync(stream, this, cancellationToken).ConfigureAwait(false);
+ if (result is ProviderDefinedType pdt)
+ {
+ if (_pdtRegistry != null)
+ {
+ var hydrated = _pdtRegistry.Hydrate(pdt);
+ if (hydrated is not ProviderDefinedType)
+ return hydrated;
+ }
+ return ProviderDefinedAttribute.HydrateIfRegistered(pdt);
+ }
+ return result;
}
}
}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
index 82421f14a1..cb9667db62 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
@@ -64,6 +64,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
{typeof(char), new CharSerializer()},
{typeof(TimeSpan), new DurationSerializer()},
{typeof(Marker), SingleTypeSerializers.MarkerSerializer},
+ {typeof(ProviderDefinedType), new CompositePDTSerializer()},
};
private readonly Dictionary<DataType, ITypeSerializer> _serializerByDataType =
@@ -96,6 +97,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
{DataType.Char, new CharSerializer()},
{DataType.Duration, new DurationSerializer()},
{DataType.Marker, SingleTypeSerializers.MarkerSerializer},
+ {DataType.CompositePDT, new CompositePDTSerializer()},
};
/// <summary>
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CompositePDTSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CompositePDTSerializer.cs
new file mode 100644
index 0000000000..49c4bdff66
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CompositePDTSerializer.cs
@@ -0,0 +1,78 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ /// <summary>
+ /// A <see cref="ProviderDefinedType"/> serializer for the CompositePDT data type.
+ /// </summary>
+ public class CompositePDTSerializer : SimpleTypeSerializer<ProviderDefinedType>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CompositePDTSerializer"/> class.
+ /// </summary>
+ public CompositePDTSerializer() : base(DataType.CompositePDT)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override async Task WriteValueAsync(ProviderDefinedType value, Stream stream,
+ GraphBinaryWriter writer, CancellationToken cancellationToken = default)
+ {
+ // Write name as fully-qualified string
+ await writer.WriteAsync(value.Name, stream, cancellationToken).ConfigureAwait(false);
+ // Write properties as fully-qualified map
+ await writer.WriteAsync((IDictionary<string, object?>)new Dictionary<string, object?>(value.Properties),
+ stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ protected override async Task<ProviderDefinedType> ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var name = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false) as string;
+ if (string.IsNullOrEmpty(name))
+ throw new IOException("CompositePDT name cannot be null or empty.");
+
+ var map = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false)
+ as IDictionary<object, object?>;
+
+ var properties = new Dictionary<string, object?>();
+ if (map != null)
+ {
+ foreach (var kv in map)
+ {
+ properties[(string)kv.Key] = kv.Value;
+ }
+ }
+
+ return new ProviderDefinedType(name!, properties);
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IProviderDefinedTypeAdapter.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IProviderDefinedTypeAdapter.cs
new file mode 100644
index 0000000000..cd7af23e3c
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IProviderDefinedTypeAdapter.cs
@@ -0,0 +1,49 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System.Collections.Generic;
+
+namespace Gremlin.Net.Structure
+{
+ /// <summary>
+ /// Adapter for hydrating a <see cref="ProviderDefinedType"/> into a strongly-typed object.
+ /// </summary>
+ /// <typeparam name="T">The target type to hydrate into.</typeparam>
+ public interface IProviderDefinedTypeAdapter<T>
+ {
+ /// <summary>
+ /// Gets the fully-qualified type name this adapter handles.
+ /// </summary>
+ string TypeName { get; }
+
+ /// <summary>
+ /// Creates a typed instance from the PDT properties.
+ /// </summary>
+ T FromProperties(IReadOnlyDictionary<string, object?> properties);
+
+ /// <summary>
+ /// Converts a typed instance back to PDT properties.
+ /// </summary>
+ IReadOnlyDictionary<string, object?> ToProperties(T obj);
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedAttribute.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedAttribute.cs
new file mode 100644
index 0000000000..c7d25667e5
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedAttribute.cs
@@ -0,0 +1,81 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Gremlin.Net.Structure
+{
+ /// <summary>
+ /// Marks a class as a provider-defined type target for hydration.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class)]
+ public class ProviderDefinedAttribute : Attribute
+ {
+ /// <summary>
+ /// Gets or sets the fully-qualified provider-defined type name.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of property names to include during dehydration.
+ /// If non-null and non-empty, only these properties are serialized.
+ /// Cannot be combined with <see cref="ExcludedFields"/>.
+ /// </summary>
+ public string[]? IncludedFields { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of property names to exclude during dehydration.
+ /// If non-null and non-empty, these properties are omitted from serialization.
+ /// Cannot be combined with <see cref="IncludedFields"/>.
+ /// </summary>
+ public string[]? ExcludedFields { get; set; }
+
+ /// <summary>
+ /// Static registry of annotated types keyed by PDT name, populated lazily during dehydration.
+ /// </summary>
+ internal static readonly ConcurrentDictionary<string, Type> RegisteredTypes = new();
+
+ /// <summary>
+ /// Hydrates a <see cref="ProviderDefinedType"/> using a registered annotated type.
+ /// Returns the original PDT if no annotated type is registered for the name.
+ /// </summary>
+ internal static object HydrateIfRegistered(ProviderDefinedType pdt)
+ {
+ if (!RegisteredTypes.TryGetValue(pdt.Name, out var type))
+ return pdt;
+ var obj = Activator.CreateInstance(type)!;
+ foreach (var (key, value) in pdt.Properties)
+ {
+ var prop = type.GetProperty(key, BindingFlags.Public | BindingFlags.Instance);
+ if (prop != null && prop.CanWrite && value != null)
+ {
+ prop.SetValue(obj, Convert.ChangeType(value, prop.PropertyType));
+ }
+ }
+ return obj;
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedType.cs
new file mode 100644
index 0000000000..ac6c9d60f9
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedType.cs
@@ -0,0 +1,70 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Gremlin.Net.Structure
+{
+ /// <summary>
+ /// Represents a provider-defined type (PDT) with a name and a set of properties.
+ /// </summary>
+ public class ProviderDefinedType
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderDefinedType"/> class.
+ /// </summary>
+ /// <param name="name">The fully-qualified name of the provider-defined type.</param>
+ /// <param name="properties">The properties of the provider-defined type.</param>
+ public ProviderDefinedType(string name, IReadOnlyDictionary<string, object?> properties)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrEmpty(name)) throw new ArgumentException("name cannot be empty", nameof(name));
+ Properties = properties ?? new Dictionary<string, object?>();
+ }
+
+ /// <summary>
+ /// Gets the fully-qualified name of this provider-defined type.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// Gets the properties of this provider-defined type.
+ /// </summary>
+ public IReadOnlyDictionary<string, object?> Properties { get; }
+
+ /// <inheritdoc />
+ public override string ToString() =>
+ $"pdt[{Name}]{{{string.Join(", ", Properties.Select(kv => $"{kv.Key}={kv.Value}"))}}}";
+
+ /// <inheritdoc />
+ public override bool Equals(object? obj) =>
+ obj is ProviderDefinedType other && Name == other.Name &&
+ Properties.Count == other.Properties.Count &&
+ Properties.All(kv => other.Properties.TryGetValue(kv.Key, out var v) && Equals(kv.Value, v));
+
+ /// <inheritdoc />
+ public override int GetHashCode() => HashCode.Combine(Name, Properties.Count);
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs
new file mode 100644
index 0000000000..096d1cbc77
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs
@@ -0,0 +1,131 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+
+namespace Gremlin.Net.Structure
+{
+ /// <summary>
+ /// Registry for <see cref="IProviderDefinedTypeAdapter{T}"/> instances that hydrate
+ /// <see cref="ProviderDefinedType"/> values into strongly-typed objects.
+ /// </summary>
+ public class ProviderDefinedTypeRegistry
+ {
+ private readonly Dictionary<string, object> _adaptersByName = new();
+ private readonly Dictionary<Type, (string typeName, object adapter)> _adaptersByType = new();
+
+ /// <summary>
+ /// Registers an adapter for a specific provider-defined type name.
+ /// </summary>
+ public void Register<T>(IProviderDefinedTypeAdapter<T> adapter)
+ {
+ _adaptersByName[adapter.TypeName] = adapter;
+ _adaptersByType[typeof(T)] = (adapter.TypeName, adapter);
+ }
+
+ /// <summary>
+ /// Creates a registry populated by scanning loaded assemblies for types implementing
+ /// <see cref="IProviderDefinedTypeAdapter{T}"/> and registering them automatically.
+ /// </summary>
+ public static ProviderDefinedTypeRegistry Build()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ try
+ {
+ foreach (var type in assembly.GetTypes())
+ {
+ var adapterInterface = type.GetInterfaces()
+ .FirstOrDefault(i => i.IsGenericType &&
+ i.GetGenericTypeDefinition() == typeof(IProviderDefinedTypeAdapter<>));
+ if (adapterInterface != null && !type.IsAbstract && !type.IsInterface)
+ {
+ try
+ {
+ var adapter = Activator.CreateInstance(type);
+ var registerMethod = typeof(ProviderDefinedTypeRegistry)
+ .GetMethod(nameof(Register))!
+ .MakeGenericMethod(adapterInterface.GetGenericArguments()[0]);
+ registerMethod.Invoke(registry, new[] { adapter });
+ }
+ catch
+ {
+ // skip types that can't be instantiated
+ }
+ }
+ }
+ }
+ catch
+ {
+ // skip assemblies that can't be reflected
+ }
+ }
+
+ return registry;
+ }
+
+ /// <summary>
+ /// Returns the type name and ToProperties method for the given CLR type, or null if not registered.
+ /// </summary>
+ internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetAdapterByType(Type type)
+ {
+ if (!_adaptersByType.TryGetValue(type, out var entry))
+ return null;
+ var method = entry.adapter.GetType().GetMethod("ToProperties");
+ if (method == null) return null;
+ return (entry.typeName, obj => (IReadOnlyDictionary<string, object?>)method.Invoke(entry.adapter, new[] { obj })!);
+ }
+
+ /// <summary>
+ /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered adapter.
+ /// Returns the original PDT if no adapter is registered or if hydration fails.
+ /// </summary>
+ public object Hydrate(ProviderDefinedType pdt)
+ {
+ if (!_adaptersByName.TryGetValue(pdt.Name, out var adapterObj))
+ return pdt;
+ try
+ {
+ var hydratedProps = new Dictionary<string, object?>();
+ foreach (var (key, value) in pdt.Properties)
+ {
+ hydratedProps[key] = value is ProviderDefinedType nested ? Hydrate(nested) : value;
+ }
+
+ var readOnlyProps = new ReadOnlyDictionary<string, object?>(hydratedProps);
+ var method = adapterObj.GetType().GetMethod("FromProperties");
+ return method!.Invoke(adapterObj, new object[] { readOnlyProps })!;
+ }
+ catch (Exception)
+ {
+ return pdt;
+ }
+ }
+ }
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs
index c34c232006..d5f28692d2 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs
@@ -22,10 +22,14 @@
#endregion
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Gremlin.Net.Driver;
using Gremlin.Net.Driver.Remote;
using Gremlin.Net.Process.Traversal;
+using Gremlin.Net.Structure;
+using Gremlin.Net.Structure.IO.GraphBinary4;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@@ -92,4 +96,100 @@ public class DriverRemoteConnectionTests
return gremlinLang;
}
}
+
+ [Fact]
+ public void ShouldRoundTripPdtViaTraversalApi()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+ using var connection = new DriverRemoteConnection(gremlinClient, "gmodern");
+ var g = AnonymousTraversalSource.Traversal().With(connection);
+
+ var pdt = new ProviderDefinedType("TestPoint",
+ new Dictionary<string, object?> { { "x", 1 }, { "y", 2 } });
+
+ var results = g.Inject<object>(pdt).ToList();
+
+ Assert.Single(results);
+ var result = Assert.IsType<ProviderDefinedType>(results[0]);
+ Assert.Equal("TestPoint", result.Name);
+ Assert.Equal(1, result.Properties["x"]);
+ Assert.Equal(2, result.Properties["y"]);
+ }
+
+ [Fact]
+ public void ShouldRoundTripTypedObjectViaRegistry()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.Register(new TestPointAdapter());
+
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer, pdtRegistry: registry);
+ using var connection = new DriverRemoteConnection(gremlinClient, "gmodern");
+ var g = AnonymousTraversalSource.Traversal().With(connection);
+
+ var point = new TestPointClass { X = 5, Y = 10 };
+
+ var results = g.Inject<object>(point).ToList();
+
+ Assert.Single(results);
+ var result = Assert.IsType<TestPointClass>(results[0]);
+ Assert.Equal(5, result.X);
+ Assert.Equal(10, result.Y);
+ }
+
+ [Fact]
+ public void ShouldRoundTripAnnotatedClass()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+ using var connection = new DriverRemoteConnection(gremlinClient, "gmodern");
+ var g = AnonymousTraversalSource.Traversal().With(connection);
+
+ var point = new AnnotatedTestPoint { X = 3, Y = 7 };
+
+ var results = g.Inject<object>(point).ToList();
+
+ Assert.Single(results);
+ var result = Assert.IsType<AnnotatedTestPoint>(results[0]);
+ Assert.Equal(3, result.X);
+ Assert.Equal(7, result.Y);
+ }
+
+ #region Test helpers
+
+ private class TestPointClass
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ }
+
+ private class TestPointAdapter : IProviderDefinedTypeAdapter<TestPointClass>
+ {
+ public string TypeName => "TestPoint";
+
+ public TestPointClass FromProperties(IReadOnlyDictionary<string, object?> properties)
+ {
+ return new TestPointClass
+ {
+ X = Convert.ToInt32(properties["x"]),
+ Y = Convert.ToInt32(properties["y"])
+ };
+ }
+
+ public IReadOnlyDictionary<string, object?> ToProperties(TestPointClass obj)
+ {
+ return new ReadOnlyDictionary<string, object?>(
+ new Dictionary<string, object?> { { "x", obj.X }, { "y", obj.Y } });
+ }
+ }
+
+ [ProviderDefined(Name = "TestPoint")]
+ private class AnnotatedTestPoint
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ }
+
+ #endregion
}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
index a1fb59efc7..fab9af53e4 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
@@ -28,6 +28,7 @@ using Gremlin.Net.Driver;
using Gremlin.Net.Driver.Exceptions;
using Gremlin.Net.Driver.Messages;
using Gremlin.Net.IntegrationTest.Util;
+using Gremlin.Net.Structure;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@@ -204,5 +205,72 @@ namespace Gremlin.Net.IntegrationTest.Driver
logger.VerifyNothingWasLogged();
}
+
+ [Fact]
+ public async Task ShouldRoundTripSimplePointPdt()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject(PDT(\"Point\", [\"x\":1, \"y\":2]))");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var pdt = Assert.IsType<ProviderDefinedType>(results[0]);
+ Assert.Equal("Point", pdt.Name);
+ Assert.Equal(2, pdt.Properties.Count);
+ Assert.Equal(1, pdt.Properties["x"]);
+ Assert.Equal(2, pdt.Properties["y"]);
+ }
+
+ [Fact]
+ public async Task ShouldRoundTripNestedPdt()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject(PDT(\"Person\", [\"name\":\"Alice\", \"age\":30, " +
+ "\"address\":PDT(\"Address\", [\"street\":\"123 Main St\", \"city\":\"Springfield\", \"zip\":\"12345\"])]))");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var pdt = Assert.IsType<ProviderDefinedType>(results[0]);
+ Assert.Equal("Person", pdt.Name);
+ Assert.Equal("Alice", pdt.Properties["name"]);
+ Assert.Equal(30, pdt.Properties["age"]);
+
+ var address = Assert.IsType<ProviderDefinedType>(pdt.Properties["address"]);
+ Assert.Equal("Address", address.Name);
+ Assert.Equal("123 Main St", address.Properties["street"]);
+ Assert.Equal("Springfield", address.Properties["city"]);
+ Assert.Equal("12345", address.Properties["zip"]);
+ }
+
+ [Fact]
+ public async Task ShouldHandlePdtInCollection()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject([PDT(\"Point\", [\"x\":1, \"y\":2]), PDT(\"Point\", [\"x\":3, \"y\":4])])");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var list = Assert.IsType<List<object>>(results[0]);
+ Assert.Equal(2, list.Count);
+
+ var p1 = Assert.IsType<ProviderDefinedType>(list[0]);
+ Assert.Equal("Point", p1.Name);
+ Assert.Equal(1, p1.Properties["x"]);
+ Assert.Equal(2, p1.Properties["y"]);
+
+ var p2 = Assert.IsType<ProviderDefinedType>(list[1]);
+ Assert.Equal("Point", p2.Name);
+ Assert.Equal(3, p2.Properties["x"]);
+ Assert.Equal(4, p2.Properties["y"]);
+ }
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
index b4eeade168..df80b589a6 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
@@ -1088,5 +1088,91 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
var result = GremlinLang.ConvertParametersToString(parameters);
Assert.Contains("\"name\":", result);
}
+
+ [Fact]
+ public void g_Inject_PDT_basic()
+ {
+ var pdt = new ProviderDefinedType("Point", new Dictionary<string, object?> { { "x", 1 }, { "y", 2 } });
+ var result = _g.Inject((object)pdt).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"Point\",[\"x\":1,\"y\":2]))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PDT_special_chars_in_name()
+ {
+ var pdt = new ProviderDefinedType("my\"type", new Dictionary<string, object?> { { "a", 1 } });
+ var result = _g.Inject((object)pdt).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"my\\\"type\",[\"a\":1]))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PDT_nested()
+ {
+ var inner = new ProviderDefinedType("Inner", new Dictionary<string, object?> { { "v", 42 } });
+ var outer = new ProviderDefinedType("Outer", new Dictionary<string, object?> { { "child", inner } });
+ var result = _g.Inject((object)outer).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"Outer\",[\"child\":PDT(\"Inner\",[\"v\":42])]))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PDT_auto_dehydration_via_attribute()
+ {
+ var point = new TestPoint { X = 10, Y = 20 };
+ var result = _g.Inject((object)point).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"geo.Point\",[\"X\":10,\"Y\":20]))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PDT_auto_dehydration_IncludedFields()
+ {
+ var point = new IncludedFieldsPoint { X = 1, Y = 2, Z = 3 };
+ var result = _g.Inject((object)point).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"IncludedFieldsPoint\",[\"X\":1]))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PDT_auto_dehydration_ExcludedFields()
+ {
+ var point = new ExcludedFieldsPoint { X = 1, Y = 2, Z = 3 };
+ var result = _g.Inject((object)point).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"ExcludedFieldsPoint\",[\"X\":1,\"Y\":2]))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PDT_auto_dehydration_both_fields_throws()
+ {
+ var point = new BothFieldsPoint { X = 1, Y = 2 };
+ Assert.Throws<ArgumentException>(() => _g.Inject((object)point).GremlinLang.GetGremlin());
+ }
+
+ [ProviderDefined(Name = "geo.Point")]
+ private class TestPoint
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ }
+
+ [ProviderDefined(IncludedFields = new[] { "X" })]
+ private class IncludedFieldsPoint
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ public int Z { get; set; }
+ }
+
+ [ProviderDefined(ExcludedFields = new[] { "Z" })]
+ private class ExcludedFieldsPoint
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ public int Z { get; set; }
+ }
+
+ [ProviderDefined(IncludedFields = new[] { "X" }, ExcludedFields = new[] { "Y" })]
+ private class BothFieldsPoint
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ }
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/ProviderDefinedTypeTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/ProviderDefinedTypeTests.cs
new file mode 100644
index 0000000000..36a6318b52
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/ProviderDefinedTypeTests.cs
@@ -0,0 +1,137 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Gremlin.Net.Structure;
+using Gremlin.Net.Structure.IO.GraphBinary4;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4
+{
+ public class ProviderDefinedTypeTests
+ {
+ private static readonly GraphBinaryWriter Writer = new();
+ private static readonly GraphBinaryReader Reader = new();
+
+ [Fact]
+ public async Task TestRoundTripWithProperties()
+ {
+ var properties = new Dictionary<string, object?> { { "x", 1 }, { "y", "hello" } };
+ var expected = new ProviderDefinedType("com.example.MyType", properties);
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as ProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal(expected.Name, actual!.Name);
+ Assert.Equal(expected.Properties, actual.Properties);
+ }
+
+ [Fact]
+ public async Task TestRoundTripWithEmptyProperties()
+ {
+ var expected = new ProviderDefinedType("com.example.Empty", new Dictionary<string, object?>());
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as ProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal(expected.Name, actual!.Name);
+ Assert.Empty(actual.Properties);
+ }
+
+ [Fact]
+ public async Task TestRoundTripWithNullPropertyValue()
+ {
+ var properties = new Dictionary<string, object?> { { "key", null } };
+ var expected = new ProviderDefinedType("com.example.NullVal", properties);
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as ProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal(expected.Name, actual!.Name);
+ Assert.Null(actual.Properties["key"]);
+ }
+
+ [Fact]
+ public async Task TestDataTypeCode()
+ {
+ var pdt = new ProviderDefinedType("com.example.Test", new Dictionary<string, object?>());
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(pdt, stream);
+
+ // First byte should be the CompositePDT type code 0xF0
+ Assert.Equal(0xF0, stream.ToArray()[0]);
+ }
+
+ [Fact]
+ public void TestConstructorThrowsOnNullName()
+ {
+ Assert.Throws<ArgumentNullException>(() =>
+ new ProviderDefinedType(null!, new Dictionary<string, object?>()));
+ }
+
+ [Fact]
+ public void TestConstructorThrowsOnEmptyName()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ new ProviderDefinedType("", new Dictionary<string, object?>()));
+ }
+
+ [Fact]
+ public void TestEquality()
+ {
+ var a = new ProviderDefinedType("com.example.T", new Dictionary<string, object?> { { "k", 1 } });
+ var b = new ProviderDefinedType("com.example.T", new Dictionary<string, object?> { { "k", 1 } });
+ Assert.Equal(a, b);
+ Assert.Equal(a.GetHashCode(), b.GetHashCode());
+ }
+
+ [Fact]
+ public void TestInequality()
+ {
+ var a = new ProviderDefinedType("com.example.A", new Dictionary<string, object?>());
+ var b = new ProviderDefinedType("com.example.B", new Dictionary<string, object?>());
+ Assert.NotEqual(a, b);
+ }
+
+ [Fact]
+ public void TestToString()
+ {
+ var pdt = new ProviderDefinedType("com.example.T", new Dictionary<string, object?> { { "x", 42 } });
+ Assert.Contains("com.example.T", pdt.ToString());
+ Assert.Contains("x=42", pdt.ToString());
+ }
+ }
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/ProviderDefinedTypeRegistryTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/ProviderDefinedTypeRegistryTests.cs
new file mode 100644
index 0000000000..a9aefc6ee2
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/ProviderDefinedTypeRegistryTests.cs
@@ -0,0 +1,196 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using Gremlin.Net.Structure;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure
+{
+ public class ProviderDefinedTypeRegistryTests
+ {
+ [Fact]
+ public void ShouldHydrateToTypedObjectWhenAdapterRegistered()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.Register(new PointAdapter());
+ var pdt = new ProviderDefinedType("geo:Point",
+ new Dictionary<string, object?> { ["x"] = 1.0, ["y"] = 2.0 });
+
+ var result = registry.Hydrate(pdt);
+
+ var point = Assert.IsType<Point>(result);
+ Assert.Equal(1.0, point.X);
+ Assert.Equal(2.0, point.Y);
+ }
+
+ [Fact]
+ public void ShouldReturnRawPdtWhenNoAdapterRegistered()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ var pdt = new ProviderDefinedType("unknown:Type",
+ new Dictionary<string, object?> { ["a"] = "b" });
+
+ var result = registry.Hydrate(pdt);
+
+ Assert.Same(pdt, result);
+ }
+
+ [Fact]
+ public void ShouldReturnRawPdtWhenAdapterThrows()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.Register(new ThrowingAdapter());
+ var pdt = new ProviderDefinedType("bad:Type",
+ new Dictionary<string, object?> { ["x"] = "oops" });
+
+ var result = registry.Hydrate(pdt);
+
+ Assert.Same(pdt, result);
+ }
+
+ [Fact]
+ public void ShouldHydrateNestedPdt()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.Register(new PointAdapter());
+ registry.Register(new LineAdapter());
+ var startPdt = new ProviderDefinedType("geo:Point",
+ new Dictionary<string, object?> { ["x"] = 0.0, ["y"] = 0.0 });
+ var endPdt = new ProviderDefinedType("geo:Point",
+ new Dictionary<string, object?> { ["x"] = 3.0, ["y"] = 4.0 });
+ var linePdt = new ProviderDefinedType("geo:Line",
+ new Dictionary<string, object?> { ["start"] = startPdt, ["end"] = endPdt });
+
+ var result = registry.Hydrate(linePdt);
+
+ var line = Assert.IsType<Line>(result);
+ Assert.Equal(0.0, line.Start.X);
+ Assert.Equal(0.0, line.Start.Y);
+ Assert.Equal(3.0, line.End.X);
+ Assert.Equal(4.0, line.End.Y);
+ }
+
+ [Fact]
+ public void ShouldHaveProviderDefinedAttributeWithNameProperty()
+ {
+ var attr = typeof(AnnotatedPoint).GetCustomAttribute<ProviderDefinedAttribute>();
+
+ Assert.NotNull(attr);
+ Assert.Equal("geo:Point", attr!.Name);
+ }
+
+ [Fact]
+ public void BuildShouldReturnRegistryWithoutCrashing()
+ {
+ var registry = ProviderDefinedTypeRegistry.Build();
+
+ Assert.NotNull(registry);
+ }
+
+ [Fact]
+ public void BuildShouldDiscoverAdapterFromAssembly()
+ {
+ var registry = ProviderDefinedTypeRegistry.Build();
+ var pdt = new ProviderDefinedType("test:Discoverable",
+ new Dictionary<string, object?> { ["value"] = "hello" });
+
+ var result = registry.Hydrate(pdt);
+
+ var obj = Assert.IsType<DiscoverableType>(result);
+ Assert.Equal("hello", obj.Value);
+ }
+
+ #region Test helpers
+
+ private class Point
+ {
+ public double X { get; init; }
+ public double Y { get; init; }
+ }
+
+ private class Line
+ {
+ public Point Start { get; init; } = null!;
+ public Point End { get; init; } = null!;
+ }
+
+ [ProviderDefined(Name = "geo:Point")]
+ private class AnnotatedPoint { }
+
+ private class PointAdapter : IProviderDefinedTypeAdapter<Point>
+ {
+ public string TypeName => "geo:Point";
+
+ public Point FromProperties(IReadOnlyDictionary<string, object?> properties) =>
+ new() { X = (double)properties["x"]!, Y = (double)properties["y"]! };
+
+ public IReadOnlyDictionary<string, object?> ToProperties(Point obj) =>
+ new Dictionary<string, object?> { ["x"] = obj.X, ["y"] = obj.Y };
+ }
+
+ private class LineAdapter : IProviderDefinedTypeAdapter<Line>
+ {
+ public string TypeName => "geo:Line";
+
+ public Line FromProperties(IReadOnlyDictionary<string, object?> properties) =>
+ new() { Start = (Point)properties["start"]!, End = (Point)properties["end"]! };
+
+ public IReadOnlyDictionary<string, object?> ToProperties(Line obj) =>
+ new Dictionary<string, object?> { ["start"] = obj.Start, ["end"] = obj.End };
+ }
+
+ private class ThrowingAdapter : IProviderDefinedTypeAdapter<object>
+ {
+ public string TypeName => "bad:Type";
+
+ public object FromProperties(IReadOnlyDictionary<string, object?> properties) =>
+ throw new InvalidOperationException("intentional failure");
+
+ public IReadOnlyDictionary<string, object?> ToProperties(object obj) =>
+ throw new InvalidOperationException("intentional failure");
+ }
+
+ #endregion
+ }
+
+ /// <summary>Test type discoverable by Build() assembly scanning.</summary>
+ public class DiscoverableType
+ {
+ public string Value { get; init; } = "";
+ }
+
+ /// <summary>Test adapter discoverable by Build() assembly scanning.</summary>
+ public class DiscoverableTypeAdapter : IProviderDefinedTypeAdapter<DiscoverableType>
+ {
+ public string TypeName => "test:Discoverable";
+
+ public DiscoverableType FromProperties(IReadOnlyDictionary<string, object?> properties) =>
+ new() { Value = (string)properties["value"]! };
+
+ public IReadOnlyDictionary<string, object?> ToProperties(DiscoverableType obj) =>
+ new Dictionary<string, object?> { ["value"] = obj.Value };
+ }
+}
diff --git a/gremlin-driver/src/main/java/examples/Connections.java b/gremlin-driver/src/main/java/examples/Connections.java
index 42fe78da31..16ce3d0652 100644
--- a/gremlin-driver/src/main/java/examples/Connections.java
+++ b/gremlin-driver/src/main/java/examples/Connections.java
@@ -24,8 +24,6 @@ import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.Graph;
-import org.apache.tinkerpop.gremlin.structure.io.AbstractIoRegistry;
-import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
import org.apache.tinkerpop.gremlin.util.MessageSerializer;
@@ -93,8 +91,7 @@ public class Connections {
// Connecting and specifying a serializer
private static void withSerializer() throws Exception {
- IoRegistry registry = new FakeIoRegistry(); // an IoRegistry instance exposed by a specific graph provider
- TypeSerializerRegistry typeSerializerRegistry = TypeSerializerRegistry.build().addRegistry(registry).create();
+ TypeSerializerRegistry typeSerializerRegistry = TypeSerializerRegistry.build().create();
MessageSerializer serializer = new GraphBinaryMessageSerializerV4(typeSerializerRegistry);
Cluster cluster = Cluster.build(SERVER_HOST).
port(SERVER_PORT).
@@ -110,6 +107,4 @@ public class Connections {
cluster.close();
g.close();
}
-
- public static class FakeIoRegistry extends AbstractIoRegistry {}
}
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java
index baae303ce9..3643aa9415 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/remote/DriverRemoteConnection.java
@@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.process.remote.traversal.RemoteTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.GremlinLang;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.Transaction;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import java.util.Optional;
@@ -54,6 +55,7 @@ public class DriverRemoteConnection implements RemoteConnection {
private transient Optional<Configuration> conf = Optional.empty();
private final boolean attachElements;
+ private ProviderDefinedTypeRegistry pdtRegistry;
public DriverRemoteConnection(final Configuration conf) {
final boolean hasClusterConf = IteratorUtils.anyMatch(conf.getKeys(), k -> k.startsWith("clusterConfiguration"));
@@ -214,6 +216,18 @@ public class DriverRemoteConnection implements RemoteConnection {
}
}
+ @Override
+ public ProviderDefinedTypeRegistry getPdtRegistry() {
+ return pdtRegistry;
+ }
+
+ /**
+ * Sets the {@link ProviderDefinedTypeRegistry} for registry-based dehydration in the gremlin-lang translator.
+ */
+ public void setPdtRegistry(final ProviderDefinedTypeRegistry pdtRegistry) {
+ this.pdtRegistry = pdtRegistry;
+ }
+
@Override
public <E> CompletableFuture<RemoteTraversal<?, E>> submitAsync(final GremlinLang gremlinLang) throws RemoteConnectionException {
if (gremlinLang.containsUnsupportedTypes()) {
diff --git a/gremlin-examples/gremlin-java/Connections.java b/gremlin-examples/gremlin-java/Connections.java
index 8dd236a7f2..966fe6d81b 100644
--- a/gremlin-examples/gremlin-java/Connections.java
+++ b/gremlin-examples/gremlin-java/Connections.java
@@ -25,8 +25,6 @@ import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.Graph;
-import org.apache.tinkerpop.gremlin.structure.io.AbstractIoRegistry;
-import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
import org.apache.tinkerpop.gremlin.util.MessageSerializer;
@@ -96,8 +94,7 @@ public class Connections {
// Connecting and specifying a serializer
private static void withSerializer() throws Exception {
- IoRegistry registry = new FakeIoRegistry(); // an IoRegistry instance exposed by a specific graph provider
- TypeSerializerRegistry typeSerializerRegistry = TypeSerializerRegistry.build().addRegistry(registry).create();
+ TypeSerializerRegistry typeSerializerRegistry = TypeSerializerRegistry.build().create();
MessageSerializer serializer = new GraphBinaryMessageSerializerV1(typeSerializerRegistry);
Cluster cluster = Cluster.build(SERVER_HOST).
port(SERVER_PORT).
@@ -113,6 +110,4 @@ public class Connections {
cluster.close();
g.close();
}
-
- public static class FakeIoRegistry extends AbstractIoRegistry {}
}
diff --git a/gremlin-go/driver/client.go b/gremlin-go/driver/client.go
index 5bee34ed8e..4707cfb00b 100644
--- a/gremlin-go/driver/client.go
+++ b/gremlin-go/driver/client.go
@@ -61,6 +61,9 @@ type ClientSettings struct {
EnableUserAgentOnConnect bool
+ // PDTRegistry enables automatic hydration of ProviderDefinedType values during deserialization.
+ PDTRegistry *PDTRegistry
+
// RequestInterceptors are functions that modify HTTP requests before sending.
RequestInterceptors []RequestInterceptor
}
@@ -105,6 +108,7 @@ func NewClient(url string, configurations ...func(settings *ClientSettings)) (*C
keepAliveInterval: settings.KeepAliveInterval,
enableCompression: settings.EnableCompression,
enableUserAgentOnConnect: settings.EnableUserAgentOnConnect,
+ pdtRegistry: settings.PDTRegistry,
}
logHandler := newLogHandler(settings.Logger, settings.LogVerbosity, settings.Language)
diff --git a/gremlin-go/driver/client_test.go b/gremlin-go/driver/client_test.go
index 9b201efb3f..c234a99918 100644
--- a/gremlin-go/driver/client_test.go
+++ b/gremlin-go/driver/client_test.go
@@ -24,6 +24,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestClient(t *testing.T) {
@@ -293,3 +294,97 @@ func AssertMarkoVertexWithoutProperties(t *testing.T, result *Result) {
assert.True(t, ok)
assert.Equal(t, 0, len(properties))
}
+
+func TestProviderDefinedTypeIntegration(t *testing.T) {
+ testNoAuthUrl := getEnvOrDefaultString("GREMLIN_SERVER_URL", noAuthUrl)
+ testNoAuthEnable := getEnvOrDefaultBool("RUN_INTEGRATION_TESTS", true)
+
+ t.Run("simple Point PDT round-trip", func(t *testing.T) {
+ skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+ client, err := NewClient(testNoAuthUrl, func(settings *ClientSettings) {
+ settings.TlsConfig = &tls.Config{}
+ settings.TraversalSource = testServerModernGraphAlias
+ })
+ require.NoError(t, err)
+ defer client.Close()
+
+ rs, err := client.Submit("g.inject(PDT(\"Point\", [\"x\":1, \"y\":2]))")
+ require.NoError(t, err)
+
+ result, ok, err := rs.One()
+ require.NoError(t, err)
+ require.True(t, ok)
+
+ pdt, ok := result.Data.(*ProviderDefinedType)
+ require.True(t, ok, "expected *ProviderDefinedType, got %T", result.Data)
+ assert.Equal(t, "Point", pdt.Name)
+ assert.Equal(t, int32(1), pdt.Properties["x"])
+ assert.Equal(t, int32(2), pdt.Properties["y"])
+ })
+
+ t.Run("nested PDT (Person with Address)", func(t *testing.T) {
+ skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+ client, err := NewClient(testNoAuthUrl, func(settings *ClientSettings) {
+ settings.TlsConfig = &tls.Config{}
+ settings.TraversalSource = testServerModernGraphAlias
+ })
+ require.NoError(t, err)
+ defer client.Close()
+
+ rs, err := client.Submit(
+ "g.inject(PDT(\"Person\", [\"name\":\"Alice\", \"age\":30, " +
+ "\"address\":PDT(\"Address\", [\"street\":\"123 Main St\", \"city\":\"Springfield\", \"zip\":\"12345\"])]))")
+ require.NoError(t, err)
+
+ result, ok, err := rs.One()
+ require.NoError(t, err)
+ require.True(t, ok)
+
+ pdt, ok := result.Data.(*ProviderDefinedType)
+ require.True(t, ok, "expected *ProviderDefinedType, got %T", result.Data)
+ assert.Equal(t, "Person", pdt.Name)
+ assert.Equal(t, "Alice", pdt.Properties["name"])
+ assert.Equal(t, int32(30), pdt.Properties["age"])
+
+ address, ok := pdt.Properties["address"].(*ProviderDefinedType)
+ require.True(t, ok, "expected nested *ProviderDefinedType for address")
+ assert.Equal(t, "Address", address.Name)
+ assert.Equal(t, "123 Main St", address.Properties["street"])
+ assert.Equal(t, "Springfield", address.Properties["city"])
+ assert.Equal(t, "12345", address.Properties["zip"])
+ })
+
+ t.Run("PDT in collection", func(t *testing.T) {
+ skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+ client, err := NewClient(testNoAuthUrl, func(settings *ClientSettings) {
+ settings.TlsConfig = &tls.Config{}
+ settings.TraversalSource = testServerModernGraphAlias
+ })
+ require.NoError(t, err)
+ defer client.Close()
+
+ rs, err := client.Submit(
+ "g.inject([PDT(\"Point\", [\"x\":1, \"y\":2]), PDT(\"Point\", [\"x\":3, \"y\":4])])")
+ require.NoError(t, err)
+
+ result, ok, err := rs.One()
+ require.NoError(t, err)
+ require.True(t, ok)
+
+ list, ok := result.Data.([]interface{})
+ require.True(t, ok, "expected []interface{}, got %T", result.Data)
+ require.Len(t, list, 2)
+
+ p1, ok := list[0].(*ProviderDefinedType)
+ require.True(t, ok)
+ assert.Equal(t, "Point", p1.Name)
+ assert.Equal(t, int32(1), p1.Properties["x"])
+ assert.Equal(t, int32(2), p1.Properties["y"])
+
+ p2, ok := list[1].(*ProviderDefinedType)
+ require.True(t, ok)
+ assert.Equal(t, "Point", p2.Name)
+ assert.Equal(t, int32(3), p2.Properties["x"])
+ assert.Equal(t, int32(4), p2.Properties["y"])
+ })
+}
\ No newline at end of file
diff --git a/gremlin-go/driver/connection.go b/gremlin-go/driver/connection.go
index 882161e36c..66947977ec 100644
--- a/gremlin-go/driver/connection.go
+++ b/gremlin-go/driver/connection.go
@@ -43,6 +43,7 @@ type connectionSettings struct {
keepAliveInterval time.Duration
enableCompression bool
enableUserAgentOnConnect bool
+ pdtRegistry *PDTRegistry
}
// connection handles HTTP request/response for Gremlin queries.
@@ -285,7 +286,12 @@ func (c *connection) getReader(resp *http.Response) (io.Reader, io.Closer, error
}
func (c *connection) streamToResultSet(reader io.Reader, rs ResultSet) {
- d := NewGraphBinaryDeserializer(reader)
+ var d *GraphBinaryDeserializer
+ if c.connSettings.pdtRegistry != nil {
+ d = NewGraphBinaryDeserializerWithRegistry(reader, c.connSettings.pdtRegistry)
+ } else {
+ d = NewGraphBinaryDeserializer(reader)
+ }
if err := d.ReadHeader(); err != nil {
if err != io.EOF {
c.logHandler.logf(Error, failedToReceiveResponse, err.Error())
diff --git a/gremlin-go/driver/driverRemoteConnection.go b/gremlin-go/driver/driverRemoteConnection.go
index 7239b8d5c4..a5a47a8c51 100644
--- a/gremlin-go/driver/driverRemoteConnection.go
+++ b/gremlin-go/driver/driverRemoteConnection.go
@@ -59,6 +59,9 @@ type DriverRemoteConnectionSettings struct {
// RequestInterceptors are functions that modify HTTP requests before sending.
RequestInterceptors []RequestInterceptor
+
+ // PDTRegistry enables registry-based dehydration in the gremlin-lang translator.
+ PDTRegistry *PDTRegistry
}
// DriverRemoteConnection is a remote connection.
@@ -103,6 +106,7 @@ func NewDriverRemoteConnection(
keepAliveInterval: settings.KeepAliveInterval,
enableCompression: settings.EnableCompression,
enableUserAgentOnConnect: settings.EnableUserAgentOnConnect,
+ pdtRegistry: settings.PDTRegistry,
}
logHandler := newLogHandler(settings.Logger, settings.LogVerbosity, settings.Language)
diff --git a/gremlin-go/driver/error_codes.go b/gremlin-go/driver/error_codes.go
index 6c0dea424c..ca03def252 100644
--- a/gremlin-go/driver/error_codes.go
+++ b/gremlin-go/driver/error_codes.go
@@ -54,7 +54,6 @@ const (
err0406EnumReaderInvalidTypeError errorCode = "E0406_GRAPH_BINARY_ENUMREADER_INVALID_TYPE_ERROR"
err0407GetSerializerToWriteUnknownTypeError errorCode = "E0407_GRAPH_BINARY_GETSERIALIZERTOWRITE_UNKNOWN_TYPE_ERROR"
err0408GetSerializerToReadUnknownTypeError errorCode = "E0408_GRAPH_BINARY_GETSERIALIZERTOREAD_UNKNOWN_TYPE_ERROR"
- err0409GetSerializerToReadUnknownCustomTypeError errorCode = "E0409_GRAPH_BINARY_GETSERIALIZERTOREAD_UNKNOWN_CUSTOM_TYPE_ERROR"
// response handling errors
err0501ResponseResultSetNotCreatedError errorCode = "E0501_RESPONSE_NO_RESULTSET_ON_DATA_RECEIVE"
diff --git a/gremlin-go/driver/graphBinaryDeserializer.go b/gremlin-go/driver/graphBinaryDeserializer.go
index 5939dbbda0..588e4d462f 100644
--- a/gremlin-go/driver/graphBinaryDeserializer.go
+++ b/gremlin-go/driver/graphBinaryDeserializer.go
@@ -57,10 +57,11 @@ import (
// The bufio.Reader wrapper provides efficient buffering without affecting the
// streaming semantics - it simply reduces the number of underlying read syscalls.
type GraphBinaryDeserializer struct {
- r *bufio.Reader
- buf [8]byte
- err error // sticky error
- bulked bool // whether the response stream uses bulked encoding
+ r *bufio.Reader
+ buf [8]byte
+ err error // sticky error
+ bulked bool // whether the response stream uses bulked encoding
+ pdtRegistry *PDTRegistry // optional: auto-hydrates ProviderDefinedType results
}
// GraphBinary flag for bulked list/set
@@ -72,6 +73,12 @@ func NewGraphBinaryDeserializer(r io.Reader) *GraphBinaryDeserializer {
return &GraphBinaryDeserializer{r: bufio.NewReaderSize(r, 8192)}
}
+// NewGraphBinaryDeserializerWithRegistry creates a new GraphBinaryDeserializer with a PDTRegistry
+// for automatic hydration of ProviderDefinedType values.
+func NewGraphBinaryDeserializerWithRegistry(r io.Reader, registry *PDTRegistry) *GraphBinaryDeserializer {
+ return &GraphBinaryDeserializer{r: bufio.NewReaderSize(r, 8192), pdtRegistry: registry}
+}
+
func (d *GraphBinaryDeserializer) readByte() (byte, error) {
if d.err != nil {
return 0, d.err
@@ -267,6 +274,8 @@ func (d *GraphBinaryDeserializer) readValue(dt dataType, flag byte) (interface{}
return d.readByteBuffer()
case tType, directionType, mergeType, gTypeType:
return d.readEnum(dt)
+ case compositePDTType:
+ return d.readCompositePDT()
default:
return nil, newError(err0408GetSerializerToReadUnknownTypeError, dt)
}
@@ -613,6 +622,43 @@ func (d *GraphBinaryDeserializer) readEnum(dt dataType) (interface{}, error) {
}
}
+func (d *GraphBinaryDeserializer) readCompositePDT() (interface{}, error) {
+ nameObj, err := d.ReadFullyQualified()
+ if err != nil {
+ return nil, err
+ }
+ name, ok := nameObj.(string)
+ if !ok || name == "" {
+ return nil, fmt.Errorf("ProviderDefinedType name must be a non-empty string")
+ }
+ propsObj, err := d.ReadFullyQualified()
+ if err != nil {
+ return nil, err
+ }
+ var props map[string]interface{}
+ if propsObj != nil {
+ raw, ok := propsObj.(map[interface{}]interface{})
+ if !ok {
+ return nil, fmt.Errorf("ProviderDefinedType properties must be a map")
+ }
+ props = make(map[string]interface{}, len(raw))
+ for k, v := range raw {
+ props[fmt.Sprint(k)] = v
+ }
+ }
+ pdt := &ProviderDefinedType{Name: name, Properties: props}
+ if d.pdtRegistry != nil {
+ hydrated := d.pdtRegistry.Hydrate(pdt)
+ if hydrated != pdt {
+ return hydrated, nil
+ }
+ }
+ if t, ok := pdtProviderTypes[name]; ok {
+ return hydratePDTProvider(t, props), nil
+ }
+ return pdt, nil
+}
+
// ReadStatus reads the response status after the EndOfStream marker.
// Returns the status code, message, exception string, and any error encountered.
// This should be called after ReadFullyQualified() returns an EndOfStream marker.
diff --git a/gremlin-go/driver/graphBinarySerializer.go b/gremlin-go/driver/graphBinarySerializer.go
index c473bb880a..2a353575c7 100644
--- a/gremlin-go/driver/graphBinarySerializer.go
+++ b/gremlin-go/driver/graphBinarySerializer.go
@@ -37,7 +37,6 @@ type dataType uint8
// dataType defined as constants.
const (
- customType dataType = 0x00
intType dataType = 0x01
longType dataType = 0x02
stringType dataType = 0x03
@@ -64,6 +63,7 @@ const (
mergeType dataType = 0x2e
gTypeType dataType = 0x30
durationType dataType = 0x81
+ compositePDTType dataType = 0xf0
markerType dataType = 0xfd
nullType dataType = 0xFE
)
@@ -478,6 +478,8 @@ func (serializer *graphBinaryTypeSerializer) getType(val interface{}) (dataType,
return bigDecimalType, nil
case *ByteBuffer, ByteBuffer:
return byteBuffer, nil
+ case *ProviderDefinedType:
+ return compositePDTType, nil
default:
switch reflect.TypeOf(val).Kind() {
case reflect.Map:
diff --git a/gremlin-go/driver/graphBinarySerializer_test.go b/gremlin-go/driver/graphBinarySerializer_test.go
index 4403ea70c4..ea43a45e6d 100644
--- a/gremlin-go/driver/graphBinarySerializer_test.go
+++ b/gremlin-go/driver/graphBinarySerializer_test.go
@@ -535,3 +535,104 @@ func TestWriterErrorPropagation(t *testing.T) {
assert.Equal(t, 22, w.written)
})
}
+
+func TestProviderDefinedTypeSerialization(t *testing.T) {
+ serializer := graphBinaryTypeSerializer{newLogHandler(&defaultLogger{}, Error, language.English)}
+
+ t.Run("round-trip simple PDT", func(t *testing.T) {
+ source := &ProviderDefinedType{
+ Name: "com.example.MyType",
+ Properties: map[string]interface{}{"key": "value", "num": int32(42)},
+ }
+ var buf bytes.Buffer
+ err := serializer.write(source, &buf)
+ assert.Nil(t, err)
+
+ d := NewGraphBinaryDeserializer(bytes.NewReader(buf.Bytes()))
+ result, err := d.ReadFullyQualified()
+ assert.Nil(t, err)
+ pdt, ok := result.(*ProviderDefinedType)
+ assert.True(t, ok)
+ assert.Equal(t, source.Name, pdt.Name)
+ assert.Equal(t, source.Properties["key"], pdt.Properties["key"])
+ assert.Equal(t, source.Properties["num"], pdt.Properties["num"])
+ })
+
+ t.Run("round-trip nested PDT", func(t *testing.T) {
+ inner := &ProviderDefinedType{
+ Name: "com.example.Inner",
+ Properties: map[string]interface{}{"x": int32(1)},
+ }
+ outer := &ProviderDefinedType{
+ Name: "com.example.Outer",
+ Properties: map[string]interface{}{"child": inner},
+ }
+ var buf bytes.Buffer
+ err := serializer.write(outer, &buf)
+ assert.Nil(t, err)
+
+ d := NewGraphBinaryDeserializer(bytes.NewReader(buf.Bytes()))
+ result, err := d.ReadFullyQualified()
+ assert.Nil(t, err)
+ pdt, ok := result.(*ProviderDefinedType)
+ assert.True(t, ok)
+ assert.Equal(t, "com.example.Outer", pdt.Name)
+ child, ok := pdt.Properties["child"].(*ProviderDefinedType)
+ assert.True(t, ok)
+ assert.Equal(t, "com.example.Inner", child.Name)
+ assert.Equal(t, int32(1), child.Properties["x"])
+ })
+
+ t.Run("empty name produces error", func(t *testing.T) {
+ data := []byte{
+ 0xf0, 0x00,
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
+ }
+ d := NewGraphBinaryDeserializer(bytes.NewReader(data))
+ _, err := d.ReadFullyQualified()
+ assert.NotNil(t, err)
+ assert.Contains(t, err.Error(), "non-empty")
+ })
+
+ t.Run("auto-hydrate with registry", func(t *testing.T) {
+ registry := NewPDTRegistry()
+ registry.RegisterFuncs("com.example.MyType",
+ func(props map[string]interface{}) (interface{}, error) {
+ return map[string]interface{}{"hydrated": true, "key": props["key"]}, nil
+ }, nil)
+
+ source := &ProviderDefinedType{
+ Name: "com.example.MyType",
+ Properties: map[string]interface{}{"key": "value"},
+ }
+ var buf bytes.Buffer
+ err := serializer.write(source, &buf)
+ assert.Nil(t, err)
+
+ d := NewGraphBinaryDeserializerWithRegistry(bytes.NewReader(buf.Bytes()), registry)
+ result, err := d.ReadFullyQualified()
+ assert.Nil(t, err)
+ m, ok := result.(map[string]interface{})
+ assert.True(t, ok)
+ assert.Equal(t, true, m["hydrated"])
+ assert.Equal(t, "value", m["key"])
+ })
+
+ t.Run("no hydration without registry", func(t *testing.T) {
+ source := &ProviderDefinedType{
+ Name: "com.example.MyType",
+ Properties: map[string]interface{}{"key": "value"},
+ }
+ var buf bytes.Buffer
+ err := serializer.write(source, &buf)
+ assert.Nil(t, err)
+
+ d := NewGraphBinaryDeserializer(bytes.NewReader(buf.Bytes()))
+ result, err := d.ReadFullyQualified()
+ assert.Nil(t, err)
+ pdt, ok := result.(*ProviderDefinedType)
+ assert.True(t, ok)
+ assert.Equal(t, "com.example.MyType", pdt.Name)
+ })
+}
diff --git a/gremlin-go/driver/graphTraversalSource.go b/gremlin-go/driver/graphTraversalSource.go
index c550bd9556..fc1938fcef 100644
--- a/gremlin-go/driver/graphTraversalSource.go
+++ b/gremlin-go/driver/graphTraversalSource.go
@@ -41,6 +41,9 @@ func NewGraphTraversalSource(graph *Graph, remoteConnection *DriverRemoteConnect
traversalStrategies ...TraversalStrategy) *GraphTraversalSource {
// TODO: revisit when updating strategies
gl := NewGremlinLang(nil)
+ if remoteConnection != nil && remoteConnection.settings != nil && remoteConnection.settings.PDTRegistry != nil {
+ gl.pdtRegistry = remoteConnection.settings.PDTRegistry
+ }
return &GraphTraversalSource{graph: graph, gremlinLang: gl, remoteConnection: remoteConnection}
}
diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go
index 2625cb10e6..b1602ecb31 100644
--- a/gremlin-go/driver/gremlinlang.go
+++ b/gremlin-go/driver/gremlinlang.go
@@ -26,6 +26,7 @@ import (
"math"
"math/big"
"reflect"
+ "sort"
"strconv"
"strings"
"time"
@@ -38,6 +39,7 @@ type GremlinLang struct {
gremlin []string
parameters map[string]interface{}
optionsStrategies []*traversalStrategy
+ pdtRegistry *PDTRegistry
}
// NewGremlinLang creates a new GremlinLang to be used in traversals.
@@ -45,6 +47,7 @@ func NewGremlinLang(gl *GremlinLang) *GremlinLang {
gremlin := make([]string, 0)
parameters := make(map[string]interface{})
optionsStrategies := make([]*traversalStrategy, 0)
+ var registry *PDTRegistry
if gl != nil {
gremlin = make([]string, len(gl.gremlin))
copy(gremlin, gl.gremlin)
@@ -56,12 +59,14 @@ func NewGremlinLang(gl *GremlinLang) *GremlinLang {
optionsStrategies = make([]*traversalStrategy, len(gl.optionsStrategies))
copy(optionsStrategies, gl.optionsStrategies)
+ registry = gl.pdtRegistry
}
return &GremlinLang{
gremlin: gremlin,
parameters: parameters,
optionsStrategies: optionsStrategies,
+ pdtRegistry: registry,
}
}
@@ -200,6 +205,16 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
case dt:
name := reflect.ValueOf(v).Type().Name()
return fmt.Sprintf("%s.%s", strings.ToUpper(name), v), nil
+ case *ProviderDefinedType:
+ props := v.Properties
+ if props == nil {
+ props = map[string]interface{}{}
+ }
+ mapStr, err := gl.translateMap(props)
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("PDT(\"%s\",%s)", escapeString(v.Name), mapStr), nil
case *Vertex:
return gl.argAsString(v.Id)
case textP:
@@ -284,6 +299,23 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) {
case []byte:
return fmt.Sprintf("Binary(\"%s\")", base64.StdEncoding.EncodeToString(v)), nil
default:
+ // Registry-based dehydration
+ if gl.pdtRegistry != nil {
+ adapter := gl.pdtRegistry.GetAdapterByType(reflect.TypeOf(arg))
+ if adapter != nil && adapter.ToProperties != nil {
+ props, err := adapter.ToProperties(arg)
+ if err == nil {
+ pdt := &ProviderDefinedType{Name: adapter.TypeName, Properties: props}
+ return gl.argAsString(pdt)
+ }
+ }
+ }
+ // Auto-dehydrate types implementing PDTProvider
+ if provider, ok := arg.(PDTProvider); ok {
+ pdt := &ProviderDefinedType{Name: provider.PDTName(), Properties: provider.PDTProperties()}
+ pdtProviderTypes[provider.PDTName()] = reflect.TypeOf(arg)
+ return gl.argAsString(pdt)
+ }
switch reflect.TypeOf(arg).Kind() {
case reflect.Map:
return gl.translateMap(arg)
@@ -308,14 +340,19 @@ func (gl *GremlinLang) translateMap(arg interface{}) (string, error) {
if size == 0 {
sb.WriteString(":")
} else {
- iter := reflect.ValueOf(arg).MapRange()
- for iter.Next() {
- k := iter.Key().Interface()
+ mapVal := reflect.ValueOf(arg)
+ keys := mapVal.MapKeys()
+ // Sort keys for deterministic output (not semantic ordering)
+ sort.Slice(keys, func(i, j int) bool {
+ return fmt.Sprintf("%v", keys[i].Interface()) < fmt.Sprintf("%v", keys[j].Interface())
+ })
+ for idx, key := range keys {
+ k := key.Interface()
kString, err := gl.argAsString(k)
if err != nil {
return "", err
}
- v := iter.Value().Interface()
+ v := mapVal.MapIndex(key).Interface()
vString, err := gl.argAsString(v)
if err != nil {
return "", err
@@ -323,8 +360,7 @@ func (gl *GremlinLang) translateMap(arg interface{}) (string, error) {
sb.WriteString(kString)
sb.WriteByte(':')
sb.WriteString(vString)
- size--
- if size > 0 {
+ if idx < len(keys)-1 {
sb.WriteString(",")
}
}
diff --git a/gremlin-go/driver/gremlinlang_test.go b/gremlin-go/driver/gremlinlang_test.go
index 719bd48f60..2bf9d9f4a4 100644
--- a/gremlin-go/driver/gremlinlang_test.go
+++ b/gremlin-go/driver/gremlinlang_test.go
@@ -858,3 +858,89 @@ func Test_ConvertParametersToString(t *testing.T) {
new(RequestOptionsBuilder).SetBindings(map[string]interface{}{"x": struct{}{}}).Create()
})
}
+
+func Test_PDT_GremlinLang(t *testing.T) {
+ t.Run("basic PDT", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ pdt := &ProviderDefinedType{Name: "MyType", Properties: map[string]interface{}{"x": int32(1), "y": "hello"}}
+ gremlin := g.Inject(pdt).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("MyType",["x":1,"y":"hello"]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+
+ t.Run("empty PDT", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ pdt := &ProviderDefinedType{Name: "Empty", Properties: map[string]interface{}{}}
+ gremlin := g.Inject(pdt).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("Empty",[:]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+
+ t.Run("PDT with special characters in name", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ pdt := &ProviderDefinedType{Name: `say"hello"`, Properties: map[string]interface{}{"v": int32(1)}}
+ gremlin := g.Inject(pdt).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("say\"hello\"",["v":1]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+
+ t.Run("PDT with backslash in name", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ pdt := &ProviderDefinedType{Name: `back\slash`, Properties: map[string]interface{}{"v": int32(1)}}
+ gremlin := g.Inject(pdt).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("back\\slash",["v":1]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+
+ t.Run("nested PDT", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ inner := &ProviderDefinedType{Name: "Inner", Properties: map[string]interface{}{"v": int32(1)}}
+ outer := &ProviderDefinedType{Name: "Outer", Properties: map[string]interface{}{"inner": inner}}
+ gremlin := g.Inject(outer).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("Outer",["inner":PDT("Inner",["v":1])]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+
+ t.Run("PDT map keys sorted", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ pdt := &ProviderDefinedType{Name: "T", Properties: map[string]interface{}{"z": int32(3), "a": int32(1), "m": int32(2)}}
+ gremlin := g.Inject(pdt).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("T",["a":1,"m":2,"z":3]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+
+ t.Run("auto-dehydrate PDTProvider", func(t *testing.T) {
+ g := NewGraphTraversalSource(nil, nil)
+ p := testGLPoint{X: 1, Y: 2}
+ gremlin := g.Inject(p).GremlinLang.GetGremlin()
+ expected := `g.inject(PDT("test:Point",["x":1,"y":2]))`
+ if gremlin != expected {
+ t.Errorf("got %v, expected %v", gremlin, expected)
+ }
+ })
+}
+
+type testGLPoint struct {
+ X int32
+ Y int32
+}
+
+func (p testGLPoint) PDTName() string {
+ return "test:Point"
+}
+
+func (p testGLPoint) PDTProperties() map[string]interface{} {
+ return map[string]interface{}{"x": p.X, "y": p.Y}
+}
diff --git a/gremlin-go/driver/pdtRegistry.go b/gremlin-go/driver/pdtRegistry.go
new file mode 100644
index 0000000000..1d14feec6c
--- /dev/null
+++ b/gremlin-go/driver/pdtRegistry.go
@@ -0,0 +1,104 @@
+/*
+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.
+*/
+
+package gremlingo
+
+import "reflect"
+
+// PDTAdapter defines how to hydrate/dehydrate a provider-defined type.
+type PDTAdapter struct {
+ TypeName string
+ FromProperties func(map[string]interface{}) (interface{}, error)
+ ToProperties func(interface{}) (map[string]interface{}, error)
+}
+
+// PDTRegistry maps type names to their hydration adapters.
+type PDTRegistry struct {
+ adaptersByName map[string]*PDTAdapter
+ adaptersByType map[reflect.Type]*PDTAdapter
+}
+
+// NewPDTRegistry creates an empty PDTRegistry.
+func NewPDTRegistry() *PDTRegistry {
+ return &PDTRegistry{adaptersByName: make(map[string]*PDTAdapter), adaptersByType: make(map[reflect.Type]*PDTAdapter)}
+}
+
+// RegisterFuncs registers hydration/dehydration functions for a type name.
+func (r *PDTRegistry) RegisterFuncs(typeName string, fromProps func(map[string]interface{}) (interface{}, error), toProps func(interface{}) (map[string]interface{}, error)) {
+ adapter := &PDTAdapter{TypeName: typeName, FromProperties: fromProps, ToProperties: toProps}
+ r.adaptersByName[typeName] = adapter
+}
+
+// RegisterFuncsWithType registers hydration/dehydration functions for a type name and associates a Go type for dehydration lookup.
+func (r *PDTRegistry) RegisterFuncsWithType(typeName string, targetType reflect.Type, fromProps func(map[string]interface{}) (interface{}, error), toProps func(interface{}) (map[string]interface{}, error)) {
+ adapter := &PDTAdapter{TypeName: typeName, FromProperties: fromProps, ToProperties: toProps}
+ r.adaptersByName[typeName] = adapter
+ r.adaptersByType[targetType] = adapter
+}
+
+// RegisterType registers a struct type for reflection-based hydration using "pdt" struct tags.
+func (r *PDTRegistry) RegisterType(typeName string, targetType reflect.Type) {
+ r.adaptersByName[typeName] = &PDTAdapter{
+ TypeName: typeName,
+ FromProperties: func(props map[string]interface{}) (interface{}, error) {
+ obj := reflect.New(targetType).Elem()
+ for i := 0; i < targetType.NumField(); i++ {
+ field := targetType.Field(i)
+ tag := field.Tag.Get("pdt")
+ if tag == "" {
+ tag = field.Name
+ }
+ if val, ok := props[tag]; ok && val != nil {
+ obj.Field(i).Set(reflect.ValueOf(val))
+ }
+ }
+ return obj.Interface(), nil
+ },
+ }
+}
+
+// GetAdapterByType returns the adapter registered for the given Go type, or nil.
+func (r *PDTRegistry) GetAdapterByType(t reflect.Type) *PDTAdapter {
+ return r.adaptersByType[t]
+}
+
+// Hydrate converts a ProviderDefinedType into a domain object using the registered adapter.
+// Returns the raw PDT if no adapter is found or if hydration fails.
+func (r *PDTRegistry) Hydrate(pdt *ProviderDefinedType) interface{} {
+ if pdt == nil {
+ return nil
+ }
+ adapter, ok := r.adaptersByName[pdt.Name]
+ if !ok {
+ return pdt
+ }
+ hydratedProps := make(map[string]interface{}, len(pdt.Properties))
+ for k, v := range pdt.Properties {
+ if nested, ok := v.(*ProviderDefinedType); ok {
+ hydratedProps[k] = r.Hydrate(nested)
+ } else {
+ hydratedProps[k] = v
+ }
+ }
+ result, err := adapter.FromProperties(hydratedProps)
+ if err != nil {
+ return pdt
+ }
+ return result
+}
diff --git a/gremlin-go/driver/pdtRegistry_test.go b/gremlin-go/driver/pdtRegistry_test.go
new file mode 100644
index 0000000000..18a97566d8
--- /dev/null
+++ b/gremlin-go/driver/pdtRegistry_test.go
@@ -0,0 +1,92 @@
+/*
+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.
+*/
+
+package gremlingo
+
+import (
+ "errors"
+ "reflect"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPDTRegistryRegisterFuncsAndHydrate(t *testing.T) {
+ reg := NewPDTRegistry()
+ reg.RegisterFuncs("x:Point", func(props map[string]interface{}) (interface{}, error) {
+ return [2]int{props["x"].(int), props["y"].(int)}, nil
+ }, nil)
+
+ pdt := &ProviderDefinedType{Name: "x:Point", Properties: map[string]interface{}{"x": 1, "y": 2}}
+ result := reg.Hydrate(pdt)
+ assert.Equal(t, [2]int{1, 2}, result)
+}
+
+func TestPDTRegistryNoAdapterReturnsRawPDT(t *testing.T) {
+ reg := NewPDTRegistry()
+ pdt := &ProviderDefinedType{Name: "x:Unknown", Properties: map[string]interface{}{"a": "b"}}
+ result := reg.Hydrate(pdt)
+ assert.Equal(t, pdt, result)
+}
+
+func TestPDTRegistryAdapterErrorReturnsRawPDT(t *testing.T) {
+ reg := NewPDTRegistry()
+ reg.RegisterFuncs("x:Bad", func(props map[string]interface{}) (interface{}, error) {
+ return nil, errors.New("fail")
+ }, nil)
+
+ pdt := &ProviderDefinedType{Name: "x:Bad", Properties: map[string]interface{}{}}
+ result := reg.Hydrate(pdt)
+ assert.Equal(t, pdt, result)
+}
+
+func TestPDTRegistryNestedHydration(t *testing.T) {
+ reg := NewPDTRegistry()
+ reg.RegisterFuncs("x:Inner", func(props map[string]interface{}) (interface{}, error) {
+ return props["val"].(string) + "!", nil
+ }, nil)
+ reg.RegisterFuncs("x:Outer", func(props map[string]interface{}) (interface{}, error) {
+ return "outer:" + props["child"].(string), nil
+ }, nil)
+
+ inner := &ProviderDefinedType{Name: "x:Inner", Properties: map[string]interface{}{"val": "hi"}}
+ outer := &ProviderDefinedType{Name: "x:Outer", Properties: map[string]interface{}{"child": inner}}
+ result := reg.Hydrate(outer)
+ assert.Equal(t, "outer:hi!", result)
+}
+
+type testPoint struct {
+ X int `pdt:"x"`
+ Y int `pdt:"y"`
+ L string // no tag, uses field name
+}
+
+func TestPDTRegistryRegisterType(t *testing.T) {
+ reg := NewPDTRegistry()
+ reg.RegisterType("x:Point", reflect.TypeOf(testPoint{}))
+
+ pdt := &ProviderDefinedType{Name: "x:Point", Properties: map[string]interface{}{"x": 3, "y": 4, "L": "label"}}
+ result := reg.Hydrate(pdt)
+ assert.Equal(t, testPoint{X: 3, Y: 4, L: "label"}, result)
+}
+
+func TestPDTRegistryHydrateNil(t *testing.T) {
+ reg := NewPDTRegistry()
+ assert.Nil(t, reg.Hydrate(nil))
+}
diff --git a/gremlin-go/driver/providerDefinedType.go b/gremlin-go/driver/providerDefinedType.go
new file mode 100644
index 0000000000..32f1ae1c69
--- /dev/null
+++ b/gremlin-go/driver/providerDefinedType.go
@@ -0,0 +1,87 @@
+/*
+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.
+*/
+
+package gremlingo
+
+import (
+ "fmt"
+ "io"
+ "reflect"
+)
+
+// ProviderDefinedType represents a provider-defined type (PDT) in GraphBinary serialization.
+type ProviderDefinedType struct {
+ Name string
+ Properties map[string]interface{}
+}
+
+// PDTProvider is implemented by types that can be auto-dehydrated into a ProviderDefinedType
+// for gremlin-lang serialization.
+type PDTProvider interface {
+ PDTName() string
+ PDTProperties() map[string]interface{}
+}
+
+// pdtProviderTypes is a package-level registry of PDTProvider types keyed by PDT name.
+// Populated lazily during dehydration and consulted during hydration as a fallback.
+var pdtProviderTypes = make(map[string]reflect.Type)
+
+func (p *ProviderDefinedType) String() string {
+ return fmt.Sprintf("pdt[%s]%v", p.Name, p.Properties)
+}
+
+// pdtWriter serializes a ProviderDefinedType as a fully-qualified string (name) followed by a fully-qualified map (properties).
+func pdtWriter(value interface{}, w io.Writer, typeSerializer *graphBinaryTypeSerializer) error {
+ pdt := value.(*ProviderDefinedType)
+ if err := typeSerializer.write(pdt.Name, w); err != nil {
+ return err
+ }
+ if pdt.Properties == nil {
+ return typeSerializer.write(map[interface{}]interface{}{}, w)
+ }
+ m := make(map[interface{}]interface{}, len(pdt.Properties))
+ for k, v := range pdt.Properties {
+ m[k] = v
+ }
+ return typeSerializer.write(m, w)
+}
+
+// hydratePDTProvider creates an instance of the registered PDTProvider type and populates
+// exported fields from the properties map using "pdt" struct tags (falling back to field name).
+func hydratePDTProvider(t reflect.Type, props map[string]interface{}) interface{} {
+ if t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+ obj := reflect.New(t).Elem()
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+ if !field.IsExported() {
+ continue
+ }
+ tag := field.Tag.Get("pdt")
+ if tag == "" {
+ key := string([]rune{rune(field.Name[0] + 32)}) + field.Name[1:]
+ tag = key
+ }
+ if val, ok := props[tag]; ok && val != nil {
+ obj.Field(i).Set(reflect.ValueOf(val))
+ }
+ }
+ return obj.Addr().Interface()
+}
\ No newline at end of file
diff --git a/gremlin-go/driver/providerDefinedType_test.go b/gremlin-go/driver/providerDefinedType_test.go
new file mode 100644
index 0000000000..91963456d8
--- /dev/null
+++ b/gremlin-go/driver/providerDefinedType_test.go
@@ -0,0 +1,36 @@
+/*
+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.
+*/
+
+package gremlingo
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProviderDefinedType(t *testing.T) {
+ t.Run("String method", func(t *testing.T) {
+ pdt := &ProviderDefinedType{
+ Name: "com.example.Test",
+ Properties: map[string]interface{}{"a": int32(1)},
+ }
+ assert.Contains(t, pdt.String(), "pdt[com.example.Test]")
+ })
+}
diff --git a/gremlin-go/driver/serializer.go b/gremlin-go/driver/serializer.go
index bd4a426208..11fec813fc 100644
--- a/gremlin-go/driver/serializer.go
+++ b/gremlin-go/driver/serializer.go
@@ -24,7 +24,6 @@ import (
"encoding/binary"
"fmt"
"io"
- "sync"
)
const graphBinaryMimeType = "application/vnd.graphbinary-v4.0"
@@ -40,22 +39,12 @@ type GraphBinarySerializer struct {
ser *graphBinaryTypeSerializer
}
-// CustomTypeReader user provided function to deserialize custom types
-// Deprecated: Custom type deserialization is handled by GraphBinaryDeserializer
-type CustomTypeReader func(data *[]byte, i *int) (interface{}, error)
-
type writer func(interface{}, io.Writer, *graphBinaryTypeSerializer) error
var serializers map[dataType]writer
-// customTypeReaderLock used to synchronize access to the customDeserializers map
-// Deprecated: Custom type deserialization is handled by GraphBinaryDeserializer
-var customTypeReaderLock = sync.RWMutex{}
-var customDeserializers map[string]CustomTypeReader
-
func init() {
initSerializers()
- customDeserializers = map[string]CustomTypeReader{}
}
func newGraphBinarySerializer(handler *logHandler) *GraphBinarySerializer {
@@ -231,21 +220,8 @@ func initSerializers() {
setType: setWriter,
byteBuffer: byteBufferWriter,
markerType: markerWriter,
+ compositePDTType: pdtWriter,
}
}
-// RegisterCustomTypeReader register a reader (deserializer) for a custom type
-// Deprecated: Custom type deserialization should be handled by extending GraphBinaryDeserializer
-func RegisterCustomTypeReader(customTypeName string, reader CustomTypeReader) {
- customTypeReaderLock.Lock()
- defer customTypeReaderLock.Unlock()
- customDeserializers[customTypeName] = reader
-}
-// UnregisterCustomTypeReader unregister a reader (deserializer) for a custom type
-// Deprecated: Custom type deserialization should be handled by extending GraphBinaryDeserializer
-func UnregisterCustomTypeReader(customTypeName string) {
- customTypeReaderLock.Lock()
- defer customTypeReaderLock.Unlock()
- delete(customDeserializers, customTypeName)
-}
diff --git a/gremlin-go/driver/traversal_test.go b/gremlin-go/driver/traversal_test.go
index bc3fb33916..93a786f355 100644
--- a/gremlin-go/driver/traversal_test.go
+++ b/gremlin-go/driver/traversal_test.go
@@ -26,6 +26,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestTraversal(t *testing.T) {
@@ -855,3 +856,109 @@ func getCount(t *testing.T, g *GraphTraversalSource) int32 {
assert.Nil(t, err)
return val
}
+
+func TestProviderDefinedTypeTraversalAPIIntegration(t *testing.T) {
+ testNoAuthUrl := getEnvOrDefaultString("GREMLIN_SERVER_URL", noAuthUrl)
+ testNoAuthEnable := getEnvOrDefaultBool("RUN_INTEGRATION_TESTS", true)
+
+ t.Run("raw PDT round-trip via Traversal API", func(t *testing.T) {
+ skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+ remote, err := NewDriverRemoteConnection(testNoAuthUrl,
+ func(settings *DriverRemoteConnectionSettings) {
+ settings.TlsConfig = &tls.Config{}
+ settings.TraversalSource = testServerModernGraphAlias
+ })
+ require.NoError(t, err)
+ defer remote.Close()
+
+ g := Traversal_().With(remote)
+ pdt := &ProviderDefinedType{Name: "TestPoint", Properties: map[string]interface{}{"x": int32(1), "y": int32(2)}}
+
+ results, err := g.Inject(pdt).ToList()
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+
+ result, ok := results[0].GetInterface().(*ProviderDefinedType)
+ require.True(t, ok, "expected *ProviderDefinedType, got %T", results[0].GetInterface())
+ assert.Equal(t, "TestPoint", result.Name)
+ assert.Equal(t, int32(1), result.Properties["x"])
+ assert.Equal(t, int32(2), result.Properties["y"])
+ })
+
+ t.Run("registry-based round-trip via typed struct", func(t *testing.T) {
+ skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+ registry := NewPDTRegistry()
+ registry.RegisterFuncsWithType("RegPoint", reflect.TypeOf(regPoint{}),
+ func(props map[string]interface{}) (interface{}, error) {
+ return ®Point{X: props["x"].(int32), Y: props["y"].(int32)}, nil
+ },
+ func(obj interface{}) (map[string]interface{}, error) {
+ p := obj.(regPoint)
+ return map[string]interface{}{"x": p.X, "y": p.Y}, nil
+ })
+
+ remote, err := NewDriverRemoteConnection(testNoAuthUrl,
+ func(settings *DriverRemoteConnectionSettings) {
+ settings.TlsConfig = &tls.Config{}
+ settings.TraversalSource = testServerModernGraphAlias
+ settings.PDTRegistry = registry
+ })
+ require.NoError(t, err)
+ defer remote.Close()
+
+ g := Traversal_().With(remote)
+ point := regPoint{X: 5, Y: 10}
+
+ results, err := g.Inject(point).ToList()
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+
+ result, ok := results[0].GetInterface().(*regPoint)
+ require.True(t, ok, "expected *regPoint, got %T", results[0].GetInterface())
+ assert.Equal(t, int32(5), result.X)
+ assert.Equal(t, int32(10), result.Y)
+ })
+
+ t.Run("PDTProvider round-trip", func(t *testing.T) {
+ skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+ remote, err := NewDriverRemoteConnection(testNoAuthUrl,
+ func(settings *DriverRemoteConnectionSettings) {
+ settings.TlsConfig = &tls.Config{}
+ settings.TraversalSource = testServerModernGraphAlias
+ })
+ require.NoError(t, err)
+ defer remote.Close()
+
+ g := Traversal_().With(remote)
+ point := pdtTestPoint{X: 3, Y: 7}
+
+ results, err := g.Inject(point).ToList()
+ require.NoError(t, err)
+ require.Len(t, results, 1)
+
+ result, ok := results[0].GetInterface().(*pdtTestPoint)
+ require.True(t, ok, "expected *pdtTestPoint, got %T", results[0].GetInterface())
+ assert.Equal(t, int32(3), result.X)
+ assert.Equal(t, int32(7), result.Y)
+ })
+}
+
+// pdtTestPoint implements PDTProvider for auto-dehydration tests.
+// regPoint is a plain struct with no PDTProvider interface - used for registry-based tests
+type regPoint struct {
+ X int32
+ Y int32
+}
+
+type pdtTestPoint struct {
+ X int32
+ Y int32
+}
+
+func (p pdtTestPoint) PDTName() string {
+ return "TestPoint"
+}
+
+func (p pdtTestPoint) PDTProperties() map[string]interface{} {
+ return map[string]interface{}{"x": p.X, "y": p.Y}
+}
\ No newline at end of file
diff --git a/gremlin-js/gremlin-javascript/lib/driver/connection.ts b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
index ea1b231d59..2608c908b2 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
@@ -53,6 +53,7 @@ export type ConnectionOptions = {
ca?: string[];
cert?: string | string[] | Buffer;
pfx?: string | Buffer;
+ pdtRegistry?: any;
reader?: any;
rejectUnauthorized?: boolean;
traversalSource?: string;
@@ -89,6 +90,9 @@ export default class Connection extends EventEmitter {
this._reader = options.reader || graphBinaryReader;
this._writer = 'writer' in options ? options.writer : graphBinaryWriter;
+ if (options.pdtRegistry) {
+ this._reader.ioc.pdtRegistry = options.pdtRegistry;
+ }
this.traversalSource = options.traversalSource || 'g';
this._enableUserAgentOnConnect = options.enableUserAgentOnConnect !== false;
diff --git a/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts b/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts
index 44007e9bd4..adf149779e 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/remote-connection.ts
@@ -39,7 +39,7 @@ export abstract class RemoteConnection {
*/
constructor(
public url: string,
- protected readonly options: RemoteConnectionOptions = {},
+ public readonly options: RemoteConnectionOptions = {},
) {}
/**
diff --git a/gremlin-js/gremlin-javascript/lib/index.ts b/gremlin-js/gremlin-javascript/lib/index.ts
index fd40186eb6..18f9e04453 100644
--- a/gremlin-js/gremlin-javascript/lib/index.ts
+++ b/gremlin-js/gremlin-javascript/lib/index.ts
@@ -25,6 +25,7 @@ import * as t from './process/traversal.js';
import * as gt from './process/graph-traversal.js';
import * as strategiesModule from './process/traversal-strategy.js';
import * as graph from './structure/graph.js';
+import { ProviderDefinedTypeRegistry } from './structure/ProviderDefinedTypeRegistry.js';
import * as rc from './driver/remote-connection.js';
import GremlinLang from './process/gremlin-lang.js';
import * as utils from './utils.js';
@@ -84,6 +85,8 @@ export const structure = {
Graph: graph.Graph,
Path: graph.Path,
Property: graph.Property,
+ ProviderDefinedType: graph.ProviderDefinedType,
+ ProviderDefinedTypeRegistry,
Vertex: graph.Vertex,
VertexProperty: graph.VertexProperty,
toLong: utils.toLong,
diff --git a/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts b/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts
index 3432735ac3..5920c48dc9 100644
--- a/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/anonymous-traversal.ts
@@ -59,10 +59,14 @@ export default class AnonymousTraversalSource {
with_(connection: RemoteConnection) {
const traversalStrategies = new TraversalStrategies();
traversalStrategies.addStrategy(new RemoteStrategy(connection));
+ const gl = new GremlinLang();
+ if (connection.options?.pdtRegistry) {
+ gl.pdtRegistry = connection.options.pdtRegistry;
+ }
return new this.traversalSourceClass!(
new Graph(),
traversalStrategies,
- new GremlinLang(),
+ gl,
this.traversalSourceClass,
this.traversalClass,
);
diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
index 6d8c09287b..6c0179b590 100644
--- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
@@ -20,19 +20,22 @@
import { P, TextP, EnumValue } from './traversal.js';
import { OptionsStrategy, TraversalStrategy } from './traversal-strategy.js';
import { Long } from '../utils.js';
-import { Vertex } from '../structure/graph.js';
+import { Vertex, ProviderDefinedType, pdtDecoratedTypes } from '../structure/graph.js';
+import { ProviderDefinedTypeRegistry } from '../structure/ProviderDefinedTypeRegistry.js';
import { Buffer } from 'buffer';
export default class GremlinLang {
private gremlin: string = '';
private optionsStrategies: OptionsStrategy[] = [];
private parameters: Map<string, any> = new Map();
+ pdtRegistry: ProviderDefinedTypeRegistry | null = null;
constructor(toClone?: GremlinLang) {
if (toClone) {
this.gremlin = toClone.gremlin;
this.optionsStrategies = [...toClone.optionsStrategies];
this.parameters = new Map(toClone.parameters);
+ this.pdtRegistry = toClone.pdtRegistry;
}
}
@@ -113,6 +116,14 @@ export default class GremlinLang {
if (typeof arg === 'function' && arg.prototype instanceof TraversalStrategy) {
return arg.name;
}
+ if (arg instanceof ProviderDefinedType) {
+ const props = arg.properties;
+ const keys = Object.keys(props);
+ const escapedName = JSON.stringify(arg.name).slice(1, -1);
+ if (keys.length === 0) return `PDT("${escapedName}",[:])`;
+ const entries = keys.map(k => `${this._argAsString(k)}:${this._argAsString(props[k])}`);
+ return `PDT("${escapedName}",[${entries.join(',')}])`;
+ }
if (arg instanceof Vertex) {
return this._argAsString(arg.id);
}
@@ -152,6 +163,24 @@ export default class GremlinLang {
if (entries.length === 0) return '[:]';
return '[' + entries.map(([k, v]) => `${this._argAsString(k)}:${this._argAsString(v)}`).join(',') + ']';
}
+ // Registry-based dehydration
+ if (this.pdtRegistry && typeof arg === 'object' && arg.constructor) {
+ const entry = this.pdtRegistry.getAdapterByClass(arg.constructor);
+ if (entry) {
+ const props = entry.serialize(arg);
+ return this._argAsString(new ProviderDefinedType(entry.typeName, props));
+ }
+ }
+ // Auto-dehydrate objects whose constructor declares a static pdtName
+ if (typeof arg === 'object' && arg.constructor?.pdtName) {
+ const name = arg.constructor.pdtName as string;
+ pdtDecoratedTypes.set(name, arg.constructor);
+ const props: Record<string, any> = {};
+ for (const key of Object.keys(arg)) {
+ if (!key.startsWith('_')) props[key] = arg[key];
+ }
+ return this._argAsString(new ProviderDefinedType(name, props));
+ }
throw new TypeError(`GremlinLang contains at least one type [${arg?.constructor?.name ?? typeof arg}] that cannot be represented as text.`);
}
diff --git a/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts b/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts
new file mode 100644
index 0000000000..3a8c300aa7
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/structure/ProviderDefinedTypeRegistry.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { ProviderDefinedType } from './graph.js';
+
+export interface PdtAdapter {
+ serialize: (obj: any) => Record<string, any>;
+ deserialize: (properties: Record<string, any>) => any;
+}
+
+/**
+ * A standalone registry that allows users to register adapters for hydrating
+ * raw {@link ProviderDefinedType} 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();
+
+ register(typeName: string, adapter: PdtAdapter, targetClass?: Function): void {
+ this._adapters.set(typeName, adapter);
+ if (targetClass) {
+ this._adaptersByClass.set(targetClass, { typeName, adapter });
+ }
+ }
+
+ hydrate(pdt: any): any {
+ if (!(pdt instanceof ProviderDefinedType)) return pdt;
+ const adapter = this._adapters.get(pdt.name);
+ if (!adapter) return pdt;
+ try {
+ const hydratedProps: Record<string, any> = {};
+ for (const [k, v] of Object.entries(pdt.properties)) {
+ hydratedProps[k] = v instanceof ProviderDefinedType ? this.hydrate(v) : v;
+ }
+ return adapter.deserialize(hydratedProps);
+ } catch (e: any) {
+ console.warn(`PDT hydration failed for '${pdt.name}': ${e.message}`);
+ return pdt;
+ }
+ }
+
+ hasAdapter(typeName: string): boolean {
+ return this._adapters.has(typeName);
+ }
+
+ getSerializer(typeName: string): ((obj: any) => Record<string, any>) | null {
+ const adapter = this._adapters.get(typeName);
+ return adapter ? adapter.serialize : null;
+ }
+
+ getAdapterByClass(cls: Function): { typeName: string; serialize: (obj: any) => Record<string, any> } | null {
+ const entry = this._adaptersByClass.get(cls);
+ if (!entry) return null;
+ return { typeName: entry.typeName, serialize: entry.adapter.serialize };
+ }
+}
diff --git a/gremlin-js/gremlin-javascript/lib/structure/graph.ts b/gremlin-js/gremlin-javascript/lib/structure/graph.ts
index c8b8644360..a6565e76bf 100644
--- a/gremlin-js/gremlin-javascript/lib/structure/graph.ts
+++ b/gremlin-js/gremlin-javascript/lib/structure/graph.ts
@@ -185,6 +185,30 @@ function areEqual(obj1: any, obj2: any) {
return false;
}
+/**
+ * Represents a composite Provider Defined Type (PDT).
+ */
+export class ProviderDefinedType {
+ readonly name: string;
+ readonly properties: Readonly<Record<string, any>>;
+
+ constructor(name: string, properties?: Record<string, any>) {
+ if (!name) throw new Error('ProviderDefinedType name cannot be null or empty');
+ this.name = name;
+ this.properties = Object.freeze(properties || {});
+ }
+
+ toString() {
+ return `pdt[${this.name}]${JSON.stringify(this.properties)}`;
+ }
+}
+
+/**
+ * Module-level registry of classes with static pdtName, keyed by PDT name.
+ * Populated lazily during dehydration and consulted during hydration.
+ */
+export const pdtDecoratedTypes: Map<string, Function> = new Map();
+
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 b8930f91c4..8adcb34bfd 100644
--- a/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
+++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/GraphBinary.js
@@ -65,6 +65,7 @@ import MarkerSerializer from './internals/MarkerSerializer.js';
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 NumberSerializationStrategy from './internals/NumberSerializationStrategy.js';
import AnySerializer from './internals/AnySerializer.js';
import GraphBinaryReader from './internals/GraphBinaryReader.js';
@@ -104,9 +105,11 @@ ioc.enumSerializer = new EnumSerializer(ioc);
// Register stub serializers for unimplemented v4 types
new StubSerializer(ioc, ioc.DataType.TREE, 'Tree');
new StubSerializer(ioc, ioc.DataType.GRAPH, 'Graph');
-new StubSerializer(ioc, ioc.DataType.COMPOSITEPDT, 'CompositePDT');
+ioc.compositePDTSerializer = new CompositePDTSerializer(ioc);
new StubSerializer(ioc, ioc.DataType.PRIMITIVEPDT, 'PrimitivePDT');
+ioc.pdtRegistry = null;
+
ioc.numberSerializationStrategy = new NumberSerializationStrategy(ioc);
ioc.anySerializer = new AnySerializer(ioc);
@@ -140,6 +143,7 @@ export const {
markerSerializer,
unspecifiedNullSerializer,
enumSerializer,
+ compositePDTSerializer,
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 c3bd93bf1d..2b17d99683 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
@@ -42,6 +42,7 @@ export default class AnySerializer {
ioc.enumSerializer,
ioc.stringSerializer,
ioc.binarySerializer,
+ ioc.compositePDTSerializer,
ioc.mapSerializer,
];
}
diff --git a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/CompositePDTSerializer.js b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/CompositePDTSerializer.js
new file mode 100644
index 0000000000..2085594bda
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/CompositePDTSerializer.js
@@ -0,0 +1,89 @@
+/*
+ * 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 { ProviderDefinedType, pdtDecoratedTypes } from '../../../graph.js';
+
+export default class CompositePDTSerializer {
+ constructor(ioc) {
+ this.ioc = ioc;
+ this.ioc.serializers[ioc.DataType.COMPOSITEPDT] = this;
+ }
+
+ canBeUsedFor(value) {
+ return value instanceof ProviderDefinedType;
+ }
+
+ serialize(item, fullyQualifiedFormat = true) {
+ if (item === undefined || item === null) {
+ if (fullyQualifiedFormat) {
+ return Buffer.from([this.ioc.DataType.COMPOSITEPDT, 0x01]);
+ }
+ const bufs = [];
+ bufs.push(this.ioc.stringSerializer.serialize('', false));
+ bufs.push(this.ioc.mapSerializer.serialize({}, false));
+ return Buffer.concat(bufs);
+ }
+
+ const bufs = [];
+ if (fullyQualifiedFormat) {
+ bufs.push(Buffer.from([this.ioc.DataType.COMPOSITEPDT, 0x00]));
+ }
+ bufs.push(this.ioc.stringSerializer.serialize(item.name, true));
+ bufs.push(this.ioc.mapSerializer.serialize(item.properties, true));
+ return Buffer.concat(bufs);
+ }
+
+ async deserializeValue(reader, valueFlag, typeCode) {
+ const name = await this.ioc.anySerializer.deserialize(reader);
+ if (!name) {
+ throw new Error('CompositePDTSerializer: name cannot be null or empty');
+ }
+ const properties = await this.ioc.anySerializer.deserialize(reader);
+ const props = properties instanceof Map ? Object.fromEntries(properties) : properties || {};
+ const pdt = new ProviderDefinedType(name, props);
+ if (this.ioc.pdtRegistry) {
+ const hydrated = this.ioc.pdtRegistry.hydrate(pdt);
+ if (!(hydrated instanceof ProviderDefinedType)) {
+ return hydrated;
+ }
+ }
+ if (pdtDecoratedTypes.has(name)) {
+ const cls = pdtDecoratedTypes.get(name);
+ const obj = Object.assign(Object.create(cls.prototype), props);
+ return obj;
+ }
+ return pdt;
+ }
+
+ async deserialize(reader) {
+ const type_code = await reader.readUInt8();
+ if (type_code !== this.ioc.DataType.COMPOSITEPDT) {
+ throw new Error(`CompositePDTSerializer: 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(`CompositePDTSerializer: 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 09a4cd8230..a85b9522ff 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 } from '../../lib/structure/graph.js';
+import { Vertex, Edge, VertexProperty, ProviderDefinedType } from '../../lib/structure/graph.js';
import { getClient } from '../helper.js';
import { cardinality } from '../../lib/process/traversal.js';
@@ -177,4 +177,69 @@ function assertVertexProperties(vertex) {
assert.ok(end);
assert.strictEqual(start.value, 1990);
assert.strictEqual(end.value, 2000);
-}
\ No newline at end of file
+}
+
+describe('ProviderDefinedType - Client', function () {
+ let pdtClient;
+ before(function () {
+ pdtClient = getClient('gmodern');
+ return pdtClient.open();
+ });
+ after(function () {
+ return pdtClient.close();
+ });
+
+ it('should round-trip a simple Point PDT', function () {
+ return pdtClient.submit('g.inject(PDT("Point", ["x":1, "y":2]))')
+ .then(function (result) {
+ assert.strictEqual(result.length, 1);
+ const pdt = result.first();
+ assert.ok(pdt instanceof ProviderDefinedType);
+ assert.strictEqual(pdt.name, 'Point');
+ assert.strictEqual(pdt.properties.x, 1);
+ assert.strictEqual(pdt.properties.y, 2);
+ });
+ });
+
+ it('should round-trip a nested PDT (Person with Address)', function () {
+ return pdtClient.submit(
+ 'g.inject(PDT("Person", ["name":"Alice", "age":30, ' +
+ '"address":PDT("Address", ["street":"123 Main St", "city":"Springfield", "zip":"12345"])]))')
+ .then(function (result) {
+ assert.strictEqual(result.length, 1);
+ const pdt = result.first();
+ assert.ok(pdt instanceof ProviderDefinedType);
+ assert.strictEqual(pdt.name, 'Person');
+ assert.strictEqual(pdt.properties.name, 'Alice');
+ assert.strictEqual(pdt.properties.age, 30);
+
+ const address = pdt.properties.address;
+ assert.ok(address instanceof ProviderDefinedType);
+ assert.strictEqual(address.name, 'Address');
+ assert.strictEqual(address.properties.street, '123 Main St');
+ assert.strictEqual(address.properties.city, 'Springfield');
+ assert.strictEqual(address.properties.zip, '12345');
+ });
+ });
+
+ it('should handle PDTs in a collection', function () {
+ return pdtClient.submit(
+ 'g.inject([PDT("Point", ["x":1, "y":2]), PDT("Point", ["x":3, "y":4])])')
+ .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 ProviderDefinedType);
+ assert.strictEqual(list[0].name, 'Point');
+ assert.strictEqual(list[0].properties.x, 1);
+ assert.strictEqual(list[0].properties.y, 2);
+
+ assert.ok(list[1] instanceof ProviderDefinedType);
+ assert.strictEqual(list[1].name, 'Point');
+ assert.strictEqual(list[1].properties.x, 3);
+ assert.strictEqual(list[1].properties.y, 4);
+ });
+ });
+});
\ 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 1ada7cabff..c21ee8992f 100644
--- a/gremlin-js/gremlin-javascript/test/integration/traversal-test.js
+++ b/gremlin-js/gremlin-javascript/test/integration/traversal-test.js
@@ -23,7 +23,8 @@
import assert from 'assert';
import { AssertionError } from 'assert';
-import {Edge, Vertex, VertexProperty} from '../../lib/structure/graph.js';
+import {Edge, Vertex, VertexProperty, ProviderDefinedType} 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';
import {
@@ -31,6 +32,7 @@ import {
OptionsStrategy, ReservedKeysVerificationStrategy, EdgeLabelVerificationStrategy, MatchAlgorithmStrategy
} from '../../lib/process/traversal-strategy.js';
import GremlinLang from '../../lib/process/gremlin-lang.js';
+import DriverRemoteConnection from '../../lib/driver/driver-remote-connection.js';
import { getConnection, getDriverRemoteConnection } from '../helper.js';
const __ = statics;
@@ -433,4 +435,112 @@ describe('Traversal', function () {
// assert.ok(!tx._sessionBasedConnection.isOpen);
// });
// });
+});
+
+let serverUrl;
+if (process.env.DOCKER_ENVIRONMENT === 'true') {
+ serverUrl = 'http://gremlin-server-test-js:45940/gremlin';
+} else {
+ serverUrl = 'http://localhost:45940/gremlin';
+}
+
+describe('ProviderDefinedType - Traversal API', function () {
+ describe('raw 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 PDT via g.inject()', async function () {
+ const g = anon.traversal().with_(pdtConnection);
+ const pdt = new ProviderDefinedType('TestPoint', { x: 1, y: 2 });
+
+ const results = await g.inject(pdt).toList();
+
+ assert.strictEqual(results.length, 1);
+ const result = results[0];
+ assert.ok(result instanceof ProviderDefinedType);
+ assert.strictEqual(result.name, 'TestPoint');
+ assert.strictEqual(result.properties.x, 1);
+ assert.strictEqual(result.properties.y, 2);
+ });
+ });
+
+ describe('registry-based round-trip via typed object', function () {
+ let pdtConnection;
+
+ class TestPoint {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ before(function () {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('TestPoint', {
+ serialize: (obj) => ({ x: obj.x, y: obj.y }),
+ deserialize: (props) => new TestPoint(props.x, props.y),
+ }, TestPoint);
+ pdtConnection = new DriverRemoteConnection(serverUrl, {
+ traversalSource: 'gmodern',
+ pdtRegistry: registry,
+ });
+ return pdtConnection.open();
+ });
+ after(function () {
+ return pdtConnection.close();
+ });
+
+ it('should auto-dehydrate on send and auto-hydrate on receive via registry', async function () {
+ const g = anon.traversal().with_(pdtConnection);
+ const point = new TestPoint(5, 10);
+
+ const results = await g.inject(point).toList();
+
+ assert.strictEqual(results.length, 1);
+ const result = results[0];
+ assert.ok(result instanceof TestPoint);
+ assert.strictEqual(result.x, 5);
+ assert.strictEqual(result.y, 10);
+ });
+ });
+
+ describe('auto-dehydration on send', function () {
+ let pdtConnection;
+
+ before(function () {
+ pdtConnection = getConnection('gmodern');
+ return pdtConnection.open();
+ });
+ after(function () {
+ return pdtConnection.close();
+ });
+
+ it('should auto-dehydrate a class with static pdtName to PDT format', async function () {
+ class TestPoint {
+ static pdtName = 'TestPoint';
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ const g = anon.traversal().with_(pdtConnection);
+ const point = new TestPoint(3, 7);
+
+ const results = await g.inject(point).toList();
+
+ assert.strictEqual(results.length, 1);
+ const result = results[0];
+ assert.ok(result instanceof TestPoint);
+ assert.strictEqual(result.x, 3);
+ assert.strictEqual(result.y, 7);
+ });
+ });
});
\ No newline at end of file
diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/CompositePDTSerializer-test.js b/gremlin-js/gremlin-javascript/test/unit/graphbinary/CompositePDTSerializer-test.js
new file mode 100644
index 0000000000..b78c3b7cf5
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/CompositePDTSerializer-test.js
@@ -0,0 +1,153 @@
+/*
+ * 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 { 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, compositePDTSerializer } = ioc;
+
+async function roundTrip(value) {
+ const bytes = anySerializer.serialize(value);
+ return anySerializer.deserialize(StreamReader.fromBuffer(bytes));
+}
+
+describe('CompositePDTSerializer', () => {
+ describe('round-trip: simple PDT', () => {
+ it('serializes and deserializes a simple ProviderDefinedType', async () => {
+ const pdt = new ProviderDefinedType('myType', { key1: 'value1', key2: 42 });
+ const result = await roundTrip(pdt);
+ assert.instanceOf(result, ProviderDefinedType);
+ assert.strictEqual(result.name, 'myType');
+ assert.strictEqual(result.properties.key1, 'value1');
+ assert.strictEqual(result.properties.key2, 42);
+ });
+
+ it('uses COMPOSITEPDT type code', () => {
+ const pdt = new ProviderDefinedType('test', { a: 1 });
+ const bytes = anySerializer.serialize(pdt);
+ assert.strictEqual(bytes[0], DataType.COMPOSITEPDT);
+ });
+ });
+
+ describe('round-trip: nested PDT', () => {
+ it('serializes and deserializes a PDT with nested PDT in properties', async () => {
+ const inner = new ProviderDefinedType('inner', { x: 'hello' });
+ const outer = new ProviderDefinedType('outer', { nested: inner, num: 99 });
+ const result = await roundTrip(outer);
+ assert.instanceOf(result, ProviderDefinedType);
+ assert.strictEqual(result.name, 'outer');
+ assert.strictEqual(result.properties.num, 99);
+ assert.instanceOf(result.properties.nested, ProviderDefinedType);
+ assert.strictEqual(result.properties.nested.name, 'inner');
+ assert.strictEqual(result.properties.nested.properties.x, 'hello');
+ });
+ });
+
+ describe('round-trip: null/undefined field value', () => {
+ it('handles null property values', async () => {
+ const pdt = new ProviderDefinedType('withNull', { present: 'yes', absent: null });
+ const result = await roundTrip(pdt);
+ assert.instanceOf(result, ProviderDefinedType);
+ assert.strictEqual(result.name, 'withNull');
+ assert.strictEqual(result.properties.present, 'yes');
+ assert.strictEqual(result.properties.absent, null);
+ });
+ });
+
+ describe('empty name rejected', () => {
+ it('constructor rejects empty string name', () => {
+ assert.throws(() => new ProviderDefinedType('', { a: 1 }), /name cannot be null or empty/);
+ });
+
+ it('constructor rejects null name', () => {
+ assert.throws(() => new ProviderDefinedType(null, { a: 1 }), /name cannot be null or empty/);
+ });
+
+ it('constructor rejects undefined name', () => {
+ assert.throws(() => new ProviderDefinedType(undefined, { a: 1 }), /name cannot be null or empty/);
+ });
+
+ it('deserializer rejects null name from wire', async () => {
+ // Manually craft bytes: type_code=0xf0, value_flag=0x00, then null string, then empty map
+ const nullString = Buffer.from([DataType.STRING, 0x01]); // null string
+ const emptyMap = Buffer.from([DataType.MAP, 0x00, 0x00, 0x00, 0x00, 0x00]); // map with 0 entries
+ const bytes = Buffer.concat([
+ Buffer.from([DataType.COMPOSITEPDT, 0x00]),
+ nullString,
+ emptyMap,
+ ]);
+ 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 ProviderDefinedType instances', () => {
+ assert.isTrue(compositePDTSerializer.canBeUsedFor(new ProviderDefinedType('t', {})));
+ });
+
+ it('returns false for plain objects', () => {
+ assert.isFalse(compositePDTSerializer.canBeUsedFor({ name: 'test' }));
+ });
+
+ it('returns false for strings', () => {
+ assert.isFalse(compositePDTSerializer.canBeUsedFor('test'));
+ });
+ });
+
+ describe('auto-hydration via ioc.pdtRegistry', () => {
+ afterEach(() => {
+ ioc.pdtRegistry = null;
+ });
+
+ it('auto-hydrates when pdtRegistry is set', async () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('myType', {
+ serialize: (obj) => obj,
+ deserialize: (props) => ({ hydrated: true, ...props }),
+ });
+ ioc.pdtRegistry = registry;
+
+ const pdt = new ProviderDefinedType('myType', { key1: 'value1', key2: 42 });
+ const bytes = anySerializer.serialize(pdt);
+ const result = await anySerializer.deserialize(StreamReader.fromBuffer(bytes));
+
+ assert.notInstanceOf(result, ProviderDefinedType);
+ assert.strictEqual(result.hydrated, true);
+ assert.strictEqual(result.key1, 'value1');
+ assert.strictEqual(result.key2, 42);
+ });
+
+ it('returns raw PDT when no pdtRegistry is set', async () => {
+ const pdt = new ProviderDefinedType('myType', { key1: 'value1' });
+ const bytes = anySerializer.serialize(pdt);
+ const result = await anySerializer.deserialize(StreamReader.fromBuffer(bytes));
+
+ assert.instanceOf(result, ProviderDefinedType);
+ assert.strictEqual(result.name, 'myType');
+ });
+ });
+});
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 40472c99f5..e30ae2e915 100644
--- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
@@ -25,7 +25,7 @@ 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 } from '../../lib/structure/graph.js';
+import { Graph, Vertex, ProviderDefinedType } from '../../lib/structure/graph.js';
import { TraversalStrategies } from '../../lib/process/traversal-strategy.js';
import { Long } from '../../lib/utils.js';
import GremlinLang from '../../lib/process/gremlin-lang.js';
@@ -494,4 +494,54 @@ describe('GremlinLang', function () {
assert.ok(result.includes("'name':'marko'"));
});
});
-});
\ No newline at end of file
+
+ describe('PDT gremlin-lang tests', function () {
+ it('should handle basic PDT', function () {
+ const pdt = new ProviderDefinedType('Point', { x: 1, y: 2 });
+ assert.strictEqual(
+ g.inject(pdt).getGremlinLang().getGremlin(),
+ "g.inject(PDT(\"Point\",['x':1,'y':2]))"
+ );
+ });
+
+ it('should handle PDT with special chars in name (quotes)', function () {
+ const pdt = new ProviderDefinedType('my"type', { a: 1 });
+ assert.strictEqual(
+ g.inject(pdt).getGremlinLang().getGremlin(),
+ "g.inject(PDT(\"my\\\"type\",['a':1]))"
+ );
+ });
+
+ it('should handle nested PDT', function () {
+ const inner = new ProviderDefinedType('Inner', { v: 42 });
+ const outer = new ProviderDefinedType('Outer', { child: inner });
+ assert.strictEqual(
+ g.inject(outer).getGremlinLang().getGremlin(),
+ "g.inject(PDT(\"Outer\",['child':PDT(\"Inner\",['v':42])]))"
+ );
+ });
+
+ it('should auto-dehydrate via static pdtName property', function () {
+ class GeoPoint {
+ static pdtName = 'geo.Point';
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+ const point = new GeoPoint(10, 20);
+ assert.strictEqual(
+ g.inject(point).getGremlinLang().getGremlin(),
+ "g.inject(PDT(\"geo.Point\",['x':10,'y':20]))"
+ );
+ });
+
+ it('should handle PDT with empty properties', function () {
+ const pdt = new ProviderDefinedType('Empty', {});
+ assert.strictEqual(
+ g.inject(pdt).getGremlinLang().getGremlin(),
+ "g.inject(PDT(\"Empty\",[:]))"
+ );
+ });
+ });
+});
diff --git a/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js b/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js
new file mode 100644
index 0000000000..1caed7cd06
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/pdt-registry-test.js
@@ -0,0 +1,157 @@
+/*
+ * 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 { ProviderDefinedType } 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';
+
+describe('ProviderDefinedTypeRegistry', () => {
+ describe('#hydrate()', () => {
+ it('should return a typed object when an adapter is registered', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('GeoPoint', {
+ serialize: (obj) => ({ lat: obj.lat, lon: obj.lon }),
+ deserialize: (props) => ({ type: 'GeoPoint', lat: props.lat, lon: props.lon }),
+ });
+
+ const pdt = new ProviderDefinedType('GeoPoint', { lat: 37.7749, lon: -122.4194 });
+ const result = registry.hydrate(pdt);
+
+ assert.deepStrictEqual(result, { type: 'GeoPoint', lat: 37.7749, lon: -122.4194 });
+ });
+
+ it('should return the raw PDT when no adapter is registered', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ const pdt = new ProviderDefinedType('Unknown', { foo: 'bar' });
+ const result = registry.hydrate(pdt);
+
+ assert.strictEqual(result, pdt);
+ assert.instanceOf(result, ProviderDefinedType);
+ });
+
+ it('should fall back gracefully when adapter throws', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('Broken', {
+ serialize: () => ({}),
+ deserialize: () => { throw new Error('adapter error'); },
+ });
+
+ const pdt = new ProviderDefinedType('Broken', { x: 1 });
+ const warnings = [];
+ const origWarn = console.warn;
+ console.warn = (msg) => warnings.push(msg);
+ try {
+ const result = registry.hydrate(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 recursively hydrate nested PDTs', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('Address', {
+ serialize: (obj) => obj,
+ deserialize: (props) => ({ type: 'Address', city: props.city, zip: props.zip }),
+ });
+ registry.register('Person', {
+ serialize: (obj) => obj,
+ deserialize: (props) => ({ type: 'Person', name: props.name, address: props.address }),
+ });
+
+ const addressPdt = new ProviderDefinedType('Address', { city: 'Portland', zip: '97201' });
+ const personPdt = new ProviderDefinedType('Person', { name: 'Alice', address: addressPdt });
+
+ const result = registry.hydrate(personPdt);
+
+ assert.deepStrictEqual(result, {
+ type: 'Person',
+ name: 'Alice',
+ address: { type: 'Address', city: 'Portland', zip: '97201' },
+ });
+ });
+
+ it('should return non-PDT values unchanged', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ assert.strictEqual(registry.hydrate('hello'), 'hello');
+ assert.strictEqual(registry.hydrate(42), 42);
+ assert.strictEqual(registry.hydrate(null), null);
+ });
+ });
+
+ describe('#hasAdapter()', () => {
+ it('should return true for registered types', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('Foo', { serialize: () => ({}), deserialize: (p) => p });
+ assert.isTrue(registry.hasAdapter('Foo'));
+ assert.isFalse(registry.hasAdapter('Bar'));
+ });
+ });
+
+ describe('#getSerializer()', () => {
+ it('should return the serialize function for registered types', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ const serFn = (obj) => ({ val: obj.val });
+ registry.register('Custom', { serialize: serFn, deserialize: (p) => p });
+ assert.strictEqual(registry.getSerializer('Custom'), serFn);
+ });
+
+ it('should return null for unregistered types', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ assert.isNull(registry.getSerializer('Missing'));
+ });
+ });
+});
+
+describe('pdtRegistry wiring through Client/Connection', () => {
+ it('should set pdtRegistry on the reader ioc when passed via Connection options', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('GeoPoint', {
+ serialize: (obj) => obj,
+ deserialize: (props) => ({ type: 'GeoPoint', ...props }),
+ });
+
+ const conn = new Connection('http://localhost:8182', { pdtRegistry: registry });
+ assert.strictEqual(conn._reader.ioc.pdtRegistry, registry);
+ });
+
+ it('should set pdtRegistry on the reader ioc when passed via Client options', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ registry.register('GeoPoint', {
+ serialize: (obj) => obj,
+ deserialize: (props) => ({ type: 'GeoPoint', ...props }),
+ });
+
+ const client = new Client('http://localhost:8182', { pdtRegistry: registry });
+ assert.strictEqual(client._connection._reader.ioc.pdtRegistry, registry);
+ });
+
+ it('should not overwrite pdtRegistry when option is not provided', () => {
+ const registry = new ProviderDefinedTypeRegistry();
+ const conn1 = new Connection('http://localhost:8182', { pdtRegistry: registry });
+ // A second connection without pdtRegistry should not clear the existing one
+ const conn2 = new Connection('http://localhost:8182');
+ assert.strictEqual(conn2._reader.ioc.pdtRegistry, registry);
+ });
+});
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index 6798ebde55..fbe8d41404 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -1636,6 +1636,7 @@ genericLiteral
| characterLiteral
| durationLiteral
| binaryLiteral
+ | pdtLiteral
| genericMapLiteral
;
@@ -1728,6 +1729,10 @@ binaryLiteral
: K_BINARYC LPAREN stringLiteral RPAREN
;
+pdtLiteral
+ : K_PDT LPAREN stringLiteral COMMA genericMapLiteral RPAREN
+ ;
+
nakedKey
: Identifier
@@ -1951,6 +1956,7 @@ keyword
| K_PAGERANKU
| K_PATH
| K_PATHU
+ | K_PDT
| K_PEERPRESSURE
| K_PEERPRESSUREU
| K_PICK
@@ -2263,6 +2269,7 @@ K_PAGERANKU: 'PageRank';
K_PAGERANK: 'pageRank';
K_PATH: 'path';
K_PATHU: 'PATH';
+K_PDT: 'PDT';
K_PEERPRESSUREU: 'PeerPressure';
K_PEERPRESSURE: 'peerPressure';
K_PICK: 'Pick';
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/client.py b/gremlin-python/src/main/python/gremlin_python/driver/client.py
index 8b5ff1466c..5c1544db94 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/client.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/client.py
@@ -43,7 +43,7 @@ class Client:
request_serializer=serializer.GraphBinarySerializersV4(),
response_serializer=None, interceptors=None, auth=None,
headers=None, enable_user_agent_on_connect=True,
- bulk_results=False, **transport_kwargs):
+ bulk_results=False, pdt_registry=None, **transport_kwargs):
log.info("Creating Client with url '%s'", url)
self._closed = False
@@ -54,6 +54,10 @@ class Client:
self._traversal_source = traversal_source
if response_serializer is None:
response_serializer = serializer.GraphBinarySerializersV4()
+ if pdt_registry is not None:
+ if request_serializer is not None:
+ request_serializer.configure_pdt_registry(pdt_registry)
+ response_serializer.configure_pdt_registry(pdt_registry)
self._auth = auth
self._response_serializer = response_serializer
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py b/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py
index fdd25280cf..79e9039404 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/driver_remote_connection.py
@@ -36,7 +36,7 @@ class DriverRemoteConnection(RemoteConnection):
request_serializer=serializer.GraphBinarySerializersV4(),
response_serializer=None, interceptors=None, auth=None,
headers=None, enable_user_agent_on_connect=True,
- bulk_results=False, **transport_kwargs):
+ bulk_results=False, pdt_registry=None, **transport_kwargs):
log.info("Creating DriverRemoteConnection with url '%s'", str(url))
self.__url = url
self.__traversal_source = traversal_source
@@ -59,6 +59,7 @@ class DriverRemoteConnection(RemoteConnection):
headers=headers,
enable_user_agent_on_connect=enable_user_agent_on_connect,
bulk_results=bulk_results,
+ pdt_registry=pdt_registry,
**transport_kwargs)
self._url = self._client._url
self._traversal_source = self._client._traversal_source
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/serializer.py b/gremlin-python/src/main/python/gremlin_python/driver/serializer.py
index 5c51f2507b..c333a4cc54 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/serializer.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/serializer.py
@@ -34,17 +34,24 @@ class GraphBinarySerializersV4(object):
int_pack = graphbinaryV4.int32_pack
- def __init__(self, reader=None, writer=None, version=None):
+ def __init__(self, reader=None, writer=None, version=None, pdt_registry=None):
if not version:
version = self.DEFAULT_VERSION
self._version = version
if not reader:
- reader = self.DEFAULT_READER_CLASS()
+ reader = self.DEFAULT_READER_CLASS(pdt_registry=pdt_registry)
self._graphbinary_reader = reader
if not writer:
writer = self.DEFAULT_WRITER_CLASS()
self._graphbinary_writer = writer
+ def configure_pdt_registry(self, pdt_registry):
+ if self._graphbinary_reader.pdt_registry is None:
+ self._graphbinary_reader.pdt_registry = pdt_registry
+ else:
+ self._graphbinary_reader.pdt_registry._adapters_by_name.update(pdt_registry._adapters_by_name)
+ self._graphbinary_reader.pdt_registry._adapters_by_class.update(pdt_registry._adapters_by_class)
+
@property
def version(self):
"""Read only property"""
diff --git a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py
index 082df46d1a..9f7d1ea29e 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/graph_traversal.py
@@ -46,6 +46,8 @@ class GraphTraversalSource(object):
self.graph_traversal = GraphTraversal
if remote_connection:
self.traversal_strategies.add_strategies([RemoteStrategy(remote_connection)])
+ if hasattr(remote_connection, 'pdt_registry') and remote_connection.pdt_registry is not None:
+ self.gremlin_lang.pdt_registry = remote_connection.pdt_registry
self.remote_connection = remote_connection
def __repr__(self):
diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index 76c91822e7..93f835c57f 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -24,7 +24,7 @@ import uuid
import warnings
from aenum import Enum
-from gremlin_python.structure.graph import Vertex, Edge, Path, Property
+from gremlin_python.structure.graph import Vertex, Edge, Path, Property, ProviderDefinedType
from .. import statics
from ..statics import long, SingleByte, SingleChar, short, bigint, BigDecimal
@@ -814,11 +814,13 @@ class GremlinLang(object):
self.gremlin = []
self.parameters = {}
self.options_strategies = []
+ self.pdt_registry = None
if gremlin_lang is not None:
self.gremlin = list(gremlin_lang.gremlin)
self.parameters = dict(gremlin_lang.parameters)
self.options_strategies = list(gremlin_lang.options_strategies)
+ self.pdt_registry = gremlin_lang.pdt_registry
def _add_to_gremlin(self, string_name, *args):
@@ -904,6 +906,9 @@ class GremlinLang(object):
else:
return tmp
+ if isinstance(arg, ProviderDefinedType):
+ return f'PDT({self._arg_as_string(arg.name)},{self._process_dict(arg.properties)})'
+
if isinstance(arg, Vertex):
return f'{self._arg_as_string(arg.id)}'
@@ -947,6 +952,25 @@ class GremlinLang(object):
if isinstance(arg, type):
return arg.__name__
+ # Registry-based dehydration
+ if self.pdt_registry is not None:
+ adapter = self.pdt_registry.get_adapter_by_class(type(arg))
+ if adapter is not None and adapter['serialize'] is not None:
+ props = adapter['serialize'](arg)
+ return self._arg_as_string(ProviderDefinedType(adapter['type_name'], props))
+
+ # Auto-dehydrate @provider_defined decorated objects
+ if hasattr(arg, '_pdt_name'):
+ included = getattr(arg, '_pdt_included_fields', None)
+ excluded = getattr(arg, '_pdt_excluded_fields', None)
+ fields = [f for f in vars(arg) if not f.startswith('_')]
+ if included:
+ fields = [f for f in fields if f in included]
+ elif excluded:
+ fields = [f for f in fields if f not in excluded]
+ pdt = ProviderDefinedType(arg._pdt_name, {f: getattr(arg, f) for f in fields})
+ return self._arg_as_string(pdt)
+
raise TypeError(f'GremlinLang contains at least one type [{type(arg).__name__}] that cannot be represented as text.')
# Do special processing needed to format predicates that come in
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/graph.py b/gremlin-python/src/main/python/gremlin_python/structure/graph.py
index 2ff6e56174..61cec674d8 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/graph.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/graph.py
@@ -141,3 +141,114 @@ class Path(object):
def __len__(self):
return len(self.objects)
+
+
+class ProviderDefinedType(object):
+ def __init__(self, name, properties):
+ if not name:
+ raise ValueError("name cannot be null or empty")
+ self._name = name
+ self._properties = dict(properties) if properties else {}
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def properties(self):
+ return self._properties
+
+ def __eq__(self, other):
+ return isinstance(other, ProviderDefinedType) and self._name == other._name and self._properties == other._properties
+
+ def __hash__(self):
+ try:
+ return hash((self._name, frozenset(self._properties.items())))
+ except TypeError:
+ return hash(self._name)
+
+ def __repr__(self):
+ return f"pdt[{self._name}]{self._properties}"
+
+
+class ProviderDefinedTypeRegistry(object):
+ def __init__(self):
+ self._adapters_by_name = {}
+ self._adapters_by_class = {}
+
+ def register(self, type_name, deserialize_fn, serialize_fn=None, target_class=None):
+ self._adapters_by_name[type_name] = {
+ 'deserialize': deserialize_fn,
+ 'serialize': serialize_fn,
+ 'target_class': target_class
+ }
+ if target_class is not None:
+ self._adapters_by_class[target_class] = {
+ 'type_name': type_name,
+ 'serialize': serialize_fn,
+ }
+
+ @classmethod
+ def build(cls):
+ """Create a registry populated by entry_points discovery.
+
+ Providers register adapters via pyproject.toml:
+ [project.entry-points."tinkerpop.pdt"]
+ my_types = "my_package:register_pdt_types"
+
+ Each entry point should be a callable that accepts a registry and registers adapters.
+ """
+ import sys
+ registry = cls()
+ if sys.version_info >= (3, 10):
+ from importlib.metadata import entry_points
+ eps = entry_points(group='tinkerpop.pdt')
+ else:
+ from importlib.metadata import entry_points
+ all_eps = entry_points()
+ eps = all_eps.get('tinkerpop.pdt', [])
+
+ for ep in eps:
+ try:
+ factory = ep.load()
+ factory(registry)
+ except Exception as e:
+ import logging
+ logging.getLogger(__name__).warning(
+ f"Failed to load PDT adapter from entry point '{ep.name}': {e}")
+ return registry
+
+ def hydrate(self, pdt):
+ """Attempt to hydrate a ProviderDefinedType. Returns typed object or raw PDT."""
+ if not isinstance(pdt, ProviderDefinedType):
+ return pdt
+ adapter = self._adapters_by_name.get(pdt.name)
+ if adapter is None:
+ return pdt
+ try:
+ hydrated_props = {k: self.hydrate(v) if isinstance(v, ProviderDefinedType) else v
+ for k, v in pdt.properties.items()}
+ return adapter['deserialize'](hydrated_props)
+ except Exception as e:
+ import logging
+ logging.getLogger(__name__).warning(f"PDT hydration failed for '{pdt.name}': {e}")
+ return pdt
+
+ def get_adapter_by_class(self, cls):
+ """Return (type_name, serialize_fn) tuple for the given class, or None."""
+ return self._adapters_by_class.get(cls)
+
+
+# Module-level registry of @provider_defined decorated classes keyed by PDT name.
+_pdt_decorated_types = {}
+
+
+def provider_defined(name=None, included_fields=None, excluded_fields=None):
+ """Decorator that marks a class as a Provider Defined Type."""
+ def decorator(cls):
+ cls._pdt_name = name or cls.__name__
+ cls._pdt_included_fields = included_fields
+ cls._pdt_excluded_fields = excluded_fields
+ _pdt_decorated_types[cls._pdt_name] = cls
+ return cls
+ return decorator
diff --git a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py
index 4cdc3a14ee..cc5d9518fd 100644
--- a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py
+++ b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV4.py
@@ -30,7 +30,8 @@ from aenum import Enum
from gremlin_python.process.traversal import Direction, T, Merge, GType
from gremlin_python.statics import FloatType, BigDecimal, ShortType, IntType, LongType, BigIntType, \
DictType, SetType, SingleByte, SingleChar
-from gremlin_python.structure.graph import Graph, Edge, Property, Vertex, VertexProperty, Path
+from gremlin_python.structure.graph import Graph, Edge, Property, Vertex, VertexProperty, Path, ProviderDefinedType, \
+ _pdt_decorated_types
from gremlin_python.structure.io.util import HashableDict, SymbolUtil, Marker
log = logging.getLogger(__name__)
@@ -73,8 +74,8 @@ class DataType(Enum):
gtype = 0x30
char = 0x80
duration = 0x81
+ composite_pdt = 0xf0
marker = 0xfd
- custom = 0x00 # todo
NULL_BYTES = [DataType.null.value, 0x01]
@@ -144,10 +145,11 @@ class GraphBinaryWriter(object):
class GraphBinaryReader(object):
- def __init__(self, deserializer_map=None):
+ def __init__(self, deserializer_map=None, pdt_registry=None):
self.deserializers = _deserializers.copy()
if deserializer_map:
self.deserializers.update(deserializer_map)
+ self.pdt_registry = pdt_registry
def read_object(self, b):
if b is None:
@@ -163,9 +165,33 @@ class GraphBinaryReader(object):
if nullable:
buff.read(1)
return None
- return self.deserializers[DataType(bt)].objectify(buff, self, nullable)
+ result = self.deserializers[DataType(bt)].objectify(buff, self, nullable)
else:
- return self.deserializers[data_type].objectify(buff, self, nullable)
+ result = self.deserializers[data_type].objectify(buff, self, nullable)
+ if self.pdt_registry is not None and isinstance(result, ProviderDefinedType):
+ hydrated = self.pdt_registry.hydrate(result)
+ if not isinstance(hydrated, ProviderDefinedType):
+ return hydrated
+ result = hydrated
+ if isinstance(result, ProviderDefinedType) and result.name in _pdt_decorated_types:
+ return self._hydrate_decorated(result)
+ return result
+
+ def _hydrate_decorated(self, pdt):
+ """Hydrate a ProviderDefinedType using a @provider_defined decorated class."""
+ cls = _pdt_decorated_types[pdt.name]
+ props = {}
+ for k, v in pdt.properties.items():
+ if isinstance(v, ProviderDefinedType) and v.name in _pdt_decorated_types:
+ props[k] = self._hydrate_decorated(v)
+ elif self.pdt_registry is not None and isinstance(v, ProviderDefinedType):
+ props[k] = self.pdt_registry.hydrate(v)
+ else:
+ props[k] = v
+ obj = cls.__new__(cls)
+ for k, v in props.items():
+ setattr(obj, k, v)
+ return obj
class _GraphBinaryTypeIO(object, metaclass=GraphBinaryTypeType):
@@ -921,4 +947,26 @@ class MarkerIO(_GraphBinaryTypeIO):
def objectify(cls, buff, reader, nullable=True):
return cls.is_null(buff, reader,
lambda b, r: Marker.of(int8_unpack(b.read(1))),
- nullable)
\ No newline at end of file
+ nullable)
+
+
+class ProviderDefinedTypeIO(_GraphBinaryTypeIO):
+ python_type = ProviderDefinedType
+ graphbinary_type = DataType.composite_pdt
+
+ @classmethod
+ def dictify(cls, obj, writer, to_extend, as_value=False, nullable=True):
+ cls.prefix_bytes(cls.graphbinary_type, as_value, nullable, to_extend)
+ StringIO.dictify(obj.name, writer, to_extend)
+ MapIO.dictify(obj.properties, writer, to_extend)
+ return to_extend
+
+ @classmethod
+ def objectify(cls, buff, reader, nullable=True):
+ return cls.is_null(buff, reader, cls._read_pdt, nullable)
+
+ @classmethod
+ def _read_pdt(cls, b, r):
+ name = r.read_object(b)
+ properties = r.read_object(b)
+ return ProviderDefinedType(name, properties)
\ No newline at end of file
diff --git a/gremlin-python/src/main/python/tests/integration/conftest.py b/gremlin-python/src/main/python/tests/integration/conftest.py
index 76215bf24c..0f8d86eb83 100644
--- a/gremlin-python/src/main/python/tests/integration/conftest.py
+++ b/gremlin-python/src/main/python/tests/integration/conftest.py
@@ -211,6 +211,29 @@ def client_with_interceptor(request):
return client
+@pytest.fixture
+def remote_connection_with_registry(request):
+ from gremlin_python.structure.graph import ProviderDefinedTypeRegistry
+ from collections import namedtuple
+
+ registry = ProviderDefinedTypeRegistry()
+ Point = namedtuple('Point', ['x', 'y'])
+ registry.register('Point',
+ deserialize_fn=lambda props: Point(x=props['x'], y=props['y']),
+ serialize_fn=lambda p: {'x': p.x, 'y': p.y},
+ target_class=Point)
+ try:
+ remote_conn = DriverRemoteConnection(anonymous_url, 'gmodern', pdt_registry=registry)
+ except OSError:
+ pytest.skip('Gremlin Server is not running')
+ else:
+ def fin():
+ remote_conn.close()
+
+ request.addfinalizer(fin)
+ return remote_conn
+
+
def json_interceptor(request):
request['headers']['content-type'] = "application/json"
request['payload'] = dumps({"gremlin": "g.inject(2)", "g": "g"})
diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_client.py b/gremlin-python/src/main/python/tests/integration/driver/test_client.py
index 00e427cd58..1fe09deaf8 100644
--- a/gremlin-python/src/main/python/tests/integration/driver/test_client.py
+++ b/gremlin-python/src/main/python/tests/integration/driver/test_client.py
@@ -26,6 +26,7 @@ from gremlin_python.driver.client import Client
from gremlin_python.driver.connection import GremlinServerError
from gremlin_python.driver.request import RequestMessage
from gremlin_python.driver.serializer import GraphBinarySerializersV4
+from gremlin_python.structure.graph import ProviderDefinedType
from gremlin_python.process.graph_traversal import __, GraphTraversalSource
from gremlin_python.process.traversal import TraversalStrategies, GValue
from gremlin_python.process.strategies import OptionsStrategy
@@ -554,3 +555,60 @@ def test_response_serializer_never_None():
def test_serializer_and_interceptor_forwarded(client_with_interceptor):
result = client_with_interceptor.submit("g.inject(1)").next()
assert 2 == result # interceptor changes request to g.inject(2)
+
+def test_simple_pdt_round_trip(client):
+ """Inject and retrieve a simple Point PDT."""
+ results = client.submit(
+ "g.inject(PDT(\"Point\", [\"x\":1, \"y\":2]))"
+ ).all().result()
+
+ assert len(results) == 1
+ pdt = results[0]
+ assert isinstance(pdt, ProviderDefinedType)
+ assert pdt.name == 'Point'
+ assert pdt.properties['x'] == 1
+ assert pdt.properties['y'] == 2
+
+
+def test_nested_pdt(client):
+ """Inject and retrieve a nested PDT (Person containing Address)."""
+ results = client.submit(
+ "g.inject(PDT(\"Person\", [\"name\":\"Alice\", \"age\":30, "
+ "\"address\":PDT(\"Address\", [\"street\":\"123 Main St\", \"city\":\"Springfield\", \"zip\":\"12345\"])]))"
+ ).all().result()
+
+ assert len(results) == 1
+ pdt = results[0]
+ assert isinstance(pdt, ProviderDefinedType)
+ assert pdt.name == 'Person'
+ assert pdt.properties['name'] == 'Alice'
+ assert pdt.properties['age'] == 30
+
+ address = pdt.properties['address']
+ assert isinstance(address, ProviderDefinedType)
+ assert address.name == 'Address'
+ assert address.properties['street'] == '123 Main St'
+ assert address.properties['city'] == 'Springfield'
+ assert address.properties['zip'] == '12345'
+
+
+def test_pdt_in_collection(client):
+ """Retrieve multiple PDTs as a list."""
+ results = client.submit(
+ "g.inject([PDT(\"Point\", [\"x\":1, \"y\":2]), PDT(\"Point\", [\"x\":3, \"y\":4])])"
+ ).all().result()
+
+ assert len(results) == 1
+ pdt_list = results[0]
+ assert isinstance(pdt_list, list)
+ assert len(pdt_list) == 2
+
+ assert isinstance(pdt_list[0], ProviderDefinedType)
+ assert pdt_list[0].name == 'Point'
+ assert pdt_list[0].properties['x'] == 1
+ assert pdt_list[0].properties['y'] == 2
+
+ assert isinstance(pdt_list[1], ProviderDefinedType)
+ assert pdt_list[1].name == 'Point'
+ assert pdt_list[1].properties['x'] == 3
+ assert pdt_list[1].properties['y'] == 4
diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py b/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py
index 604190b0c6..28746486b7 100644
--- a/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py
+++ b/gremlin-python/src/main/python/tests/integration/driver/test_driver_remote_connection.py
@@ -26,7 +26,7 @@ from gremlin_python.statics import long
from gremlin_python.process.traversal import TraversalStrategy, P, Order, T, DT, GValue, Cardinality, Scope
from gremlin_python.process.graph_traversal import __
from gremlin_python.process.anonymous_traversal import traversal
-from gremlin_python.structure.graph import Vertex, Edge, Graph
+from gremlin_python.structure.graph import Vertex, Edge, Graph, ProviderDefinedType, provider_defined
from gremlin_python.process.strategies import SubgraphStrategy, SeedStrategy, ReservedKeysVerificationStrategy
from gremlin_python.structure.io.util import HashableDict
from gremlin_python.driver.connection import GremlinServerError
@@ -289,3 +289,36 @@ class TestDriverRemoteConnection(object):
g = traversal().with_(remote_connection_with_interceptor)
result = g.inject(1).next()
assert 2 == result # interceptor changes request to g.inject(2)
+
+ def test_pdt_round_trip_via_traversal(self, remote_connection):
+ g = traversal().with_(remote_connection)
+ pdt = ProviderDefinedType('Point', {'x': 1, 'y': 2})
+ result = g.inject(pdt).next()
+ assert isinstance(result, ProviderDefinedType)
+ assert result.name == 'Point'
+ assert result.properties == {'x': 1, 'y': 2}
+
+ def test_pdt_registry_round_trip_via_traversal(self, remote_connection_with_registry):
+ from collections import namedtuple
+ Point = namedtuple('Point', ['x', 'y'])
+ g = traversal().with_(remote_connection_with_registry)
+ point = Point(x=10, y=20)
+ result = g.inject(point).next()
+ # Registry auto-dehydrates on send and auto-hydrates on receive
+ assert isinstance(result, Point)
+ assert result.x == 10
+ assert result.y == 20
+
+ def test_pdt_annotation_auto_dehydrate_via_traversal(self, remote_connection):
+ @provider_defined(name='TestPoint')
+ class TestPoint:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ g = traversal().with_(remote_connection)
+ point = TestPoint(5, 10)
+ result = g.inject(point).next()
+ assert isinstance(result, TestPoint)
+ assert result.x == 5
+ assert result.y == 10
diff --git a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
index edf0515317..74a386163d 100644
--- a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
+++ b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py
@@ -579,3 +579,38 @@ class TestGremlinLang(object):
result = GremlinLang.convert_parameters_to_string({'name': "it's a test"})
assert "'name'" in result
assert "it" in result
+
+ def test_provider_defined_auto_dehydration(self):
+ from gremlin_python.structure.graph import ProviderDefinedType, provider_defined
+ g = traversal().with_(None)
+
+ @provider_defined(name="com.example.Point")
+ class Point:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ p = Point(1, 2)
+ gremlin = g.inject(p).gremlin_lang.get_gremlin()
+ assert "PDT('com.example.Point',['x':1,'y':2])" in gremlin
+
+ def test_pdt_special_characters_in_name(self):
+ from gremlin_python.structure.graph import ProviderDefinedType
+ g = traversal().with_(None)
+
+ pdt = ProviderDefinedType('say"hello"', {'v': 1})
+ gremlin = g.inject(pdt).gremlin_lang.get_gremlin()
+ assert "PDT('say\"hello\"',['v':1])" in gremlin
+
+ pdt2 = ProviderDefinedType('back\\slash', {'v': 1})
+ gremlin2 = g.inject(pdt2).gremlin_lang.get_gremlin()
+ assert "PDT('back\\\\slash',['v':1])" in gremlin2
+
+ def test_pdt_nested(self):
+ from gremlin_python.structure.graph import ProviderDefinedType
+ g = traversal().with_(None)
+
+ inner = ProviderDefinedType('Inner', {'v': 1})
+ outer = ProviderDefinedType('Outer', {'inner': inner})
+ gremlin = g.inject(outer).gremlin_lang.get_gremlin()
+ assert "PDT('Outer',['inner':PDT('Inner',['v':1])])" in gremlin
diff --git a/gremlin-python/src/main/python/tests/unit/structure/io/test_graphbinaryV4.py b/gremlin-python/src/main/python/tests/unit/structure/io/test_graphbinaryV4.py
index c1f2ac540e..21b9cf7173 100644
--- a/gremlin-python/src/main/python/tests/unit/structure/io/test_graphbinaryV4.py
+++ b/gremlin-python/src/main/python/tests/unit/structure/io/test_graphbinaryV4.py
@@ -23,7 +23,7 @@ from collections import OrderedDict
from datetime import datetime, timedelta, timezone
from gremlin_python.statics import long, bigint, BigDecimal, SingleByte, SingleChar
-from gremlin_python.structure.graph import Graph, Vertex, Edge, Property, VertexProperty, Path
+from gremlin_python.structure.graph import Graph, Vertex, Edge, Property, VertexProperty, Path, ProviderDefinedType
from gremlin_python.structure.io.graphbinaryV4 import GraphBinaryWriter, GraphBinaryReader
from gremlin_python.process.traversal import Direction
from gremlin_python.structure.io.util import Marker
@@ -316,3 +316,25 @@ class TestGraphBinaryV4(object):
assert len(re1.properties) == 1
assert re1.properties[0].key == "weight"
assert re1.properties[0].value == 0.5
+
+ def test_provider_defined_type(self):
+ pdt = ProviderDefinedType('Point', {'x': 1, 'y': 2})
+ result = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(pdt))
+ assert isinstance(result, ProviderDefinedType)
+ assert result.name == 'Point'
+ assert result.properties == {'x': 1, 'y': 2}
+
+ def test_provider_defined_type_nested(self):
+ inner = ProviderDefinedType('Address', {'street': 'Main'})
+ outer = ProviderDefinedType('Person', {'name': 'Alice', 'address': inner})
+ result = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(outer))
+ assert result.name == 'Person'
+ assert result.properties['name'] == 'Alice'
+ assert isinstance(result.properties['address'], ProviderDefinedType)
+ assert result.properties['address'].name == 'Address'
+
+ def test_provider_defined_type_null_field(self):
+ pdt = ProviderDefinedType('NullableType', {'value': None, 'name': 'test'})
+ result = self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(pdt))
+ assert result.properties['value'] is None
+ assert result.properties['name'] == 'test'
diff --git a/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py b/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py
new file mode 100644
index 0000000000..2d5a47cb08
--- /dev/null
+++ b/gremlin-python/src/main/python/tests/unit/structure/io/test_provider_defined_type.py
@@ -0,0 +1,220 @@
+"""
+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 pytest
+
+from gremlin_python.structure.graph import ProviderDefinedType, ProviderDefinedTypeRegistry, provider_defined
+from gremlin_python.structure.io.graphbinaryV4 import GraphBinaryWriter, GraphBinaryReader
+
+
+class TestProviderDefinedType(object):
+ graphbinary_writer = GraphBinaryWriter()
+ graphbinary_reader = GraphBinaryReader()
+
+ def test_empty_name_rejected(self):
+ with pytest.raises(ValueError):
+ ProviderDefinedType("", {"x": 1})
+
+ def test_none_name_rejected(self):
+ with pytest.raises(ValueError):
+ ProviderDefinedType(None, {"x": 1})
+
+
+class TestProviderDefinedTypeRegistry(object):
+
+ def test_hydrate_simple(self):
+ registry = ProviderDefinedTypeRegistry()
+ registry.register("com.example.Point", lambda props: (props["x"], props["y"]))
+ pdt = ProviderDefinedType("com.example.Point", {"x": 1.0, "y": 2.0})
+ result = registry.hydrate(pdt)
+ assert result == (1.0, 2.0)
+
+ def test_hydrate_no_adapter_returns_raw(self):
+ registry = ProviderDefinedTypeRegistry()
+ pdt = ProviderDefinedType("com.example.Unknown", {"a": 1})
+ result = registry.hydrate(pdt)
+ assert result is pdt
+
+ def test_hydrate_adapter_throws_falls_back(self):
+ registry = ProviderDefinedTypeRegistry()
+ registry.register("com.example.Bad", lambda props: 1 / 0)
+ pdt = ProviderDefinedType("com.example.Bad", {"x": 1})
+ result = registry.hydrate(pdt)
+ assert result is pdt
+
+ def test_hydrate_nested(self):
+ from collections import namedtuple
+ Inner = namedtuple("Inner", ["val"])
+ Outer = namedtuple("Outer", ["child", "count"])
+ registry = ProviderDefinedTypeRegistry()
+ registry.register("com.example.Inner", lambda props: Inner(props["val"].upper()))
+ registry.register("com.example.Outer", lambda props: Outer(props["child"], props["count"]))
+ inner = ProviderDefinedType("com.example.Inner", {"val": "hello"})
+ outer = ProviderDefinedType("com.example.Outer", {"child": inner, "count": 42})
+ result = registry.hydrate(outer)
+ assert result == Outer(Inner("HELLO"), 42)
+
+ def test_hydrate_non_pdt_passthrough(self):
+ registry = ProviderDefinedTypeRegistry()
+ assert registry.hydrate("plain string") == "plain string"
+ assert registry.hydrate(42) == 42
+
+ def test_dehydrate_simple(self):
+ from collections import namedtuple
+ Point = namedtuple("Point", ["x", "y"])
+ registry = ProviderDefinedTypeRegistry()
+ registry.register("com.example.Point",
+ deserialize_fn=lambda props: Point(props["x"], props["y"]),
+ serialize_fn=lambda p: {"x": p.x, "y": p.y},
+ target_class=Point)
+ adapter = registry.get_adapter_by_class(Point)
+ props = adapter['serialize'](Point(1.0, 2.0))
+ assert props == {"x": 1.0, "y": 2.0}
+
+ def test_dehydrate_no_adapter_returns_none(self):
+ registry = ProviderDefinedTypeRegistry()
+ assert registry.get_adapter_by_class(str) is None
+
+ def test_dehydrate_no_serialize_fn_returns_none(self):
+ registry = ProviderDefinedTypeRegistry()
+ registry.register("com.example.Thing", deserialize_fn=lambda props: props, target_class=dict)
+ adapter = registry.get_adapter_by_class(dict)
+ assert adapter['serialize'] is None
+
+
+class TestProviderDefinedTypeRegistryBuild(object):
+
+ def test_build_returns_registry_with_no_entry_points(self):
+ registry = ProviderDefinedTypeRegistry.build()
+ assert isinstance(registry, ProviderDefinedTypeRegistry)
+
+ def test_build_loads_entry_point(self):
+ from unittest.mock import patch, MagicMock
+
+ mock_ep = MagicMock()
+ mock_ep.name = "mock_adapter"
+ mock_ep.load.return_value = lambda reg: reg.register("com.mock.Type", lambda props: props)
+
+ with patch("importlib.metadata.entry_points") as mock_entry_points:
+ import sys
+ if sys.version_info >= (3, 10):
+ mock_entry_points.return_value = [mock_ep]
+ else:
+ mock_entry_points.return_value = {'tinkerpop.pdt': [mock_ep]}
+
+ registry = ProviderDefinedTypeRegistry.build()
+ assert "com.mock.Type" in registry._adapters_by_name
+
+ def test_build_handles_failing_entry_point(self):
+ from unittest.mock import patch, MagicMock
+
+ mock_ep = MagicMock()
+ mock_ep.name = "bad_adapter"
+ mock_ep.load.side_effect = RuntimeError("boom")
+
+ with patch("importlib.metadata.entry_points") as mock_entry_points:
+ import sys
+ if sys.version_info >= (3, 10):
+ mock_entry_points.return_value = [mock_ep]
+ else:
+ mock_entry_points.return_value = {'tinkerpop.pdt': [mock_ep]}
+
+ registry = ProviderDefinedTypeRegistry.build()
+ assert isinstance(registry, ProviderDefinedTypeRegistry)
+ assert len(registry._adapters_by_name) == 0
+
+
+class TestReaderAutoHydration(object):
+
+ def test_reader_auto_hydrates_with_registry(self):
+ registry = ProviderDefinedTypeRegistry()
+ registry.register("com.example.Point", lambda props: {"x": props["x"], "y": props["y"], "hydrated": True})
+ writer = GraphBinaryWriter()
+ reader = GraphBinaryReader(pdt_registry=registry)
+
+ pdt = ProviderDefinedType("com.example.Point", {"x": 1.0, "y": 2.0})
+ result = reader.read_object(writer.write_object(pdt))
+ assert result == {"x": 1.0, "y": 2.0, "hydrated": True}
+
+ def test_reader_no_registry_returns_raw_pdt(self):
+ writer = GraphBinaryWriter()
+ reader = GraphBinaryReader()
+
+ pdt = ProviderDefinedType("com.example.Unregistered", {"x": 1.0, "y": 2.0})
+ result = reader.read_object(writer.write_object(pdt))
+ assert isinstance(result, ProviderDefinedType)
+ assert result == pdt
+
+
+class TestProviderDefinedDecorator(object):
+
+ def test_decorator_sets_metadata_with_name(self):
+ @provider_defined(name="com.example.Point", included_fields=["x", "y"])
+ class Point:
+ pass
+
+ assert Point._pdt_name == "com.example.Point"
+ assert Point._pdt_included_fields == ["x", "y"]
+ assert Point._pdt_excluded_fields is None
+
+ def test_decorator_defaults_to_class_name(self):
+ @provider_defined()
+ class MyType:
+ pass
+
+ assert MyType._pdt_name == "MyType"
+ assert MyType._pdt_included_fields is None
+ assert MyType._pdt_excluded_fields is None
+
+ def test_decorator_excluded_fields(self):
+ @provider_defined(excluded_fields=["internal"])
+ class Foo:
+ pass
+
+ assert Foo._pdt_excluded_fields == ["internal"]
+
+
+class TestPdtRegistryWiring(object):
+
+ def test_serializer_passes_registry_to_reader(self):
+ pytest.importorskip("aiohttp")
+ from gremlin_python.driver.serializer import GraphBinarySerializersV4
+ registry = ProviderDefinedTypeRegistry()
+ s = GraphBinarySerializersV4(pdt_registry=registry)
+ assert s._graphbinary_reader.pdt_registry is registry
+
+ def test_client_passes_registry_to_serializers(self):
+ pytest.importorskip("aiohttp")
+ from unittest.mock import patch
+ from gremlin_python.driver.client import Client
+ registry = ProviderDefinedTypeRegistry()
+ with patch.object(Client, '_fill_pool'):
+ c = Client("ws://localhost:8182/gremlin", "g", pdt_registry=registry)
+ assert c._request_serializer._graphbinary_reader.pdt_registry is registry
+ assert c._response_serializer._graphbinary_reader.pdt_registry is registry
+
+ def test_driver_remote_connection_passes_registry(self):
+ pytest.importorskip("aiohttp")
+ from unittest.mock import patch
+ from gremlin_python.driver.client import Client
+ from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
+ registry = ProviderDefinedTypeRegistry()
+ with patch.object(Client, '_fill_pool'):
+ drc = DriverRemoteConnection("ws://localhost:8182/gremlin", "g", pdt_registry=registry)
+ assert drc._client._response_serializer._graphbinary_reader.pdt_registry is registry
diff --git a/gremlin-server/pom.xml b/gremlin-server/pom.xml
index df4c5e6da6..9b40be61ee 100644
--- a/gremlin-server/pom.xml
+++ b/gremlin-server/pom.xml
@@ -173,6 +173,16 @@ limitations under the License.
</archive>
</configuration>
</plugin>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Address.java
similarity index 66%
copy from gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
copy to gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Address.java
index a54580cab0..cc01cdd8a7 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Address.java
@@ -16,17 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+package org.apache.tinkerpop.gremlin.server.pdt;
-import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
-/**
- * Represents a serializer for a custom (provider specific) serializer.
- * @param <T>
- */
-public interface CustomTypeSerializer<T> extends TypeSerializer<T> {
- /**
- * Gets the custom type name.
- */
- String getTypeName();
+@ProviderDefined(name = "Address")
+public class Address {
+ public String street;
+ public String city;
+ public String zip;
+
+ public Address(final String street, final String city, final String zip) {
+ this.street = street;
+ this.city = city;
+ this.zip = zip;
+ }
}
diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePerson.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Person.java
similarity index 57%
rename from gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePerson.java
rename to gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Person.java
index 2af646b6de..96b878f950 100644
--- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePerson.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Person.java
@@ -16,31 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.tinkerpop.gremlin.util.ser.binary.types.sample;
+package org.apache.tinkerpop.gremlin.server.pdt;
-import java.time.OffsetDateTime;
-import java.util.Objects;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
-/**
- * A sample custom data type containing few properties.
- */
-public class SamplePerson {
- private final String name;
- private final OffsetDateTime birthDate;
-
- SamplePerson(final String name, final OffsetDateTime birthDate) {
- Objects.requireNonNull(name);
- Objects.requireNonNull(birthDate);
+@ProviderDefined(name = "Person")
+public class Person {
+ public String name;
+ public int age;
+ public Address address;
+ public Person(final String name, final int age, final Address address) {
this.name = name;
- this.birthDate = birthDate;
- }
-
- public String getName() {
- return name;
- }
-
- public OffsetDateTime getBirthDate() {
- return birthDate;
+ this.age = age;
+ this.address = address;
}
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Point.java
similarity index 67%
rename from gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
rename to gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Point.java
index a54580cab0..1bb8470061 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CustomTypeSerializer.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/Point.java
@@ -16,17 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+package org.apache.tinkerpop.gremlin.server.pdt;
-import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined;
/**
- * Represents a serializer for a custom (provider specific) serializer.
- * @param <T>
+ * A simple test class annotated with {@link ProviderDefined} for PDT integration testing.
*/
-public interface CustomTypeSerializer<T> extends TypeSerializer<T> {
- /**
- * Gets the custom type name.
- */
- String getTypeName();
+@ProviderDefined(name = "Point")
+public class Point {
+ public int x;
+ public int y;
+
+ public Point(final int x, final int y) {
+ this.x = x;
+ this.y = y;
+ }
}
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/ProviderDefinedTypeIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/ProviderDefinedTypeIntegrateTest.java
new file mode 100644
index 0000000000..083c1de2a7
--- /dev/null
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/pdt/ProviderDefinedTypeIntegrateTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+package org.apache.tinkerpop.gremlin.server.pdt;
+
+import org.apache.http.Consts;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.apache.tinkerpop.gremlin.driver.Client;
+import org.apache.tinkerpop.gremlin.driver.Cluster;
+import org.apache.tinkerpop.gremlin.driver.RequestOptions;
+import org.apache.tinkerpop.gremlin.driver.Result;
+import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.server.AbstractGremlinServerIntegrationTest;
+import org.apache.tinkerpop.gremlin.server.TestClientFactory;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeAdapter;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
+import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
+import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Integration tests verifying that Provider Defined Types (PDT) flow end-to-end through gremlin-server.
+ * <p>
+ * The PDT serialization converts {@code @ProviderDefined}-annotated objects to {@link ProviderDefinedType}
+ * during GraphBinary serialization. On the client side, values are always deserialized as
+ * {@link ProviderDefinedType} (unless a {@code ProviderDefinedTypeAdapter} is registered).
+ */
+public class ProviderDefinedTypeIntegrateTest extends AbstractGremlinServerIntegrationTest {
+
+ private static final RequestOptions GROOVY = RequestOptions.build().language("gremlin-groovy").create();
+
+ private final Supplier<Graph> graphGetter =
+ () -> server.getServerGremlinExecutor().getGraphManager().getGraph("graph");
+
+ @After
+ public void cleanup() {
+ final Graph graph = graphGetter.get();
+ graph.traversal().V().hasLabel("location").drop().iterate();
+ }
+
+ @Test
+ public void shouldStoreAndRetrievePdtViaGremlinLang() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ try {
+ // Store a Point as a vertex property using gremlin-lang PDT literal
+ client.submit(
+ "g.addV('location').property('point', PDT(\"Point\", [\"x\":1, \"y\":2])).iterate()").all().get();
+
+ // Retrieve the property value - it should come back as ProviderDefinedType
+ final List<Result> results = client.submit(
+ "g.V().hasLabel('location').values('point')").all().get();
+
+ assertEquals(1, results.size());
+ final Object obj = results.get(0).getObject();
+ assertTrue("Expected ProviderDefinedType but got: " + obj.getClass().getName(),
+ obj instanceof ProviderDefinedType);
+
+ final ProviderDefinedType pdt = (ProviderDefinedType) obj;
+ assertEquals("Point", pdt.getName());
+ assertEquals(2, pdt.getProperties().size());
+ assertEquals(1, pdt.getProperties().get("x"));
+ assertEquals(2, pdt.getProperties().get("y"));
+ } finally {
+ cluster.close();
+ }
+ }
+
+ @Test
+ public void shouldRetrievePdtViaBytecodeTraversal() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ try {
+ // Store via gremlin-lang PDT literal
+ client.submit(
+ "g.addV('location').property('point', PDT(\"Point\", [\"x\":10, \"y\":20])).iterate()").all().get();
+
+ // Retrieve via bytecode traversal (gremlin-lang)
+ final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster));
+ final Object result = g.V().hasLabel("location").values("point").next();
+
+ assertTrue("Expected ProviderDefinedType but got: " + result.getClass().getName(),
+ result instanceof ProviderDefinedType);
+
+ final ProviderDefinedType pdt = (ProviderDefinedType) result;
+ assertEquals("Point", pdt.getName());
+ assertEquals(10, pdt.getProperties().get("x"));
+ assertEquals(20, pdt.getProperties().get("y"));
+ } finally {
+ cluster.close();
+ }
+ }
+
+ @Test
+ public void shouldStoreAndRetrievePdtWithAdditionalProperties() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ try {
+ // Store via gremlin-lang PDT literal with additional vertex properties
+ client.submit(
+ "g.addV('location').property('point', PDT(\"Point\", [\"x\":5, \"y\":6])).property('name', 'office')").all().get();
+
+ // Retrieve via bytecode traversal and verify PDT alongside normal properties
+ final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster));
+ final Object pointValue = g.V().hasLabel("location").values("point").next();
+
+ assertTrue("Expected ProviderDefinedType but got: " + pointValue.getClass().getName(),
+ pointValue instanceof ProviderDefinedType);
+
+ final ProviderDefinedType pdt = (ProviderDefinedType) pointValue;
+ assertEquals("Point", pdt.getName());
+ assertEquals(5, pdt.getProperties().get("x"));
+ assertEquals(6, pdt.getProperties().get("y"));
+ } finally {
+ cluster.close();
+ }
+ }
+
+ @Test
+ public void shouldStorePdtAsOriginalObjectInTinkerGraph() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ try {
+ // Store a Point via Groovy script (needed to create the actual Point object in TinkerGraph)
+ client.submit(
+ "g.addV('location').property('point', new org.apache.tinkerpop.gremlin.server.pdt.Point(3, 4)).iterate()",
+ GROOVY).all().get();
+
+ // Verify TinkerGraph stores the original Point object (not a ProviderDefinedType)
+ final Graph graph = graphGetter.get();
+ final Vertex v = graph.traversal().V().hasLabel("location").next();
+ final Object storedValue = v.property("point").value();
+
+ assertTrue("TinkerGraph should store the original Point object but got: " + storedValue.getClass().getName(),
+ storedValue instanceof Point);
+
+ final Point storedPoint = (Point) storedValue;
+ assertEquals(3, storedPoint.x);
+ assertEquals(4, storedPoint.y);
+ } finally {
+ cluster.close();
+ }
+ }
+
+ @Test
+ public void shouldRoundTripPdtViaInjectScript() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ try {
+ // Inject a Point directly via gremlin-lang PDT literal
+ final List<Result> results = client.submit(
+ "g.inject(PDT(\"Point\", [\"x\":7, \"y\":8]))").all().get();
+
+ assertEquals(1, results.size());
+ final Object obj = results.get(0).getObject();
+ assertTrue("Expected ProviderDefinedType but got: " + obj.getClass().getName(),
+ obj instanceof ProviderDefinedType);
+
+ final ProviderDefinedType pdt = (ProviderDefinedType) obj;
+ assertEquals("Point", pdt.getName());
+ assertEquals(7, pdt.getProperties().get("x"));
+ assertEquals(8, pdt.getProperties().get("y"));
+ } finally {
+ cluster.close();
+ }
+ }
+
+ @Test
+ public void shouldReturnPdtAsGraphSONCompositePdtInHttpResponse() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpPost httppost = new HttpPost(TestClientFactory.createURLString());
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.addHeader("Accept", "application/json");
+ httppost.setEntity(new StringEntity(
+ "{\"gremlin\":\"g.inject(org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType.from(new org.apache.tinkerpop.gremlin.server.pdt.Point(1, 2)))\",\"language\":\"gremlin-groovy\"}",
+ Consts.UTF_8));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ assertEquals("application/json", response.getEntity().getContentType().getValue());
+
+ final String json = EntityUtils.toString(response.getEntity());
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final JsonNode root = objectMapper.readTree(json);
+
+ // Navigate: result.data.@value[0] should be the PDT
+ final JsonNode pdtNode = root.get("result").get("data")
+ .get(GraphSONTokens.VALUEPROP).get(0);
+
+ assertEquals("g:CompositePdt", pdtNode.get("@type").asText());
+
+ final JsonNode value = pdtNode.get(GraphSONTokens.VALUEPROP);
+ assertEquals("Point", value.get("type").asText());
+
+ final JsonNode fields = value.get("fields");
+ assertEquals("g:Int32", fields.get("x").get("@type").asText());
+ assertEquals(1, fields.get("x").get(GraphSONTokens.VALUEPROP).intValue());
+ assertEquals("g:Int32", fields.get("y").get("@type").asText());
+ assertEquals(2, fields.get("y").get(GraphSONTokens.VALUEPROP).intValue());
+ }
+ }
+
+ @Test
+ public void shouldHydratePdtViaRegistryFromDriverResult() throws Exception {
+ final Cluster cluster = TestClientFactory.build().create();
+ final Client client = cluster.connect();
+
+ try {
+ // Set up a client-side registry with a Point adapter
+ final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty();
+ registry.register(new ProviderDefinedTypeAdapter<Point>() {
+ @Override public String typeName() { return "Point"; }
+ @Override public Class<Point> targetClass() { return Point.class; }
+ @Override public Map<String, Object> toProperties(final Point obj) {
+ final Map<String, Object> m = new HashMap<>();
+ m.put("x", obj.x);
+ m.put("y", obj.y);
+ return m;
+ }
+ @Override public Point fromProperties(final Map<String, Object> properties) {
+ return new Point((int) properties.get("x"), (int) properties.get("y"));
+ }
+ });
+
+ // Retrieve a PDT from the server via the driver
+ final List<Result> results = client.submit(
+ "g.inject(new org.apache.tinkerpop.gremlin.server.pdt.Point(9, 11))",
+ GROOVY).all().get();
+
+ assertEquals(1, results.size());
+ final Object raw = results.get(0).getObject();
+ assertTrue(raw instanceof ProviderDefinedType);
+
+ // Hydrate the PDT using the client-side registry
+ final Object hydrated = registry.hydrate((ProviderDefinedType) raw);
+ assertTrue("Expected Point but got: " + hydrated.getClass().getName(),
+ hydrated instanceof Point);
+
+ final Point point = (Point) hydrated;
+ assertEquals(9, point.x);
+ assertEquals(11, point.y);
+ } finally {
+ cluster.close();
+ }
+ }
+}
diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/GraphBinaryMessageSerializerV4.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/GraphBinaryMessageSerializerV4.java
index e9c7a97f82..795712ff76 100644
--- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/GraphBinaryMessageSerializerV4.java
+++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/ser/GraphBinaryMessageSerializerV4.java
@@ -23,24 +23,20 @@ import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.io.Buffer;
-import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
-import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryIo;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryMapper;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
import org.apache.tinkerpop.gremlin.structure.io.binary.Marker;
import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
-import org.apache.tinkerpop.gremlin.structure.io.binary.types.CustomTypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry;
import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
import org.apache.tinkerpop.gremlin.util.message.ResponseStatus;
import org.apache.tinkerpop.gremlin.util.ser.binary.RequestMessageSerializer;
-import org.javatuples.Pair;
import org.javatuples.Triplet;
import java.io.IOException;
import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@@ -48,7 +44,6 @@ import java.util.List;
import java.util.Map;
public class GraphBinaryMessageSerializerV4 extends AbstractMessageSerializer<GraphBinaryMapper> {
- public static final String TOKEN_CUSTOM = "custom";
public static final String TOKEN_BUILDER = "builder";
private GraphBinaryReader reader;
@@ -74,6 +69,14 @@ public class GraphBinaryMessageSerializerV4 extends AbstractMessageSerializer<Gr
requestSerializer = new RequestMessageSerializer();
}
+ public GraphBinaryMessageSerializerV4(final TypeSerializerRegistry registry, final ProviderDefinedTypeRegistry pdtRegistry) {
+ reader = new GraphBinaryReader(registry, pdtRegistry);
+ writer = new GraphBinaryWriter(registry);
+ mapper = new GraphBinaryMapper(writer, reader);
+
+ requestSerializer = new RequestMessageSerializer();
+ }
+
public GraphBinaryMessageSerializerV4(final TypeSerializerRegistry.Builder builder) {
this(builder.create());
}
@@ -100,27 +103,6 @@ public class GraphBinaryMessageSerializerV4 extends AbstractMessageSerializer<Gr
builder = TypeSerializerRegistry.build();
}
- final List<String> classNameList = getListStringFromConfig(TOKEN_IO_REGISTRIES, config);
- classNameList.forEach(className -> {
- try {
- final Class<?> clazz = Class.forName(className);
- try {
- final Method instanceMethod = tryInstanceMethod(clazz);
- final IoRegistry ioreg = (IoRegistry) instanceMethod.invoke(null);
- final List<Pair<Class, CustomTypeSerializer>> classSerializers = ioreg.find(GraphBinaryIo.class, CustomTypeSerializer.class);
- for (Pair<Class,CustomTypeSerializer> cs : classSerializers) {
- builder.addCustomType(cs.getValue0(), cs.getValue1());
- }
- } catch (Exception methodex) {
- throw new IllegalStateException(String.format("Could not instantiate IoRegistry from an instance() method on %s", className), methodex);
- }
- } catch (Exception ex) {
- throw new IllegalStateException(ex);
- }
- });
-
- addCustomClasses(config, builder);
-
final TypeSerializerRegistry registry = builder.create();
reader = new GraphBinaryReader(registry);
writer = new GraphBinaryWriter(registry);
@@ -133,34 +115,6 @@ public class GraphBinaryMessageSerializerV4 extends AbstractMessageSerializer<Gr
return new String[] {MIME_TYPE};
}
- private void addCustomClasses(final Map<String, Object> config, final TypeSerializerRegistry.Builder builder) {
- final List<String> classNameList = getListStringFromConfig(TOKEN_CUSTOM, config);
-
- classNameList.forEach(serializerDefinition -> {
- final String className;
- final String serializerName;
- if (serializerDefinition.contains(";")) {
- final String[] split = serializerDefinition.split(";");
- if (split.length != 2)
- throw new IllegalStateException(String.format("Invalid format for serializer definition [%s] - expected <class>;<serializer-class>", serializerDefinition));
-
- className = split[0];
- serializerName = split[1];
- } else {
- throw new IllegalStateException(String.format("Invalid format for serializer definition [%s] - expected <class>;<serializer-class>", serializerDefinition));
- }
-
- try {
- final Class clazz = Class.forName(className);
- final Class serializerClazz = Class.forName(serializerName);
- final CustomTypeSerializer serializer = (CustomTypeSerializer) serializerClazz.newInstance();
- builder.addCustomType(clazz, serializer);
- } catch (Exception ex) {
- throw new IllegalStateException("CustomTypeSerializer could not be instantiated", ex);
- }
- });
- }
-
@Override
public ByteBuf serializeRequestAsBinary(RequestMessage requestMessage, ByteBufAllocator allocator) throws SerializationException {
final ByteBuf buffer = allocator.buffer();
diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerRegistryTest.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerRegistryTest.java
index aaba1987a9..c5618c4eb9 100644
--- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerRegistryTest.java
+++ b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/TypeSerializerRegistryTest.java
@@ -18,8 +18,6 @@
*/
package org.apache.tinkerpop.gremlin.util.ser.binary;
-import org.apache.tinkerpop.gremlin.util.ser.binary.types.sample.SamplePerson;
-import org.apache.tinkerpop.gremlin.util.ser.binary.types.sample.SamplePersonSerializer;
import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
@@ -93,18 +91,18 @@ public class TypeSerializerRegistryTest {
String message = null;
try {
- registry.getSerializer(SamplePerson.class);
+ registry.getSerializer(StringBuilder.class);
} catch (IOException ex) {
message = ex.getMessage();
}
- assertEquals("Serializer for type org.apache.tinkerpop.gremlin.util.ser.binary.types.sample.SamplePerson not found", message);
+ assertEquals("Serializer not found for type java.lang.StringBuilder. If this is a provider-defined type, annotate the class with @ProviderDefined.", message);
assertEquals(1, called[0]);
}
@Test
public void shouldUseFallbackResolverReturnValue() throws IOException {
- TypeSerializer expected = new SamplePersonSerializer();
+ TypeSerializer expected = new TestUUIDSerializer();
final int[] called = {0};
final TypeSerializerRegistry registry = TypeSerializerRegistry.build()
.withFallbackResolver(t -> {
@@ -112,7 +110,7 @@ public class TypeSerializerRegistryTest {
return expected;
}).create();
- TypeSerializer<SamplePerson> serializer = registry.getSerializer(SamplePerson.class);
+ TypeSerializer<StringBuilder> serializer = registry.getSerializer(StringBuilder.class);
assertEquals(1, called[0]);
assertSame(expected, serializer);
}
diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePersonSerializer.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePersonSerializer.java
deleted file mode 100644
index bf6c6cae79..0000000000
--- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePersonSerializer.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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.
- */
-package org.apache.tinkerpop.gremlin.util.ser.binary.types.sample;
-
-import org.apache.tinkerpop.gremlin.util.ser.SerializationException;
-import org.apache.tinkerpop.gremlin.structure.io.Buffer;
-import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
-import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
-import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
-import org.apache.tinkerpop.gremlin.structure.io.binary.types.CustomTypeSerializer;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.time.OffsetDateTime;
-
-/**
- * A sample custom type serializer.
- */
-public final class SamplePersonSerializer implements CustomTypeSerializer<SamplePerson> {
- private final byte[] typeInfoBuffer = new byte[] { 0, 0, 0, 0 };
-
- @Override
- public String getTypeName() {
- return "sampleProvider.SamplePerson";
- }
-
- @Override
- public DataType getDataType() {
- return DataType.CUSTOM;
- }
-
- @Override
- public SamplePerson read(final Buffer buffer, final GraphBinaryReader context) throws IOException {
- // {custom type info}, {value_flag} and {value}
- // No custom_type_info
- if (buffer.readInt() != 0) {
- throw new SerializationException("{custom_type_info} should not be provided for this custom type");
- }
-
- return readValue(buffer, context, true);
- }
-
- @Override
- public SamplePerson readValue(final Buffer buffer, final GraphBinaryReader context, final boolean nullable) throws IOException {
- if (nullable) {
- final byte valueFlag = buffer.readByte();
- if ((valueFlag & 1) == 1) {
- return null;
- }
- }
-
- // Read the byte length of the value bytes
- final int valueLength = buffer.readInt();
-
- if (valueLength <= 0) {
- throw new SerializationException(String.format("Unexpected value length: %d", valueLength));
- }
-
- if (valueLength > buffer.readableBytes()) {
- throw new SerializationException(
- String.format("Not enough readable bytes: %d (expected %d)", valueLength, buffer.readableBytes()));
- }
-
- final String name = context.readValue(buffer, String.class, false);
- final OffsetDateTime birthDate = context.readValue(buffer, OffsetDateTime.class, false);
-
- return new SamplePerson(name, birthDate);
- }
-
- @Override
- public void write(final SamplePerson value, final Buffer buffer, final GraphBinaryWriter context) throws IOException {
- // Write {custom type info}, {value_flag} and {value}
- buffer.writeBytes(typeInfoBuffer);
-
- writeValue(value, buffer, context, true);
- }
-
- @Override
- public void writeValue(final SamplePerson value, final Buffer buffer, final GraphBinaryWriter context, final boolean nullable) throws IOException {
- if (value == null) {
- if (!nullable) {
- throw new SerializationException("Unexpected null value when nullable is false");
- }
-
- context.writeValueFlagNull(buffer);
- return;
- }
-
- if (nullable) {
- context.writeValueFlagNone(buffer);
- }
-
- final String name = value.getName();
-
- // value_length = name_byte_length + name_bytes + long
- buffer.writeInt(4 + name.getBytes(StandardCharsets.UTF_8).length + 8);
-
- context.writeValue(name, buffer, false);
- context.writeValue(value.getBirthDate(), buffer, false);
- }
-}
diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePersonSerializerTest.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePersonSerializerTest.java
deleted file mode 100644
index 83f64d2fcd..0000000000
--- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/sample/SamplePersonSerializerTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.
- */
-package org.apache.tinkerpop.gremlin.util.ser.binary.types.sample;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufAllocator;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import org.apache.tinkerpop.gremlin.util.ser.NettyBufferFactory;
-import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
-import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4;
-import org.apache.tinkerpop.gremlin.structure.io.AbstractIoRegistry;
-import org.apache.tinkerpop.gremlin.structure.io.Buffer;
-import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryIo;
-import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
-import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
-import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.time.LocalDateTime;
-import java.time.OffsetDateTime;
-import java.time.ZoneOffset;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import static org.apache.tinkerpop.gremlin.util.MockitoHamcrestMatcherAdapter.reflectionEquals;
-import static org.apache.tinkerpop.gremlin.util.ser.AbstractMessageSerializer.TOKEN_IO_REGISTRIES;
-import static org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4.TOKEN_CUSTOM;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-public class SamplePersonSerializerTest {
-
- private static final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
- private static final NettyBufferFactory bufferFactory = new NettyBufferFactory();
-
- @Test
- public void shouldCustomSerializationWithPerson() throws IOException {
- final GraphBinaryMessageSerializerV4 serializer = new GraphBinaryMessageSerializerV4(
- TypeSerializerRegistry.build().addCustomType(SamplePerson.class, new SamplePersonSerializer()).create());
- assertPerson(serializer);
- }
-
- @Test
- public void shouldSerializePersonViaIoRegistry() throws IOException {
- final GraphBinaryMessageSerializerV4 serializer = new GraphBinaryMessageSerializerV4();
- final Map<String,Object> config = new HashMap<>();
- config.put(TOKEN_IO_REGISTRIES, Collections.singletonList(CustomIoRegistry.class.getName()));
- serializer.configure(config, Collections.emptyMap());
-
- assertPerson(serializer);
- }
-
- @Test
- public void shouldSerializePersonViaCustom() throws IOException {
- final GraphBinaryMessageSerializerV4 serializer = new GraphBinaryMessageSerializerV4();
- final Map<String,Object> config = new HashMap<>();
- config.put(TOKEN_CUSTOM, Collections.singletonList(String.format("%s;%s",
- SamplePerson.class.getCanonicalName(), SamplePersonSerializer.class.getCanonicalName())));
- serializer.configure(config, Collections.emptyMap());
-
- assertPerson(serializer);
- }
-
- @Test
- public void readValueAndWriteValueShouldBeSymmetric() throws IOException {
- final TypeSerializerRegistry registry = TypeSerializerRegistry.build()
- .addCustomType(SamplePerson.class, new SamplePersonSerializer()).create();
- final GraphBinaryReader reader = new GraphBinaryReader(registry);
- final GraphBinaryWriter writer = new GraphBinaryWriter(registry);
-
- final SamplePerson person = new SamplePerson("Matias",
- OffsetDateTime.of(LocalDateTime.of(2005, 8, 5, 1, 0), ZoneOffset.UTC));
-
- for (boolean nullable: new boolean[] { true, false }) {
- final Buffer buffer = bufferFactory.create(allocator.buffer());
- writer.writeValue(person, buffer, nullable);
- final SamplePerson actual = reader.readValue(buffer, SamplePerson.class, nullable);
-
- assertThat(actual, reflectionEquals(person));
- buffer.release();
- }
- }
-
- private void assertPerson(final GraphBinaryMessageSerializerV4 serializer) throws IOException {
- final OffsetDateTime birthDate = OffsetDateTime.of(LocalDateTime.of(2010, 4, 29, 5, 30), ZoneOffset.UTC);
- final SamplePerson person = new SamplePerson("Olivia", birthDate);
-
- final ByteBuf serialized = serializer.serializeResponseAsBinary(
- ResponseMessage.build().result(Collections.singletonList(person)).code(HttpResponseStatus.OK).create(), allocator);
-
- final ResponseMessage deserialized = serializer.deserializeBinaryResponse(serialized);
-
- final SamplePerson actual = (SamplePerson) deserialized.getResult().getData().get(0);
- assertThat(actual, reflectionEquals(person));
- }
-
- public static class CustomIoRegistry extends AbstractIoRegistry {
- private static final CustomIoRegistry ioreg = new CustomIoRegistry();
-
- private CustomIoRegistry() {
- register(GraphBinaryIo.class, SamplePerson.class, new SamplePersonSerializer());
- }
-
- public static CustomIoRegistry instance() {
- return ioreg;
- }
- }
-}