From 2c2db9f42719fec178e12f18512eb31d57a31931 Mon Sep 17 00:00:00 2001 From: Ken Hu <106191785+kenhuuu@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:03:21 -0800 Subject: [PATCH 1/2] Add GraphBinary4 type serializers to gremlin-dotnet This is just the type serializers but not the message serializers which will be implemented later. Uses the same style as GraphBinary1 despite not having custom types. --- .../Structure/IO/GraphBinary4/DataType.cs | 142 ++++ .../IO/GraphBinary4/GraphBinaryReader.cs | 96 +++ .../IO/GraphBinary4/GraphBinaryWriter.cs | 139 +++ .../IO/GraphBinary4/ITypeSerializer.cs | 102 +++ .../Structure/IO/GraphBinary4/Marker.cs | 86 ++ .../IO/GraphBinary4/StreamExtensions.cs | 281 +++++++ .../IO/GraphBinary4/TypeSerializerRegistry.cs | 219 +++++ .../IO/GraphBinary4/Types/ArraySerializer.cs | 65 ++ .../Types/BigDecimalSerializer.cs | 95 +++ .../Types/BigIntegerSerializer.cs | 62 ++ .../IO/GraphBinary4/Types/BinarySerializer.cs | 61 ++ .../IO/GraphBinary4/Types/CharSerializer.cs | 87 ++ .../GraphBinary4/Types/DateTimeSerializer.cs | 78 ++ .../GraphBinary4/Types/DurationSerializer.cs | 63 ++ .../IO/GraphBinary4/Types/EdgeSerializer.cs | 89 ++ .../IO/GraphBinary4/Types/EnumSerializer.cs | 93 ++ .../IO/GraphBinary4/Types/ListSerializer.cs | 95 +++ .../IO/GraphBinary4/Types/MapSerializer.cs | 76 ++ .../IO/GraphBinary4/Types/PathSerializer.cs | 74 ++ .../GraphBinary4/Types/PropertySerializer.cs | 68 ++ .../IO/GraphBinary4/Types/SetSerializer.cs | 77 ++ .../Types/SimpleTypeSerializer.cs | 143 ++++ .../Types/SingleTypeSerializer.cs | 133 +++ .../IO/GraphBinary4/Types/StringSerializer.cs | 62 ++ .../IO/GraphBinary4/Types/UuidSerializer.cs | 111 +++ .../Types/VertexPropertySerializer.cs | 78 ++ .../IO/GraphBinary4/Types/VertexSerializer.cs | 68 ++ .../IO/GraphBinary4/GraphBinary4Tests.cs | 794 ++++++++++++++++++ .../Structure/IO/GraphBinary4/Model.cs | 210 +++++ .../IO/GraphBinary4/RoundTripTests.cs | 364 ++++++++ 30 files changed, 4111 insertions(+) create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs create mode 100644 gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs create mode 100644 gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs create mode 100644 gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs new file mode 100644 index 00000000000..8ae3199f163 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs @@ -0,0 +1,142 @@ +#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.IO.GraphBinary4 +{ + /// + /// Represents a GraphBinary 4.0 data type. + /// + public class DataType : IEquatable + { +#pragma warning disable 1591 + public static readonly DataType Int = new DataType(0x01); + public static readonly DataType Long = new DataType(0x02); + public static readonly DataType String = new DataType(0x03); + public static readonly DataType DateTime = new DataType(0x04); + public static readonly DataType Double = new DataType(0x07); + public static readonly DataType Float = new DataType(0x08); + public static readonly DataType List = new DataType(0x09); + public static readonly DataType Map = new DataType(0x0A); + public static readonly DataType Set = new DataType(0x0B); + public static readonly DataType Uuid = new DataType(0x0C); + public static readonly DataType Edge = new DataType(0x0D); + public static readonly DataType Path = new DataType(0x0E); + public static readonly DataType Property = new DataType(0x0F); + // Not yet implemented + // public static readonly DataType Graph = new DataType(0x10); + public static readonly DataType Vertex = new DataType(0x11); + public static readonly DataType VertexProperty = new DataType(0x12); + public static readonly DataType Direction = new DataType(0x18); + public static readonly DataType T = new DataType(0x20); + public static readonly DataType Merge = new DataType(0x21); + public static readonly DataType BigDecimal = new DataType(0x22); + public static readonly DataType BigInteger = new DataType(0x23); + public static readonly DataType Byte = new DataType(0x24); + public static readonly DataType Binary = new DataType(0x25); + public static readonly DataType Short = new DataType(0x26); + public static readonly DataType Boolean = new DataType(0x27); + // Not yet implemented + // public static readonly DataType Tree = new DataType(0x2B); + // public static readonly DataType CompositePDT = new DataType(0xF0); + // public static readonly DataType PrimitivePDT = new DataType(0xF1); + public static readonly DataType Char = new DataType(0x80); + public static readonly DataType Duration = new DataType(0x81); + public static readonly DataType Marker = new DataType(0xFD); +#pragma warning restore 1591 + + /// + /// A null value for an unspecified Object value. + /// + public static readonly DataType UnspecifiedNull = new DataType(0xFE); + + private DataType(int code) + { + TypeCode = (byte) code; + } + + /// + /// Gets the type code of this data type. + /// + public byte TypeCode { get; } + + /// + /// Creates a new instance for the given type code. + /// + public static DataType FromTypeCode(int code) + { + return new DataType(code); + } + + /// + public bool Equals(DataType? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return TypeCode == other.TypeCode; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((DataType) obj); + } + + /// + public override int GetHashCode() + { + return TypeCode.GetHashCode(); + } + + /// + /// Determines whether two specified have the same values. + /// + public static bool operator ==(DataType? first, DataType? second) + { + if (ReferenceEquals(null, first)) + { + return ReferenceEquals(null, second); + } + + return first.Equals(second); + } + + /// + /// Determines whether two specified have different values. + /// + public static bool operator !=(DataType? first, DataType? second) + { + return !(first == second); + } + + /// + public override string ToString() + { + return $"DataType{{ TypeCode = {TypeCode} }}"; + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs new file mode 100644 index 00000000000..ffe9f08f97a --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs @@ -0,0 +1,96 @@ +#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 +{ + /// + /// Allows to deserialize objects from GraphBinary v4. + /// + public class GraphBinaryReader + { + private readonly TypeSerializerRegistry _registry; + + /// + /// Initializes a new instance of the class. + /// + /// The to use for deserialization. + public GraphBinaryReader(TypeSerializerRegistry? registry = null) + { + _registry = registry ?? TypeSerializerRegistry.Instance; + } + + /// + /// Reads only the value for a specific type . + /// + /// The GraphBinary data to parse. + /// The token to cancel the operation. The default value is None. + /// The type of the object to read. + /// The read value. + public async Task ReadNullableValueAsync(Stream stream, + CancellationToken cancellationToken = default) + { + var typedSerializer = _registry.GetSerializerFor(typeof(T)); + return await typedSerializer.ReadNullableValueAsync(stream, this, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Reads only the value for a specific type . + /// + /// The GraphBinary data to parse. + /// The token to cancel the operation. The default value is None. + /// The type of the object to read. + /// The read value. + public async Task ReadNonNullableValueAsync(Stream stream, + CancellationToken cancellationToken = default) + { + var typedSerializer = _registry.GetSerializerFor(typeof(T)); + return await typedSerializer.ReadNonNullableValueAsync(stream, this, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Reads the type code, information and value with fully-qualified format. + /// + /// The GraphBinary data to parse. + /// The token to cancel the operation. The default value is None. + /// The read value. + public async Task ReadAsync(Stream stream, CancellationToken cancellationToken = default) + { + var type = DataType.FromTypeCode(await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false)); + + if (type == DataType.UnspecifiedNull) + { + await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); // read value byte to advance the index + return null; + } + + var typeSerializer = _registry.GetSerializerFor(type); + return await typeSerializer.ReadAsync(stream, this, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs new file mode 100644 index 00000000000..c5c4fc6a7d9 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs @@ -0,0 +1,139 @@ +#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; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4 +{ + /// + /// Allows to serialize objects to GraphBinary v4. + /// + public class GraphBinaryWriter + { + private const byte ValueFlagNull = 1; + private const byte ValueFlagNone = 0; + + /// + /// A representing the version of the GraphBinary v4 specification. + /// + public const byte VersionByte = 0x84; + + private static readonly byte[] UnspecifiedNullBytes = {DataType.UnspecifiedNull.TypeCode, 0x01}; + + private readonly TypeSerializerRegistry _registry; + + /// + /// Initializes a new instance of the class. + /// + /// The to use for serialization. + public GraphBinaryWriter(TypeSerializerRegistry? registry = null) + { + _registry = registry ?? TypeSerializerRegistry.Instance; + } + + /// + /// Writes a nullable value without including type information. + /// + /// The value to write. + /// The stream to write to. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + public async Task WriteNullableValueAsync(object? value, Stream stream, + CancellationToken cancellationToken = default) + { + if (value == null) + { + await WriteValueFlagNullAsync(stream, cancellationToken).ConfigureAwait(false); + return; + } + + var valueType = value.GetType(); + var serializer = _registry.GetSerializerFor(valueType); + await serializer.WriteNullableValueAsync(value, stream, this, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a non-nullable value without including type information. + /// + /// The value to write. + /// The stream to write to. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + public async Task WriteNonNullableValueAsync(object value, Stream stream, + CancellationToken cancellationToken = default) + { + if (value == null) throw new IOException($"{nameof(value)} cannot be null"); + var valueType = value.GetType(); + var serializer = _registry.GetSerializerFor(valueType); + await serializer.WriteNonNullableValueAsync(value, stream, this, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes an object in fully-qualified format, containing {type_code}{type_info}{value_flag}{value}. + /// + /// The value to write. + /// The stream to write to. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + public async Task WriteAsync(object? value, Stream stream, CancellationToken cancellationToken = default) + { + if (value == null) + { + await stream.WriteAsync(UnspecifiedNullBytes, cancellationToken).ConfigureAwait(false); + return; + } + + var valueType = value.GetType(); + var serializer = _registry.GetSerializerFor(valueType); + + await stream.WriteByteAsync(serializer.DataType.TypeCode, cancellationToken).ConfigureAwait(false); + await serializer.WriteAsync(value, stream, this, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a single byte representing the null value_flag. + /// + /// The stream to write to. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + public async Task WriteValueFlagNullAsync(Stream stream, CancellationToken cancellationToken = default) + { + await stream.WriteByteAsync(ValueFlagNull, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a single byte with value 0, representing an unset value_flag. + /// + /// The stream to write to. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + public async Task WriteValueFlagNoneAsync(Stream stream, CancellationToken cancellationToken = default) { + await stream.WriteByteAsync(ValueFlagNone, cancellationToken).ConfigureAwait(false); + } + + + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs new file mode 100644 index 00000000000..9a195ef8126 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs @@ -0,0 +1,102 @@ +#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 +{ + /// + /// Represents a serializer for a certain type. + /// + public interface ITypeSerializer + { + /// + /// Gets the that supported by this serializer. + /// + DataType DataType { get; } + + /// + /// Writes the type code, information and value to a stream. + /// + /// The value to write. + /// The stream to write to. + /// A that can be used to write nested values. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + Task WriteAsync(object value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default); + + /// + /// Writes the nullable value to a stream, composed by the value flag and the sequence of bytes. + /// + /// The value to write. + /// The stream to write to. + /// A that can be used to write nested values. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + Task WriteNullableValueAsync(object? value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default); + + /// + /// Writes the non-nullable value to a stream, composed by the value flag and the sequence of bytes. + /// + /// The value to write. + /// The stream to write to. + /// A that can be used to write nested values. + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + Task WriteNonNullableValueAsync(object value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default); + + /// + /// Reads the type information and value from the stream. + /// + /// The GraphBinary data to parse. + /// A that can be used to read nested values. + /// The token to cancel the operation. The default value is None. + /// The read value. + Task ReadAsync(Stream stream, GraphBinaryReader reader, CancellationToken cancellationToken = default); + + /// + /// Reads the value from the stream (not the type information). + /// + /// The GraphBinary data to parse. + /// A that can be used to read nested values. + /// The token to cancel the operation. The default value is None. + /// The read value. + Task ReadNullableValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default); + + /// + /// Reads the value from the stream (not the type information). + /// + /// The GraphBinary data to parse. + /// A that can be used to read nested values. + /// The token to cancel the operation. The default value is None. + /// The read value. + Task ReadNonNullableValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs new file mode 100644 index 00000000000..d13f1d3fbb0 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs @@ -0,0 +1,86 @@ +#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.IO.GraphBinary4 +{ + /// + /// Represents a special marker type for stream boundaries in GraphBinary 4.0 serialization. + /// + /// + /// Markers are used to indicate stream boundaries in GraphBinary 4.0 responses. + /// The marker indicates the end of result data in a response, + /// signaling that the footer (status code, message, exception) follows. + /// + public sealed class Marker + { + /// + /// Marker indicating the end of the result stream. + /// + /// + /// In GraphBinary 4.0 response format, this marker (type code 0xFD with value 0x00) + /// separates the result data from the response footer. + /// + public static readonly Marker EndOfStream = new Marker(0); + + /// + /// Gets the byte value of this marker as used on the wire. + /// + public byte Value { get; } + + private Marker(byte value) + { + Value = value; + } + + /// + /// Returns the instance for the given byte value. + /// + /// The marker byte value. + /// The corresponding instance. + /// Thrown if the value is not a known marker. + public static Marker Of(byte value) + { + if (value != 0) + { + throw new ArgumentException($"Unknown marker value: {value}", nameof(value)); + } + return EndOfStream; + } + + /// + public override string ToString() => $"Marker({Value})"; + + /// + public override bool Equals(object? obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Marker other && Value == other.Value; + } + + /// + public override int GetHashCode() => Value.GetHashCode(); + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs new file mode 100644 index 00000000000..e85767289f0 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs @@ -0,0 +1,281 @@ +#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; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4 +{ + /// + /// Provides extension methods for that are mostly useful when implementing GraphBinary + /// serializers. + /// + public static class StreamExtensions + { + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteByteAsync(this Stream stream, byte value, + CancellationToken cancellationToken = default) + { + await stream.WriteAsync(new[] {value}, 0, 1, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadByteAsync(this Stream stream, CancellationToken cancellationToken = default) + { + var readBuffer = new byte[1]; + await stream.ReadAsync(readBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + return readBuffer[0]; + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteSByteAsync(this Stream stream, sbyte value, + CancellationToken cancellationToken = default) + { + await stream.WriteByteAsync((byte)value, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadSByteAsync(this Stream stream, CancellationToken cancellationToken = default) + { + return (sbyte)await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously writes an to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteIntAsync(this Stream stream, int value, + CancellationToken cancellationToken = default) + { + var bytes = BitConverter.GetBytes(value); + await stream.WriteAsync(new[] { bytes[3], bytes[2], bytes[1], bytes[0] }, 0, 4, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads an from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadIntAsync(this Stream stream, CancellationToken cancellationToken = default) + { + var bytes = new byte[4]; + await stream.ReadAsync(bytes, 0, 4, cancellationToken).ConfigureAwait(false); + return BitConverter.ToInt32(new []{bytes[3], bytes[2], bytes[1], bytes[0]}, 0); + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteLongAsync(this Stream stream, long value, + CancellationToken cancellationToken = default) + { + var bytes = BitConverter.GetBytes(value); + await stream + .WriteAsync(new[] { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0] }, 0, + 8, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadLongAsync(this Stream stream, CancellationToken cancellationToken = default) + { + var bytes = new byte[8]; + await stream.ReadAsync(bytes, 0, 8, cancellationToken).ConfigureAwait(false); + return BitConverter.ToInt64( + new[] {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]}, 0); + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteFloatAsync(this Stream stream, float value, + CancellationToken cancellationToken = default) + { + var bytes = BitConverter.GetBytes(value); + await stream.WriteAsync(new[] { bytes[3], bytes[2], bytes[1], bytes[0] }, 0, 4, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadFloatAsync(this Stream stream, + CancellationToken cancellationToken = default) + { + var bytes = new byte[4]; + await stream.ReadAsync(bytes, 0, 4, cancellationToken).ConfigureAwait(false); + return BitConverter.ToSingle(new []{bytes[3], bytes[2], bytes[1], bytes[0]}, 0); + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteDoubleAsync(this Stream stream, double value, + CancellationToken cancellationToken = default) + { + var bytes = BitConverter.GetBytes(value); + await stream + .WriteAsync(new[] {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]}, 0, + 8, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadDoubleAsync(this Stream stream, + CancellationToken cancellationToken = default) + { + var bytes = new byte[8]; + await stream.ReadAsync(bytes, 0, 8, cancellationToken).ConfigureAwait(false); + return BitConverter.ToDouble( + new[] {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]}, 0); + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteShortAsync(this Stream stream, short value, + CancellationToken cancellationToken = default) + { + var bytes = BitConverter.GetBytes(value); + await stream.WriteAsync(new[] { bytes[1], bytes[0] }, 0, 2, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadShortAsync(this Stream stream, + CancellationToken cancellationToken = default) + { + var bytes = new byte[2]; + await stream.ReadAsync(bytes, 0, 2, cancellationToken).ConfigureAwait(false); + return BitConverter.ToInt16(new []{bytes[1], bytes[0]}, 0); + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteBoolAsync(this Stream stream, bool value, + CancellationToken cancellationToken = default) + { + await stream.WriteByteAsync((byte)(value ? 1 : 0), cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a . + /// + /// The to read from. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadBoolAsync(this Stream stream, CancellationToken cancellationToken = default) + { + var b = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + return b switch + { + 1 => true, + 0 => false, + _ => throw new IOException($"Cannot read byte {b} as a boolean.") + }; + } + + /// + /// Asynchronously writes a to a . + /// + /// The to write the to. + /// The to write. + /// The token to cancel the operation. The default value is None. + public static async Task WriteAsync(this Stream stream, byte[] value, + CancellationToken cancellationToken = default) + { + await stream.WriteAsync(value, 0, value.Length, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads a from a into a buffer. + /// + /// The to read from. + /// The number of bytes to read. + /// The token to cancel the operation. The default value is None. + /// The read . + public static async Task ReadAsync(this Stream stream, int count, + CancellationToken cancellationToken = default) + { + var buffer = new byte[count]; + await stream.ReadAsync(buffer, 0, count, cancellationToken).ConfigureAwait(false); + return buffer; + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs new file mode 100644 index 00000000000..82421f14a13 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs @@ -0,0 +1,219 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Numerics; +using Gremlin.Net.Process.Traversal; +using Gremlin.Net.Structure.IO.GraphBinary4.Types; + +namespace Gremlin.Net.Structure.IO.GraphBinary4 +{ + /// + /// Provides GraphBinary 4.0 serializers for different types. + /// + public class TypeSerializerRegistry + { + private readonly Dictionary _serializerByType = + new Dictionary + { + {typeof(int), SingleTypeSerializers.IntSerializer}, + {typeof(long), SingleTypeSerializers.LongSerializer}, + {typeof(string), new StringSerializer()}, + {typeof(DateTimeOffset), new DateTimeSerializer()}, + {typeof(double), SingleTypeSerializers.DoubleSerializer}, + {typeof(float), SingleTypeSerializers.FloatSerializer}, + {typeof(Guid), new UuidSerializer()}, + {typeof(Edge), new EdgeSerializer()}, + {typeof(Path), new PathSerializer()}, + {typeof(Property), new PropertySerializer()}, + {typeof(Vertex), new VertexSerializer()}, + {typeof(VertexProperty), new VertexPropertySerializer()}, + {typeof(Direction), EnumSerializers.DirectionSerializer}, + {typeof(Merge), EnumSerializers.MergeSerializer}, + {typeof(T), EnumSerializers.TSerializer}, + {typeof(decimal), new BigDecimalSerializer()}, + {typeof(BigInteger), new BigIntegerSerializer()}, + {typeof(sbyte), SingleTypeSerializers.ByteSerializer}, + {typeof(byte[]), new BinarySerializer()}, + {typeof(short), SingleTypeSerializers.ShortSerializer}, + {typeof(bool), SingleTypeSerializers.BooleanSerializer}, + {typeof(char), new CharSerializer()}, + {typeof(TimeSpan), new DurationSerializer()}, + {typeof(Marker), SingleTypeSerializers.MarkerSerializer}, + }; + + private readonly Dictionary _serializerByDataType = + new Dictionary + { + {DataType.Int, SingleTypeSerializers.IntSerializer}, + {DataType.Long, SingleTypeSerializers.LongSerializer}, + {DataType.String, new StringSerializer()}, + {DataType.DateTime, new DateTimeSerializer()}, + {DataType.Double, SingleTypeSerializers.DoubleSerializer}, + {DataType.Float, SingleTypeSerializers.FloatSerializer}, + {DataType.List, new ListSerializer()}, + {DataType.Map, new MapSerializer()}, + {DataType.Set, new SetSerializer, object>()}, + {DataType.Uuid, new UuidSerializer()}, + {DataType.Edge, new EdgeSerializer()}, + {DataType.Path, new PathSerializer()}, + {DataType.Property, new PropertySerializer()}, + {DataType.Vertex, new VertexSerializer()}, + {DataType.VertexProperty, new VertexPropertySerializer()}, + {DataType.Direction, EnumSerializers.DirectionSerializer}, + {DataType.Merge, EnumSerializers.MergeSerializer}, + {DataType.T, EnumSerializers.TSerializer}, + {DataType.BigDecimal, new BigDecimalSerializer()}, + {DataType.BigInteger, new BigIntegerSerializer()}, + {DataType.Byte, SingleTypeSerializers.ByteSerializer}, + {DataType.Binary, new BinarySerializer()}, + {DataType.Short, SingleTypeSerializers.ShortSerializer}, + {DataType.Boolean, SingleTypeSerializers.BooleanSerializer}, + {DataType.Char, new CharSerializer()}, + {DataType.Duration, new DurationSerializer()}, + {DataType.Marker, SingleTypeSerializers.MarkerSerializer}, + }; + + /// + /// Provides a default instance. + /// + public static readonly TypeSerializerRegistry Instance = new TypeSerializerRegistry(); + + /// + /// Gets a serializer for the given type of the value to be serialized. + /// + /// Type of the value to be serialized. + /// A serializer for the provided type. + /// Thrown when no serializer can be found for the type. + public ITypeSerializer GetSerializerFor(Type valueType) + { + if (_serializerByType.TryGetValue(valueType, out var serializerForType)) + { + return serializerForType; + } + + if (IsDictionaryType(valueType, out var dictKeyType, out var dictValueType)) + { + var serializerType = typeof(MapSerializer<,>).MakeGenericType(dictKeyType, dictValueType); + var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType); + _serializerByType[valueType] = serializer ?? + throw new IOException( + $"Cannot create a serializer for the dictionary type {valueType}."); + return serializer; + } + + if (IsSetType(valueType)) + { + var memberType = valueType.GetGenericArguments()[0]; + var serializerType = typeof(SetSerializer<,>).MakeGenericType(valueType, memberType); + var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType); + _serializerByType[valueType] = serializer ?? + throw new IOException( + $"Cannot create a serializer for the set type {valueType}."); + return serializer; + } + + if (valueType.IsArray) + { + var memberType = valueType.GetElementType(); + var serializerType = typeof(ArraySerializer<>).MakeGenericType(memberType!); + var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType); + _serializerByType[valueType] = serializer ?? + throw new IOException( + $"Cannot create a serializer for the array type {valueType}."); + return serializer; + } + + if (IsListType(valueType)) + { + var memberType = valueType.GetGenericArguments()[0]; + var serializerType = typeof(ListSerializer<>).MakeGenericType(memberType); + var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType); + _serializerByType[valueType] = serializer ?? + throw new IOException( + $"Cannot create a serializer for the list type {valueType}."); + return serializer; + } + + foreach (var supportedType in new List(_serializerByType.Keys)) + { + if (supportedType.IsAssignableFrom(valueType)) + { + var serializer = _serializerByType[supportedType]; + _serializerByType[valueType] = serializer; + return serializer; + } + } + + throw new InvalidOperationException($"No serializer found for type '{valueType}'."); + } + + /// + /// Gets a serializer for the given GraphBinary 4.0 data type code. + /// + /// The GraphBinary 4.0 data type. + /// A serializer for the provided data type. + public ITypeSerializer GetSerializerFor(DataType dataType) + { + return _serializerByDataType[dataType]; + } + + private static bool IsDictionaryType(Type type, [NotNullWhen(returnValue: true)] out Type? keyType, + [NotNullWhen(returnValue: true)] out Type? valueType) + { + var maybeInterfaceType = type + .GetInterfaces() + .FirstOrDefault(implementedInterfaceType => implementedInterfaceType.IsConstructedGenericType && implementedInterfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + + if (maybeInterfaceType is { } interfaceType) + { + keyType = interfaceType.GetGenericArguments()[0]; + valueType = interfaceType.GetGenericArguments()[1]; + return true; + } + + keyType = null; + valueType = null; + return false; + } + + private static bool IsSetType(Type type) + { + return type.GetInterfaces().Any(implementedInterface => implementedInterface.IsConstructedGenericType && + implementedInterface.GetGenericTypeDefinition() == + typeof(ISet<>)); + } + + private static bool IsListType(Type type) + { + return type.GetInterfaces().Any(implementedInterface => implementedInterface.IsConstructedGenericType && + implementedInterface.GetGenericTypeDefinition() == + typeof(IList<>)); + } + + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs new file mode 100644 index 00000000000..c4f939b5162 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer that serializes .NET arrays to GraphBinary4 lists. + /// + /// The type of elements in the array. + public class ArraySerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public ArraySerializer() : base(DataType.List) + { + } + + /// + protected override async Task WriteValueAsync(TMember[] value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteNonNullableValueAsync(value.Length, stream, cancellationToken).ConfigureAwait(false); + + foreach (var item in value) + { + await writer.WriteAsync(item, stream, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Currently not supported as GraphBinary has no array data type. + /// + protected override Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException("Reading an array is not supported"); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs new file mode 100644 index 00000000000..e5bf0c79058 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs @@ -0,0 +1,95 @@ +#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.Numerics; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer that serializes values as BigDecimal in GraphBinary4. + /// + public class BigDecimalSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public BigDecimalSerializer() : base(DataType.BigDecimal) + { + } + + /// + protected override async Task WriteValueAsync(decimal value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + var (unscaledValue, scale) = GetUnscaledValueAndScale(value); + await writer.WriteNonNullableValueAsync(scale, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(unscaledValue, stream, cancellationToken).ConfigureAwait(false); + } + + private static (BigInteger, int) GetUnscaledValueAndScale(decimal input) + { + var parts = decimal.GetBits(input); + + var sign = (parts[3] & 0x80000000) != 0; + + var scale = (parts[3] >> 16) & 0x7F; + + var lowBytes = BitConverter.GetBytes(parts[0]); + var middleBytes = BitConverter.GetBytes(parts[1]); + var highBytes = BitConverter.GetBytes(parts[2]); + var valueBytes = new byte[12]; + lowBytes.CopyTo(valueBytes, 0); + middleBytes.CopyTo(valueBytes, 4); + highBytes.CopyTo(valueBytes, 8); + var bigInt = new BigInteger(valueBytes); + + if (sign) + { + bigInt = -bigInt; + } + + return (bigInt, scale); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var scale = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken) + .ConfigureAwait(false); + var unscaled = (BigInteger)await reader.ReadNonNullableValueAsync(stream, cancellationToken) + .ConfigureAwait(false); + + return ConvertScaleAndUnscaledValue(scale, unscaled); + } + + private static decimal ConvertScaleAndUnscaledValue(int scale, BigInteger unscaledValue) + { + return (decimal) unscaledValue * (decimal) Math.Pow(10, -scale); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs new file mode 100644 index 00000000000..4a555a0fad9 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs @@ -0,0 +1,62 @@ +#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.Linq; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer for values. + /// + public class BigIntegerSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public BigIntegerSerializer() : base(DataType.BigInteger) + { + } + + /// + protected override async Task WriteValueAsync(BigInteger value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + var bytes = value.ToByteArray().Reverse().ToArray(); + await writer.WriteNonNullableValueAsync(bytes.Length, stream, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false); + var bytes = await stream.ReadAsync(length, cancellationToken).ConfigureAwait(false); + return new BigInteger(bytes.Reverse().ToArray()); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs new file mode 100644 index 00000000000..58723e77a2e --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs @@ -0,0 +1,61 @@ +#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 +{ + + /// + /// A serializer for byte[]. + /// + public class BinarySerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public BinarySerializer() : base(DataType.Binary) + { + } + + /// + protected override async Task WriteValueAsync(byte[] value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteNonNullableValueAsync(value.Length, stream, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(value, 0, value.Length, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false); + var buffer = new byte[length]; + await stream.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); + return buffer; + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs new file mode 100644 index 00000000000..7ea3cb5cc5d --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs @@ -0,0 +1,87 @@ +#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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer. + /// + public class CharSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public CharSerializer() : base(DataType.Char) + { + } + + /// + protected override async Task WriteValueAsync(char value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + var bytes = Encoding.UTF8.GetBytes(value.ToString()); + await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var firstByte = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + var byteLength = 1; + // A byte with the first byte ON (10000000) signals that more bytes are needed to represent the UTF-8 char + if ((firstByte & 0x80) > 0) + { + if ((firstByte & 0xf0) == 0xf0) + { // 0xf0 = 11110000 + byteLength = 4; + } else if ((firstByte & 0xe0) == 0xe0) + { //11100000 + byteLength = 3; + } else if ((firstByte & 0xc0) == 0xc0) + { //11000000 + byteLength = 2; + } + } + + byte[] bytes; + if (byteLength == 1) + { + bytes = new[] {firstByte}; + } + else + { + bytes = new byte[byteLength]; + bytes[0] = firstByte; + await stream.ReadAsync(bytes, 1, byteLength - 1, cancellationToken).ConfigureAwait(false); + } + + return Encoding.UTF8.GetChars(bytes)[0]; + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs new file mode 100644 index 00000000000..b5fc932af63 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs @@ -0,0 +1,78 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer for values in GraphBinary 4.0. + /// + public class DateTimeSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public DateTimeSerializer() : base(DataType.DateTime) + { + } + + /// + protected override async Task WriteValueAsync(DateTimeOffset value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await stream.WriteIntAsync(value.Year, cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync((byte)value.Month, cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync((byte)value.Day, cancellationToken).ConfigureAwait(false); + + // time as Long nanoseconds since midnight + var timeNanos = (value.Hour * 3600L + value.Minute * 60L + value.Second) * 1_000_000_000L + + value.Millisecond * 1_000_000L + + (value.Ticks % TimeSpan.TicksPerMillisecond) * 100L; + await stream.WriteLongAsync(timeNanos, cancellationToken).ConfigureAwait(false); + + await stream.WriteIntAsync((int)value.Offset.TotalSeconds, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var year = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false); + var month = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + var day = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + var timeNanos = await stream.ReadLongAsync(cancellationToken).ConfigureAwait(false); + var offsetSeconds = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false); + + // Convert nanoseconds to TimeSpan (100 nanoseconds per tick) + var timeSpan = TimeSpan.FromTicks(timeNanos / 100); + var offset = TimeSpan.FromSeconds(offsetSeconds); + + return new DateTimeOffset(year, month, day, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, + timeSpan.Milliseconds, offset).AddTicks(timeSpan.Ticks % TimeSpan.TicksPerMillisecond); + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs new file mode 100644 index 00000000000..0b6e8d59a0e --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs @@ -0,0 +1,63 @@ +#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; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer that serializes values as Duration in GraphBinary. + /// + public class DurationSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public DurationSerializer() : base(DataType.Duration) + { + } + + /// + protected override async Task WriteValueAsync(TimeSpan value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await stream.WriteLongAsync((long)value.TotalSeconds, cancellationToken).ConfigureAwait(false); + // Get sub-second ticks and convert to nanoseconds (1 tick = 100 nanoseconds) + await stream.WriteIntAsync((int)(value.Ticks % TimeSpan.TicksPerSecond * 100), cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var seconds = await stream.ReadLongAsync(cancellationToken).ConfigureAwait(false); + var nanoseconds = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false); + + // Convert nanoseconds to ticks (1 tick = 100 nanoseconds) + return TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanoseconds / 100); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs new file mode 100644 index 00000000000..b3a8097de5e --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs @@ -0,0 +1,89 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// An serializer for GraphBinary 4.0. + /// In v4, labels are serialized as List of Strings instead of a single String. + /// + public class EdgeSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public EdgeSerializer() : base(DataType.Edge) + { + } + + /// + protected override async Task WriteValueAsync(Edge value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false); + + await writer.WriteAsync(value.InV.Id, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List { value.InV.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(value.OutV.Id, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List { value.OutV.Label }, stream, cancellationToken).ConfigureAwait(false); + + // Placeholder for the parent vertex + await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); + + // placeholder for the properties + await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); + var label = labelList.Count > 0 ? labelList[0] ?? "" : ""; + + var inVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var inVLabelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); + var inV = new Vertex(inVId, inVLabelList.Count > 0 ? inVLabelList[0] ?? "" : ""); + + var outVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var outVLabelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); + var outV = new Vertex(outVId, outVLabelList.Count > 0 ? outVLabelList[0] ?? "" : ""); + + // discard possible parent vertex + await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + + var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray(); + + return new Edge(id, outV, label, inV, propertiesAsArray); + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs new file mode 100644 index 00000000000..c885c97bf9f --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs @@ -0,0 +1,93 @@ +#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; +using System.Threading.Tasks; +using Gremlin.Net.Process.Traversal; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// Provides serializers for enum types supported in GraphBinary 4.0. + /// + /// + /// GraphBinary 4.0 only supports Direction, T, and Merge enum types. + /// The following types were removed in v4: Barrier, Cardinality, Column, DT, GType, + /// Operator, Order, Pick, Pop, Scope. + /// + public static class EnumSerializers + { + /// + /// A serializer for values. + /// + public static readonly EnumSerializer DirectionSerializer = + new EnumSerializer(DataType.Direction, Direction.GetByValue); + + /// + /// A serializer for values. + /// + public static readonly EnumSerializer MergeSerializer = + new EnumSerializer(DataType.Merge, Merge.GetByValue); + + /// + /// A serializer for values. + /// + public static readonly EnumSerializer TSerializer = + new EnumSerializer(DataType.T, T.GetByValue); + } + + /// + /// Generalized serializer for enum types. + /// + /// The type of the enum to serialize. + public class EnumSerializer : SimpleTypeSerializer + where TEnum : EnumWrapper + { + private readonly Func _readFunc; + + internal EnumSerializer(DataType dataType, Func readFunc) : base(dataType) + { + _readFunc = readFunc; + } + + /// + protected override async Task WriteValueAsync(TEnum value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteAsync(value.EnumValue, stream, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + // This should probably be `reader.ReadNonNullableValueAsync(stream, cancellationToken)` instead, + // but it's the same in other GLVs and changing this would be a breaking change for the GraphBinary format. + var enumValue = (string?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (enumValue == null) throw new IOException($"Read null as a value for {DataType}"); + return _readFunc.Invoke(enumValue); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs new file mode 100644 index 00000000000..73c4179fac0 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs @@ -0,0 +1,95 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A generic list serializer for GraphBinary 4.0. + /// + /// The type of elements in the list. + public class ListSerializer : SimpleTypeSerializer> + { + private const byte ValueFlagBulked = 0x02; + + /// + /// Initializes a new instance of the class. + /// + public ListSerializer() : base(DataType.List) + { + } + + /// + protected override async Task WriteValueAsync(IList value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteNonNullableValueAsync(value.Count, stream, cancellationToken).ConfigureAwait(false); + + foreach (var item in value) + { + await writer.WriteAsync(item, stream, cancellationToken).ConfigureAwait(false); + } + } + + /// + protected override Task> ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + // Default to standard format (valueFlag = 0x00) + return ReadValueAsync(stream, reader, 0x00, cancellationToken); + } + + /// + protected override async Task> ReadValueAsync(Stream stream, GraphBinaryReader reader, + byte valueFlag, CancellationToken cancellationToken = default) + { + var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false); + var result = new List(); + var isBulked = (valueFlag & ValueFlagBulked) != 0; + + for (var i = 0; i < length; i++) + { + var item = (TMember?)await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + + if (isBulked) + { + var bulk = await stream.ReadLongAsync(cancellationToken).ConfigureAwait(false); + for (var j = 0; j < bulk; j++) + { + result.Add(item); + } + } + else + { + result.Add(item); + } + } + + return result; + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs new file mode 100644 index 00000000000..f8e9172d14b --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs @@ -0,0 +1,76 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A generic dictionary serializer for GraphBinary 4.0. + /// + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + public class MapSerializer : SimpleTypeSerializer> where TKey : notnull + { + /// + /// Initializes a new instance of the class. + /// + public MapSerializer() : base(DataType.Map) + { + } + + /// + protected override async Task WriteValueAsync(IDictionary value, Stream stream, + GraphBinaryWriter writer, CancellationToken cancellationToken = default) + { + await writer.WriteNonNullableValueAsync(value.Count, stream, cancellationToken).ConfigureAwait(false); + + foreach (var key in value.Keys) + { + await writer.WriteAsync(key, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(value[key], stream, cancellationToken).ConfigureAwait(false); + } + } + + /// + protected override async Task> ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var length = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false); + var result = new Dictionary(length); + + for (var i = 0; i < length; i++) + { + var key = (TKey?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (key == null) throw new IOException("Read null as the key for a dictionary."); + var value = (TValue?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + result.Add(key, value); + } + + return result; + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs new file mode 100644 index 00000000000..03c6a260490 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs @@ -0,0 +1,74 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer. + /// + public class PathSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public PathSerializer() : base(DataType.Path) + { + } + + /// + protected override async Task WriteValueAsync(Path value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteAsync(value.Labels, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(value.Objects, stream, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var readLabelObjects = + (List?)await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (readLabelObjects == null) throw new IOException("Read null, but expected a list of labels"); + var labels = new List>(); + foreach (var labelObjectList in readLabelObjects) + { + var labelSet = new HashSet(); + foreach (var labelObj in (HashSet) labelObjectList) + { + labelSet.Add((string) labelObj); + } + labels.Add(labelSet); + } + + var objects = (List?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (objects == null) throw new IOException("Read null, but expected a list of objects"); + return new Path(labels, objects); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs new file mode 100644 index 00000000000..6eba38a2b60 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs @@ -0,0 +1,68 @@ +#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 +{ + /// + /// A serializer. + /// + public class + PropertySerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public PropertySerializer() : base(DataType.Property) + { + } + + /// + protected override async Task WriteValueAsync(Property value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteNonNullableValueAsync(value.Key, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(value.Value, stream, cancellationToken).ConfigureAwait(false); + + // placeholder for the parent element + await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var p = new Property( + (string)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false), + await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false)); + + // discard parent element + await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + + return p; + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs new file mode 100644 index 00000000000..c656ae706f5 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs @@ -0,0 +1,77 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A generic set serializer for GraphBinary 4.0. + /// + /// The type of the set to serialize. + /// The type of elements in the set. + public class SetSerializer : SimpleTypeSerializer + where TSet : ISet, new() + { + /// + /// Initializes a new instance of the class. + /// + public SetSerializer() : base(DataType.Set) + { + } + + /// + protected override async Task WriteValueAsync(TSet value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + var enumerable = (IEnumerable) value; + var list = enumerable.Cast().ToList(); + + await writer.WriteNonNullableValueAsync(list.Count, stream, cancellationToken).ConfigureAwait(false); + + foreach (var item in list) + { + await writer.WriteAsync(item, stream, cancellationToken).ConfigureAwait(false); + } + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false); + var result = new TSet(); + for (var i = 0; i < length; i++) + { + result.Add((TMember?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false)); + } + + return result; + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs new file mode 100644 index 00000000000..8115959560e --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs @@ -0,0 +1,143 @@ +#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 +{ + /// + /// Base class for serialization of types that don't contain type specific information only {type_code}, + /// {value_flag} and {value}. + /// + /// The supported type. + public abstract class SimpleTypeSerializer : ITypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + protected SimpleTypeSerializer(DataType dataType) + { + DataType = dataType; + } + + /// + public DataType DataType { get; } + + /// + public async Task WriteAsync(object? value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await WriteNullableValueAsync((T?) value, stream, writer, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task WriteNullableValueAsync(object? value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + if (value == null) + { + await writer.WriteValueFlagNullAsync(stream, cancellationToken).ConfigureAwait(false); + return; + } + + await writer.WriteValueFlagNoneAsync(stream, cancellationToken).ConfigureAwait(false); + + await WriteValueAsync((T) value, stream, writer, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task WriteNonNullableValueAsync(object value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await WriteValueAsync((T) value, stream, writer, cancellationToken).ConfigureAwait(false); + } + + /// + /// Writes a non-nullable value into a stream. + /// + /// The value to write. + /// The stream to write to. + /// A . + /// The token to cancel the operation. The default value is None. + /// A task that represents the asynchronous write operation. + protected abstract Task WriteValueAsync(T value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default); + + /// + public async Task ReadAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + return await ReadNullableValueAsync(stream, reader, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task ReadNullableValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var valueFlag = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + if ((valueFlag & 1) == 1) + { + return null; + } + + return await ReadValueAsync(stream, reader, valueFlag, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task ReadNonNullableValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + return (await ReadValueAsync(stream, reader, cancellationToken).ConfigureAwait(false))!; + } + + /// + /// Reads a non-nullable value according to the type format with the value flag. + /// + /// The GraphBinary data to parse. + /// A . + /// The value flag byte that was read. + /// The token to cancel the operation. The default value is None. + /// The read value. + /// + /// Override this method to handle type-specific value flags (e.g., bulked format for List). + /// The default implementation delegates to . + /// + protected virtual Task ReadValueAsync(Stream stream, GraphBinaryReader reader, byte valueFlag, + CancellationToken cancellationToken = default) + { + return ReadValueAsync(stream, reader, cancellationToken); + } + + /// + /// Reads a non-nullable value according to the type format. + /// + /// The GraphBinary data to parse. + /// A . + /// The token to cancel the operation. The default value is None. + /// The read value. + protected abstract Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs new file mode 100644 index 00000000000..6164581be57 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs @@ -0,0 +1,133 @@ +#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; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// Provides serializers for types that can be represented as a single value and that can be read and write in a + /// single operation. + /// + public static class SingleTypeSerializers + { + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer IntSerializer = new SingleTypeSerializer(DataType.Int, + (value, stream, cancellationToken) => stream.WriteIntAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadIntAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer LongSerializer = new SingleTypeSerializer(DataType.Long, + (value, stream, cancellationToken) => stream.WriteLongAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadLongAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer DoubleSerializer = + new SingleTypeSerializer(DataType.Double, + (value, stream, cancellationToken) => stream.WriteDoubleAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadDoubleAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer FloatSerializer = + new SingleTypeSerializer(DataType.Float, + (value, stream, cancellationToken) => stream.WriteFloatAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadFloatAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer ShortSerializer = + new SingleTypeSerializer(DataType.Short, + (value, stream, cancellationToken) => stream.WriteShortAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadShortAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer BooleanSerializer = + new SingleTypeSerializer(DataType.Boolean, + (value, stream, cancellationToken) => stream.WriteBoolAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadBoolAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer ByteSerializer = new SingleTypeSerializer(DataType.Byte, + (value, stream, cancellationToken) => stream.WriteSByteAsync(value, cancellationToken), + (stream, cancellationToken) => stream.ReadSByteAsync(cancellationToken)); + + /// + /// A serializer for values. + /// + public static readonly SingleTypeSerializer MarkerSerializer = new SingleTypeSerializer(DataType.Marker, + (value, stream, cancellationToken) => stream.WriteByteAsync(value.Value, cancellationToken), + async (stream, cancellationToken) => + { + var markerValue = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + return Marker.Of(markerValue); + }); + } + + /// + /// Represents a serializer for types that can be represented as a single value and that can be read and write in a + /// single operation. + /// + public class SingleTypeSerializer : SimpleTypeSerializer + { + private readonly Func _writeFunc; + private readonly Func> _readFunc; + + internal SingleTypeSerializer(DataType dataType, Func writeFunc, + Func> readFunc) + : base(dataType) + { + _writeFunc = writeFunc; + _readFunc = readFunc; + } + + /// + protected override async Task WriteValueAsync(T value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await _writeFunc.Invoke(value, stream, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + return await _readFunc.Invoke(stream, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs new file mode 100644 index 00000000000..73cce54aa5f --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs @@ -0,0 +1,62 @@ +#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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer. + /// + public class StringSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public StringSerializer() : base(DataType.String) + { + } + + /// + protected override async Task WriteValueAsync(string value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + var bytes = Encoding.UTF8.GetBytes(value); + await writer.WriteNonNullableValueAsync(bytes.Length, stream, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false); + var bytes = new byte[length]; + await stream.ReadAsync(bytes, 0, length, cancellationToken).ConfigureAwait(false); + return Encoding.UTF8.GetString(bytes); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs new file mode 100644 index 00000000000..dbecb308678 --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs @@ -0,0 +1,111 @@ +#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; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer that serializes values as Uuid in GraphBinary. + /// + public class UuidSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public UuidSerializer() : base(DataType.Uuid) + { + } + + /// + protected override async Task WriteValueAsync(Guid value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + var bytes = value.ToByteArray(); + + // first 4 bytes in reverse order: + await stream.WriteByteAsync(bytes[3], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[2], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[1], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[0], cancellationToken).ConfigureAwait(false); + + // 2 bytes in reverse order: + await stream.WriteByteAsync(bytes[5], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[4], cancellationToken).ConfigureAwait(false); + + // 3 bytes in reverse order: + await stream.WriteByteAsync(bytes[7], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[6], cancellationToken).ConfigureAwait(false); + + // 3 bytes: + await stream.WriteByteAsync(bytes[8], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[9], cancellationToken).ConfigureAwait(false); + + // last 6 bytes: + await stream.WriteByteAsync(bytes[10], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[11], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[12], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[13], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[14], cancellationToken).ConfigureAwait(false); + await stream.WriteByteAsync(bytes[15], cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var bytes = new byte[16]; + + // first 4 bytes in reverse order: + bytes[3] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[2] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[1] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[0] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + + // 2 bytes in reverse order: + bytes[5] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[4] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + + // 2 bytes in reverse order: + bytes[7] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[6] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + + // 2 bytes: + bytes[8] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[9] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + + // last 6 bytes: + bytes[10] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[11] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[12] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[13] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[14] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + bytes[15] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); + + return new Guid(bytes); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs new file mode 100644 index 00000000000..89f4acd767b --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs @@ -0,0 +1,78 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer for GraphBinary 4.0. + /// In v4, labels are serialized as List of Strings instead of a single String. + /// + public class VertexPropertySerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public VertexPropertySerializer() : base(DataType.VertexProperty) + { + } + + /// + protected override async Task WriteValueAsync(VertexProperty value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(value.Value, stream, cancellationToken).ConfigureAwait(false); + + // placeholder for the parent vertex + await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); + + // placeholder for properties + await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); + var label = labelList.Count > 0 ? labelList[0] ?? "" : ""; + var value = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + + // discard the parent vertex + await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + + var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray(); + + return new VertexProperty(id, label, value, null, propertiesAsArray); + } + } +} diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs new file mode 100644 index 00000000000..9bad7be2d7a --- /dev/null +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs @@ -0,0 +1,68 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Gremlin.Net.Structure.IO.GraphBinary4.Types +{ + /// + /// A serializer for GraphBinary 4.0. + /// In v4, labels are serialized as List of Strings instead of a single String. + /// + public class VertexSerializer : SimpleTypeSerializer + { + /// + /// Initializes a new instance of the class. + /// + public VertexSerializer() : base(DataType.Vertex) + { + } + + /// + protected override async Task WriteValueAsync(Vertex value, Stream stream, GraphBinaryWriter writer, + CancellationToken cancellationToken = default) + { + await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); // properties + } + + /// + protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader, + CancellationToken cancellationToken = default) + { + var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false); + var label = labelList.Count > 0 ? labelList[0] ?? "" : ""; + + var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray(); + + return new Vertex(id, label, propertiesAsArray); + } + } +} diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs new file mode 100644 index 00000000000..14d8a95e4d7 --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs @@ -0,0 +1,794 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using Gremlin.Net.Process.Traversal; +using Gremlin.Net.Structure; +using Gremlin.Net.Structure.IO.GraphBinary4; +using Xunit; +using Path = Gremlin.Net.Structure.Path; + +namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4 +{ + public class GraphBinary4Tests + { + [Fact] + public async Task TestNull() + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(null, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Null(actual); + } + + [Fact] + public async Task TestInt() + { + const int expected = 100; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, new byte[]{0x00, 0x00, 0x00, 0x01})] + [InlineData(257, new byte[]{0x00, 0x00, 0x01, 0x01})] + [InlineData(-1, new byte[]{0xFF, 0xFF, 0xFF, 0xFF})] + [InlineData(-2, new byte[]{0xFF, 0xFF, 0xFF, 0xFE})] + public async Task TestIntSpec(int value, byte[] expected) + { + var writer = CreateGraphBinaryWriter(); + var serializationStream = new MemoryStream(); + + await writer.WriteNonNullableValueAsync(value, serializationStream); + + var serBytes = serializationStream.ToArray(); + Assert.Equal(expected, serBytes); + } + + [Fact] + public async Task TestLong() + { + const long expected = 100; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(100.01f)] + [InlineData(float.NaN)] + [InlineData(float.NegativeInfinity)] + [InlineData(float.PositiveInfinity)] + public async Task TestFloat(float expected) + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDouble() + { + const double expected = 100.001; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestShort() + { + const short expected = 100; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDateTime() + { + var expected = DateTimeOffset.ParseExact("2016-12-14 16:14:36.295000", "yyyy-MM-dd HH:mm:ss.ffffff", + CultureInfo.InvariantCulture); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDateTimeWithNonUtcOffset() + { + // 2007-12-03T10:15:30+01:00 — the example from the spec + var expected = new DateTimeOffset(2007, 12, 3, 10, 15, 30, TimeSpan.FromHours(1)); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDateTimeWithNegativeOffset() + { + var expected = new DateTimeOffset(2024, 6, 15, 8, 30, 0, TimeSpan.FromHours(-5)); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestMarkerEndOfStream() + { + var expected = Marker.EndOfStream; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestMarkerEndOfStreamSpec() + { + var writer = CreateGraphBinaryWriter(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(Marker.EndOfStream, serializationStream); + + Assert.Equal(new byte[] { 0xFD, 0x00, 0x00 }, serializationStream.ToArray()); + } + + [Fact] + public async Task TestString() + { + const string expected = "serialize this!"; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("serialize this!", "serialize that!", "serialize that!", "stop telling me what to serialize")] + [InlineData(1, 2, 3, 4, 5)] + [InlineData(0.1, 1.1, 2.5, double.NaN)] + [InlineData(0.1f, 1.1f, 2.5f, float.NaN)] + public async Task TestHomogeneousList(params object[] listMembers) + { + var expected = new List(listMembers); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestHomogeneousTypeSafeList() + { + var expected = new List {"test", "123"}; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestHeterogeneousList() + { + var expected = new List + {"serialize this!", 0, "serialize that!", "serialize that!", 1, "stop telling me what to serialize", 2}; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + /// + /// Tests the bulked list format (value_flag=0x02) where each item is followed by a Long bulk count. + /// The writer doesn't produce this format, so we construct the bytes manually to test the reader. + /// + [Fact] + public async Task TestBulkedList() + { + // Manually construct: type=List(0x12), value_flag=0x02 (bulked), count=2 items + // item 1: String "a", bulk=2 → ["a", "a"] + // item 2: String "b", bulk=1 → ["b"] + var stream = new MemoryStream(); + var writer = CreateGraphBinaryWriter(); + // type code + bulked flag + stream.WriteByte(DataType.List.TypeCode); + stream.WriteByte(0x02); // ValueFlagBulk + await writer.WriteNonNullableValueAsync(2, stream); + await writer.WriteAsync("a", stream); + await writer.WriteNonNullableValueAsync(2L, stream); + await writer.WriteAsync("b", stream); + await writer.WriteNonNullableValueAsync(1L, stream); + + stream.Position = 0; + var reader = CreateGraphBinaryReader(); + var actual = (List) await reader.ReadAsync(stream); + + Assert.Equal(new List { "a", "a", "b" }, actual); + } + + [Fact] + public async Task TestArray() + { + var expected = new string[] {"hallo", "welt"}; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("serialize this!", "serialize that!", "serialize that!", "stop telling me what to serialize")] + [InlineData(1, 2, 3, 4, 5)] + [InlineData(0.1, 1.1, 2.5, double.NaN)] + [InlineData(0.1f, 1.1f, 2.5f, float.NaN)] + public async Task TestHomogeneousSet(params object[] listMembers) + { + var expected = new HashSet(listMembers); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestHomogeneousTypeSafeSet() + { + var expected = new HashSet {"test", "123"}; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteNonNullableValueAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadNonNullableValueAsync>(serializationStream); + + Assert.Equal(expected, actual); + Assert.Equal(expected.GetType(), actual.GetType()); + } + + [Fact] + public async Task TestHeterogeneousSet() + { + var expected = new HashSet + {"serialize this!", 0, "serialize that!", "serialize that!", 1, "stop telling me what to serialize", 2}; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDictionary() + { + var expected = new Dictionary + { + {"yo", "what"}, + {"go", "no!"}, + {"number", 123}, + {321, "crazy with the number for a key"}, + {987, new List {"go", "deep", new Dictionary {{"here", "!"}}}} + }; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestHomogeneousTypeSafeDictionary() + { + var expected = new Dictionary + { + {"number", 123}, + {"and", 456}, + {"nothing else", 789} + }; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestHomogeneousTypeSafeDictionaryWithCorrectTyping() + { + var expected = new Dictionary + { + {"number", 123}, + {"and", 456}, + {"nothing else", 789} + }; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteNonNullableValueAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadNonNullableValueAsync>(serializationStream); + + Assert.Equal(expected, actual); + Assert.Equal(expected.GetType(), actual.GetType()); + } + + [Fact] + public async Task TestGuid() + { + var expected = Guid.Parse("41d2e28a-20a4-4ab0-b379-d810dede3786"); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestGuidSerialization() + { + var toSerialize = Guid.Parse("00112233-4455-6677-8899-aabbccddeeff"); + var writer = CreateGraphBinaryWriter(); + var serializationStream = new MemoryStream(); + + await writer.WriteNonNullableValueAsync(toSerialize, serializationStream); + + var expected = new byte[] + {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}; + Assert.Equal(expected, serializationStream.ToArray()); + } + + [Fact] + public async Task TestVertex() + { + var expected = new Vertex(123, "person"); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task WriteNonNullableValueShouldThrowForNullValue() + { + var writer = CreateGraphBinaryWriter(); + var serializationStream = new MemoryStream(); + + await Assert.ThrowsAsync(() => writer.WriteNonNullableValueAsync(null!, serializationStream)); + } + + [Fact] + public async Task TestEdge() + { + var expected = new Edge(123, new Vertex(1, "person"), "developed", new Vertex(10, "software")); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestPath() + { + var expected = + new Path( + new List> + {new HashSet {"a", "b"}, new HashSet {"c", "d"}, new HashSet {"e"}}, + new List {1, 2, 3}); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestProperty() + { + var expected = new Property("name", "stephen", null); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestVertexProperty() + { + var expected = new VertexProperty(123, "name", "stephen", null); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDirection() + { + var expected = Direction.Out; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestMerge() + { + var expected = Merge.OnCreate; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestT() + { + var expected = T.Label; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1)] + [InlineData(123)] + [InlineData(-1)] + [InlineData(-128)] + [InlineData(127)] + public async Task TestSByte(sbyte expected) + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData((sbyte)0, new byte[] { 0x00 })] + [InlineData((sbyte)1, new byte[] { 0x01 })] + [InlineData((sbyte)127, new byte[] { 0x7F })] + [InlineData((sbyte)-1, new byte[] { 0xFF })] + [InlineData((sbyte)-128, new byte[] { 0x80 })] + public async Task TestSByteSerializationSpec(sbyte value, byte[] expected) + { + var writer = CreateGraphBinaryWriter(); + var serializationStream = new MemoryStream(); + + await writer.WriteNonNullableValueAsync(value, serializationStream); + + var serBytes = serializationStream.ToArray(); + Assert.Equal(expected, serBytes); + } + + [Theory] + [InlineData((sbyte)0)] + [InlineData((sbyte)1)] + [InlineData((sbyte)127)] + [InlineData((sbyte)-1)] + [InlineData((sbyte)-128)] + [InlineData((sbyte)42)] + [InlineData((sbyte)-42)] + public async Task TestSByteRoundTrip(sbyte expected) + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + Assert.IsType(actual); + } + + [Fact] + public async Task TestSByteMinMaxValues() + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + + // Test minimum value + var minStream = new MemoryStream(); + await writer.WriteAsync(sbyte.MinValue, minStream); + minStream.Position = 0; + var actualMin = await reader.ReadAsync(minStream); + Assert.Equal(sbyte.MinValue, actualMin); + + // Test maximum value + var maxStream = new MemoryStream(); + await writer.WriteAsync(sbyte.MaxValue, maxStream); + maxStream.Position = 0; + var actualMax = await reader.ReadAsync(maxStream); + Assert.Equal(sbyte.MaxValue, actualMax); + } + + [Fact] + public async Task TestBinary() + { + var expected = new byte[] {1, 2, 3}; + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestBoolean(bool expected) + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData('a')] + [InlineData('0')] + [InlineData('¢')] + [InlineData('€')] + public async Task TestChar(char expected) + { + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestDuration() + { + var expected = new TimeSpan(1, 2, 3, 4, 5); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task TestBigInteger() + { + var expected = BigInteger.Parse("123456789987654321123456789987654321"); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("190.035")] + [InlineData("0.19")] + [InlineData("1900")] + [InlineData("-1900")] + [InlineData("100000000000000")] + [InlineData("100000000000000000000000000")] + public async Task TestBigDecimal(string decimalValue) + { + var expected = Decimal.Parse(decimalValue); + var writer = CreateGraphBinaryWriter(); + var reader = CreateGraphBinaryReader(); + var serializationStream = new MemoryStream(); + + await writer.WriteAsync(expected, serializationStream); + serializationStream.Position = 0; + var actual = await reader.ReadAsync(serializationStream); + + Assert.Equal(expected, actual); + } + + private static GraphBinaryWriter CreateGraphBinaryWriter() + { + return new GraphBinaryWriter(); + } + + private static GraphBinaryReader CreateGraphBinaryReader() + { + return new GraphBinaryReader(); + } + } +} \ No newline at end of file diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs new file mode 100644 index 00000000000..d749d6624e1 --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs @@ -0,0 +1,210 @@ +#region License + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Numerics; +using Gremlin.Net.Process.Traversal; +using Gremlin.Net.Structure; + +namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4 +{ + /// + /// Defines the supported types for GraphBinary 4.0 IO and provides test entries for round-trip testing. + /// + /// The following models aren't supported: + /// tinker-graph Graph type not implemented + /// traversal-tree Tree type not implemented + /// max-offsetdatetime DateTimeOffset.MaxValue exceeds serialization range + /// min-offsetdatetime DateTimeOffset.MinValue exceeds serialization range + /// forever-duration TimeSpan cannot represent Duration.FOREVER + /// pos-bigdecimal Java BigDecimal precision (33 digits) exceeds C# decimal (28-29 digits) + /// neg-bigdecimal Java BigDecimal precision (33 digits) exceeds C# decimal (28-29 digits) + /// var-type-map Dictionary doesn't support null key + /// + public static class Model + { + private static readonly Property SinceProperty = new("since", 2009, null); + private static readonly Property StartTime2005 = new("startTime", 2005, null); + private static readonly Property EndTime2005 = new("endTime", 2005, null); + private static readonly Property StartTime2001 = new("startTime", 2001, null); + private static readonly Property EndTime2004 = new("endTime", 2004, null); + private static readonly Property StartTime1997 = new("startTime", 1997, null); + private static readonly Property EndTime2001 = new("endTime", 2001, null); + private static readonly Property PropertyAB = new("a", "b", null); + + private static readonly VertexProperty SantaFe = new(9, "location", "santa fe", null, + new dynamic[] { new Property("startTime", 2005, null) }); + private static readonly VertexProperty Brussels = new(8, "location", "brussels", null, + new dynamic[] { StartTime2005, EndTime2005 }); + private static readonly VertexProperty SantaCruz = new(7, "location", "santa cruz", null, + new dynamic[] { StartTime2001, EndTime2004 }); + private static readonly VertexProperty SanDiego = new(6, "location", "san diego", null, + new dynamic[] { StartTime1997, EndTime2001 }); + private static readonly VertexProperty NameMarko = new(0, "name", "marko", null); + + public static Dictionary Entries { get; } = new() + { + // BigInteger + ["pos-biginteger"] = BigInteger.Parse("123456789987654321123456789987654321"), + ["neg-biginteger"] = BigInteger.Parse("-123456789987654321123456789987654321"), + + // Byte (sbyte in C#) + ["min-byte"] = sbyte.MinValue, // -128 + ["max-byte"] = sbyte.MaxValue, // 127 + + // Binary (byte[]) + ["empty-binary"] = Array.Empty(), + ["str-binary"] = System.Text.Encoding.UTF8.GetBytes("some bytes for you"), + + // Double + ["max-double"] = double.MaxValue, + ["min-double"] = double.Epsilon, + ["neg-max-double"] = -double.MaxValue, + ["neg-min-double"] = -double.Epsilon, + ["nan-double"] = double.NaN, + ["pos-inf-double"] = double.PositiveInfinity, + ["neg-inf-double"] = double.NegativeInfinity, + ["neg-zero-double"] = -0.0, + + // Float + // Note: Java's Float.MIN_VALUE is the smallest positive float (equivalent to C#'s float.Epsilon), + // not the most negative float. + ["max-float"] = float.MaxValue, + ["min-float"] = float.Epsilon, // Java Float.MIN_VALUE = smallest positive float + ["neg-max-float"] = -float.MaxValue, + ["neg-min-float"] = -float.Epsilon, // negated Java Float.MIN_VALUE + ["nan-float"] = float.NaN, + ["pos-inf-float"] = float.PositiveInfinity, + ["neg-inf-float"] = float.NegativeInfinity, + ["neg-zero-float"] = -0.0f, + + // Char + ["single-byte-char"] = 'a', + ["multi-byte-char"] = '\u03A9', // Greek capital letter Omega + + // Null + ["unspecified-null"] = null, + + // Boolean + ["true-boolean"] = true, + ["false-boolean"] = false, + + // String + ["single-byte-string"] = "abc", + ["mixed-string"] = "abc\u0391\u0392\u0393", // abc + Greek letters Alpha, Beta, Gamma + + // BulkSet (represented as List with duplicates) + ["var-bulklist"] = new List { "marko", "josh", "josh" }, + ["empty-bulklist"] = new List(), + + // Duration (TimeSpan) + ["zero-duration"] = TimeSpan.Zero, + + // Edge + ["traversal-edge"] = new Edge(13, new Vertex(1, "person"), "develops", new Vertex(10, "software"), + new dynamic[] { SinceProperty }), + ["no-prop-edge"] = new Edge(13, new Vertex(1, "person"), "develops", new Vertex(10, "software")), + + // Int + ["max-int"] = int.MaxValue, + ["min-int"] = int.MinValue, + + // Long + ["max-long"] = long.MaxValue, + ["min-long"] = long.MinValue, + + // List + ["var-type-list"] = new List { 1, "person", true, null }, + ["empty-list"] = new List(), + + // Map + ["var-type-map"] = new Dictionary + { + { "test", 123 }, + { DateTimeOffset.FromUnixTimeMilliseconds(1481295), "red" }, + { new List { 1, 2, 3 }, DateTimeOffset.FromUnixTimeMilliseconds(1481295) }, + // Note: null key not supported in C# Dictionary + }, + ["empty-map"] = new Dictionary(), + + // Path + ["traversal-path"] = new Path( + new List> { new HashSet(), new HashSet(), new HashSet() }, + new List { new Vertex(1, "person"), new Vertex(10, "software"), new Vertex(11, "software") }), + ["empty-path"] = new Path(new List>(), new List()), + ["prop-path"] = new Path( + new List> { new HashSet(), new HashSet(), new HashSet() }, + new List + { + new Vertex(1, "person", new dynamic[] + { + new VertexProperty(123, "name", "stephen", null, new dynamic[] + { + new VertexProperty(0, "name", "marko", null), + new VertexProperty(6, "location", new List(), null) + }) + }), + new Vertex(10, "software"), + new Vertex(11, "software") + }), + + // Property + ["edge-property"] = new Property("since", 2009, null), + ["null-property"] = new Property("", null, null), + + // Set + ["var-type-set"] = new HashSet { 2, "person", true, null }, + ["empty-set"] = new HashSet(), + + // Short + ["max-short"] = short.MaxValue, + ["min-short"] = short.MinValue, + + // UUID (Guid) + ["specified-uuid"] = Guid.Parse("41d2e28a-20a4-4ab0-b379-d810dede3786"), + ["nil-uuid"] = Guid.Empty, + + // Vertex + ["no-prop-vertex"] = new Vertex(1, "person"), + ["traversal-vertex"] = new Vertex(1, "person", new dynamic[] + { + new Property("name", NameMarko, null), + new Property("location", new List { SanDiego, SantaCruz, Brussels, SantaFe }, null) + }), + + // VertexProperty + ["traversal-vertexproperty"] = new VertexProperty(0L, "name", "marko", null), + ["meta-vertexproperty"] = new VertexProperty(1, "person", "stephen", null, + new dynamic[] { PropertyAB }), + ["set-cardinality-vertexproperty"] = new VertexProperty(1, "person", + new HashSet { "stephen", "marko" }, null, new dynamic[] { PropertyAB }), + + // T enum + ["id-t"] = T.Id, + + // Direction enum + ["out-direction"] = Direction.Out, + }; + } +} diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs new file mode 100644 index 00000000000..c270105fa24 --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs @@ -0,0 +1,364 @@ +#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.IO.GraphBinary4; +using Xunit; + +namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4 +{ + /// + /// Round trip testing of GraphBinary 4.0 compared to a correct "model". + /// Set the IO_TEST_DIRECTORY environment variable to the directory where + /// the .gbin files that represent the serialized "model" are located. + /// + public class RoundTripTests + { + private const string GremlinTestDir = "gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/"; + private const string DirectorySearchPattern = "gremlin-dotnet"; + + private static readonly string TestResourceDirectory; + private static readonly GraphBinaryWriter Writer = new(); + private static readonly GraphBinaryReader Reader = new(); + + static RoundTripTests() + { + var envDir = Environment.GetEnvironmentVariable("IO_TEST_DIRECTORY"); + if (!string.IsNullOrEmpty(envDir)) + { + TestResourceDirectory = envDir; + } + else + { + // Find the root directory by searching for gremlin-dotnet in the current path + var currentDir = AppDomain.CurrentDomain.BaseDirectory; + var index = currentDir.IndexOf(DirectorySearchPattern, StringComparison.Ordinal); + var defaultDir = index >= 0 ? currentDir[..index] : currentDir; + TestResourceDirectory = Path.Combine(defaultDir, GremlinTestDir); + } + } + + private static object? GetEntry(string title) => Model.Entries[title]; + + private static byte[] ReadFileByName(string resourceName) + { + var fullName = Path.Combine(TestResourceDirectory, $"{resourceName}-v4.gbin"); + return File.ReadAllBytes(fullName); + } + + /// + /// Runs the regular set of tests for the type: + /// 1. model to deserialized object + /// 2. written bytes to read bytes + /// 3. round tripped (read then written) bytes to read bytes + /// + private async Task Run(string resourceName, Func? comparator = null) + { + var resourceBytes = ReadFileByName(resourceName); + var model = GetEntry(resourceName); + + using var readStream = new MemoryStream(resourceBytes); + var read = await Reader.ReadAsync(readStream); + + if (comparator != null) + Assert.True(comparator(model, read)); + else + Assert.Equal(model, read); + + using var writeStream = new MemoryStream(); + await Writer.WriteAsync(model, writeStream); + Assert.Equal(resourceBytes, writeStream.ToArray()); + + using var roundTripReadStream = new MemoryStream(resourceBytes); + var roundTripRead = await Reader.ReadAsync(roundTripReadStream); + using var roundTripWriteStream = new MemoryStream(); + await Writer.WriteAsync(roundTripRead, roundTripWriteStream); + Assert.Equal(resourceBytes, roundTripWriteStream.ToArray()); + } + + /// + /// Runs the read test which compares the model to deserialized object. + /// This should only be used in cases where there is only a deserializer + /// but no serializer for the same type. + /// + private async Task RunRead(string resourceName, Func? comparator = null) + { + var resourceBytes = ReadFileByName(resourceName); + var model = GetEntry(resourceName); + + using var readStream = new MemoryStream(resourceBytes); + var read = await Reader.ReadAsync(readStream); + + if (comparator != null) + Assert.True(comparator(model, read)); + else + Assert.Equal(model, read); + } + + /// + /// Runs a reduced set of tests for the type: + /// 1. model to deserialized object + /// 2. model to round tripped (written then read) object + /// Use this in cases where the regular Run() function is too stringent. + /// E.g. when ordering doesn't matter like for sets. + /// + private async Task RunWriteRead(string resourceName, Func? comparator = null) + { + var resourceBytes = ReadFileByName(resourceName); + var model = GetEntry(resourceName); + + using var readStream = new MemoryStream(resourceBytes); + var read = await Reader.ReadAsync(readStream); + + using var writeStream = new MemoryStream(); + await Writer.WriteAsync(model, writeStream); + using var roundTripStream = new MemoryStream(writeStream.ToArray()); + var roundTripped = await Reader.ReadAsync(roundTripStream); + + if (comparator != null) + { + Assert.True(comparator(model, read)); + Assert.True(comparator(model, roundTripped)); + } + else + { + Assert.Equal(model, read); + Assert.Equal(model, roundTripped); + } + } + + private static bool NanComparator(object? x, object? y) + { + return x switch + { + double dx when y is double dy => double.IsNaN(dx) && double.IsNaN(dy), + float fx when y is float fy => float.IsNaN(fx) && float.IsNaN(fy), + _ => false + }; + } + + // BigInteger tests + [Fact] + public Task TestPosBigInteger() => Run("pos-biginteger"); + + [Fact] + public Task TestNegBigInteger() => Run("neg-biginteger"); + + // Byte tests + [Fact] + public Task TestMinByte() => Run("min-byte"); + + [Fact] + public Task TestMaxByte() => Run("max-byte"); + + // Binary tests + [Fact] + public Task TestEmptyBinary() => Run("empty-binary"); + + [Fact] + public Task TestStrBinary() => Run("str-binary"); + + // Double tests + [Fact] + public Task TestMaxDouble() => Run("max-double"); + + [Fact] + public Task TestMinDouble() => Run("min-double"); + + [Fact] + public Task TestNegMaxDouble() => Run("neg-max-double"); + + [Fact] + public Task TestNegMinDouble() => Run("neg-min-double"); + + [Fact] + public Task TestNanDouble() => RunWriteRead("nan-double", NanComparator); // C# has different NaN representation + + [Fact] + public Task TestPosInfDouble() => Run("pos-inf-double"); + + [Fact] + public Task TestNegInfDouble() => Run("neg-inf-double"); + + [Fact] + public Task TestNegZeroDouble() => Run("neg-zero-double"); + + // Float tests + [Fact] + public Task TestMaxFloat() => Run("max-float"); + + [Fact] + public Task TestMinFloat() => Run("min-float"); + + [Fact] + public Task TestNegMaxFloat() => Run("neg-max-float"); + + [Fact] + public Task TestNegMinFloat() => Run("neg-min-float"); + + [Fact] + public Task TestNanFloat() => RunWriteRead("nan-float", NanComparator); // C# has different NaN representation + + [Fact] + public Task TestPosInfFloat() => Run("pos-inf-float"); + + [Fact] + public Task TestNegInfFloat() => Run("neg-inf-float"); + + [Fact] + public Task TestNegZeroFloat() => Run("neg-zero-float"); + + // Char tests + [Fact] + public Task TestSingleByteChar() => Run("single-byte-char"); + + [Fact] + public Task TestMultiByteChar() => Run("multi-byte-char"); + + // Null test + [Fact] + public Task TestUnspecifiedNull() => Run("unspecified-null"); + + // Boolean tests + [Fact] + public Task TestTrueBoolean() => Run("true-boolean"); + + [Fact] + public Task TestFalseBoolean() => Run("false-boolean"); + + // String tests + [Fact] + public Task TestSingleByteString() => Run("single-byte-string"); + + [Fact] + public Task TestMixedString() => Run("mixed-string"); + + // BulkSet tests (read only - BulkSet deserialized as List) + [Fact] + public Task TestVarBulkList() => RunRead("var-bulklist"); + + [Fact] + public Task TestEmptyBulkList() => RunRead("empty-bulklist"); + + // Duration tests + [Fact] + public Task TestZeroDuration() => Run("zero-duration"); + + // Edge tests + [Fact] + public Task TestTraversalEdge() => RunWriteRead("traversal-edge"); // properties aren't serialized + + [Fact] + public Task TestNoPropEdge() => RunWriteRead("no-prop-edge"); // properties written as null not empty list + + // Int tests + [Fact] + public Task TestMaxInt() => Run("max-int"); + + [Fact] + public Task TestMinInt() => Run("min-int"); + + // Long tests + [Fact] + public Task TestMaxLong() => Run("max-long"); + + [Fact] + public Task TestMinLong() => Run("min-long"); + + // List tests + [Fact] + public Task TestVarTypeList() => Run("var-type-list"); + + [Fact] + public Task TestEmptyList() => Run("empty-list"); + + // Map tests + [Fact] + public Task TestEmptyMap() => Run("empty-map"); + + // Path tests + [Fact] + public Task TestTraversalPath() => RunWriteRead("traversal-path"); // properties written as null not empty list + + [Fact] + public Task TestEmptyPath() => Run("empty-path"); + + [Fact] + public Task TestPropPath() => RunWriteRead("prop-path"); // properties aren't serialized + + // Property tests + [Fact] + public Task TestEdgeProperty() => Run("edge-property"); + + [Fact] + public Task TestNullProperty() => Run("null-property"); + + // Set tests + [Fact] + public Task TestVarTypeSet() => RunWriteRead("var-type-set"); // order not guaranteed in HashSet + + [Fact] + public Task TestEmptySet() => Run("empty-set"); + + // Short tests + [Fact] + public Task TestMaxShort() => Run("max-short"); + + [Fact] + public Task TestMinShort() => Run("min-short"); + + // UUID tests + [Fact] + public Task TestSpecifiedUuid() => Run("specified-uuid"); + + [Fact] + public Task TestNilUuid() => Run("nil-uuid"); + + // Vertex tests + [Fact] + public Task TestNoPropVertex() => RunWriteRead("no-prop-vertex"); // properties written as null not empty list + + [Fact] + public Task TestTraversalVertex() => RunWriteRead("traversal-vertex"); // properties aren't serialized + + // VertexProperty tests + [Fact] + public Task TestTraversalVertexProperty() => RunWriteRead("traversal-vertexproperty"); // properties aren't serialized + + [Fact] + public Task TestMetaVertexProperty() => RunWriteRead("meta-vertexproperty"); // properties aren't serialized + + [Fact] + public Task TestSetCardinalityVertexProperty() => RunWriteRead("set-cardinality-vertexproperty"); // properties aren't serialized + + // T enum test + [Fact] + public Task TestIdT() => Run("id-t"); + + // Direction enum test + [Fact] + public Task TestOutDirection() => Run("out-direction"); + } +} From 1b3c1fa4da8291a06e509914f0d8f25b244df777 Mon Sep 17 00:00:00 2001 From: Ken Hu <106191785+kenhuuu@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:04:55 -0800 Subject: [PATCH 2/2] Fix incorrect typecode for Merge --- .../src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 8ae3199f163..9490a34302e 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs @@ -50,7 +50,6 @@ public class DataType : IEquatable public static readonly DataType VertexProperty = new DataType(0x12); public static readonly DataType Direction = new DataType(0x18); public static readonly DataType T = new DataType(0x20); - public static readonly DataType Merge = new DataType(0x21); public static readonly DataType BigDecimal = new DataType(0x22); public static readonly DataType BigInteger = new DataType(0x23); public static readonly DataType Byte = new DataType(0x24); @@ -59,6 +58,8 @@ public class DataType : IEquatable public static readonly DataType Boolean = new DataType(0x27); // Not yet implemented // public static readonly DataType Tree = new DataType(0x2B); + public static readonly DataType Merge = new DataType(0x2E); + // Not yet implemented // public static readonly DataType CompositePDT = new DataType(0xF0); // public static readonly DataType PrimitivePDT = new DataType(0xF1); public static readonly DataType Char = new DataType(0x80);