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 fdff7b20a3e7849e93fcd1811e542455c28df1c3 Author: Cole Greer <[email protected]> AuthorDate: Wed Jun 24 20:59:47 2026 -0700 Add PrimitivePDT support to gremlin-dotnet GLV Implements PrimitivePDT in the .NET GLV, mirroring composite support and applying the review lessons from the Python GLV. - PrimitiveProviderDefinedType (Name, Value) + IPrimitivePdtAdapter<T> (TypeName/FromString/ToString) in Structure/. - DataType.PrimitivePDT (0xF1) enabled; PrimitivePDTSerializer writes/reads two fully-qualified Strings; registered in TypeSerializerRegistry. - ProviderDefinedTypeRegistry gains an explicit primitive adapter path (register + GetPrimitiveAdapterByType + HydratePrimitive), mirroring the composite/primitive naming split used across the other GLVs. - GraphBinaryReader hydrates PrimitiveProviderDefinedType via the registry. - GremlinLang text translation emits PDT("name","value") for a PrimitiveProviderDefinedType and auto-dehydrates registered types — the client-side text path that was the Python gap. - ADAPTER-OVER-ATTRIBUTE precedence: a registered adapter takes priority over the [ProviderDefined] attribute on dehydration (matching the Java/Python fix in ef194e358f), applied in the text path. - Client wiring reuses the existing SetPdtRegistry / GremlinClient / DriverRemoteConnection path. No GraphSON g:PrimitivePdt read path added (consistent with the .NET driver's GraphBinary-based V4 response handling). Tests: 57 unit tests pass (serializer round-trip incl. opaque-value fidelity, registry hydration, gremlin-lang text emission, adapter-over-attribute precedence). Integration tests (raw, opaque value, in-collection, nested-in-composite, registered) pass against the test server: 6/6. tinkerpop-2gy.11 Assisted-by: Kiro:claude-opus-4.8 --- .../Gremlin.Net/Process/Traversal/GremlinLang.cs | 15 +- .../Structure/IO/GraphBinary4/DataType.cs | 3 +- .../Structure/IO/GraphBinary4/GraphBinaryReader.cs | 12 +- .../IO/GraphBinary4/TypeSerializerRegistry.cs | 2 + .../GraphBinary4/Types/PrimitivePDTSerializer.cs | 66 +++++++ .../Gremlin.Net/Structure/IPrimitivePdtAdapter.cs | 47 +++++ .../Structure/PrimitiveProviderDefinedType.cs | 65 +++++++ .../Structure/ProviderDefinedTypeRegistry.cs | 138 +++++++++++--- .../Driver/DriverRemoteConnectionTests.cs | 51 +++++- .../Driver/GremlinClientTests.cs | 72 ++++++++ .../Process/Traversal/GremlinLangTests.cs | 73 ++++++++ .../PrimitiveProviderDefinedTypeTests.cs | 204 +++++++++++++++++++++ .../Structure/PrimitivePdtRegistryTests.cs | 126 +++++++++++++ 13 files changed, 844 insertions(+), 30 deletions(-) diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs index 684a76c2f6..ce9bb1e1d5 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs @@ -356,6 +356,11 @@ namespace Gremlin.Net.Process.Traversal sb2.Append(']'); return $"PDT(\"{EscapeJava(pdt.Name)}\",{sb2})"; } + + if (arg is PrimitiveProviderDefinedType primitivePdt) + { + return $"PDT(\"{EscapeJava(primitivePdt.Name)}\",\"{EscapeJava(primitivePdt.Value)}\")"; + } if (arg is IDictionary dict) return AsString(dict); @@ -377,9 +382,17 @@ namespace Gremlin.Net.Process.Traversal // Precedence: a registered adapter intentionally takes priority over the [ProviderDefined] // attribute so that explicit adapters can override attribute-derived dehydration behavior. + // Check primitive adapter first, then composite. if (PdtRegistry != null) { - var adapterInfo = PdtRegistry.GetAdapterByType(arg.GetType()); + var primitiveInfo = PdtRegistry.GetPrimitiveAdapterByType(arg.GetType()); + if (primitiveInfo != null) + { + var (adapterTypeName, toStr) = primitiveInfo.Value; + return ArgAsString(new PrimitiveProviderDefinedType(adapterTypeName, toStr(arg))); + } + + var adapterInfo = PdtRegistry.GetCompositeAdapterByType(arg.GetType()); if (adapterInfo != null) { var (adapterTypeName, toFields) = adapterInfo.Value; 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 a8844bbb47..bc48d9edf4 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,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4 // 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 PrimitivePDT = new DataType(0xF1); + public static readonly DataType PrimitivePDT = new DataType(0xF1); public static readonly DataType Char = new DataType(0x80); public static readonly DataType Duration = new DataType(0x81); public static readonly DataType Marker = new DataType(0xFD); 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 97f0dd76fe..76d4b40999 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs @@ -99,12 +99,22 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4 { if (_pdtRegistry != null) { - var hydrated = _pdtRegistry.Hydrate(pdt); + var hydrated = _pdtRegistry.HydrateComposite(pdt); if (hydrated is not ProviderDefinedType) return hydrated; } return ProviderDefinedAttribute.HydrateIfRegistered(pdt); } + if (result is PrimitiveProviderDefinedType primitivePdt) + { + if (_pdtRegistry != null) + { + var hydrated = _pdtRegistry.HydratePrimitive(primitivePdt); + if (hydrated is not PrimitiveProviderDefinedType) + return hydrated; + } + return primitivePdt; + } return result; } } 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 8c48079668..e0f02fea61 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs @@ -66,6 +66,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4 {typeof(TimeSpan), new DurationSerializer()}, {typeof(Marker), SingleTypeSerializers.MarkerSerializer}, {typeof(ProviderDefinedType), new CompositePDTSerializer()}, + {typeof(PrimitiveProviderDefinedType), new PrimitivePDTSerializer()}, }; private readonly Dictionary<DataType, ITypeSerializer> _serializerByDataType = @@ -100,6 +101,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4 {DataType.Duration, new DurationSerializer()}, {DataType.Marker, SingleTypeSerializers.MarkerSerializer}, {DataType.CompositePDT, new CompositePDTSerializer()}, + {DataType.PrimitivePDT, new PrimitivePDTSerializer()}, }; /// <summary> diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PrimitivePDTSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PrimitivePDTSerializer.cs new file mode 100644 index 0000000000..9d84f8c191 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PrimitivePDTSerializer.cs @@ -0,0 +1,66 @@ +#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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// <summary> + /// A <see cref="PrimitiveProviderDefinedType"/> serializer for the PrimitivePDT data type. + /// Wire format: two fully-qualified Strings {name}{value}. + /// </summary> + public class PrimitivePDTSerializer : SimpleTypeSerializer<PrimitiveProviderDefinedType> + { + /// <summary> + /// Initializes a new instance of the <see cref="PrimitivePDTSerializer"/> class. + /// </summary> + public PrimitivePDTSerializer() : base(DataType.PrimitivePDT) + { + } + + /// <inheritdoc /> + protected override async Task WriteValueAsync(PrimitiveProviderDefinedType value, Stream stream, + GraphBinaryWriter writer, CancellationToken cancellationToken = default) + { + await writer.WriteAsync(value.Name, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(value.Value, stream, cancellationToken).ConfigureAwait(false); + } + + /// <inheritdoc /> + protected override async Task<PrimitiveProviderDefinedType> 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("PrimitivePDT name cannot be null or empty."); + + var value = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false) as string; + if (value == null) + throw new IOException("PrimitivePDT value cannot be null."); + + return new PrimitiveProviderDefinedType(name!, value); + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IPrimitivePdtAdapter.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IPrimitivePdtAdapter.cs new file mode 100644 index 0000000000..78240a0e15 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IPrimitivePdtAdapter.cs @@ -0,0 +1,47 @@ +#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 + +namespace Gremlin.Net.Structure +{ + /// <summary> + /// Adapter for hydrating a <see cref="PrimitiveProviderDefinedType"/> into a strongly-typed object. + /// </summary> + /// <typeparam name="T">The target type to hydrate into.</typeparam> + public interface IPrimitivePdtAdapter<T> + { + /// <summary> + /// Gets the fully-qualified type name this adapter handles. + /// </summary> + string TypeName { get; } + + /// <summary> + /// Creates a typed instance from the opaque string value. + /// </summary> + T FromString(string value); + + /// <summary> + /// Converts a typed instance to its opaque string representation. + /// </summary> + string ToString(T obj); + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/PrimitiveProviderDefinedType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/PrimitiveProviderDefinedType.cs new file mode 100644 index 0000000000..83f8932666 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/PrimitiveProviderDefinedType.cs @@ -0,0 +1,65 @@ +#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; + +namespace Gremlin.Net.Structure +{ + /// <summary> + /// Represents a primitive provider-defined type (PDT) with a name and an opaque string value. + /// </summary> + public class PrimitiveProviderDefinedType + { + /// <summary> + /// Initializes a new instance of the <see cref="PrimitiveProviderDefinedType"/> class. + /// </summary> + /// <param name="name">The fully-qualified name of the provider-defined type.</param> + /// <param name="value">The opaque string value.</param> + public PrimitiveProviderDefinedType(string name, string value) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + if (string.IsNullOrEmpty(name)) throw new ArgumentException("name cannot be empty", nameof(name)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// <summary> + /// Gets the fully-qualified name of this primitive provider-defined type. + /// </summary> + public string Name { get; } + + /// <summary> + /// Gets the opaque string value of this primitive provider-defined type. + /// </summary> + public string Value { get; } + + /// <inheritdoc /> + public override string ToString() => $"pdt[{Name}]{{{Value}}}"; + + /// <inheritdoc /> + public override bool Equals(object? obj) => + obj is PrimitiveProviderDefinedType other && Name == other.Name && Value == other.Value; + + /// <inheritdoc /> + public override int GetHashCode() => HashCode.Combine(Name, Value); + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs index 427a35ca44..46843d74b0 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs @@ -30,27 +30,39 @@ 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. + /// Registry for <see cref="IProviderDefinedTypeAdapter{T}"/> and <see cref="IPrimitivePdtAdapter{T}"/> + /// instances that hydrate provider-defined types 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(); + private readonly Dictionary<string, object> _compositeAdaptersByName = new(); + private readonly Dictionary<Type, (string typeName, object adapter)> _compositeAdaptersByType = new(); + private readonly Dictionary<string, object> _primitiveAdaptersByName = new(); + private readonly Dictionary<Type, (string typeName, object adapter)> _primitiveAdaptersByType = new(); /// <summary> - /// Registers an adapter for a specific provider-defined type name. + /// Registers a composite 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); + _compositeAdaptersByName[adapter.TypeName] = adapter; + _compositeAdaptersByType[typeof(T)] = (adapter.TypeName, adapter); + } + + /// <summary> + /// Registers a primitive adapter for a specific provider-defined type name. + /// </summary> + public void RegisterPrimitive<T>(IPrimitivePdtAdapter<T> adapter) + { + _primitiveAdaptersByName[adapter.TypeName] = adapter; + _primitiveAdaptersByType[typeof(T)] = (adapter.TypeName, adapter); } /// <summary> /// Creates a registry populated by scanning loaded assemblies for: /// <list type="bullet"> - /// <item>Types implementing <see cref="IProviderDefinedTypeAdapter{T}"/> (adapter-based hydration)</item> + /// <item>Types implementing <see cref="IProviderDefinedTypeAdapter{T}"/> (composite adapter-based hydration)</item> + /// <item>Types implementing <see cref="IPrimitivePdtAdapter{T}"/> (primitive adapter-based hydration)</item> /// <item>Types annotated with <see cref="ProviderDefinedAttribute"/> (annotation-based round-trip)</item> /// </list> /// </summary> @@ -64,18 +76,38 @@ namespace Gremlin.Net.Structure { foreach (var type in assembly.GetTypes()) { - // Register adapter implementations - var adapterInterface = type.GetInterfaces() + // Register composite adapter implementations + var compositeInterface = type.GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IProviderDefinedTypeAdapter<>)); - if (adapterInterface != null && !type.IsAbstract && !type.IsInterface) + if (compositeInterface != null && !type.IsAbstract && !type.IsInterface) { try { var adapter = Activator.CreateInstance(type); var registerMethod = typeof(ProviderDefinedTypeRegistry) .GetMethod(nameof(Register))! - .MakeGenericMethod(adapterInterface.GetGenericArguments()[0]); + .MakeGenericMethod(compositeInterface.GetGenericArguments()[0]); + registerMethod.Invoke(registry, new[] { adapter }); + } + catch + { + // skip types that can't be instantiated + } + } + + // Register primitive adapter implementations + var primitiveInterface = type.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IPrimitivePdtAdapter<>)); + if (primitiveInterface != null && !type.IsAbstract && !type.IsInterface) + { + try + { + var adapter = Activator.CreateInstance(type); + var registerMethod = typeof(ProviderDefinedTypeRegistry) + .GetMethod(nameof(RegisterPrimitive))! + .MakeGenericMethod(primitiveInterface.GetGenericArguments()[0]); registerMethod.Invoke(registry, new[] { adapter }); } catch @@ -103,11 +135,11 @@ namespace Gremlin.Net.Structure } /// <summary> - /// Returns the type name and ToFields method for the given CLR type, or null if not registered. + /// Returns the type name and ToFields method for the given CLR type, or null if not registered as composite. /// </summary> - internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetAdapterByType(Type type) + internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetCompositeAdapterByType(Type type) { - if (!_adaptersByType.TryGetValue(type, out var entry)) + if (!_compositeAdaptersByType.TryGetValue(type, out var entry)) return null; var method = entry.adapter.GetType().GetMethod("ToFields"); if (method == null) return null; @@ -115,25 +147,50 @@ namespace Gremlin.Net.Structure } /// <summary> - /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered adapter. + /// Returns the type name and ToString method for the given CLR type, or null if not registered as primitive. + /// </summary> + internal (string typeName, Func<object, string>)? GetPrimitiveAdapterByType(Type type) + { + if (!_primitiveAdaptersByType.TryGetValue(type, out var entry)) + return null; + var method = entry.adapter.GetType().GetMethod("ToString", new[] { type }); + if (method == null) return null; + return (entry.typeName, obj => (string)method.Invoke(entry.adapter, new[] { obj })!); + } + + /// <summary> + /// Returns the type name and ToFields method for the given CLR type (composite), + /// or type name and ToString method (primitive). Checks primitive first, then composite. + /// Returns null if not registered. + /// </summary> + internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetAdapterByType(Type type) + { + // Check composite adapters (backward compatibility with existing callers) + return GetCompositeAdapterByType(type); + } + + /// <summary> + /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered composite adapter. /// Returns the original PDT if no adapter is registered or if hydration fails. /// </summary> - public object Hydrate(ProviderDefinedType pdt) + public object HydrateComposite(ProviderDefinedType pdt) { - if (!_adaptersByName.TryGetValue(pdt.Name, out var adapterObj)) + if (!_compositeAdaptersByName.TryGetValue(pdt.Name, out var adapterObj)) { // No adapter for outer — still recurse into nested PDT fields Dictionary<string, object?>? resolved = null; foreach (var (key, value) in pdt.Fields) { + object? hydrated = value; if (value is ProviderDefinedType nested) + hydrated = HydrateComposite(nested); + else if (value is PrimitiveProviderDefinedType nestedPrim) + hydrated = HydratePrimitive(nestedPrim); + + if (!ReferenceEquals(hydrated, value)) { - var hydrated = Hydrate(nested); - if (!ReferenceEquals(hydrated, nested)) - { - resolved ??= new Dictionary<string, object?>(pdt.Fields); - resolved[key] = hydrated; - } + resolved ??= new Dictionary<string, object?>(pdt.Fields); + resolved[key] = hydrated; } } return resolved != null ? new ProviderDefinedType(pdt.Name, resolved) : pdt; @@ -143,7 +200,12 @@ namespace Gremlin.Net.Structure var hydratedFields = new Dictionary<string, object?>(); foreach (var (key, value) in pdt.Fields) { - hydratedFields[key] = value is ProviderDefinedType nested ? Hydrate(nested) : value; + if (value is ProviderDefinedType nested) + hydratedFields[key] = HydrateComposite(nested); + else if (value is PrimitiveProviderDefinedType nestedPrim) + hydratedFields[key] = HydratePrimitive(nestedPrim); + else + hydratedFields[key] = value; } var readOnlyFields = new ReadOnlyDictionary<string, object?>(hydratedFields); @@ -155,5 +217,31 @@ namespace Gremlin.Net.Structure return pdt; } } + + /// <summary> + /// Hydrates a <see cref="PrimitiveProviderDefinedType"/> into a typed object using a registered primitive adapter. + /// Returns the original primitive PDT if no adapter is registered or if hydration fails. + /// </summary> + public object HydratePrimitive(PrimitiveProviderDefinedType pdt) + { + if (!_primitiveAdaptersByName.TryGetValue(pdt.Name, out var adapterObj)) + return pdt; + try + { + var method = adapterObj.GetType().GetMethod("FromString"); + return method!.Invoke(adapterObj, new object[] { pdt.Value })!; + } + catch (Exception) + { + return pdt; + } + } + + /// <summary> + /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered composite adapter. + /// Returns the original PDT if no adapter is registered or if hydration fails. + /// </summary> + [Obsolete("Use HydrateComposite instead.")] + public object Hydrate(ProviderDefinedType pdt) => HydrateComposite(pdt); } } diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs index 3aaedb218e..e28742bef2 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs @@ -192,4 +192,53 @@ public class DriverRemoteConnectionTests } #endregion -} \ No newline at end of file + + [Fact] + public void ShouldRoundTripPrimitivePdtViaTraversalApi() + { + 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 PrimitiveProviderDefinedType("TestToken", "abc123"); + + var results = g.Inject<object>(pdt).ToList(); + + Assert.Single(results); + var result = Assert.IsType<PrimitiveProviderDefinedType>(results[0]); + Assert.Equal("TestToken", result.Name); + Assert.Equal("abc123", result.Value); + } + + [Fact] + public void ShouldRoundTripPrimitiveTypedObjectViaRegistry() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new TestUint32Adapter()); + + var gremlinServer = new GremlinServer(TestHost, TestPort); + using var gremlinClient = new GremlinClient(gremlinServer, pdtRegistry: registry); + using var connection = new DriverRemoteConnection(gremlinClient, "gmodern", pdtRegistry: registry); + var g = AnonymousTraversalSource.Traversal().With(connection); + + var val = 42u; + + var results = g.Inject<object>(val).ToList(); + + Assert.Single(results); + Assert.IsType<uint>(results[0]); + Assert.Equal(42u, (uint)results[0]); + } + + #region Test helpers (primitive) + + private class TestUint32Adapter : IPrimitivePdtAdapter<uint> + { + public string TypeName => "TestToken"; + public uint FromString(string value) => uint.Parse(value); + public string ToString(uint obj) => obj.ToString(); + } + + #endregion +} diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs index a5fdac7c8e..91507a517d 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs @@ -272,5 +272,77 @@ namespace Gremlin.Net.IntegrationTest.Driver Assert.Equal(3, p2.Fields["x"]); Assert.Equal(4, p2.Fields["y"]); } + + [Fact] + public async Task ShouldRoundTripSimplePrimitivePdt() + { + var gremlinServer = new GremlinServer(TestHost, TestPort); + using var gremlinClient = new GremlinClient(gremlinServer); + + var response = await gremlinClient.SubmitAsync<object>( + "g.inject(PDT(\"Uint32\", \"42\"))"); + var results = await response.ToListAsync(); + + Assert.Single(results); + var pdt = Assert.IsType<PrimitiveProviderDefinedType>(results[0]); + Assert.Equal("Uint32", pdt.Name); + Assert.Equal("42", pdt.Value); + } + + [Fact] + public async Task ShouldRoundTripPrimitivePdtWithOpaqueValue() + { + var gremlinServer = new GremlinServer(TestHost, TestPort); + using var gremlinClient = new GremlinClient(gremlinServer); + + var response = await gremlinClient.SubmitAsync<object>( + "g.inject(PDT(\"Token\", \"007-abc\"))"); + var results = await response.ToListAsync(); + + Assert.Single(results); + var pdt = Assert.IsType<PrimitiveProviderDefinedType>(results[0]); + Assert.Equal("Token", pdt.Name); + Assert.Equal("007-abc", pdt.Value); + } + + [Fact] + public async Task ShouldHandlePrimitivePdtInCollection() + { + var gremlinServer = new GremlinServer(TestHost, TestPort); + using var gremlinClient = new GremlinClient(gremlinServer); + + var response = await gremlinClient.SubmitAsync<object>( + "g.inject([PDT(\"Uint32\", \"1\"), PDT(\"Uint32\", \"2\")])"); + 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<PrimitiveProviderDefinedType>(list[0]); + Assert.Equal("1", p1.Value); + + var p2 = Assert.IsType<PrimitiveProviderDefinedType>(list[1]); + Assert.Equal("2", p2.Value); + } + + [Fact] + public async Task ShouldRoundTripPrimitivePdtNestedInComposite() + { + var gremlinServer = new GremlinServer(TestHost, TestPort); + using var gremlinClient = new GremlinClient(gremlinServer); + + var response = await gremlinClient.SubmitAsync<object>( + "g.inject(PDT(\"Measurement\", [\"unit\":\"kg\", \"value\":PDT(\"Uint32\", \"100\")]))"); + var results = await response.ToListAsync(); + + Assert.Single(results); + var pdt = Assert.IsType<ProviderDefinedType>(results[0]); + Assert.Equal("Measurement", pdt.Name); + Assert.Equal("kg", pdt.Fields["unit"]); + var inner = Assert.IsType<PrimitiveProviderDefinedType>(pdt.Fields["value"]); + Assert.Equal("Uint32", inner.Name); + Assert.Equal("100", inner.Value); + } } } 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 daca1acc07..bae2258f4a 100644 --- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs @@ -1231,5 +1231,78 @@ namespace Gremlin.Net.UnitTest.Process.Traversal { public string Tag { get; set; } = ""; } + + [Fact] + public void g_Inject_PrimitivePDT_basic() + { + var pdt = new PrimitiveProviderDefinedType("Uint32", "42"); + var result = _g.Inject((object)pdt).GremlinLang.GetGremlin(); + Assert.Equal("g.inject(PDT(\"Uint32\",\"42\"))", result); + } + + [Fact] + public void g_Inject_PrimitivePDT_special_chars_in_value() + { + var pdt = new PrimitiveProviderDefinedType("Token", "hello\"world"); + var result = _g.Inject((object)pdt).GremlinLang.GetGremlin(); + Assert.Equal("g.inject(PDT(\"Token\",\"hello\\\"world\"))", result); + } + + [Fact] + public void g_Inject_PrimitivePDT_leading_zeros() + { + var pdt = new PrimitiveProviderDefinedType("Padded", "007"); + var result = _g.Inject((object)pdt).GremlinLang.GetGremlin(); + Assert.Equal("g.inject(PDT(\"Padded\",\"007\"))", result); + } + + [Fact] + public void g_Inject_PrimitivePDT_auto_dehydration_via_primitive_adapter() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new TestUint32Adapter()); + + var g = new GraphTraversalSource(); + g.GremlinLang.PdtRegistry = registry; + + var result = g.Inject((object)99u).GremlinLang.GetGremlin(); + Assert.Equal("g.inject(PDT(\"test:Uint32\",\"99\"))", result); + } + + [Fact] + public void g_Inject_PrimitivePDT_adapter_takes_precedence_over_attribute() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new PrimitiveAdapterForAnnotatedType()); + + var g = new GraphTraversalSource(); + g.GremlinLang.PdtRegistry = registry; + + var obj = new AnnotatedButPrimitiveAdapted { Data = "hello" }; + var result = g.Inject((object)obj).GremlinLang.GetGremlin(); + + // The primitive adapter should win over [ProviderDefined] attribute + Assert.Equal("g.inject(PDT(\"prim:Adapted\",\"hello\"))", result); + } + + [ProviderDefined(Name = "attr.Annotated")] + private class AnnotatedButPrimitiveAdapted + { + public string Data { get; set; } = ""; + } + + private class PrimitiveAdapterForAnnotatedType : IPrimitivePdtAdapter<AnnotatedButPrimitiveAdapted> + { + public string TypeName => "prim:Adapted"; + public AnnotatedButPrimitiveAdapted FromString(string value) => new() { Data = value }; + public string ToString(AnnotatedButPrimitiveAdapted obj) => obj.Data; + } + + private class TestUint32Adapter : IPrimitivePdtAdapter<uint> + { + public string TypeName => "test:Uint32"; + public uint FromString(string value) => uint.Parse(value); + public string ToString(uint obj) => obj.ToString(); + } } } diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/PrimitiveProviderDefinedTypeTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/PrimitiveProviderDefinedTypeTests.cs new file mode 100644 index 0000000000..e96d03e451 --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/PrimitiveProviderDefinedTypeTests.cs @@ -0,0 +1,204 @@ +#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.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 PrimitiveProviderDefinedTypeTests + { + private static readonly GraphBinaryWriter Writer = new(); + private static readonly GraphBinaryReader Reader = new(); + + [Fact] + public async Task TestRoundTripBasic() + { + var expected = new PrimitiveProviderDefinedType("com.example.Uint32", "42"); + + using var stream = new MemoryStream(); + await Writer.WriteAsync(expected, stream); + stream.Position = 0; + var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType; + + Assert.NotNull(actual); + Assert.Equal(expected.Name, actual!.Name); + Assert.Equal(expected.Value, actual.Value); + } + + [Fact] + public async Task TestRoundTripWithLeadingZeros() + { + var expected = new PrimitiveProviderDefinedType("com.example.Padded", "007"); + + using var stream = new MemoryStream(); + await Writer.WriteAsync(expected, stream); + stream.Position = 0; + var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType; + + Assert.NotNull(actual); + Assert.Equal("007", actual!.Value); + } + + [Fact] + public async Task TestRoundTripWithLargeNumber() + { + var expected = new PrimitiveProviderDefinedType("com.example.BigNum", + "99999999999999999999999999999999"); + + using var stream = new MemoryStream(); + await Writer.WriteAsync(expected, stream); + stream.Position = 0; + var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType; + + Assert.NotNull(actual); + Assert.Equal("99999999999999999999999999999999", actual!.Value); + } + + [Fact] + public async Task TestRoundTripNonNumericValue() + { + var expected = new PrimitiveProviderDefinedType("com.example.Token", "abc-def-123"); + + using var stream = new MemoryStream(); + await Writer.WriteAsync(expected, stream); + stream.Position = 0; + var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType; + + Assert.NotNull(actual); + Assert.Equal("abc-def-123", actual!.Value); + } + + [Fact] + public async Task TestRoundTripEmptyValue() + { + var expected = new PrimitiveProviderDefinedType("com.example.Empty", ""); + + using var stream = new MemoryStream(); + await Writer.WriteAsync(expected, stream); + stream.Position = 0; + var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType; + + Assert.NotNull(actual); + Assert.Equal("", actual!.Value); + } + + [Fact] + public async Task TestDataTypeCode() + { + var pdt = new PrimitiveProviderDefinedType("com.example.Test", "val"); + + using var stream = new MemoryStream(); + await Writer.WriteAsync(pdt, stream); + + Assert.Equal(0xF1, stream.ToArray()[0]); + } + + [Fact] + public void TestConstructorThrowsOnNullName() + { + Assert.Throws<ArgumentNullException>(() => + new PrimitiveProviderDefinedType(null!, "val")); + } + + [Fact] + public void TestConstructorThrowsOnEmptyName() + { + Assert.Throws<ArgumentException>(() => + new PrimitiveProviderDefinedType("", "val")); + } + + [Fact] + public void TestConstructorThrowsOnNullValue() + { + Assert.Throws<ArgumentNullException>(() => + new PrimitiveProviderDefinedType("com.example.T", null!)); + } + + [Fact] + public void TestEquality() + { + var a = new PrimitiveProviderDefinedType("com.example.T", "42"); + var b = new PrimitiveProviderDefinedType("com.example.T", "42"); + Assert.Equal(a, b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void TestInequality() + { + var a = new PrimitiveProviderDefinedType("com.example.A", "1"); + var b = new PrimitiveProviderDefinedType("com.example.B", "1"); + Assert.NotEqual(a, b); + } + + [Fact] + public void TestToString() + { + var pdt = new PrimitiveProviderDefinedType("com.example.T", "42"); + Assert.Contains("com.example.T", pdt.ToString()); + Assert.Contains("42", pdt.ToString()); + } + + [Fact] + public async Task TestHydrationWithRegistry() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new TestUint32Adapter()); + var reader = new GraphBinaryReader(pdtRegistry: registry); + + var pdt = new PrimitiveProviderDefinedType("test:Uint32", "123"); + using var stream = new MemoryStream(); + await Writer.WriteAsync(pdt, stream); + stream.Position = 0; + var result = await reader.ReadAsync(stream); + + Assert.IsType<uint>(result); + Assert.Equal(123u, (uint)result); + } + + [Fact] + public async Task TestNoHydrationWithoutRegistry() + { + var pdt = new PrimitiveProviderDefinedType("test:Uint32", "456"); + using var stream = new MemoryStream(); + await Writer.WriteAsync(pdt, stream); + stream.Position = 0; + var result = await Reader.ReadAsync(stream); + + Assert.IsType<PrimitiveProviderDefinedType>(result); + Assert.Equal("456", ((PrimitiveProviderDefinedType)result).Value); + } + + private class TestUint32Adapter : IPrimitivePdtAdapter<uint> + { + public string TypeName => "test:Uint32"; + public uint FromString(string value) => uint.Parse(value); + public string ToString(uint obj) => obj.ToString(); + } + } +} diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/PrimitivePdtRegistryTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/PrimitivePdtRegistryTests.cs new file mode 100644 index 0000000000..ad33728399 --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/PrimitivePdtRegistryTests.cs @@ -0,0 +1,126 @@ +#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 Gremlin.Net.Structure; +using Xunit; + +namespace Gremlin.Net.UnitTest.Structure +{ + public class PrimitivePdtRegistryTests + { + [Fact] + public void ShouldHydratePrimitiveWhenAdapterRegistered() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new Uint32Adapter()); + var pdt = new PrimitiveProviderDefinedType("test:Uint32", "42"); + + var result = registry.HydratePrimitive(pdt); + + Assert.IsType<uint>(result); + Assert.Equal(42u, (uint)result); + } + + [Fact] + public void ShouldReturnRawPrimitivePdtWhenNoAdapterRegistered() + { + var registry = new ProviderDefinedTypeRegistry(); + var pdt = new PrimitiveProviderDefinedType("unknown:Type", "hello"); + + var result = registry.HydratePrimitive(pdt); + + Assert.Same(pdt, result); + } + + [Fact] + public void ShouldReturnRawPrimitivePdtWhenAdapterThrows() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new ThrowingPrimitiveAdapter()); + var pdt = new PrimitiveProviderDefinedType("bad:Type", "oops"); + + var result = registry.HydratePrimitive(pdt); + + Assert.Same(pdt, result); + } + + [Fact] + public void ShouldHydratePrimitiveNestedInComposite() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new Uint32Adapter()); + var inner = new PrimitiveProviderDefinedType("test:Uint32", "99"); + var outer = new ProviderDefinedType("unregistered:Wrapper", + new Dictionary<string, object?> { ["val"] = inner, ["label"] = "test" }); + + var result = registry.HydrateComposite(outer); + + var rawOuter = Assert.IsType<ProviderDefinedType>(result); + Assert.Equal(99u, (uint)rawOuter.Fields["val"]!); + Assert.Equal("test", rawOuter.Fields["label"]); + } + + [Fact] + public void ShouldGetPrimitiveAdapterByType() + { + var registry = new ProviderDefinedTypeRegistry(); + registry.RegisterPrimitive(new Uint32Adapter()); + + var info = registry.GetPrimitiveAdapterByType(typeof(uint)); + + Assert.NotNull(info); + Assert.Equal("test:Uint32", info!.Value.typeName); + Assert.Equal("123", info.Value.Item2(123u)); + } + + [Fact] + public void ShouldReturnNullForUnregisteredPrimitiveType() + { + var registry = new ProviderDefinedTypeRegistry(); + + var info = registry.GetPrimitiveAdapterByType(typeof(uint)); + + Assert.Null(info); + } + + #region Test helpers + + private class Uint32Adapter : IPrimitivePdtAdapter<uint> + { + public string TypeName => "test:Uint32"; + public uint FromString(string value) => uint.Parse(value); + public string ToString(uint obj) => obj.ToString(); + } + + private class ThrowingPrimitiveAdapter : IPrimitivePdtAdapter<object> + { + public string TypeName => "bad:Type"; + public object FromString(string value) => throw new InvalidOperationException("intentional"); + public string ToString(object obj) => throw new InvalidOperationException("intentional"); + } + + #endregion + } +}
