This is an automated email from the ASF dual-hosted git repository. Cole-Greer pushed a commit to branch simplePDT in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit ef194e358f17ab2b49bf327995176b31b38e1659 Author: Cole Greer <[email protected]> AuthorDate: Wed Jun 24 17:05:14 2026 -0700 PDT review fixes: adapter precedence, Python gremlin-lang primitive support, registry naming, and integration coverage Outcome of the post-implementation review pass over the PrimitivePDT work. Touches several already-merged beads; details and departures below. Production fixes: - GraphBinaryWriter.dehydrateToPdt now prefers a registered adapter over the @ProviderDefined annotation, matching the documented precedence in GremlinLang.argAsString ("a registered adapter takes priority"). Previously the binary write path preferred the annotation, diverging from the request path. (refs tinkerpop-lka, tinkerpop-2gy.3) - gremlin-python: GremlinLang._arg_as_string did not handle PrimitiveProviderDefinedType, raising TypeError when a primitive PDT was used as a traversal argument. Added the PDT("name","value") text emission and primitive-adapter auto-dehydration (primitive checked before composite), mirroring the Java side. This gap shipped in the Python GLV bead and was caught by the new integration tests. (refs tinkerpop-2gy.8) - Renamed the composite adapter accessors/maps to be explicit about "composite" in both Java (getCompositeAdapterByName/ByClass, compositeAdaptersBy*) and Python (get_composite_adapter_by_class, _composite_adapters_by_*), now that a parallel primitive set exists. (refs tinkerpop-2gy.1, tinkerpop-2gy.3) Test coverage added (these gaps allowed the above bugs to ship): - GraphBinaryWriterPdtTest: precedence regression test asserting a registered adapter wins over @ProviderDefined on the binary write path. - GremlinDriverIntegrateTest: PrimitivePDT traversal-API integration tests covering the unregistered base case, registered auto de/hydration, and the registered nested (composite-containing-primitive) case. Reuses the server-side Uint32/Uint32Adapter/Measurement fixtures rather than duplicating them. (refs tinkerpop-2gy.6, tinkerpop-2gy.7) - gremlin-python: traversal-API integration tests (raw/unregistered, registered hydration, in-collection, nested-in-composite) mirroring the composite suite. (refs tinkerpop-2gy.8) Departure note: the gremlin-python GLV (tinkerpop-2gy.8) does not add a GraphSON g:PrimitivePdt read path because the Python driver is GraphBinary-only for V4; an orphaned GraphSON reader added during implementation was removed. Assisted-by: Kiro:claude-opus-4.8 --- .../gremlin/process/traversal/GremlinLang.java | 5 +- .../structure/io/binary/GraphBinaryWriter.java | 37 +++++----- .../io/graphson/GraphSONTypeIdResolver.java | 2 +- .../graphson/PdtGraphSONSerializerProviderV4.java | 2 +- .../io/graphson/PdtGraphSONSerializersV4.java | 5 +- .../io/pdt/ProviderDefinedTypeAdapter.java | 2 +- .../io/pdt/ProviderDefinedTypeRegistry.java | 25 +++---- .../io/pdt/ProviderDefinedTypeRegistryTest.java | 6 +- .../python/gremlin_python/driver/serializer.py | 4 +- .../python/gremlin_python/process/traversal.py | 11 ++- .../main/python/gremlin_python/structure/graph.py | 14 ++-- .../src/main/python/tests/integration/conftest.py | 28 ++++++++ .../python/tests/integration/driver/test_client.py | 53 ++++++++++++++- .../driver/test_driver_remote_connection.py | 19 +++++- .../structure/io/test_provider_defined_type.py | 10 +-- .../gremlin/server/GremlinDriverIntegrateTest.java | 79 ++++++++++++++++++++++ .../GremlinServerPrimitivePdtIntegrateTest.java | 2 - .../util/ser/binary/GraphBinaryWriterPdtTest.java | 49 ++++++++++++-- 18 files changed, 278 insertions(+), 75 deletions(-) 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 3470de1981..5d041614e8 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 @@ -35,7 +35,6 @@ import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.CompositePDTAdapter; -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; @@ -280,13 +279,11 @@ public class GremlinLang implements Cloneable, Serializable { if (pdtRegistry != null) { final Optional<PrimitivePDTAdapter<?>> primitiveAdapter = pdtRegistry.getPrimitiveAdapterByClass(arg.getClass()); if (primitiveAdapter.isPresent()) { - @SuppressWarnings("unchecked") final String value = ((PrimitivePDTAdapter) primitiveAdapter.get()).toValue(arg); return argAsString(new PrimitiveProviderDefinedType(primitiveAdapter.get().typeName(), value)); } - final Optional<ProviderDefinedTypeAdapter<?>> adapter = pdtRegistry.getAdapterByClass(arg.getClass()); + final Optional<CompositePDTAdapter<?>> adapter = pdtRegistry.getCompositeAdapterByClass(arg.getClass()); if (adapter.isPresent()) { - @SuppressWarnings("unchecked") final Map<String, Object> fields = ((CompositePDTAdapter) adapter.get()).toFields(arg); return argAsString(new ProviderDefinedType(adapter.get().typeName(), fields)); } 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 7bbdb6d0f6..e7b47590a5 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 @@ -25,7 +25,6 @@ import org.apache.tinkerpop.gremlin.structure.io.pdt.CompositePDTAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; 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.structure.io.Buffer; @@ -89,7 +88,7 @@ public class GraphBinaryWriter { final Class<?> objectClass = value.getClass(); - final TypeSerializer<T> serializer = (TypeSerializer<T>) getSerializerOrAdapterFallback(objectClass); + final TypeSerializer<T> serializer = getSerializerOrAdapterFallback(objectClass); if (serializer instanceof ProviderDefinedTypeSerializer && !(value instanceof ProviderDefinedType)) { serializer.writeValue((T) dehydrateToPdt(value, objectClass), buffer, this, nullable); return; @@ -112,9 +111,12 @@ public class GraphBinaryWriter { } final Class<?> objectClass = value.getClass(); - final TypeSerializer<T> serializer = (TypeSerializer<T>) getSerializerOrAdapterFallback(objectClass); + final TypeSerializer<T> serializer = getSerializerOrAdapterFallback(objectClass); 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) dehydrateToPdt(value, objectClass), buffer); return; } @@ -125,6 +127,8 @@ public class GraphBinaryWriter { } if (serializer instanceof TransformSerializer) { + // For historical reasons, there are types that need to be transformed into another type + // before serialization, e.g., Map.Entry final TransformSerializer<T> transformSerializer = (TransformSerializer<T>) serializer; write(transformSerializer.transform(value), buffer); return; @@ -177,17 +181,16 @@ public class GraphBinaryWriter { * Attempts to get a serializer for the given class. If no serializer is found and the pdtRegistry * has an adapter for the class (composite or primitive), returns the appropriate PDT serializer. */ - @SuppressWarnings("unchecked") private <DT> TypeSerializer<DT> getSerializerOrAdapterFallback(final Class<?> type) throws IOException { try { return (TypeSerializer<DT>) registry.getSerializer(type); } catch (final IOException e) { if (pdtRegistry != null) { - if (pdtRegistry.getAdapterByClass(type).isPresent()) { - return (TypeSerializer<DT>) registry.getSerializer(DataType.COMPOSITE_PDT); + if (pdtRegistry.getCompositeAdapterByClass(type).isPresent()) { + return registry.getSerializer(DataType.COMPOSITE_PDT); } if (pdtRegistry.getPrimitiveAdapterByClass(type).isPresent()) { - return (TypeSerializer<DT>) registry.getSerializer(DataType.PRIMITIVE_PDT); + return registry.getSerializer(DataType.PRIMITIVE_PDT); } } throw e; @@ -198,32 +201,26 @@ public class GraphBinaryWriter { * Dehydrates a value to a {@link ProviderDefinedType} using annotation reflection or an adapter from the * pdtRegistry. */ - @SuppressWarnings({"unchecked", "rawtypes"}) private ProviderDefinedType dehydrateToPdt(final Object value, final Class<?> objectClass) { - // Prefer annotation-based conversion - if (objectClass.isAnnotationPresent(org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefined.class)) { - return ProviderDefinedType.from(value); - } - // Fall back to adapter-based conversion + // A registered adapter takes priority if (pdtRegistry != null) { - final Optional<ProviderDefinedTypeAdapter<?>> opt = pdtRegistry.getAdapterByClass(objectClass); + final Optional<CompositePDTAdapter<?>> opt = pdtRegistry.getCompositeAdapterByClass(objectClass); if (opt.isPresent()) { - final CompositePDTAdapter adapter = (CompositePDTAdapter) opt.get(); - final Map<String, Object> fields = adapter.toFields(value); - return new ProviderDefinedType(adapter.typeName(), fields); + final CompositePDTAdapter adapter = opt.get(); + return new ProviderDefinedType(adapter.typeName(), adapter.toFields(value)); } } - // Should not reach here since getSerializerOrAdapterFallback already validated + // @ProviderDefined annotation base case return ProviderDefinedType.from(value); } + /** * Dehydrates a value to a {@link PrimitiveProviderDefinedType} using a {@link PrimitivePDTAdapter} from the * pdtRegistry. */ - @SuppressWarnings({"unchecked", "rawtypes"}) private PrimitiveProviderDefinedType dehydrateToPrimitivePdt(final Object value, final Class<?> objectClass) { - final PrimitivePDTAdapter adapter = (PrimitivePDTAdapter) pdtRegistry.getPrimitiveAdapterByClass(objectClass).get(); + final PrimitivePDTAdapter adapter = pdtRegistry.getPrimitiveAdapterByClass(objectClass).get(); return new PrimitiveProviderDefinedType(adapter.typeName(), adapter.toValue(value)); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java index cec70fb058..de4314a7ef 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONTypeIdResolver.java @@ -85,7 +85,7 @@ public class GraphSONTypeIdResolver implements TypeIdResolver { public String idFromValueAndType(final Object o, final Class<?> aClass) { if (!typeToId.containsKey(aClass)) { // Check if pdtRegistry has an adapter for this class - if (pdtRegistry != null && pdtRegistry.getAdapterByClass(aClass).isPresent()) { + if (pdtRegistry != null && pdtRegistry.getCompositeAdapterByClass(aClass).isPresent()) { return typeToId.get(org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType.class); } if (pdtRegistry != null && pdtRegistry.getPrimitiveAdapterByClass(aClass).isPresent()) { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java index 008be0ec3b..45a349d103 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/PdtGraphSONSerializerProviderV4.java @@ -55,7 +55,7 @@ final class PdtGraphSONSerializerProviderV4 extends DefaultSerializerProvider { @Override public JsonSerializer<Object> getUnknownTypeSerializer(final Class<?> aClass) { - if (pdtRegistry != null && pdtRegistry.getAdapterByClass(aClass).isPresent()) { + if (pdtRegistry != null && pdtRegistry.getCompositeAdapterByClass(aClass).isPresent()) { return pdtAdapterSerializer; } if (pdtRegistry != null && pdtRegistry.getPrimitiveAdapterByClass(aClass).isPresent()) { 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 index 193392f8b1..91dd87b0dd 100644 --- 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 @@ -22,7 +22,6 @@ import org.apache.tinkerpop.gremlin.structure.io.pdt.CompositePDTAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; 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.core.JsonGenerator; import org.apache.tinkerpop.shaded.jackson.core.JsonParser; @@ -221,11 +220,11 @@ final class PdtGraphSONSerializersV4 { } private ProviderDefinedType toPdt(final Object value) throws IOException { - final Optional<ProviderDefinedTypeAdapter<?>> opt = registry.getAdapterByClass(value.getClass()); + final Optional<CompositePDTAdapter<?>> opt = registry.getCompositeAdapterByClass(value.getClass()); if (!opt.isPresent()) { throw new IOException("No adapter found for " + value.getClass().getName()); } - final CompositePDTAdapter adapter = (CompositePDTAdapter) opt.get(); + final CompositePDTAdapter adapter = opt.get(); final Map<String, Object> fields = adapter.toFields(value); return new ProviderDefinedType(adapter.typeName(), fields); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java index 3612cda193..bcd3fcfebf 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/pdt/ProviderDefinedTypeAdapter.java @@ -20,7 +20,7 @@ package org.apache.tinkerpop.gremlin.structure.io.pdt; /** * Common supertype for all PDT adapters. Exposes the type name and target class; - * serialization-specific methods live in subtypes ({@link CompositePDTAdapter}). + * serialization-specific methods live in subtypes ({@link CompositePDTAdapter} and {@link PrimitivePDTAdapter}). */ public interface ProviderDefinedTypeAdapter<T> { String typeName(); 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 index bb3b7eb3f4..fc33b92415 100644 --- 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 @@ -40,8 +40,8 @@ public final class ProviderDefinedTypeRegistry { private static final Logger logger = LoggerFactory.getLogger(ProviderDefinedTypeRegistry.class); - private final Map<String, CompositePDTAdapter<?>> adaptersByName = new ConcurrentHashMap<>(); - private final Map<Class<?>, CompositePDTAdapter<?>> adaptersByClass = new ConcurrentHashMap<>(); + private final Map<String, CompositePDTAdapter<?>> compositeAdaptersByName = new ConcurrentHashMap<>(); + private final Map<Class<?>, CompositePDTAdapter<?>> compositeAdaptersByClass = new ConcurrentHashMap<>(); private final Map<String, PrimitivePDTAdapter<?>> primitiveAdaptersByName = new ConcurrentHashMap<>(); private final Map<Class<?>, PrimitivePDTAdapter<?>> primitiveAdaptersByClass = new ConcurrentHashMap<>(); @@ -50,7 +50,6 @@ public final class ProviderDefinedTypeRegistry { /** * Creates a registry populated via {@link ServiceLoader} discovery. */ - @SuppressWarnings("rawtypes") public static ProviderDefinedTypeRegistry create() { final ProviderDefinedTypeRegistry registry = new ProviderDefinedTypeRegistry(); for (final ProviderDefinedTypeAdapter adapter : ServiceLoader.load(ProviderDefinedTypeAdapter.class)) { @@ -75,7 +74,7 @@ public final class ProviderDefinedTypeRegistry { public void register(final ProviderDefinedTypeAdapter<?> adapter) { if (adapter instanceof PrimitivePDTAdapter) { final PrimitivePDTAdapter<?> primitive = (PrimitivePDTAdapter<?>) adapter; - if (adaptersByClass.containsKey(primitive.targetClass())) + if (compositeAdaptersByClass.containsKey(primitive.targetClass())) throw new IllegalArgumentException("Class " + primitive.targetClass().getName() + " is already registered as a composite PDT adapter"); primitiveAdaptersByName.put(primitive.typeName(), primitive); @@ -85,8 +84,8 @@ public final class ProviderDefinedTypeRegistry { if (primitiveAdaptersByClass.containsKey(composite.targetClass())) throw new IllegalArgumentException("Class " + composite.targetClass().getName() + " is already registered as a primitive PDT adapter"); - adaptersByName.put(composite.typeName(), composite); - adaptersByClass.put(composite.targetClass(), composite); + compositeAdaptersByName.put(composite.typeName(), composite); + compositeAdaptersByClass.put(composite.targetClass(), composite); } } @@ -102,12 +101,12 @@ public final class ProviderDefinedTypeRegistry { } } - public Optional<ProviderDefinedTypeAdapter<?>> getAdapterByName(final String name) { - return Optional.ofNullable(adaptersByName.get(name)); + public Optional<CompositePDTAdapter<?>> getCompositeAdapterByName(final String name) { + return Optional.ofNullable(compositeAdaptersByName.get(name)); } - public Optional<ProviderDefinedTypeAdapter<?>> getAdapterByClass(final Class<?> clazz) { - return Optional.ofNullable(adaptersByClass.get(clazz)); + public Optional<CompositePDTAdapter<?>> getCompositeAdapterByClass(final Class<?> clazz) { + return Optional.ofNullable(compositeAdaptersByClass.get(clazz)); } public Optional<PrimitivePDTAdapter<?>> getPrimitiveAdapterByName(final String name) { @@ -126,7 +125,6 @@ public final class ProviderDefinedTypeRegistry { * Returns the original PDT (with nested values hydrated) if no adapter is found for the outer type, * or if the adapter throws an exception. */ - @SuppressWarnings({"unchecked", "rawtypes"}) public Object hydrate(final ProviderDefinedType pdt) { // recursively hydrate nested PDTs in the fields map, whether or not the outer has an adapter boolean nestedChanged = false; @@ -138,7 +136,7 @@ public final class ProviderDefinedTypeRegistry { hydrated.put(entry.getKey(), value); } - final CompositePDTAdapter adapter = adaptersByName.get(pdt.getName()); + final CompositePDTAdapter adapter = compositeAdaptersByName.get(pdt.getName()); if (adapter == null) { // No adapter for the outer type: return it raw, but with any registered nested types hydrated. // Preserve identity when nothing nested was hydrated. @@ -154,7 +152,6 @@ public final class ProviderDefinedTypeRegistry { } } - @SuppressWarnings({"unchecked", "rawtypes"}) private Object hydrateValue(final Object value) { if (value instanceof ProviderDefinedType) return hydrate((ProviderDefinedType) value); @@ -186,7 +183,6 @@ public final class ProviderDefinedTypeRegistry { * {@link PrimitivePDTAdapter}. Returns the original primitive PDT if no adapter is found or if the * adapter throws an exception. */ - @SuppressWarnings({"unchecked", "rawtypes"}) public Object hydratePrimitive(final PrimitiveProviderDefinedType pdt) { final PrimitivePDTAdapter adapter = primitiveAdaptersByName.get(pdt.getName()); if (adapter == null) { @@ -206,7 +202,6 @@ public final class ProviderDefinedTypeRegistry { /** * A reflective adapter synthesized from a {@link ProviderDefined}-annotated class. */ - @SuppressWarnings({"unchecked", "rawtypes"}) private static final class AnnotatedTypeAdapter<T> implements CompositePDTAdapter<T> { private final String typeName; private final Class<T> targetClass; 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 index 3442038337..d82174776c 100644 --- 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 @@ -188,7 +188,7 @@ public class ProviderDefinedTypeRegistryTest { final PointAdapter adapter = new PointAdapter(); registry.register(adapter); - final Optional<ProviderDefinedTypeAdapter<?>> found = registry.getAdapterByClass(Point.class); + final Optional<CompositePDTAdapter<?>> found = registry.getCompositeAdapterByClass(Point.class); assertTrue(found.isPresent()); assertEquals("Point", found.get().typeName()); } @@ -338,7 +338,7 @@ public class ProviderDefinedTypeRegistryTest { final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty(); registry.register(AnnotatedPoint.class); - final Optional<ProviderDefinedTypeAdapter<?>> adapter = registry.getAdapterByClass(AnnotatedPoint.class); + final Optional<CompositePDTAdapter<?>> adapter = registry.getCompositeAdapterByClass(AnnotatedPoint.class); assertTrue(adapter.isPresent()); assertEquals("AnnotatedPoint", adapter.get().typeName()); } @@ -537,7 +537,6 @@ public class ProviderDefinedTypeRegistryTest { @Override public String typeName() { return "Container"; } @Override public Class<Map> targetClass() { return Map.class; } @Override public Map<String, Object> toFields(Map obj) { return new HashMap<>(); } - @SuppressWarnings("unchecked") @Override public Map fromFields(Map<String, Object> fields) { return fields; } }); @@ -548,7 +547,6 @@ public class ProviderDefinedTypeRegistryTest { final Object result = registry.hydrate(containerPdt); assertTrue(result instanceof Map); - @SuppressWarnings("unchecked") final Map<String, Object> resultMap = (Map<String, Object>) result; assertTrue(resultMap.get("id") instanceof Uint32); assertEquals(99L, ((Uint32) resultMap.get("id")).value); 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 47182b04da..ff02f4dd68 100644 --- a/gremlin-python/src/main/python/gremlin_python/driver/serializer.py +++ b/gremlin-python/src/main/python/gremlin_python/driver/serializer.py @@ -49,8 +49,8 @@ class GraphBinarySerializersV4(object): 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) + self._graphbinary_reader.pdt_registry._composite_adapters_by_name.update(pdt_registry._composite_adapters_by_name) + self._graphbinary_reader.pdt_registry._composite_adapters_by_class.update(pdt_registry._composite_adapters_by_class) self._graphbinary_reader.pdt_registry._primitive_adapters_by_name.update(pdt_registry._primitive_adapters_by_name) self._graphbinary_reader.pdt_registry._primitive_adapters_by_class.update(pdt_registry._primitive_adapters_by_class) 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 cbc035ee2c..1ab8679a7d 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, ProviderDefinedType +from gremlin_python.structure.graph import Vertex, Edge, Path, Property, ProviderDefinedType, PrimitiveProviderDefinedType from .. import statics from ..statics import long, SingleByte, SingleChar, short, bigint, BigDecimal @@ -909,6 +909,9 @@ class GremlinLang(object): if isinstance(arg, ProviderDefinedType): return f'PDT({self._arg_as_string(arg.name)},{self._process_dict(arg.fields)})' + if isinstance(arg, PrimitiveProviderDefinedType): + return f'PDT({self._arg_as_string(arg.name)},{self._arg_as_string(arg.value)})' + if isinstance(arg, Vertex): return f'{self._arg_as_string(arg.id)}' @@ -956,7 +959,11 @@ class GremlinLang(object): # precedence over the @provider_defined decorator fallback below, allowing # explicit adapters to override decorator-derived behavior. if self.pdt_registry is not None: - adapter = self.pdt_registry.get_adapter_by_class(type(arg)) + primitive_adapter = self.pdt_registry.get_primitive_adapter_by_class(type(arg)) + if primitive_adapter is not None and primitive_adapter['to_value'] is not None: + value = primitive_adapter['to_value'](arg) + return self._arg_as_string(PrimitiveProviderDefinedType(primitive_adapter['type_name'], value)) + adapter = self.pdt_registry.get_composite_adapter_by_class(type(arg)) if adapter is not None and adapter['serialize'] is not None: fields = adapter['serialize'](arg) return self._arg_as_string(ProviderDefinedType(adapter['type_name'], fields)) 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 5d66e12bac..ac05c5c989 100644 --- a/gremlin-python/src/main/python/gremlin_python/structure/graph.py +++ b/gremlin-python/src/main/python/gremlin_python/structure/graph.py @@ -204,19 +204,19 @@ class PrimitiveProviderDefinedType(object): class ProviderDefinedTypeRegistry(object): def __init__(self): - self._adapters_by_name = {} - self._adapters_by_class = {} + self._composite_adapters_by_name = {} + self._composite_adapters_by_class = {} self._primitive_adapters_by_name = {} self._primitive_adapters_by_class = {} def register(self, type_name, deserialize_fn, serialize_fn=None, target_class=None): - self._adapters_by_name[type_name] = { + self._composite_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] = { + self._composite_adapters_by_class[target_class] = { 'type_name': type_name, 'serialize': serialize_fn, } @@ -293,7 +293,7 @@ class ProviderDefinedTypeRegistry(object): else: hydrated_fields[k] = v - adapter = self._adapters_by_name.get(pdt.name) + adapter = self._composite_adapters_by_name.get(pdt.name) if adapter is None: return ProviderDefinedType(pdt.name, hydrated_fields) if changed else pdt try: @@ -317,9 +317,9 @@ class ProviderDefinedTypeRegistry(object): logging.getLogger(__name__).warning(f"Primitive PDT hydration failed for '{pdt.name}': {e}") return pdt - def get_adapter_by_class(self, cls): + def get_composite_adapter_by_class(self, cls): """Return (type_name, serialize_fn) tuple for the given class, or None.""" - return self._adapters_by_class.get(cls) + return self._composite_adapters_by_class.get(cls) def get_primitive_adapter_by_class(self, cls): """Return adapter dict for the given class, or None.""" diff --git a/gremlin-python/src/main/python/tests/integration/conftest.py b/gremlin-python/src/main/python/tests/integration/conftest.py index de685fb4b9..0c4b48b06f 100644 --- a/gremlin-python/src/main/python/tests/integration/conftest.py +++ b/gremlin-python/src/main/python/tests/integration/conftest.py @@ -43,12 +43,19 @@ verbose_logging = False # Shared namedtuple used by remote_connection_with_registry fixture and its tests. RegistryPoint = namedtuple('RegistryPoint', ['x', 'y']) +# Shared namedtuple used by remote_connection_with_primitive_registry fixture and its tests. +RegistryUint32 = namedtuple('RegistryUint32', ['value']) @pytest.fixture def registry_point_class(): return RegistryPoint + [email protected] +def registry_uint32_class(): + return RegistryUint32 + logging.basicConfig(format='%(asctime)s [%(levelname)8s] [%(filename)15s:%(lineno)d - %(funcName)10s()] - %(message)s', level=logging.DEBUG if verbose_logging else logging.INFO) @@ -241,6 +248,27 @@ def remote_connection_with_registry(request): return remote_conn [email protected] +def remote_connection_with_primitive_registry(request): + from gremlin_python.structure.graph import ProviderDefinedTypeRegistry + + registry = ProviderDefinedTypeRegistry() + registry.register_primitive('Uint32', + from_value=lambda v: RegistryUint32(value=int(v)), + to_value=lambda u: str(u.value), + target_class=RegistryUint32) + 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 34fa004e12..7addce6d24 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,7 +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.structure.graph import ProviderDefinedType, PrimitiveProviderDefinedType from gremlin_python.process.graph_traversal import __, GraphTraversalSource from gremlin_python.process.traversal import TraversalStrategies, GValue from gremlin_python.process.strategies import OptionsStrategy @@ -612,3 +612,54 @@ def test_pdt_in_collection(client): assert pdt_list[1].name == 'Point' assert pdt_list[1].fields['x'] == 3 assert pdt_list[1].fields['y'] == 4 + + +def test_primitive_pdt_round_trip(client): + """Inject and retrieve a primitive Uint32 PDT (opaque string value).""" + results = client.submit( + "g.inject(PDT(\"Uint32\", \"4294967295\"))" + ).all().result() + + assert len(results) == 1 + pdt = results[0] + assert isinstance(pdt, PrimitiveProviderDefinedType) + assert pdt.name == 'Uint32' + assert pdt.value == '4294967295' + + +def test_primitive_pdt_in_collection(client): + """Retrieve multiple primitive PDTs of different kinds as a list.""" + results = client.submit( + "g.inject([PDT(\"Uint32\", \"42\"), PDT(\"TinkerId\", \"abc-123\")])" + ).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], PrimitiveProviderDefinedType) + assert pdt_list[0].name == 'Uint32' + assert pdt_list[0].value == '42' + + assert isinstance(pdt_list[1], PrimitiveProviderDefinedType) + assert pdt_list[1].name == 'TinkerId' + assert pdt_list[1].value == 'abc-123' + + +def test_primitive_pdt_nested_in_composite(client): + """Inject and retrieve a composite PDT containing a nested primitive PDT.""" + results = client.submit( + "g.inject(PDT(\"Measurement\", [\"unit\":\"meters\", \"quantity\":PDT(\"Uint32\", \"100\")]))" + ).all().result() + + assert len(results) == 1 + pdt = results[0] + assert isinstance(pdt, ProviderDefinedType) + assert pdt.name == 'Measurement' + assert pdt.fields['unit'] == 'meters' + + quantity = pdt.fields['quantity'] + assert isinstance(quantity, PrimitiveProviderDefinedType) + assert quantity.name == 'Uint32' + assert quantity.value == '100' 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 cf853d43b8..f488442cc8 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, ProviderDefinedType, provider_defined +from gremlin_python.structure.graph import Vertex, Edge, Graph, ProviderDefinedType, PrimitiveProviderDefinedType, 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 @@ -320,3 +320,20 @@ class TestDriverRemoteConnection(object): assert isinstance(result, TestPoint) assert result.x == 5 assert result.y == 10 + + def test_primitive_pdt_round_trip_via_traversal(self, remote_connection): + g = traversal().with_(remote_connection) + pdt = PrimitiveProviderDefinedType('Uint32', '4294967295') + result = g.inject(pdt).next() + assert isinstance(result, PrimitiveProviderDefinedType) + assert result.name == 'Uint32' + assert result.value == '4294967295' + + def test_primitive_pdt_registry_round_trip_via_traversal(self, remote_connection_with_primitive_registry, + registry_uint32_class): + g = traversal().with_(remote_connection_with_primitive_registry) + u = registry_uint32_class(value=42) + result = g.inject(u).next() + # Registry auto-dehydrates on send (to_value) and auto-hydrates on receive (from_value) + assert isinstance(result, registry_uint32_class) + assert result.value == 42 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 index 147e4b670e..13c5c1b17d 100644 --- 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 @@ -88,18 +88,18 @@ class TestProviderDefinedTypeRegistry(object): deserialize_fn=lambda fields: Point(fields["x"], fields["y"]), serialize_fn=lambda p: {"x": p.x, "y": p.y}, target_class=Point) - adapter = registry.get_adapter_by_class(Point) + adapter = registry.get_composite_adapter_by_class(Point) fields = adapter['serialize'](Point(1.0, 2.0)) assert fields == {"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 + assert registry.get_composite_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 fields: fields, target_class=dict) - adapter = registry.get_adapter_by_class(dict) + adapter = registry.get_composite_adapter_by_class(dict) assert adapter['serialize'] is None def test_hydrate_inner_registered_in_unregistered_outer(self): @@ -144,7 +144,7 @@ class TestProviderDefinedTypeRegistryBuild(object): mock_entry_points.return_value = {'tinkerpop.pdt': [mock_ep]} registry = ProviderDefinedTypeRegistry.create() - assert "com.mock.Type" in registry._adapters_by_name + assert "com.mock.Type" in registry._composite_adapters_by_name def test_build_handles_failing_entry_point(self): from unittest.mock import patch, MagicMock @@ -162,7 +162,7 @@ class TestProviderDefinedTypeRegistryBuild(object): registry = ProviderDefinedTypeRegistry.create() assert isinstance(registry, ProviderDefinedTypeRegistry) - assert len(registry._adapters_by_name) == 0 + assert len(registry._composite_adapters_by_name) == 0 class TestReaderAutoHydration(object): diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java index 1416f35887..c328707d72 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java @@ -41,6 +41,10 @@ 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.CompositePDTAdapter; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedTypeRegistry; +import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; +import org.apache.tinkerpop.gremlin.server.pdt.Measurement; +import org.apache.tinkerpop.gremlin.server.pdt.Uint32; +import org.apache.tinkerpop.gremlin.server.pdt.Uint32Adapter; import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex; import org.apache.tinkerpop.gremlin.util.ExceptionHelper; import org.apache.tinkerpop.gremlin.util.TimeUtil; @@ -1395,6 +1399,80 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration } } + @Test + public void shouldRoundTripRawPrimitivePdtViaTraversal() { + // Unregistered base case: with no adapter, a PrimitiveProviderDefinedType round-trips as-is. + final Cluster cluster = TestClientFactory.build().create(); + try { + final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster)); + final PrimitiveProviderDefinedType pdt = new PrimitiveProviderDefinedType("UnregisteredPrimitive", "4294967295"); + final Object result = g.inject(pdt).next(); + + assertTrue("Expected PrimitiveProviderDefinedType but got: " + result.getClass().getName(), + result instanceof PrimitiveProviderDefinedType); + final PrimitiveProviderDefinedType r = (PrimitiveProviderDefinedType) result; + assertEquals("UnregisteredPrimitive", r.getName()); + assertEquals("4294967295", r.getValue()); + } finally { + cluster.close(); + } + } + + @Test + public void shouldRoundTripRegisteredPrimitivePdtViaTraversal() { + // Registered case: a raw provider object auto-dehydrates on send and auto-hydrates on receive. + final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty(); + registry.register(new Uint32Adapter()); + + final Cluster cluster = TestClientFactory.build() + .serializer(new GraphBinaryMessageSerializerV4(TypeSerializerRegistry.INSTANCE, registry)) + .create(); + try { + final DriverRemoteConnection connection = DriverRemoteConnection.using(cluster); + connection.setPdtRegistry(registry); + final GraphTraversalSource g = traversal().with(connection); + + final Object result = g.inject(new Uint32(42L)).next(); + + assertTrue("Expected Uint32 but got: " + result.getClass().getName(), result instanceof Uint32); + assertEquals(42L, ((Uint32) result).getValue()); + } finally { + cluster.close(); + } + } + + @Test + public void shouldRoundTripRegistryNestedPrimitivePdtViaTraversal() { + // Registered nested case: a composite containing a primitive PDT field, both adapters registered, + // auto-dehydrates recursively on send and auto-hydrates recursively on receive. + // Outer composite is the @ProviderDefined "Measurement" (registered by class); inner primitive + // uses the Uint32 adapter. Exercises recursive de/hydration across both PDT kinds. + final ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.empty(); + registry.register(new Uint32Adapter()); + registry.register(Measurement.class); + + final Cluster cluster = TestClientFactory.build() + .serializer(new GraphBinaryMessageSerializerV4(TypeSerializerRegistry.INSTANCE, registry)) + .create(); + try { + final DriverRemoteConnection connection = DriverRemoteConnection.using(cluster); + connection.setPdtRegistry(registry); + final GraphTraversalSource g = traversal().with(connection); + + final Object result = g.inject(new Measurement("meters", new Uint32(100L))).next(); + + assertTrue("Expected Measurement but got: " + result.getClass().getName(), + result instanceof Measurement); + final Measurement m = (Measurement) result; + assertEquals("meters", m.unit); + assertTrue("Expected nested Uint32 but got: " + m.quantity.getClass().getName(), + m.quantity instanceof Uint32); + assertEquals(100L, m.quantity.getValue()); + } finally { + cluster.close(); + } + } + // --- PDT helper types --- static class TestPoint { @@ -1424,4 +1502,5 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration public TestAnnotatedPoint() {} TestAnnotatedPoint(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/GremlinServerPrimitivePdtIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerPrimitivePdtIntegrateTest.java index def25a2935..dcdae9d535 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerPrimitivePdtIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerPrimitivePdtIntegrateTest.java @@ -23,7 +23,6 @@ import org.apache.tinkerpop.gremlin.driver.Cluster; import org.apache.tinkerpop.gremlin.driver.Result; import org.apache.tinkerpop.gremlin.server.pdt.TinkerId; import org.apache.tinkerpop.gremlin.server.pdt.Uint32; -import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; import org.apache.tinkerpop.gremlin.structure.io.pdt.ProviderDefinedType; import org.junit.After; import org.junit.Before; @@ -32,7 +31,6 @@ import org.junit.Test; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; diff --git a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryWriterPdtTest.java b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryWriterPdtTest.java index 0d57c515e5..817b6c6dc0 100644 --- a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryWriterPdtTest.java +++ b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryWriterPdtTest.java @@ -23,12 +23,7 @@ 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.TypeSerializerRegistry; -import org.apache.tinkerpop.gremlin.structure.io.pdt.CompositePDTAdapter; -import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitivePDTAdapter; -import org.apache.tinkerpop.gremlin.structure.io.pdt.PrimitiveProviderDefinedType; -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.ProviderDefinedTypeRegistry; +import org.apache.tinkerpop.gremlin.structure.io.pdt.*; import org.apache.tinkerpop.gremlin.util.ser.NettyBufferFactory; import org.junit.Test; @@ -37,6 +32,7 @@ import java.util.LinkedHashMap; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -77,6 +73,24 @@ public class GraphBinaryWriterPdtTest { } } + @ProviderDefined(name = "AnnotatedName") + static class AnnotatedDual { + int x = 7; + } + + static class AnnotatedDualAdapter implements CompositePDTAdapter<AnnotatedDual> { + @Override public String typeName() { return "AdapterName"; } + @Override public Class<AnnotatedDual> targetClass() { return AnnotatedDual.class; } + @Override public Map<String, Object> toFields(final AnnotatedDual obj) { + final Map<String, Object> m = new LinkedHashMap<>(); + m.put("viaAdapter", obj.x * 10); + return m; + } + @Override public AnnotatedDual fromFields(final Map<String, Object> fields) { + return new AnnotatedDual(); + } + } + @Test public void shouldAutoConvertAnnotatedObjectToPdt() throws IOException { final Buffer buffer = bufferFactory.create(allocator.buffer()); @@ -121,6 +135,29 @@ public class GraphBinaryWriterPdtTest { assertEquals(42, result.value); } + /** + * A registered adapter takes precedence over the {@link ProviderDefined} annotation when dehydrating on + * the write path, consistent with GremlinLang.argAsString. AnnotatedDual is both annotated and has a + * registered CompositePDTAdapter; the adapter's type name and fields must win. + */ + @Test + public void shouldPreferRegisteredAdapterOverAnnotationOnWritePath() throws IOException { + final ProviderDefinedTypeRegistry pdtRegistry = ProviderDefinedTypeRegistry.empty(); + pdtRegistry.register(new AnnotatedDualAdapter()); + + final GraphBinaryWriter registryWriter = new GraphBinaryWriter(TypeSerializerRegistry.INSTANCE, pdtRegistry); + + final Buffer buffer = bufferFactory.create(allocator.buffer()); + registryWriter.write(new AnnotatedDual(), buffer); + buffer.readerIndex(0); + + // Read with a registry-free reader to inspect the raw serialized form (no hydration). + final ProviderDefinedType result = reader.read(buffer); + assertEquals("AdapterName", result.getName()); + assertEquals(70, result.getFields().get("viaAdapter")); + assertFalse(result.getFields().containsKey("x")); + } + @Test public void shouldNotDoubleWrapProviderDefinedType() throws IOException { final Map<String, Object> fields = new LinkedHashMap<>();
