Cole-Greer commented on code in PR #3433: URL: https://github.com/apache/tinkerpop/pull/3433#discussion_r3393260574
########## docs/src/dev/provider/index.asciidoc: ########## @@ -1334,6 +1334,213 @@ can be used as a reference on how these files can be used and its link:https://github.com/apache/tinkerpop/blob/x.y.z/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/Model.java[model] shows the Java representation of those files. +[[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. + +==== 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 = "mygraph:Point") +public class Point { + public double x; + public double y; + + public Point(double x, double y) { + this.x = x; + this.y = y; + } +} +---- + +The `name` attribute is a unique identifier for the type. It is strongly recommended to namespace type names using +your graph's identifier as a prefix (e.g. `"mygraph:Point"`). This avoids collisions when clients interact with +multiple providers and makes the origin of a type immediately clear. By default, all fields are included. Use +`includedFields` or `excludedFields` to control which fields are serialized: + +[source,java] +---- +@ProviderDefined(name = "mygraph:Point", includedFields = {"x", "y"}) +public class Point { ... } + +// or exclude specific fields +@ProviderDefined(name = "mygraph:Person", excludedFields = {"internalId"}) +public class Person { ... } +---- + +NOTE: For annotation-based round-trip hydration (see <<round-trip-support>>), an annotated class must expose a no-arg +constructor and the mapped fields must be directly settable (e.g. public fields). Classes that cannot meet these +requirements — for example those with immutable `final` fields or no default constructor — should instead use a +`ProviderDefinedTypeAdapter` (see <<adapter-for-types-you-don-t-own>>), which gives full control over construction. + +==== Nested Types + +PDT supports nested custom types. Each nested type must also be annotated: + +[source,java] +---- +@ProviderDefined(name = "mygraph:Address") +public class Address { + public String street; + public String city; +} + +@ProviderDefined(name = "mygraph:Person") +public class Person { + public String name; + public Address address; +} +---- + +When serialized, the `address` field is itself encoded as a PDT value. + +[[adapter-for-types-you-don-t-own]] +==== 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 "mygraph: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")); + } +} +---- + +[[round-trip-support]] +==== Round-Trip Support (Dehydration and Hydration) + +There is an important distinction between *dehydration* (serializing a type for sending) and *hydration* (deserializing +a received PDT back into a language-native type). + +*Dehydration* is handled automatically for `@ProviderDefined`-annotated classes and adapter-registered types. When a +user passes an annotated object into a Gremlin traversal or script, TinkerPop converts it to a PDT on the wire +without any extra configuration. + +*Hydration* — reconstructing an incoming PDT back into the original typed object — requires the driver to know which +class corresponds to a given PDT name. Without this mapping, the driver will return a generic `ProviderDefinedType` +object. To enable automatic round-trip hydration, providers must expose a pre-configured `ProviderDefinedTypeRegistry` +to users. How that registry is populated differs by language: + +===== Java + +Register annotated classes explicitly with the registry. `register(Class<?>...)` inspects the `@ProviderDefined` +annotation to derive the type name and field mapping automatically: + +[source,java] +---- +ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.build(); +registry.register(Point.class, Address.class, Person.class); +---- + +Adapter types (for classes you don't own) are discovered automatically via `ServiceLoader` when using +`ProviderDefinedTypeRegistry.build()`. Register them 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 +---- + +===== Python + +Hydration is fully automatic for `@provider_defined`-decorated classes. The decorator registers the class at +definition time (import time), so any annotated type round-trips without any additional setup. + +===== .NET + +`[ProviderDefined]`-annotated types are discovered automatically. Calling `ProviderDefinedTypeRegistry.Build()` +scans all loaded assemblies for `[ProviderDefined]`-annotated types and registers them for hydration. No extra +configuration is needed — providers simply annotate their types and users call `Build()` to create the registry. + +===== JavaScript + +Register hydration adapters explicitly on a `ProviderDefinedTypeRegistry` instance, then pass it to the connection: + +[source,javascript] +---- +const registry = new ProviderDefinedTypeRegistry(); +registry.register('mygraph:Point', { + serialize: (obj) => ({ x: obj.x, y: obj.y }), + deserialize: (props) => new Point(props.x, props.y) +}, Point); +---- + +===== Go + +Register types on a `PDTRegistry` instance. Go supports either reflection-based registration (using `pdt` struct +tags) or explicit function registration: + +[source,go] +---- +registry := NewPDTRegistry() +registry.RegisterType("mygraph:Point", reflect.TypeOf(Point{})) +---- + +===== Provider Factory Pattern + +Regardless of language, the recommended pattern is for providers to expose a factory method that returns a +pre-configured `ProviderDefinedTypeRegistry`. This shields end users from needing to know which types exist or how +the registry is populated: + +[source,java] +---- +// In the provider's client library +public class MyGraphTypeRegistry { + public static ProviderDefinedTypeRegistry build() { + ProviderDefinedTypeRegistry registry = ProviderDefinedTypeRegistry.build(); // discovers ServiceLoader adapters + registry.register(Point.class, Address.class, Person.class); // registers annotated types + return registry; + } +} +---- + +End users configure their connection in one line: + +[source,java] +---- +DriverRemoteConnection conn = DriverRemoteConnection.using(cluster); +conn.setPdtRegistry(MyGraphTypeRegistry.build()); +GraphTraversalSource g = traversal().with(conn); +---- + +With this in place, `Point` objects round-trip transparently in both directions — the annotation handles outbound +serialization and the registry handles inbound reconstruction. + +For driver users consuming PDTs, see the <<gremlin-variants,Gremlin Variants>> reference documentation for +each language driver. + Review Comment: I've folded details about the String keys and non-empty field names into the basic usage section. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
