diff --git a/Directory.Packages.props b/Directory.Packages.props
index cc3db9f5..f09a6f23 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,6 +6,7 @@
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 5aa18efb..3155c5ee 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -24,6 +24,14 @@ services:
TARANTOOL_USER_NAME: admin
TARANTOOL_USER_PASSWORD: adminPassword
TARANTOOL_SLAB_ALLOC_ARENA: 0.1
+
+ tarantool_2_11:
+ image: tarantool/tarantool:2.11
+ ports:
+ - "3311:3301"
+ environment:
+ TARANTOOL_USER_NAME: admin
+ TARANTOOL_USER_PASSWORD: adminPassword
redis:
image: redis:3.0-alpine
diff --git a/src/progaudi.tarantool/Converters/DateTimeConverter.cs b/src/progaudi.tarantool/Converters/DateTimeConverter.cs
new file mode 100644
index 00000000..c90d337f
--- /dev/null
+++ b/src/progaudi.tarantool/Converters/DateTimeConverter.cs
@@ -0,0 +1,87 @@
+using ProGaudi.MsgPack.Light;
+using ProGaudi.Tarantool.Client.Model.Enums;
+using ProGaudi.Tarantool.Client.Utils;
+using System;
+using System.Buffers.Binary;
+
+namespace ProGaudi.Tarantool.Client.Converters
+{
+ ///
+ /// Converter for Tarantool datetime values, implemeted as MsgPack extension.
+ /// See https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
+ ///
+ internal class DateTimeConverter : IMsgPackConverter, IMsgPackConverter
+ {
+ private const byte MP_DATETIME = 0x04;
+ private static readonly DateTime UnixEpocUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ public void Initialize(MsgPackContext context)
+ {
+ }
+
+ public DateTime Read(IMsgPackReader reader)
+ {
+ var dataType = reader.ReadByte();
+ var mpHeader = reader.ReadByte();
+ if (mpHeader != MP_DATETIME)
+ {
+ throw ExceptionHelper.UnexpectedMsgPackHeader(mpHeader, MP_DATETIME);
+ }
+
+ if (dataType == MsgPackExtDataTypes.FixExt8)
+ {
+ var seconds = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4));
+ var nanoSeconds = BinaryPrimitives.ReadInt16LittleEndian(reader.ReadBytes(2));
+ var _ = reader.ReadBytes(2);// also need to extract tzoffset; tzindex;
+ return UnixEpocUtc.AddSeconds(seconds).AddTicks(nanoSeconds / 100);
+ }
+ else if (dataType == MsgPackExtDataTypes.FixExt16)
+ {
+ var seconds = BinaryPrimitives.ReadInt64LittleEndian(reader.ReadBytes(8));
+ var nanoSeconds = BinaryPrimitives.ReadInt32LittleEndian(reader.ReadBytes(4));
+ var _ = reader.ReadBytes(4);// also need to extract tzoffset; tzindex;
+ return UnixEpocUtc.AddSeconds(seconds).AddTicks(nanoSeconds / 100);
+ }
+
+ throw ExceptionHelper.UnexpectedDataType(dataType, MsgPackExtDataTypes.FixExt8, MsgPackExtDataTypes.FixExt16);
+ }
+
+ DateTimeOffset IMsgPackConverter.Read(IMsgPackReader reader)
+ {
+ return Read(reader);
+ }
+
+ public void Write(DateTimeOffset value, IMsgPackWriter writer)
+ {
+ var timeSpan = value.ToUniversalTime().Subtract(UnixEpocUtc);
+ long seconds = (long)timeSpan.TotalSeconds;
+ timeSpan = timeSpan.Subtract(TimeSpan.FromSeconds(seconds));
+ int nanoSeconds = (int)(timeSpan.Ticks * 100);
+ int _ = 0;// also need to extract tzoffset; tzindex;
+
+ writer.Write(MsgPackExtDataTypes.FixExt16);
+ writer.Write(MP_DATETIME);
+
+ var byteArray = new byte[8];
+ var span = new Span(byteArray);
+ BinaryPrimitives.WriteInt64LittleEndian(span, seconds);
+ writer.Write(byteArray);
+
+ byteArray = new byte[4];
+ span = new Span(byteArray);
+ BinaryPrimitives.WriteInt32LittleEndian(span, nanoSeconds);
+ writer.Write(byteArray);
+
+ byteArray = new byte[4];
+ span = new Span(byteArray);
+ BinaryPrimitives.WriteInt32LittleEndian(span, _);
+ writer.Write(byteArray);
+
+ }
+
+ public void Write(DateTime value, IMsgPackWriter writer)
+ {
+ Write((DateTimeOffset)value, writer);
+ }
+ }
+}
diff --git a/src/progaudi.tarantool/Converters/DecimalConverter.cs b/src/progaudi.tarantool/Converters/DecimalConverter.cs
new file mode 100644
index 00000000..541dc835
--- /dev/null
+++ b/src/progaudi.tarantool/Converters/DecimalConverter.cs
@@ -0,0 +1,240 @@
+using ProGaudi.MsgPack.Light;
+using ProGaudi.Tarantool.Client.Model.Enums;
+using ProGaudi.Tarantool.Client.Utils;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace ProGaudi.Tarantool.Client.Converters
+{
+ ///
+ /// Converter for Tarantool decimal values, implemented as MsgPack extension.
+ /// Format described in https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type
+ /// Limitation: .NET decimal max scale is 28 digits, when Tarantool decimal max scale is 38 digits
+ ///
+ public class DecimalConverter : IMsgPackConverter
+ {
+ private static readonly byte[] SupportedFixTypes = new byte[5]
+ {
+ MsgPackExtDataTypes.FixExt1,
+ MsgPackExtDataTypes.FixExt2,
+ MsgPackExtDataTypes.FixExt4,
+ MsgPackExtDataTypes.FixExt8,
+ MsgPackExtDataTypes.FixExt16
+ };
+ private static readonly byte[] SupportedNonFixTypes = new byte[3]
+ {
+ MsgPackExtDataTypes.Ext8,
+ MsgPackExtDataTypes.Ext16,
+ MsgPackExtDataTypes.Ext32
+ };
+
+ private const byte MP_DECIMAL = 0x01;
+ private const byte DECIMAL_PLUS = 0x0C;
+ private const byte DECIMAL_MINUS = 0x0D;
+ private const byte DECIMAL_MINUS_ALT = 0x0B;
+
+ public void Initialize(MsgPackContext context)
+ {
+ }
+
+ public decimal Read(IMsgPackReader reader)
+ {
+ var dataType = reader.ReadByte();
+ var fixedDataType = true;
+ var len = 0;
+ switch (dataType)
+ {
+ case MsgPackExtDataTypes.Ext8:
+ case MsgPackExtDataTypes.Ext16:
+ case MsgPackExtDataTypes.Ext32:
+ fixedDataType = false;
+ break;
+ case MsgPackExtDataTypes.FixExt1:
+ len = 1;
+ break;
+ case MsgPackExtDataTypes.FixExt2:
+ len = 2;
+ break;
+ case MsgPackExtDataTypes.FixExt4:
+ len = 4;
+ break;
+ case MsgPackExtDataTypes.FixExt8:
+ len = 8;
+ break;
+ case MsgPackExtDataTypes.FixExt16:
+ len = 16;
+ break;
+ default:
+ throw ExceptionHelper.UnexpectedDataType(dataType, SupportedFixTypes.Union(SupportedNonFixTypes).ToArray());
+ }
+
+ if (!fixedDataType)
+ {
+ len = reader.ReadByte();
+ }
+
+ var mpHeader = reader.ReadByte();
+ if (mpHeader != MP_DECIMAL)
+ {
+ throw ExceptionHelper.UnexpectedMsgPackHeader(mpHeader, MP_DECIMAL);
+ }
+
+ var data = reader.ReadBytes((uint)len).ToArray();
+
+ // used Java impl https://github.com/tarantool/cartridge-java/blob/1ca12332b870167b86d3e38891ab74527dfc8a19/src/main/java/io/tarantool/driver/mappers/converters/value/defaults/DefaultExtensionValueToBigDecimalConverter.java
+
+ // Extract sign from the last nibble
+ int signum = (byte)(SecondNibbleFromByte(data[len - 1]));
+ if (signum == DECIMAL_MINUS || signum == DECIMAL_MINUS_ALT)
+ {
+ signum = -1;
+ }
+ else if (signum <= 0x09)
+ {
+ throw new IOException("The sign nibble has wrong value");
+ }
+ else
+ {
+ signum = 1;
+ }
+
+ int scale = data[0];
+ if (scale > 28)
+ {
+ throw new OverflowException($"Maximum .NET decimal scale is exceeded. Maximum: 28. Actual: {scale}");
+ }
+
+ int skipIndex = 1; //skip byte with scale
+
+ int digitsNum = (len - skipIndex) << 1;
+ char digit = CharFromDigit(FirstNibbleFromByte(data[len - 1]), digitsNum - 1);
+
+ char[] digits = new char[digitsNum];
+ int pos = 2 * (len - skipIndex) - 1;
+
+ digits[pos--] = digit;
+ for (int i = len - 2; i >= skipIndex; i--)
+ {
+ digits[pos--] = CharFromDigit(SecondNibbleFromByte(data[i]), pos);
+ digits[pos--] = CharFromDigit(FirstNibbleFromByte(data[i]), pos);
+ }
+
+ return CreateDecimalFromDigits(digits, scale, signum < 0);
+ }
+
+ public void Write(decimal value, IMsgPackWriter writer)
+ {
+ (int scale, decimal unscaledValue) = ExtractScaleFromDecimal(value);
+
+ // used Java impl https://github.com/tarantool/cartridge-java/blob/1ca12332b870167b86d3e38891ab74527dfc8a19/src/main/java/io/tarantool/driver/mappers/converters/value/defaults/DefaultExtensionValueToBigDecimalConverter.java
+ var unscaledValueStr = unscaledValue.ToString();
+ byte signum = value >= 0 ? DECIMAL_PLUS : DECIMAL_MINUS;
+ int digitsNum = unscaledValueStr.Length;
+
+ int len = (digitsNum >> 1) + 1;
+ byte[] payload = new byte[len];
+ payload[len - 1] = signum;
+ int pos = 0;
+ char[] digits = unscaledValueStr.Substring(pos).ToCharArray();
+ pos = digits.Length - 1;
+ for (int i = len - 1; i > 0; i--)
+ {
+ payload[i] |= (byte)(DigitFromChar(digits[pos--]) << 4);
+ payload[i - 1] |= (byte)DigitFromChar(digits[pos--]);
+ }
+ if (pos == 0)
+ {
+ payload[0] |= (byte)(DigitFromChar(digits[pos]) << 4);
+ }
+
+ writer.Write(MsgPackExtDataTypes.Ext8);
+ writer.Write((byte)(len + 1));
+ writer.Write(MP_DECIMAL);
+ writer.Write((byte)scale);
+ writer.Write(payload);
+ }
+
+ private static (int, decimal) ExtractScaleFromDecimal(decimal val)
+ {
+ var bits = decimal.GetBits(val);
+ int scale = (bits[3] >> 16) & 0x7F;
+ decimal unscaledValue = new Decimal(bits[0], bits[1], bits[2], false, 0);
+ return (scale, unscaledValue);
+ }
+
+ private static int UnsignedRightShift(int signed, int places)
+ {
+ unchecked
+ {
+ var unsigned = (uint)signed;
+ unsigned >>= places;
+ return (int)unsigned;
+ }
+ }
+
+ private static int FirstNibbleFromByte(byte val)
+ {
+ return UnsignedRightShift(val & 0xF0, 4);
+ }
+
+ private static int SecondNibbleFromByte(byte val)
+ {
+ return val & 0x0F;
+ }
+
+ private static char CharFromDigit(int val, int pos)
+ {
+ var digit = (char)val;
+ if (digit > 9)
+ {
+ throw new IOException(String.Format("Invalid digit at position %d", pos));
+ }
+ return digit;
+ }
+
+ private static int DigitFromChar(char val)
+ {
+ return val - '0';
+ }
+
+ private static decimal CreateDecimalFromDigits(char[] digits, int scale, bool isNegative)
+ {
+ int pos = 0;
+ while (pos < digits.Length && digits[pos] == 0)
+ {
+ pos++;
+ }
+
+ if (pos == digits.Length)
+ {
+ return 0;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (; pos < digits.Length; pos++)
+ {
+ sb.Append((int)digits[pos]);
+ }
+
+ if (scale >= sb.Length)
+ {
+ sb.Insert(0, String.Join("", Enumerable.Range(0, scale - sb.Length + 1).Select(_ => "0")));
+ }
+
+ if (scale > 0)
+ {
+ sb.Insert(sb.Length - scale, ".");
+ }
+
+ if (isNegative)
+ {
+ sb.Insert(0, '-');
+ }
+
+ return Decimal.Parse(sb.ToString(), CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/src/progaudi.tarantool/Converters/GuidConverter.cs b/src/progaudi.tarantool/Converters/GuidConverter.cs
new file mode 100644
index 00000000..5547d067
--- /dev/null
+++ b/src/progaudi.tarantool/Converters/GuidConverter.cs
@@ -0,0 +1,65 @@
+using ProGaudi.MsgPack.Light;
+using ProGaudi.Tarantool.Client.Model.Enums;
+using ProGaudi.Tarantool.Client.Utils;
+using System;
+using System.Buffers.Binary;
+using System.Linq;
+
+namespace ProGaudi.Tarantool.Client.Converters
+{
+ ///
+ /// Converter for Tarantool uuid values, implemented as MsgPack extension.
+ /// See https://www.tarantool.io/ru/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type
+ ///
+ internal class GuidConverter : IMsgPackConverter
+ {
+ private static readonly byte GuidDataType = MsgPackExtDataTypes.FixExt16;
+ private const byte MP_UUID = 0x02;
+
+ public void Initialize(MsgPackContext context)
+ {
+ }
+
+ public Guid Read(IMsgPackReader reader)
+ {
+ var dataType = reader.ReadByte();
+ if (dataType != GuidDataType)
+ {
+ throw ExceptionHelper.UnexpectedDataType(dataType, GuidDataType);
+ }
+
+ var mpHeader = reader.ReadByte();
+ if (mpHeader != MP_UUID)
+ {
+ throw ExceptionHelper.UnexpectedMsgPackHeader(mpHeader, MP_UUID);
+ }
+
+ int intToken = BinaryPrimitives.ReadInt32BigEndian(reader.ReadBytes(4));
+ short shortToken1 = BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2));
+ short shortToken2 = BinaryPrimitives.ReadInt16BigEndian(reader.ReadBytes(2));
+
+ return new Guid(intToken, shortToken1, shortToken2, reader.ReadBytes(8).ToArray());
+ }
+
+ public void Write(Guid value, IMsgPackWriter writer)
+ {
+ writer.Write(GuidDataType);
+ writer.Write(MP_UUID);
+
+ var byteArray = value.ToByteArray();
+
+ // big-endian swap
+ SwapTwoBytes(byteArray, 0, 3);
+ SwapTwoBytes(byteArray, 1, 2);
+ SwapTwoBytes(byteArray, 4, 5);
+ SwapTwoBytes(byteArray, 6, 7);
+
+ writer.Write(byteArray);
+ }
+
+ private static void SwapTwoBytes(byte[] array, int index1, int index2)
+ {
+ (array[index1], array[index2]) = (array[index2], array[index1]);
+ }
+ }
+}
diff --git a/src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs b/src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs
new file mode 100644
index 00000000..ff9d86aa
--- /dev/null
+++ b/src/progaudi.tarantool/Model/Enums/MsgPackExtDataTypes.cs
@@ -0,0 +1,14 @@
+namespace ProGaudi.Tarantool.Client.Model.Enums
+{
+ internal class MsgPackExtDataTypes
+ {
+ public const byte Ext8 = 0xc7;
+ public const byte Ext16 = 0xc8;
+ public const byte Ext32 = 0xc9;
+ public const byte FixExt1 = 0xd4;
+ public const byte FixExt2 = 0xd5;
+ public const byte FixExt4 = 0xd6;
+ public const byte FixExt8 = 0xd7;
+ public const byte FixExt16 = 0xd8;
+ }
+}
diff --git a/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs b/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs
index d2710f97..f42570ff 100644
--- a/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs
+++ b/src/progaudi.tarantool/TarantoolConvertersRegistrator.cs
@@ -4,6 +4,7 @@
using ProGaudi.Tarantool.Client.Model;
using ProGaudi.Tarantool.Client.Model.Enums;
using ProGaudi.Tarantool.Client.Model.Responses;
+using System;
namespace ProGaudi.Tarantool.Client
{
@@ -52,6 +53,11 @@ public static void Register(MsgPackContext context)
context.RegisterConverter(new PingPacketConverter());
context.RegisterConverter(new ExecuteSqlRequestConverter());
+ context.RegisterConverter(new DecimalConverter());
+ context.RegisterConverter(new GuidConverter());
+ context.RegisterConverter(new DateTimeConverter());
+ context.RegisterConverter(new DateTimeConverter());
+
context.RegisterGenericConverter(typeof(TupleConverter<>));
context.RegisterGenericConverter(typeof(TupleConverter<,>));
context.RegisterGenericConverter(typeof(TupleConverter<,,>));
diff --git a/src/progaudi.tarantool/Utils/ExceptionHelper.cs b/src/progaudi.tarantool/Utils/ExceptionHelper.cs
index c2b4df3e..2766b9c0 100644
--- a/src/progaudi.tarantool/Utils/ExceptionHelper.cs
+++ b/src/progaudi.tarantool/Utils/ExceptionHelper.cs
@@ -37,6 +37,16 @@ public static Exception UnexpectedDataType(DataTypes expected, DataTypes actual)
return new ArgumentException($"Unexpected data type: {expected} is expected, but got {actual}.");
}
+ public static Exception UnexpectedDataType(byte actualCode, params byte[] expectedCodes)
+ {
+ return new ArgumentException($"Unexpected data type: {String.Join(", ", expectedCodes)} is expected, but got {actualCode}.");
+ }
+
+ public static Exception UnexpectedMsgPackHeader(byte actual, byte expected)
+ {
+ return new ArgumentException($"Unexpected msgpack header: {expected} is expected, but got {actual}.");
+ }
+
public static Exception NotConnected()
{
return new InvalidOperationException("Can't perform operation. Looks like we are not connected to tarantool. Call 'Connect' method before calling any other operations.");
diff --git a/src/progaudi.tarantool/progaudi.tarantool.csproj b/src/progaudi.tarantool/progaudi.tarantool.csproj
index 6f3a7633..d2fc56ec 100644
--- a/src/progaudi.tarantool/progaudi.tarantool.csproj
+++ b/src/progaudi.tarantool/progaudi.tarantool.csproj
@@ -31,6 +31,7 @@
+
diff --git a/tests/progaudi.tarantool.tests/ConnectionStringFactory.cs b/tests/progaudi.tarantool.tests/ConnectionStringFactory.cs
index ad0b6c25..8b99a6bf 100644
--- a/tests/progaudi.tarantool.tests/ConnectionStringFactory.cs
+++ b/tests/progaudi.tarantool.tests/ConnectionStringFactory.cs
@@ -23,6 +23,25 @@ public static async Task GetRedisConnectionString()
return $"{await ResolveHostname(redisUrl)}:6379";
}
+
+ public static string GetLatestTarantoolConnectionString(string userName = null, string password = null)
+ {
+ userName ??= "admin";
+ password ??= "adminPassword";
+ return BuildConnectionString(userName, password, 3311);
+ }
+
+ private static string BuildConnectionString(string userName, string password, int port)
+ {
+ var userToken = (userName, password)
+ switch
+ {
+ (null, null) => "",
+ (_, null) => $"{userName}@",
+ _ => $"{userName}:{password}@",
+ };
+ return $"{userToken}127.0.0.1:{port}";
+ }
private static async Task ResolveHostname(string host)
{
diff --git a/tests/progaudi.tarantool.tests/DataTypes/DeserializationTests.cs b/tests/progaudi.tarantool.tests/DataTypes/DeserializationTests.cs
new file mode 100644
index 00000000..7966c117
--- /dev/null
+++ b/tests/progaudi.tarantool.tests/DataTypes/DeserializationTests.cs
@@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Xunit;
+using Shouldly;
+using System.Text;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ProGaudi.Tarantool.Client.Tests.DataTypes
+{
+ ///
+ /// Test suite, where we create and return some values in Tarantool via Lua and Eval command,
+ /// and check that this value deserialize into corresponding C# class/structure correctly
+ ///
+ public class DeserializationTests
+ {
+ [Fact]
+ public async Task DeserializeNull_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval("return box.NULL");
+ result.Data.Length.ShouldBe(1);
+ result.Data[0].ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task DeserializeNil_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval("return nil");
+ result.Data.Length.ShouldBe(1);
+ result.Data[0].ShouldBeNull();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task DeserializeBoolean_ShouldBeCorrectAsync(bool val)
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval($"return {(val ? "true" : "false")}");
+ result.Data.Length.ShouldBe(1);
+ result.Data[0].ShouldBe(val);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(0)]
+ [InlineData(-1)]
+ public async Task DeserializeInt_ShouldBeCorrectAsync(int val)
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval($"return {val}");
+ result.Data.ShouldBe(new[] { val });
+ }
+
+ [Fact]
+ public async Task DeserializeFloat64_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval("return math.sqrt(2)");
+ result.Data.Length.ShouldBe(1);
+ Math.Abs(result.Data[0] - Math.Sqrt(2)).ShouldBeLessThan(double.Epsilon);
+ }
+
+ [Fact]
+ public async Task DeserializeString_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var expectedStr = "Tarantool tickles, makes spiders giggle";
+ var result = await tarantoolClient.Eval($"return '{expectedStr}'");
+ result.Data.ShouldBe(new[] { expectedStr });
+ }
+
+ [Theory]
+ [InlineData("2.7182818284590452353602874714")]
+ [InlineData("2.718281828459045235360287471")]
+ [InlineData("2.71828182845904523536028747")]
+ [InlineData("2.7182818284590452353602874")]
+ [InlineData("2.718281828459045235360287")]
+ [InlineData("2.71828182845904523536028")]
+ [InlineData("2.7182818284590452353602")]
+ [InlineData("2.718281828459045235360")]
+ [InlineData("2.71828182845904523536")]
+ [InlineData("2.7182818284590452353")]
+ [InlineData("2.718281828459045235")]
+ [InlineData("2.71828182845904523")]
+ [InlineData("2.7182818284590452")]
+ [InlineData("2.718281828459045")]
+ [InlineData("2.71828182845904")]
+ [InlineData("2.7182818284590")]
+ [InlineData("2.718281828459")]
+ [InlineData("2.71828182845")]
+ [InlineData("2.7182818284")]
+ [InlineData("2.718281828")]
+ [InlineData("2.71828182")]
+ [InlineData("2.7182818")]
+ [InlineData("2.718281")]
+ [InlineData("2.71828")]
+ [InlineData("2.7182")]
+ [InlineData("2.718")]
+ [InlineData("2.71")]
+ [InlineData("2.7")]
+ [InlineData("2")]
+ [InlineData("0")]
+ [InlineData("100000")]
+ [InlineData("0.1")]
+ [InlineData("0.01")]
+ [InlineData("0.001")]
+ [InlineData("0.0001")]
+ public async Task DeserializeExtAsDecimal_CorrectValue_ShouldBeDeserializedCorrectlyAsync(string str)
+ {
+ var n = decimal.Parse(str, CultureInfo.InvariantCulture);
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{str}\")");
+ result.Data.ShouldBe(new[] { n });
+
+ var negativeResult = await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"-{str}\")");
+ negativeResult.Data.ShouldBe(new[] { -n });
+ }
+
+ [Theory]
+ [InlineData("0.12345678901234567890123456789")]// scale == 29 (max possible in .net is 28)
+ [InlineData("0.12345678901234567890123456789012345678")]// scale == 38 (max possible in .net is 28)
+ [InlineData("79228162514264337593543950336")]// max .net decimal + 1
+ [InlineData("-79228162514264337593543950336")]// min .net decimal - 1
+ public async Task DeserializeExtAsDecimal_IncorrectValue_OverflowExceptionThrown(string str)
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+
+ await Assert.ThrowsAsync(async () =>
+ await tarantoolClient.Eval($"local decimal = require(\"decimal\"); return decimal.new(\"{str}\")"));
+ }
+
+ [Fact]
+ public async Task DeserializeBinary8_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval($"local msgpack = require(\"msgpack\"); return msgpack.object_from_raw('\\xc4\\x06foobar')");
+ var expectedByteArray = Encoding.ASCII.GetBytes("foobar");
+ result.Data.ShouldBe(new[] { expectedByteArray });
+ }
+
+ [Fact]
+ public async Task DeserializeBinary16_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var stringLen256 = string.Join("", Enumerable.Range(0, 256).Select(_ => "x"));
+ var result = await tarantoolClient.Eval($"local msgpack = require(\"msgpack\"); return msgpack.object_from_raw('\\xc5\\x01\\x00{stringLen256}')");
+ var expectedByteArray = Encoding.ASCII.GetBytes(stringLen256);
+ result.Data.ShouldBe(new[] { expectedByteArray });
+ }
+
+ [Fact]
+ public async Task DeserializeExtAsGuid_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var guid = new Guid();
+ var result = await tarantoolClient.Eval($"local uuid = require(\"uuid\"); return uuid.fromstr(\"{guid}\")");
+ result.Data.ShouldBe(new[] { guid });
+ }
+
+ [Fact]
+ public async Task DeserializeExtAsDatetime_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var dt = DateTime.UtcNow;
+ var query = $"local dt = require(\"datetime\"); " +
+ $"return dt.new {{ msec = {dt.Millisecond}, sec = {dt.Second}, min = {dt.Minute}, hour = {dt.Hour}, day = {dt.Day}, month = {dt.Month}, year = {dt.Year} }}";
+ // TODO: test tzoffset, nsec/usec additionally
+ var result = await tarantoolClient.Eval(query);
+ result.Data.Length.ShouldBe(1);
+ var actualDt = result.Data[0];
+ actualDt.Date.ShouldBe(dt.Date);
+ actualDt.Hour.ShouldBe(dt.Hour);
+ actualDt.Minute.ShouldBe(dt.Minute);
+ actualDt.Second.ShouldBe(dt.Second);
+ actualDt.Millisecond.ShouldBe(dt.Millisecond);
+
+ var resultOffset = await tarantoolClient.Eval(query);
+ resultOffset.Data.Length.ShouldBe(1);
+ var actualOffset = resultOffset.Data[0];
+ actualOffset.Date.ShouldBe(dt.Date);
+ actualOffset.Hour.ShouldBe(dt.Hour);
+ actualOffset.Minute.ShouldBe(dt.Minute);
+ actualOffset.Second.ShouldBe(dt.Second);
+ actualOffset.Millisecond.ShouldBe(dt.Millisecond);
+ }
+
+ [Fact]
+ public async Task DeserializeIntArray_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var arr = new[] { 1, 2, 3};
+ var result = await tarantoolClient.Eval($"return {{{string.Join(",", arr)}}}");
+ result.Data.ShouldBe(new[] { arr });
+ }
+
+ [Fact]
+ public async Task DeserializeBooleanArray_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var arr = new[] { false, true, false };
+ var result = await tarantoolClient.Eval($"return {{{string.Join(",", arr.Select(x => x.ToString().ToLower()))}}}");
+ result.Data.ShouldBe(new[] { arr });
+ }
+
+ [Fact]
+ public async Task DeserializeFloat64Array_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval("return { math.sqrt(2), math.sqrt(3), math.sqrt(5) }");
+ result.Data.Length.ShouldBe(1);
+ result.Data[0].Length.ShouldBe(3);
+ Math.Abs(result.Data[0][0] - Math.Sqrt(2)).ShouldBeLessThan(double.Epsilon);
+ Math.Abs(result.Data[0][1] - Math.Sqrt(3)).ShouldBeLessThan(double.Epsilon);
+ Math.Abs(result.Data[0][2] - Math.Sqrt(5)).ShouldBeLessThan(double.Epsilon);
+ }
+
+ [Fact]
+ public async Task DeserializeStringArray_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var arr = new[] { "foo", "bar", "foobar" };
+ var result = await tarantoolClient.Eval($"return {{{string.Join(",", arr.Select(x => "'" + x + "'"))}}}");
+ result.Data.ShouldBe(new[] { arr });
+ }
+
+ [Fact]
+ public async Task DeserializeMixedArrayToTuple_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval>("return { 1, true, 'foo'}");
+ result.Data.Length.ShouldBe(1);
+ result.Data[0].Item1.ShouldBe(1);
+ result.Data[0].Item2.ShouldBe(true);
+ result.Data[0].Item3.ShouldBe("foo");
+ }
+
+ [Fact]
+ public async Task DeserializeMapToDictionary_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var expectedDict = new Dictionary()
+ {
+ { "foo", 1 },
+ { "bar", 2 },
+ { "baz", 3 }
+ };
+ var result = await tarantoolClient.Eval>($"a = {{}}; {string.Join("; ", expectedDict.Select(kvp => $"a['{kvp.Key}']={kvp.Value}"))} return a");
+ result.Data.Length.ShouldBe(1);
+ var actualDict = result.Data[0];
+ foreach (var key in expectedDict.Keys) // order doesn't preserve, so we need to check key by key
+ {
+ actualDict.ContainsKey(key).ShouldBeTrue();
+ actualDict[key].ShouldBe(expectedDict[key]);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/progaudi.tarantool.tests/DataTypes/SerializationTests.cs b/tests/progaudi.tarantool.tests/DataTypes/SerializationTests.cs
new file mode 100644
index 00000000..7b44c7df
--- /dev/null
+++ b/tests/progaudi.tarantool.tests/DataTypes/SerializationTests.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using ProGaudi.Tarantool.Client.Model;
+using Shouldly;
+using Xunit;
+
+namespace ProGaudi.Tarantool.Client.Tests.DataTypes
+{
+ ///
+ /// Test suite, where we check correct data types serialization, when we pass values into Eval command,
+ /// and also check that this value deserialize into corresponding C# class/structure correctly
+ ///
+ public class SerializationTests
+ {
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task SerializeBoolean_ShouldBeCorrectAsync(bool val)
+ {
+ await AssertThatYouGetWhatYouGive(val);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ [InlineData(1000000)]
+ public async Task SerializeInt_ShouldBeCorrectAsync(int val)
+ {
+ await AssertThatYouGetWhatYouGive(val);
+ }
+
+ [Theory]
+ [InlineData("test")]
+ public async Task SerializeString_ShouldBeCorrectAsync(string val)
+ {
+ await AssertThatYouGetWhatYouGive(val);
+ }
+
+ [Fact]
+ public async Task SerializeGuid_ShouldBeCorrectAsync()
+ {
+ await AssertThatYouGetWhatYouGive(Guid.NewGuid());
+ }
+
+ [Theory]
+ [InlineData("0")]
+ [InlineData("100500")]
+ [InlineData("0.1234567890123456789012345678")]
+ public async Task SerializeDecimal_ShouldBeCorrectAsync(string val)
+ {
+ var n = decimal.Parse(val, CultureInfo.InvariantCulture);
+ await AssertThatYouGetWhatYouGive(n);
+ await AssertThatYouGetWhatYouGive(-n);
+ }
+
+ [Fact]
+ public async Task SerializeDatetime_ShouldBeCorrectAsync()
+ {
+ var dt = DateTime.UtcNow;
+ await AssertThatYouGetWhatYouGive(dt);
+ await AssertThatYouGetWhatYouGive((DateTimeOffset)dt);
+ }
+
+ [Fact]
+ public async Task SerializeTuple_ShouldBeCorrectAsync()
+ {
+ await AssertThatYouGetWhatYouGive(Tuple.Create(1, true, "test", 1m));
+ }
+
+ [Fact]
+ public async Task SerializeDictionary_ShouldBeCorrectAsync()
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var expectedDict = new Dictionary()
+ {
+ { "foo", 1 },
+ { "bar", 2 },
+ { "baz", 3 }
+ };
+ var result = await tarantoolClient.Eval>, Dictionary>($"return ...", TarantoolTuple.Create(expectedDict));
+
+ result.Data.Length.ShouldBe(1);
+ var actualDict = result.Data[0];
+ foreach (var key in expectedDict.Keys) // order doesn't preserve, so we need to check key by key
+ {
+ actualDict.ContainsKey(key).ShouldBeTrue();
+ actualDict[key].ShouldBe(expectedDict[key]);
+ }
+ }
+
+ private static async Task AssertThatYouGetWhatYouGive(T val)
+ {
+ using var tarantoolClient = await Client.Box.Connect(ConnectionStringFactory.GetLatestTarantoolConnectionString());
+ var result = await tarantoolClient.Eval, T>($"return ...", TarantoolTuple.Create(val));
+ result.Data.Length.ShouldBe(1);
+ result.Data[0].ShouldBe(val);
+ }
+ }
+}