From e2073aa5266c4fdccbaa30b23217b7b0ea119f79 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Wed, 9 Oct 2024 12:44:08 +0200 Subject: [PATCH 1/3] feat: Replace document/ref models with generic ref --- Fauna.Test/E2E/E2ELinq.Test.cs | 97 +++ Fauna.Test/Fauna.Test.csproj | 3 + Fauna.Test/Integration.Tests.cs | 28 +- Fauna.Test/Performance/TestDataModels.cs | 2 +- .../Serialization/Deserializer.Tests.cs | 819 ------------------ Fauna.Test/Serialization/RoundTrip.Tests.cs | 301 ------- Fauna.Test/Serialization/Serializer.Tests.cs | 120 +-- .../Serializers/BaseRefSerializer.Tests.cs | 163 ++++ .../Serializers/ClassSerializer.Tests.cs | 48 +- .../Serializers/DictionarySerializer.Tests.cs | 160 ++++ .../Serializers/DynamicSerializer.Tests.cs | 196 +++++ .../Serializers/ListSerializer.Tests.cs | 15 + .../NullableStructSerializer.Tests.cs | 35 + .../Serializers/PageSerializer.Tests.cs | 67 ++ .../Serializers/SerializerFixtures.cs | 596 ------------- .../Serializers/TypedSerializer.Tests.cs | 605 ++++++++++++- Fauna.Test/Serialization/TestClasses.cs | 32 +- Fauna.Test/Types/Document.Tests.cs | 54 -- Fauna.Test/Types/NamedDocument.Tests.cs | 55 -- Fauna/Mapping/Attributes.cs | 46 +- Fauna/Mapping/FieldInfo.cs | 6 +- Fauna/Mapping/MappingInfo.cs | 26 +- Fauna/Serialization/BaseRefSerializer.cs | 167 ++++ Fauna/Serialization/ClassSerializer.cs | 28 +- Fauna/Serialization/DictionarySerializer.cs | 57 +- Fauna/Serialization/DocumentSerializer.cs | 73 -- Fauna/Serialization/DynamicSerializer.cs | 120 +-- .../IPartialDocumentSerializer.cs | 9 + Fauna/Serialization/InternalDocument.cs | 60 -- .../Serialization/NamedDocumentSerializer.cs | 73 -- Fauna/Serialization/NamedRefSerializer.cs | 77 -- .../NullableDocumentSerializer.cs | 63 -- Fauna/Serialization/RefSerializer.cs | 76 -- Fauna/Serialization/Serializer.cs | 52 +- Fauna/Serialization/Utf8FaunaReader.cs | 4 +- Fauna/Types/BaseDocument.cs | 105 --- Fauna/Types/BaseRef.cs | 82 ++ Fauna/Types/BaseRefBuilder.cs | 33 + Fauna/Types/Document.cs | 37 - Fauna/Types/NamedDocument.cs | 38 - Fauna/Types/NamedRef.cs | 54 +- Fauna/Types/NullableDocument.cs | 73 -- Fauna/Types/Ref.cs | 52 +- README.md | 37 +- 44 files changed, 1983 insertions(+), 2861 deletions(-) create mode 100644 Fauna.Test/E2E/E2ELinq.Test.cs delete mode 100644 Fauna.Test/Serialization/Deserializer.Tests.cs delete mode 100644 Fauna.Test/Serialization/RoundTrip.Tests.cs create mode 100644 Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs create mode 100644 Fauna.Test/Serialization/Serializers/DictionarySerializer.Tests.cs create mode 100644 Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs create mode 100644 Fauna.Test/Serialization/Serializers/NullableStructSerializer.Tests.cs create mode 100644 Fauna.Test/Serialization/Serializers/PageSerializer.Tests.cs delete mode 100644 Fauna.Test/Serialization/Serializers/SerializerFixtures.cs delete mode 100644 Fauna.Test/Types/Document.Tests.cs delete mode 100644 Fauna.Test/Types/NamedDocument.Tests.cs create mode 100644 Fauna/Serialization/BaseRefSerializer.cs delete mode 100644 Fauna/Serialization/DocumentSerializer.cs create mode 100644 Fauna/Serialization/IPartialDocumentSerializer.cs delete mode 100644 Fauna/Serialization/InternalDocument.cs delete mode 100644 Fauna/Serialization/NamedDocumentSerializer.cs delete mode 100644 Fauna/Serialization/NamedRefSerializer.cs delete mode 100644 Fauna/Serialization/NullableDocumentSerializer.cs delete mode 100644 Fauna/Serialization/RefSerializer.cs delete mode 100644 Fauna/Types/BaseDocument.cs create mode 100644 Fauna/Types/BaseRef.cs create mode 100644 Fauna/Types/BaseRefBuilder.cs delete mode 100644 Fauna/Types/Document.cs delete mode 100644 Fauna/Types/NamedDocument.cs delete mode 100644 Fauna/Types/NullableDocument.cs diff --git a/Fauna.Test/E2E/E2ELinq.Test.cs b/Fauna.Test/E2E/E2ELinq.Test.cs new file mode 100644 index 00000000..51268212 --- /dev/null +++ b/Fauna.Test/E2E/E2ELinq.Test.cs @@ -0,0 +1,97 @@ +using System.Diagnostics.CodeAnalysis; +using Fauna.Exceptions; +using Fauna.Linq; +using Fauna.Mapping; +using Fauna.Types; +using NUnit.Framework; +using static Fauna.Query; +using static Fauna.Test.Helpers.TestClientHelper; + +namespace Fauna.Test.E2E; + + +public class E2ELinqTestDb : DataContext +{ + public class E2ELinqTestAuthor + { + [Id] public string? Id { get; set; } + [AllowNull] public string Name { get; set; } + } + + public class E2ELinqTestBook + { + [Id] public string? Id { get; set; } + [AllowNull] public string Name { get; set; } + [AllowNull] public Ref Author { get; set; } + } + + + [Name("E2ELinqTest_Author")] + public class AuthorCol : Collection { } + + [Name("E2ELinqTest_Book")] + public class BookCol : Collection { } + + public AuthorCol Author { get => GetCollection(); } + + public BookCol Book { get => GetCollection(); } +} + +public class E2ELinqTest +{ + private static readonly Client s_client = GetLocalhostClient(); + private static readonly E2ELinqTestDb s_db = s_client.DataContext(); + + [OneTimeSetUp] + public void SetUp() + { + s_db.QueryAsync(FQL($"Collection.byName('E2ELinqTest_Author')?.delete()")).Wait(); + s_db.QueryAsync(FQL($"Collection.byName('E2ELinqTest_Book')?.delete()")).Wait(); + s_db.QueryAsync(FQL($"Collection.create({{name: 'E2ELinqTest_Author'}})")).Wait(); + s_db.QueryAsync(FQL($"Collection.create({{name: 'E2ELinqTest_Book'}})")).Wait(); + var res = s_db.QueryAsync>>(FQL($"E2ELinqTest_Author.create({{name: 'Leo Tolstoy'}})")).Result; + s_db.QueryAsync(FQL($"E2ELinqTest_Book.create({{name: 'War and Peace', author: {res.Data} }})")).Wait(); + } + + [Test] + public async Task E2ELinq_ObtainRefWithoutProjection() + { + var res = await s_db.Book + .Where(b => b.Name == "War and Peace") + .SingleAsync(); + Assert.AreEqual("War and Peace", res.Name); + Assert.IsNotNull(res.Author.Id); + Assert.AreEqual(new Module("E2ELinqTest_Author"), res.Author.Collection); + Assert.IsNull(res.Author.Exists); + Assert.IsFalse(res.Author.IsLoaded); + } + + [Test] + public async Task E2ELinq_ProjectRefIntoDocument() + { + var res = await s_db.Book + .Where(b => b.Name == "War and Peace") + .Select(b => new { b.Name, Author = new { b.Author.Get().Name } }) + .SingleAsync(); + Assert.AreEqual("War and Peace", res.Name); + Assert.AreEqual("Leo Tolstoy", res.Author.Name); + } + + [Test] + public void E2ELinq_ProjectDeletedRefThrows() + { + var aut = s_db.QueryAsync>>(FQL($"E2ELinqTest_Author.create({{name: 'Mary Shelley'}})")).Result; + s_db.QueryAsync(FQL($"E2ELinqTest_Book.create({{name: 'Frankenstein', author: {aut.Data} }})")).Wait(); + s_db.QueryAsync(FQL($"E2ELinqTest_Author.where(a => a.name == 'Mary Shelley').forEach(d => d.delete())")).Wait(); + + var ex = Assert.Throws(() => s_db.Book + .Where(b => b.Name == "Frankenstein") + .Select(b => new { b.Name, Author = new { b.Author.Get().Name } }) + .Single())!; + + Assert.IsNotNull(ex.Id); + Assert.IsNull(ex.Name); + Assert.AreEqual(new Module("E2ELinqTest_Author"), ex.Collection); + Assert.AreEqual("not found", ex.Cause); + } +} diff --git a/Fauna.Test/Fauna.Test.csproj b/Fauna.Test/Fauna.Test.csproj index 34662b73..c23e7c85 100644 --- a/Fauna.Test/Fauna.Test.csproj +++ b/Fauna.Test/Fauna.Test.csproj @@ -27,6 +27,9 @@ setup/fauna/%(Filename)%(Extension) + + + net6.0;net8.0 enable diff --git a/Fauna.Test/Integration.Tests.cs b/Fauna.Test/Integration.Tests.cs index 5d762d03..bcda8fc9 100644 --- a/Fauna.Test/Integration.Tests.cs +++ b/Fauna.Test/Integration.Tests.cs @@ -248,34 +248,22 @@ public async Task Paginate_EmbeddedSetFlattened() } [Test] - public void NullNamedDocumentThrowsNullDocumentException() + public async Task NullNamedDocument() { var q = FQL($"Collection.byName('Fake')"); - var e = Assert.ThrowsAsync(async () => await _client.QueryAsync(q)); + var r = await _client.QueryAsync>>(q); + var d = r.Data; + Assert.AreEqual("Fake", d.Name); + Assert.AreEqual("Collection", d.Collection.Name); + Assert.AreEqual("not found", d.Cause); + + var e = Assert.Throws(() => d.Get()); Assert.NotNull(e); Assert.AreEqual("Fake", e!.Name); Assert.AreEqual("Collection", e.Collection.Name); Assert.AreEqual("not found", e.Cause); } - [Test] - public async Task NullNamedDocument() - { - var q = FQL($"Collection.byName('Fake')"); - var r = await _client.QueryAsync>(q); - switch (r.Data) - { - case NullDocument d: - Assert.AreEqual("Fake", d.Name); - Assert.AreEqual("Collection", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"Expected NullDocument but received {r.Data.GetType()}"); - break; - } - } - [Test] public async Task StatsCollector() { diff --git a/Fauna.Test/Performance/TestDataModels.cs b/Fauna.Test/Performance/TestDataModels.cs index d26dff0e..e21cfc41 100644 --- a/Fauna.Test/Performance/TestDataModels.cs +++ b/Fauna.Test/Performance/TestDataModels.cs @@ -24,7 +24,7 @@ internal class Product public bool InStock { get; init; } = false; [Field] - public Ref? Manufacturer { get; init; } + public Ref? Manufacturer { get; init; } } /// diff --git a/Fauna.Test/Serialization/Deserializer.Tests.cs b/Fauna.Test/Serialization/Deserializer.Tests.cs deleted file mode 100644 index 0432ddb8..00000000 --- a/Fauna.Test/Serialization/Deserializer.Tests.cs +++ /dev/null @@ -1,819 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Serialization; -using Fauna.Types; -using NUnit.Framework; - -namespace Fauna.Test.Serialization; - -[TestFixture] -public class DeserializerTests -{ - private readonly MappingContext ctx; - public DeserializerTests() - { - var colls = new Dictionary { - { "MappedColl", typeof(ClassForDocument) } - }; - ctx = new(colls); - } - - public object? Deserialize(string str) => - DeserializeImpl(str, ctx => Serializer.Dynamic); - - public T Deserialize(string str) where T : notnull => - DeserializeImpl(str, ctx => Serializer.Generate(ctx)); - - public T? DeserializeNullable(string str) => - DeserializeImpl(str, ctx => Serializer.GenerateNullable(ctx)); - - public T DeserializeImpl(string str, Func> deserFunc) - { - var reader = new Utf8FaunaReader(str); - reader.Read(); - var deser = deserFunc(ctx); - var obj = deser.Deserialize(ctx, ref reader); - - if (reader.Read()) - { - throw new SerializationException($"Token stream is not exhausted but should be: {reader.CurrentTokenType}"); - } - - return obj; - } - - private void RunTestCases(Dictionary cases) where T : notnull - { - foreach (KeyValuePair entry in cases) - { - var result = Deserialize(entry.Key); - Assert.AreEqual(entry.Value, result); - } - } - - [Test] - public void CastDeserializer() - { - var deser = Serializer.Generate(ctx); - // should cast w/o failing due to covariance. - var obj = (ISerializer)deser; - - Assert.AreEqual(deser, obj); - } - - [Test] - public void DeserializeValuesDynamic() - { - var tests = new Dictionary - { - {"\"hello\"", "hello"}, - {@"{""@int"":""42""}", 42}, - {@"{""@long"":""42""}", 42L}, - {@"{""@double"": ""1.2""}", 1.2d}, - {@"{""@date"": ""2023-12-03""}", new DateOnly(2023, 12, 3)}, - {@"{""@time"": ""2023-12-03T05:52:10.000001-09:00""}", new DateTime(2023, 12, 3, 14, 52, 10, 0, DateTimeKind.Utc).AddTicks(10).ToLocalTime()}, - {"true", true}, - {"false", false}, - {"null", null}, - }; - - foreach (KeyValuePair entry in tests) - { - var result = Deserialize(entry.Key); - Assert.AreEqual(entry.Value, result); - } - } - - [Test] - public void DeserializeNullableGeneric() - { - var result = DeserializeNullable("null"); - Assert.IsNull(result); - } - - [Test] - public void DeserializeDocument() - { - const string given = @" - { - ""@doc"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""name"":""name_value"" - } - }"; - - var actual = Deserialize(given); - switch (actual) - { - case Document d: - Assert.AreEqual("123", d.Id); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), d.Ts); - Assert.AreEqual("name_value", d["name"]); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullDocument() - { - const string given = @" - { - ""@ref"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var actual = Deserialize(given); - switch (actual) - { - case NullDocument d: - Assert.AreEqual("123", d.Id); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeDocumentGeneric() - { - const string given = @" - { - ""@doc"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""name"":""name_value"" - } - }"; - - var actual = Deserialize(given); - Assert.AreEqual("123", actual.Id); - Assert.AreEqual(new Module("MyColl"), actual.Collection); - Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), actual.Ts); - Assert.AreEqual("name_value", actual["name"]); - } - - [Test] - public void DeserializeNonNullDocumentGeneric() - { - const string given = @" - { - ""@doc"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""name"":""name_value"" - } - }"; - - var actual = Deserialize>(given); - switch (actual) - { - case NonNullDocument d: - Assert.AreEqual("123", d.Value!.Id); - Assert.AreEqual(new Module("MyColl"), d.Value!.Collection); - Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), d.Value!.Ts); - Assert.AreEqual("name_value", d.Value!["name"]); - break; - default: - Assert.Fail($"result is type: {actual.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullDocumentGeneric() - { - const string given = @" - { - ""@ref"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var actual = Deserialize>(given); - - switch (actual) - { - case NullDocument d: - Assert.AreEqual("123", d.Id); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"result is type: {actual.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullDocumentGenericThrowsWithoutWrapper() - { - const string given = @" - { - ""@ref"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var e = Assert.Throws(() => Deserialize(given)); - Assert.NotNull(e); - Assert.AreEqual("123", e!.Id); - Assert.AreEqual("MyColl", e.Collection.Name); - Assert.AreEqual("not found", e.Cause); - Assert.AreEqual("Document 123 in collection MyColl is null: not found", e.Message); - } - - [Test] - public void DeserializeDocumentToNonNullDocumentClass() - { - const string given = @" - { - ""@doc"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - var actual = Deserialize>(given); - switch (actual) - { - case NonNullDocument d: - Assert.AreEqual("123", d.Value!.Id); - Assert.AreEqual("user_value", d.Value!.UserField); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeDocumentToNullDocumentClass() - { - const string given = @" - { - ""@ref"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var actual = Deserialize>(given); - switch (actual) - { - case NullDocument d: - Assert.AreEqual("123", d.Id); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullDocumentClassThrowsWithoutWrapper() - { - const string given = @" - { - ""@ref"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var e = Assert.Throws(() => Deserialize(given)); - Assert.NotNull(e); - Assert.AreEqual("123", e!.Id); - Assert.AreEqual("MyColl", e.Collection.Name); - Assert.AreEqual("not found", e.Cause); - Assert.AreEqual("Document 123 in collection MyColl is null: not found", e.Message); - } - - [Test] - public void DeserializeDocumentToRegisteredClass() - { - const string given = @" - { - ""@doc"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MappedColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - if (Deserialize(given) is ClassForDocument actual) - { - Assert.AreEqual("user_value", actual.UserField); - Assert.AreEqual("123", actual.Id); - } - else - { - Assert.Fail(); - } - } - - [Test] - public void DeserializeRegisteredClassToDocument() - { - const string given = @" - { - ""@doc"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MappedColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - var actual = Deserialize(given); - Assert.AreEqual("123", actual.Id); - Assert.AreEqual(new Module("MappedColl"), actual.Collection); - Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), actual.Ts); - Assert.AreEqual("user_value", actual["user_field"]); - } - - [Test] - public void DeserializeNamedDocumentUnchecked() - { - const string given = @" - { - ""@doc"":{ - ""name"":""DocName"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - var actual = Deserialize(given); - switch (actual) - { - case NamedDocument d: - Assert.AreEqual("DocName", d.Name); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), d.Ts); - Assert.AreEqual("user_value", d["user_field"]); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullNamedDocumentUnchecked() - { - const string given = @" - { - ""@ref"":{ - ""name"":""RefName"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var actual = Deserialize(given); - switch (actual) - { - case NullDocument d: - Assert.AreEqual("RefName", d.Name); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNamedDocumentChecked() - { - const string given = @" - { - ""@doc"":{ - ""name"":""DocName"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - var actual = Deserialize(given); - Assert.AreEqual("DocName", actual.Name); - Assert.AreEqual(new Module("MyColl"), actual.Collection); - Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), actual.Ts); - Assert.AreEqual("user_value", actual["user_field"]); - } - - [Test] - public void DeserializeNullNamedDocumentChecked() - { - const string given = @" - { - ""@ref"":{ - ""name"":""RefName"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var actual = Deserialize>(given); - - switch (actual) - { - case NullDocument d: - Assert.AreEqual("RefName", d.Name); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"result is type: {actual.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullNamedDocumentThrowsWithoutWrapper() - { - const string given = @" - { - ""@ref"":{ - ""name"":""RefName"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var e = Assert.Throws(() => Deserialize(given)); - Assert.NotNull(e); - Assert.AreEqual("RefName", e!.Name); - Assert.AreEqual("MyColl", e.Collection.Name); - Assert.AreEqual("not found", e.Cause); - Assert.AreEqual("Document RefName in collection MyColl is null: not found", e.Message); - } - - [Test] - public void DeserializeNamedDocumentToClass() - { - const string given = @" - { - ""@doc"":{ - ""name"":""DocName"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - var actual = Deserialize(given); - Assert.AreEqual("DocName", actual.Name); - Assert.AreEqual("user_value", actual.UserField); - } - - [Test] - public void DeserializeNonNullNamedDocumentToClass() - { - const string given = @" - { - ""@doc"":{ - ""name"":""DocName"", - ""coll"":{""@mod"":""MyColl""}, - ""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""}, - ""user_field"":""user_value"" - } - }"; - - var actual = Deserialize>(given); - switch (actual) - { - case NonNullDocument d: - Assert.AreEqual("DocName", d.Value!.Name); - Assert.AreEqual("user_value", d.Value!.UserField); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullNamedDocumentToClass() - { - const string given = @" - { - ""@ref"":{ - ""name"":""RefName"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var actual = Deserialize>(given); - switch (actual) - { - case NullDocument d: - Assert.AreEqual("RefName", d.Name); - Assert.AreEqual("MyColl", d.Collection.Name); - Assert.AreEqual("not found", d.Cause); - break; - default: - Assert.Fail($"result is type: {actual?.GetType()}"); - break; - } - } - - [Test] - public void DeserializeNullNamedDocumentClassThrowsWithoutWrapper() - { - const string given = @" - { - ""@ref"":{ - ""name"":""RefName"", - ""coll"":{""@mod"":""MyColl""}, - ""exists"":false, - ""cause"":""not found"" - } - }"; - - var e = Assert.Throws(() => Deserialize(given)); - Assert.NotNull(e); - Assert.AreEqual("RefName", e!.Name); - Assert.AreEqual("MyColl", e.Collection.Name); - Assert.AreEqual("not found", e.Cause); - Assert.AreEqual("Document RefName in collection MyColl is null: not found", e.Message); - } - - [Test] - public void DeserializeRef() - { - const string given = @" - { - ""@ref"":{ - ""id"":""123"", - ""coll"":{""@mod"":""MyColl""} - } - }"; - - var actual = Deserialize(given); - Assert.AreEqual("123", actual.Id); - Assert.AreEqual(new Module("MyColl"), actual.Collection); - } - - [Test] - public void DeserializeNamedRef() - { - const string given = @" - { - ""@ref"":{ - ""name"":""RefName"", - ""coll"":{""@mod"":""MyColl""} - } - }"; - - var actual = Deserialize(given); - Assert.AreEqual("RefName", actual.Name); - Assert.AreEqual(new Module("MyColl"), actual.Collection); - } - - [Test] - public void DeserializeObject() - { - const string given = @" - { - ""aString"": ""foo"", - ""anObject"": { ""baz"": ""luhrmann"" }, - ""anInt"": { ""@int"": ""2147483647"" }, - ""aLong"":{ ""@long"": ""9223372036854775807"" }, - ""aDouble"":{ ""@double"": ""3.14159"" }, - ""aDate"":{ ""@date"": ""2023-12-03"" }, - ""aTime"":{ ""@time"": ""2023-12-03T14:52:10.001001Z"" }, - ""true"": true, - ""false"": false, - ""null"": null - } - "; - - var inner = new Dictionary - { - { "baz", "luhrmann" } - }; - - var expected = new Dictionary - { - { "aString", "foo" }, - { "anObject", inner }, - { "anInt", 2147483647 }, - { "aLong", 9223372036854775807 }, - { "aDouble", 3.14159d }, - { "aDate", new DateOnly(2023, 12, 3) }, - { "aTime", new DateTime(2023, 12, 3, 14, 52, 10, 1, DateTimeKind.Utc).AddTicks(10).ToLocalTime() }, - { "true", true }, - { "false", false }, - { "null", null } - }; - - var result = Deserialize(given); - Assert.AreEqual(expected, result); - } - - [Test] - public void DeserializeEscapedObject() - { - const string given = @" - { - ""@object"": { - ""@int"": ""notanint"", - ""anInt"": { ""@int"": ""123"" }, - ""@object"": ""notanobject"", - ""anEscapedObject"": { ""@object"": { ""@long"": ""notalong"" } } - } - } - "; - - var inner = new Dictionary - { - { "@long", "notalong" } - }; - - var expected = new Dictionary - { - { "@int", "notanint" }, - { "anInt", 123 }, - { "@object", "notanobject" }, - { "anEscapedObject", inner } - - }; - - var result = Deserialize(given); - Assert.AreEqual(expected, result); - } - - [Test] - public void DeserializeIntoGenericDictionary() - { - const string given = @"{ -""k1"": { ""@int"": ""1"" }, -""k2"": { ""@int"": ""2"" }, -""k3"": { ""@int"": ""3"" } -}"; - var expected = new Dictionary() - { - {"k1", 1}, - {"k2", 2}, - {"k3", 3} - }; - var actual = Deserialize>(given); - Assert.AreEqual(expected, actual); - } - - [Test] - public void DeserializeIntoPoco() - { - - const string given = @" - { - ""firstName"": ""Baz2"", - ""lastName"": ""Luhrmann2"", - ""age"": { ""@int"": ""612"" } - } - "; - - var p = Deserialize(given); - Assert.AreEqual("Baz2", p.FirstName); - Assert.AreEqual("Luhrmann2", p.LastName); - Assert.AreEqual(612, p.Age); - } - - [Test] - public void DeserializeIntoPocoWithAttributes() - { - const string given = @" - { - ""first_name"": ""Baz2"", - ""last_name"": ""Luhrmann2"", - ""age"": { ""@int"": ""612"" }, - ""ignored"": ""should be null"" - } - "; - - var p = Deserialize(given); - Assert.AreEqual("Baz2", p.FirstName); - Assert.AreEqual("Luhrmann2", p.LastName); - Assert.AreEqual(612, p.Age); - Assert.IsNull(p.Ignored); - } - - [Test] - public void DeserializeIntoPageWithPrimitive() - { - const string given = @"{ - ""@set"": { - ""after"": ""next_page_cursor"", - ""data"": [ - {""@int"":""1""}, - {""@int"":""2""}, - {""@int"":""3""} - ] - } - }"; - - var result = Deserialize>(given); - Assert.IsNotNull(result); - Assert.AreEqual(new List { 1, 2, 3 }, result.Data); - Assert.AreEqual("next_page_cursor", result.After); - } - - [Test] - public void DeserializeIntoPageWithSingleValue() - { - const string given = @"""SingleValue"""; - - var result = Deserialize>(given); - Assert.IsNotNull(result); - Assert.AreEqual(new List { "SingleValue" }, result.Data); - Assert.IsNull(result.After); - } - - [Test] - public void DeserializeIntoPageWithUserDefinedClass() - { - const string given = @"{ - ""@set"": { - ""after"": ""next_page_cursor"", - ""data"": [ - {""first_name"":""Alice"",""last_name"":""Smith"",""age"":{""@int"":""30""}}, - {""first_name"":""Bob"",""last_name"":""Jones"",""age"":{""@int"":""40""}} - ] - } - }"; - - var result = Deserialize>(given); - Assert.IsNotNull(result); - Assert.AreEqual(2, result.Data.Count); - Assert.AreEqual("Alice", result.Data[0].FirstName); - Assert.AreEqual("Smith", result.Data[0].LastName); - Assert.AreEqual(30, result.Data[0].Age); - Assert.AreEqual("Bob", result.Data[1].FirstName); - Assert.AreEqual("Jones", result.Data[1].LastName); - Assert.AreEqual(40, result.Data[1].Age); - Assert.AreEqual("next_page_cursor", result.After); - } - - [Test] - public void DeserializeNullableStructAsValue() - { - const string given = @"{ - ""val"":{""@int"":""42""} - }"; - - var result = Deserialize(given); - Assert.IsNotNull(result); - Assert.AreEqual(42, result.Val); - } - - [Test] - public void DeserializeNullableStructAsNull() - { - const string given = @"{ - ""val"": null - }"; - - var result = Deserialize(given); - Assert.IsNotNull(result); - Assert.AreEqual(null, result.Val); - } -} diff --git a/Fauna.Test/Serialization/RoundTrip.Tests.cs b/Fauna.Test/Serialization/RoundTrip.Tests.cs deleted file mode 100644 index 03be8cf8..00000000 --- a/Fauna.Test/Serialization/RoundTrip.Tests.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System.Text; -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Serialization; -using Fauna.Types; -using NUnit.Framework; - -namespace Fauna.Test.Serialization; - -[TestFixture] -public class RoundTripTests -{ - private static readonly MappingContext ctx = new(); - private const string IntWire = @"{""@int"":""42""}"; - private const string LongWire = @"{""@long"":""42""}"; - private const string DoubleWire = @"{""@double"":""42""}"; - private const string DoubleWithDecimalWire = @"{""@double"":""42.2""}"; - private const string TrueWire = "true"; - private const string FalseWire = "false"; - private const string DateTimeWire = @"{""@time"":""2023-12-15T01:01:01.0010011Z""}"; - private const string DateWire = @"{""@date"":""2023-12-15""}"; - private const string ModuleWire = @"{""@mod"":""Foo""}"; - private const string DocumentWire = @"{""@doc"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; - private const string DocumentRefWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""}}}"; - private const string NullDocumentWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; - private const string NamedDocumentWire = @"{""@doc"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; - private const string NullNamedDocumentWire = @"{""@ref"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; - private const string NamedDocumentRefWire = @"{""@ref"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""}}}"; - private const string ObjectWithShortWire = @"{""a_short"":{""@int"":""42""}}"; - - public static string Serialize(object? obj) - { - using var stream = new MemoryStream(); - using var writer = new Utf8FaunaWriter(stream); - - ISerializer ser = DynamicSerializer.Singleton; - if (obj is not null) ser = Serializer.Generate(ctx, obj.GetType()); - ser.Serialize(ctx, writer, obj); - - writer.Flush(); - return Encoding.UTF8.GetString(stream.ToArray()); - } - - public static T Deserialize(string str) where T : notnull - { - var reader = new Utf8FaunaReader(str); - reader.Read(); - var obj = Serializer.Generate(ctx).Deserialize(ctx, ref reader); - if (reader.Read()) - { - throw new SerializationException($"Token stream is not exhausted but should be: {reader.CurrentTokenType}"); - } - - return obj; - } - - [Test] - public void RoundTripByte() - { - var deserialized = Deserialize(IntWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(IntWire, serialized); - } - - [Test] - public void RoundTripSByte() - { - var deserialized = Deserialize(IntWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(IntWire, serialized); - } - - [Test] - public void RoundTripShort() - { - var deserialized = Deserialize(IntWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(IntWire, serialized); - } - - [Test] - public void RoundTripUShort() - { - var deserialized = Deserialize(IntWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(IntWire, serialized); - } - - [Test] - public void RoundTripInt() - { - var deserialized = Deserialize(IntWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(IntWire, serialized); - } - - [Test] - public void RoundTripUInt() - { - var deserialized = Deserialize(LongWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(LongWire, serialized); - } - - [Test] - public void RoundTripLong() - { - var deserialized = Deserialize(LongWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(LongWire, serialized); - } - - [Test] - public void RoundTripFloat() - { - var deserialized = Deserialize(DoubleWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DoubleWire, serialized); - } - - [Test] - public void RoundTripDouble() - { - var deserialized = Deserialize(DoubleWithDecimalWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DoubleWithDecimalWire, serialized); - } - - [Test] - public void RoundTripTrue() - { - var deserialized = Deserialize(TrueWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(TrueWire, serialized); - } - - [Test] - public void RoundTripFalse() - { - var deserialized = Deserialize(FalseWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(FalseWire, serialized); - } - - [Test] - public void RoundTripDateTime() - { - var deserialized = Deserialize(DateTimeWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DateTimeWire, serialized); - } - - [Test] - public void RoundTripDateOnly() - { - var deserialized = Deserialize(DateWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DateWire, serialized); - } - - [Test] - public void RoundTripModule() - { - var deserialized = Deserialize(ModuleWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(ModuleWire, serialized); - } - - [Test] - public void RoundTripClassWithShort() - { - var deserialized = Deserialize(ObjectWithShortWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(ObjectWithShortWire, serialized); - } - - [Test] - public void RoundTripClassAsDocumentIsNotSupported() - { - var deserialized = Deserialize(DocumentWire); - string serialized = Serialize(deserialized); - Assert.AreNotEqual(DocumentWire, serialized); - } - - [Test] - public void RoundTripDocumentRef() - { - var deserialized = Deserialize(DocumentRefWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DocumentRefWire, serialized); - } - - [Test] - public void RoundTripNonNullDocumentRef() - { - var deserialized = Deserialize>(DocumentRefWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DocumentRefWire, serialized); - } - - [Test] - public void RoundTripNullDocumentRefChangesToDocumentRef() - { - var deserialized = Deserialize>(NullDocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DocumentRefWire, serialized); - } - - [Test] - public void RoundTripDocumentChangesToDocumentReference() - { - var deserialized = Deserialize(DocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DocumentRefWire, serialized); - } - - [Test] - public void RoundTripNonNullDocumentChangesToDocumentReference() - { - var deserialized = Deserialize>(DocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DocumentRefWire, serialized); - } - - [Test] - public void RoundTripNullDocumentChangesToDocumentReference() - { - var deserialized = Deserialize>(NullDocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(DocumentRefWire, serialized); - } - - [Test] - public void RoundTripNamedDocumentRef() - { - var deserialized = Deserialize(NamedDocumentRefWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(NamedDocumentRefWire, serialized); - } - - [Test] - public void RoundTripNonNullNamedDocumentRef() - { - var deserialized = Deserialize>(NamedDocumentRefWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(NamedDocumentRefWire, serialized); - } - - [Test] - public void RoundTripNullNamedDocumentRefChangesToNamedDocumentRef() - { - var deserialized = Deserialize>(NullNamedDocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(NamedDocumentRefWire, serialized); - } - - [Test] - public void RoundTripNamedDocumentChangesToNamedDocumentReference() - { - var deserialized = Deserialize(NamedDocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(NamedDocumentRefWire, serialized); - } - - [Test] - public void RoundTripNonNullNamedDocumentChangesToNamedDocumentReference() - { - var deserialized = Deserialize>(NamedDocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(NamedDocumentRefWire, serialized); - } - - [Test] - public void RoundTripNullNamedDocumentChangesToNamedDocumentReference() - { - var deserialized = Deserialize>(NullNamedDocumentWire); - var serialized = Serialize(deserialized); - Assert.AreEqual(NamedDocumentRefWire, serialized); - } - - [Test] - public void RegisterDeregisterCustomSerializer() - { - var s = new IntToStringSerializer(); - Serializer.Register(s); - - const int i = 42; - var ser = Serialize(i); - Assert.AreEqual(@"""42""", ser); - - var deser = Deserialize(ser); - Assert.AreEqual(i, deser); - - Serializer.Deregister(typeof(int)); - ser = Serialize(i); - - Assert.AreEqual(@"{""@int"":""42""}", ser); - deser = Deserialize(ser); - Assert.AreEqual(i, deser); - } -} diff --git a/Fauna.Test/Serialization/Serializer.Tests.cs b/Fauna.Test/Serialization/Serializer.Tests.cs index f7ee1e76..5036ec84 100644 --- a/Fauna.Test/Serialization/Serializer.Tests.cs +++ b/Fauna.Test/Serialization/Serializer.Tests.cs @@ -1,15 +1,14 @@ using System.Text; +using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; -using Fauna.Types; using NUnit.Framework; namespace Fauna.Test.Serialization; -[TestFixture] -public class SerializerTests +public class SeralizerTests { - private static readonly MappingContext ctx = new(); + private static readonly MappingContext s_ctx = new(); public static string Serialize(object? obj) { @@ -17,114 +16,45 @@ public static string Serialize(object? obj) using var writer = new Utf8FaunaWriter(stream); ISerializer ser = DynamicSerializer.Singleton; - if (obj is not null) ser = Serializer.Generate(ctx, obj.GetType()); - ser.Serialize(ctx, writer, obj); + if (obj is not null) ser = Serializer.Generate(s_ctx, obj.GetType()); + ser.Serialize(s_ctx, writer, obj); writer.Flush(); return Encoding.UTF8.GetString(stream.ToArray()); } - [Test] - public void SerializeValues() + public static T Deserialize(string str) where T : notnull { - var dt = new DateTime(2023, 12, 13, 12, 12, 12, 1, DateTimeKind.Utc).AddTicks(10); - - var tests = new Dictionary - { - {"null", null}, - {@"{""@mod"":""module""}", new Module("module")}, - {@"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""ACollection""}}}", new Ref("123", new Module("ACollection"))}, - {@"{""@ref"":{""id"":""124"",""coll"":{""@mod"":""ACollection""}}}", new Document("124", new Module("ACollection"), DateTime.Now)} - }; - - foreach (var (expected, test) in tests) + var reader = new Utf8FaunaReader(str); + reader.Read(); + var obj = Serializer.Generate(s_ctx).Deserialize(s_ctx, ref reader); + if (reader.Read()) { - var result = Serialize(test); - Assert.AreEqual(expected, result); + throw new SerializationException($"Token stream is not exhausted but should be: {reader.CurrentTokenType}"); } - } - - [Test] - public void SerializeDictionary() - { - var test = new Dictionary() - { - { "answer", 42 }, - { "foo", "bar" }, - { "list", new List()}, - { "obj", new Dictionary()} - - }; - var actual = Serialize(test); - Assert.AreEqual(@"{""answer"":{""@int"":""42""},""foo"":""bar"",""list"":[],""obj"":{}}", actual); + return obj; } - [Test] - public void SerializeDictionaryWithTagConflicts() - { - var tests = new Dictionary, string>() - { - { new() { { "@date", "not" } }, @"{""@object"":{""@date"":""not""}}" }, - { new() { { "@doc", "not" } }, @"{""@object"":{""@doc"":""not""}}" }, - { new() { { "@double", "not" } }, @"{""@object"":{""@double"":""not""}}" }, - { new() { { "@int", "not" } }, @"{""@object"":{""@int"":""not""}}" }, - { new() { { "@long", "not" } }, @"{""@object"":{""@long"":""not""}}" }, - { new() { { "@mod", "not" } }, @"{""@object"":{""@mod"":""not""}}" }, - { new() { { "@object", "not" } }, @"{""@object"":{""@object"":""not""}}" }, - { new() { { "@ref", "not" } }, @"{""@object"":{""@ref"":""not""}}" }, - { new() { { "@set", "not" } }, @"{""@object"":{""@set"":""not""}}" }, - { new() { { "@time", "not" } }, @"{""@object"":{""@time"":""not""}}" } - }; - - foreach (var (test, expected) in tests) - { - var actual = Serialize(test); - Assert.AreEqual(expected, actual); - } - } [Test] - public void SerializeAnonymousClassObject() + public void RegisterDeregisterCustomSerializer() { - var obj = new { FirstName = "John", LastName = "Doe" }; - var expected = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}"; - var actual = Serialize(obj); - Assert.AreEqual(expected, actual); - } + var s = new IntToStringSerializer(); + Serializer.Register(s); - [Test] - public void SerializeEmptyCollections() - { - var tests = new Dictionary - { - { new List(), "[]" }, - { new Dictionary(), "{}" }, - { new List { new List(), new Dictionary() }, "[[],{}]" } - }; + const int i = 42; + string ser = Serialize(i); + Assert.AreEqual(@"""42""", ser); - foreach (var (value, expected) in tests) - { - var actual = Serialize(value); - Assert.AreEqual(expected, actual); - } - } + int deser = Deserialize(ser); + Assert.AreEqual(i, deser); - [Test] - public void SerializeNullableStructAsNull() - { - var i = new int?(); - - var actual = Serialize(i); - Assert.AreEqual("null", actual); - } - - [Test] - public void SerializeNullableStructAsValue() - { - var i = new int?(42); + Serializer.Deregister(typeof(int)); + ser = Serialize(i); - var actual = Serialize(i); - Assert.AreEqual(@"{""@int"":""42""}", actual); + Assert.AreEqual(@"{""@int"":""42""}", ser); + deser = Deserialize(ser); + Assert.AreEqual(i, deser); } } diff --git a/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs new file mode 100644 index 00000000..beeea116 --- /dev/null +++ b/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs @@ -0,0 +1,163 @@ + +using Fauna.Exceptions; +using Fauna.Mapping; +using Fauna.Serialization; +using Fauna.Types; +using NUnit.Framework; + +namespace Fauna.Test.Serialization; + +public class BaseRefSerializerTests +{ + private readonly MappingContext _ctx; + private const string DocumentWire = @"{""@doc"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + private const string MappedDocumentWire = @"{""@doc"":{""id"":""123"",""coll"":{""@mod"":""MappedColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + private const string DocumentRefWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""}}}"; + private const string NullDocumentWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; + private const string NamedDocumentWire = @"{""@doc"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + private const string NullNamedDocumentWire = @"{""@ref"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; + private const string NamedDocumentRefWire = @"{""@ref"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""}}}"; + + public BaseRefSerializerTests() + { + var collections = new Dictionary { + { "MappedColl", typeof(ClassForDocument) } + }; + _ctx = new MappingContext(collections); + } + + [Test] + public void RoundTripRef() + { + var serializer = Serializer.Generate>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, DocumentRefWire)!; + Assert.AreEqual("123", deserialized.Id); + Assert.AreEqual("MyColl", deserialized.Collection.Name); + + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(DocumentRefWire, serialized); + } + + + [Test] + public void RoundTripDocument() + { + var serializer = Serializer.Generate>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, DocumentWire)!; + Assert.AreEqual("123", deserialized.Id); + Assert.AreEqual("MyColl", deserialized.Collection.Name); + + var doc = deserialized.Get(); + Assert.AreEqual("123", doc.Id); + Assert.AreEqual("MyColl", doc.Coll?.Name); + Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), doc.Ts); + Assert.AreEqual("user_value", doc.UserField); + + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(DocumentRefWire, serialized); + } + + [Test] + public void RoundTripNullDoc() + { + var serializer = Serializer.Generate>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, NullDocumentWire)!; + Assert.AreEqual("123", deserialized.Id); + Assert.AreEqual("MyColl", deserialized.Collection.Name); + Assert.IsFalse(deserialized.Exists); + Assert.AreEqual("not found", deserialized.Cause); + + var ex = Assert.Throws(() => deserialized.Get())!; + Assert.AreEqual("123", ex.Id); + Assert.AreEqual("MyColl", ex.Collection.Name); + Assert.AreEqual("not found", ex.Cause); + + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(DocumentRefWire, serialized); + } + + [Test] + public void RoundTripNamedRef() + { + var serializer = Serializer.Generate>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, NamedDocumentRefWire)!; + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(NamedDocumentRefWire, serialized); + } + + [Test] + public void RoundTripNamedDocument() + { + var serializer = Serializer.Generate>>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, NamedDocumentWire)!; + Assert.AreEqual("Foo", deserialized.Name); + Assert.AreEqual("MyColl", deserialized.Collection.Name); + + var doc = deserialized.Get(); + Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), doc["ts"]); + Assert.AreEqual("user_value", doc["user_field"]); + + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(NamedDocumentRefWire, serialized); + } + + [Test] + public void RoundTripNamedDocumentAsClass() + { + var serializer = Serializer.Generate>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, NamedDocumentWire)!; + Assert.AreEqual("Foo", deserialized.Name); + Assert.AreEqual("MyColl", deserialized.Collection.Name); + + var doc = deserialized.Get(); + Assert.AreEqual("Foo", doc.Name); + Assert.AreEqual("user_value", doc.UserField); + + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(NamedDocumentRefWire, serialized); + } + + [Test] + public void RoundTripNullNamedDoc() + { + var serializer = Serializer.Generate>(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, NullNamedDocumentWire)!; + Assert.AreEqual("Foo", deserialized.Name); + Assert.AreEqual("MyColl", deserialized.Collection.Name); + Assert.IsFalse(deserialized.Exists); + Assert.AreEqual("not found", deserialized.Cause); + + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreEqual(NamedDocumentRefWire, serialized); + } + + [Test] + public void DeserializeRegisteredClassToDictionary() + { + var serializer = Serializer.Generate>>(_ctx); + + var actual = Helpers.Deserialize(serializer, _ctx, MappedDocumentWire)!; + Assert.AreEqual("123", actual.Id); + Assert.AreEqual(new Module("MappedColl"), actual.Collection); + + var doc = actual.Get(); + Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), doc["ts"]); + Assert.AreEqual("user_value", doc["user_field"]); + } + + [Test] + public void DeserializeDocumentToDictionary() + { + var serializer = Serializer.Generate>>(_ctx); + var actual = Helpers.Deserialize(serializer, _ctx, DocumentWire)!; + + Assert.AreEqual("123", actual.Id); + Assert.AreEqual(new Module("MyColl"), actual.Collection); + + var dict = actual.Get(); + Assert.AreEqual("123", dict["id"]); + Assert.AreEqual(new Module("MyColl"), dict["coll"]); + Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), dict["ts"]); + Assert.AreEqual("user_value", dict["user_field"]); + } +} diff --git a/Fauna.Test/Serialization/Serializers/ClassSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/ClassSerializer.Tests.cs index f06cb277..183a8172 100644 --- a/Fauna.Test/Serialization/Serializers/ClassSerializer.Tests.cs +++ b/Fauna.Test/Serialization/Serializers/ClassSerializer.Tests.cs @@ -1,3 +1,4 @@ +using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; using Fauna.Types; @@ -8,6 +9,8 @@ namespace Fauna.Test.Serialization; public class ClassSerializerTests { private readonly MappingContext _ctx; + private const string DocumentWire = @"{""@doc"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + private const string NullDocumentWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; public ClassSerializerTests() { @@ -18,7 +21,7 @@ public ClassSerializerTests() } [Test] - public void RoundTripListClassWithoutAttributes() + public void RoundTripClassWithoutAttributes() { var serializer = Serializer.Generate(_ctx); @@ -111,6 +114,26 @@ public void SerializeMappedClassDoesNotSkipClientGeneratedId() Assert.AreEqual(wire, serialized); } + [Test] + public void DeserializeClassWithSpecialIdAndCollName() + { + var serializer = Serializer.Generate(_ctx); + var expected = new ClassForDocumentWithSpecialNames + { + TheId = "123", + TheCollection = new Module("MyColl"), + TheTs = DateTime.Parse("2023-12-15T01:01:01.0010010Z"), + UserField = "user_value" + }; + + var actual = Helpers.Deserialize(serializer, _ctx, DocumentWire); + Assert.NotNull(actual); + Assert.AreEqual(expected.TheId, actual!.TheId); + Assert.AreEqual(expected.TheCollection, actual.TheCollection); + Assert.AreEqual(expected.TheTs, actual.TheTs); + Assert.AreEqual(expected.UserField, actual.UserField); + } + [Test] public void SerializeUnmappedClassSkipsIdCollTs() { @@ -149,13 +172,34 @@ public void InvalidFieldNamesThrowsArgumentException(string classWithBadFields) { var badClass = Type.GetType(classWithBadFields); Assert.IsNotNull(badClass, $"Couldn't find Type from class name: {classWithBadFields}"); - var serializer = Serializer.Generate(_ctx, badClass!); + Serializer.Generate(_ctx, badClass!); }); Assert.IsNotNull(ex); Assert.IsTrue(ex!.Message.Contains("Duplicate field name")); } + [Test] + public void DeserializeNullDocumentClassThrowsWithoutWrapper() + { + var serializer = Serializer.Generate(_ctx); + var e = Assert.Throws(() => Helpers.Deserialize(serializer, _ctx, NullDocumentWire)); + Assert.NotNull(e); + Assert.AreEqual("123", e!.Id); + Assert.AreEqual("MyColl", e.Collection.Name); + Assert.AreEqual("not found", e.Cause); + Assert.AreEqual("Document 123 in collection MyColl is null: not found", e.Message); + } + + [Test] + public void RoundTripClassAsDocumentIsNotSupported() + { + var serializer = Serializer.Generate(_ctx); + var deserialized = Helpers.Deserialize(serializer, _ctx, DocumentWire); + string serialized = Helpers.Serialize(serializer, _ctx, deserialized); + Assert.AreNotEqual(DocumentWire, serialized); + } + [Test] public void ValidateFieldInfoOnSerializerCtx() { diff --git a/Fauna.Test/Serialization/Serializers/DictionarySerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/DictionarySerializer.Tests.cs new file mode 100644 index 00000000..2908402e --- /dev/null +++ b/Fauna.Test/Serialization/Serializers/DictionarySerializer.Tests.cs @@ -0,0 +1,160 @@ +using System.Text.RegularExpressions; +using Fauna.Mapping; +using Fauna.Serialization; +using NUnit.Framework; + +namespace Fauna.Test.Serialization; + +public class DictionarySerializerTests +{ + private static readonly MappingContext s_ctx = new(); + + [Test] + public void RoundTripDictionaryWithTagConflicts() + { + var serializer = Serializer.Generate>(s_ctx); + var tests = new Dictionary, string> + { + { new() { { "@date", "not" } }, @"{""@object"":{""@date"":""not""}}" }, + { new() { { "@doc", "not" } }, @"{""@object"":{""@doc"":""not""}}" }, + { new() { { "@double", "not" } }, @"{""@object"":{""@double"":""not""}}" }, + { new() { { "@int", "not" } }, @"{""@object"":{""@int"":""not""}}" }, + { new() { { "@long", "not" } }, @"{""@object"":{""@long"":""not""}}" }, + { new() { { "@mod", "not" } }, @"{""@object"":{""@mod"":""not""}}" }, + { new() { { "@object", "not" } }, @"{""@object"":{""@object"":""not""}}" }, + { new() { { "@ref", "not" } }, @"{""@object"":{""@ref"":""not""}}" }, + { new() { { "@set", "not" } }, @"{""@object"":{""@set"":""not""}}" }, + { new() { { "@time", "not" } }, @"{""@object"":{""@time"":""not""}}" } + }; + + foreach ((Dictionary obj, string wire) in tests) + { + Dictionary deserialized = Helpers.Deserialize(serializer, s_ctx, wire)!; + Assert.AreEqual(obj, deserialized); + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(wire, serialized); + } + } + + [Test] + public void RoundTripEmptyObject() + { + var serializer = Serializer.Generate>(s_ctx); + + const string wire = "{}"; + var list = new Dictionary(); + + var deserialized = Helpers.Deserialize(serializer, s_ctx, wire); + Assert.AreEqual(list, deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(wire, serialized); + } + + [Test] + public void RoundTripObject() + { + const string given = @" + { + ""aString"": ""foo"", + ""anObject"": { ""baz"": ""luhrmann"" }, + ""anInt"": { ""@int"": ""2147483647"" }, + ""aLong"":{ ""@long"": ""9223372036854775807"" }, + ""aDouble"":{ ""@double"": ""3.14159"" }, + ""aDate"":{ ""@date"": ""2023-12-03"" }, + ""aTime"":{ ""@time"": ""2023-12-03T14:52:10.0010010Z"" }, + ""true"": true, + ""false"": false, + ""null"": null + } + "; + + var inner = new Dictionary + { + { "baz", "luhrmann" } + }; + + var expected = new Dictionary + { + { "aString", "foo" }, + { "anObject", inner }, + { "anInt", 2147483647 }, + { "aLong", 9223372036854775807 }, + { "aDouble", 3.14159d }, + { "aDate", new DateOnly(2023, 12, 3) }, + { "aTime", new DateTime(2023, 12, 3, 14, 52, 10, 1, DateTimeKind.Utc).AddTicks(10).ToLocalTime() }, + { "true", true }, + { "false", false }, + { "null", null } + }; + + var serializer = Serializer.Generate>(s_ctx); + + var deserialized = Helpers.Deserialize(serializer, s_ctx, given); + Assert.AreEqual(expected, deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(Regex.Replace(given, @"\s+", ""), serialized); + } + + [Test] + public void RoundTripDeserializeEscapedObject() + { + const string given = @" + { + ""@object"": { + ""@int"": ""notanint"", + ""anInt"": { ""@int"": ""123"" }, + ""@object"": ""notanobject"", + ""anEscapedObject"": { ""@object"": { ""@long"": ""notalong"" } } + } + } + "; + + var inner = new Dictionary + { + { "@long", "notalong" } + }; + + var expected = new Dictionary + { + { "@int", "notanint" }, + { "anInt", 123 }, + { "@object", "notanobject" }, + { "anEscapedObject", inner } + + }; + + var serializer = Serializer.Generate>(s_ctx); + + var deserialized = Helpers.Deserialize(serializer, s_ctx, given); + Assert.AreEqual(expected, deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(Regex.Replace(given, @"\s+", ""), serialized); + } + + [Test] + public void RoundTripDictionaryWithValueType() + { + const string given = @"{ + ""k1"": { ""@int"": ""1"" }, + ""k2"": { ""@int"": ""2"" }, + ""k3"": { ""@int"": ""3"" } + }"; + var expected = new Dictionary() + { + {"k1", 1}, + {"k2", 2}, + {"k3", 3} + }; + + var serializer = Serializer.Generate>(s_ctx); + + var deserialized = Helpers.Deserialize(serializer, s_ctx, given); + Assert.AreEqual(expected, deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(Regex.Replace(given, @"\s+", ""), serialized); + } +} diff --git a/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs new file mode 100644 index 00000000..aa1f364d --- /dev/null +++ b/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs @@ -0,0 +1,196 @@ + +using System.Reflection.Metadata; +using Fauna.Mapping; +using Fauna.Serialization; +using Fauna.Types; +using NUnit.Framework; + +namespace Fauna.Test.Serialization; + +public class DynamicSerializerTests +{ + private readonly MappingContext _ctx; + private const string DocumentWire = @"{""@doc"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + private const string RefWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""}}}"; + private const string NullDocumentWire = @"{""@ref"":{""id"":""123"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; + private const string NamedDocumentWire = @"{""@doc"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + private const string NamedRefWire = @"{""@ref"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""}}}"; + private const string NullNamedDocumentWire = @"{""@ref"":{""name"":""Foo"",""coll"":{""@mod"":""MyColl""},""exists"":false,""cause"":""not found""}}"; + + public DynamicSerializerTests() + { + var collections = new Dictionary { + { "MappedColl", typeof(ClassForDocument) } + }; + _ctx = new MappingContext(collections); + } + + [Test] + public void RoundTripValues() + { + var tests = new Dictionary + { + {"\"hello\"", "hello"}, + {@"{""@int"":""42""}", 42}, + {@"{""@long"":""42""}", 42L}, + {@"{""@double"":""1.2""}", 1.2d}, + {@"{""@date"":""2023-12-03""}", new DateOnly(2023, 12, 3)}, + {@"{""@time"":""2023-12-03T05:52:10.000001-09:00""}", new DateTime(2023, 12, 3, 14, 52, 10, 0, DateTimeKind.Utc).AddTicks(10).ToLocalTime()}, + {"true", true}, + {"false", false}, + {"null", null}, + }; + + foreach (KeyValuePair entry in tests) + { + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, entry.Key); + Assert.AreEqual(entry.Value, deserialized); + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual(entry.Value is DateTime ? "{\"@time\":\"2023-12-03T14:52:10.0000010Z\"}" : entry.Key, + serialized); + } + } + + [Test] + public void RoundTripNestedEmptyCollections() + { + const string wire = "[[],{}]"; + var expected = new List { new List(), new Dictionary() }; + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, wire); + Assert.AreEqual(expected, deserialized); + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual(wire, serialized); + } + + [Test] + public void RoundTripDocument() + { + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, DocumentWire); + switch (deserialized) + { + case Ref> d: + Assert.AreEqual("123", d.Id); + Assert.AreEqual(new Module("MyColl"), d.Collection); + var dict = d.Get(); + Assert.AreEqual("123", dict["id"]); + Assert.AreEqual(new Module("MyColl"), dict["coll"]); + Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), dict["ts"]); + Assert.AreEqual("user_value", dict["user_field"]); + break; + default: + Assert.Fail($"result is type: {deserialized?.GetType()}"); + break; + } + + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual(RefWire, serialized); + } + + [Test] + public void RoundTripNullDocument() + { + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, NullDocumentWire); + switch (deserialized) + { + case Ref> d: + Assert.AreEqual("123", d.Id); + Assert.AreEqual("MyColl", d.Collection.Name); + Assert.AreEqual("not found", d.Cause); + Assert.IsFalse(d.Exists); + break; + default: + Assert.Fail($"result is type: {deserialized?.GetType()}"); + break; + } + + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual(RefWire, serialized); + } + + [Test] + public void RoundTripNamedDocument() + { + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, NamedDocumentWire); + switch (deserialized) + { + case NamedRef> d: + Assert.AreEqual("Foo", d.Name); + Assert.AreEqual("MyColl", d.Collection.Name); + var doc = d.Get(); + + Assert.AreEqual(DateTime.Parse("2023-12-15T01:01:01.0010010Z"), doc["ts"]); + Assert.AreEqual("user_value", doc["user_field"]); + break; + default: + Assert.Fail($"result is type: {deserialized?.GetType()}"); + break; + } + + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual(NamedRefWire, serialized); + } + + [Test] + public void RoundTripNullNamedDocument() + { + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, NullNamedDocumentWire); + + switch (deserialized) + { + case NamedRef> d: + Assert.AreEqual("Foo", d.Name); + Assert.AreEqual("MyColl", d.Collection.Name); + Assert.AreEqual("not found", d.Cause); + Assert.IsFalse(d.Exists); + break; + default: + Assert.Fail($"result is type: {deserialized?.GetType()}"); + break; + } + + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual(NamedRefWire, serialized); + } + + [Test] + public void RoundTripAnonymousClass() + { + var obj = new { FirstName = "John", LastName = "Doe" }; + const string wire = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}"; + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, wire); + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, obj); + Assert.AreEqual(wire, serialized); + } + + [Test] + public void RoundTripMappedClass() + { + const string mapped = @"{""@doc"":{""id"":""123"",""coll"":{""@mod"":""MappedColl""},""ts"":{""@time"":""2023-12-15T01:01:01.0010010Z""},""user_field"":""user_value""}}"; + var obj = new ClassForDocument + { + Id = "123", + Coll = new Module("MappedColl"), + Ts = DateTime.Parse("2023-12-15T01:01:01.0010010Z"), + UserField = "user_value" + }; + + object? deserialized = Helpers.Deserialize(Serializer.Dynamic, _ctx, mapped); + switch (deserialized) + { + case Ref c: + Assert.AreEqual(obj.Id, c.Id); + Assert.AreEqual(obj.Coll, c.Collection); + + var resObj = (ClassForDocument)c.Get(); + Assert.AreEqual(obj.Ts, resObj.Ts); + Assert.AreEqual(obj.UserField, resObj.UserField); + break; + default: + Assert.Fail($"result is type: {deserialized?.GetType()}"); + break; + } + + string serialized = Helpers.Serialize(Serializer.Dynamic, _ctx, deserialized); + Assert.AreEqual("{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"MappedColl\"}}}", serialized); + } +} diff --git a/Fauna.Test/Serialization/Serializers/ListSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/ListSerializer.Tests.cs index e785945d..08aabcee 100644 --- a/Fauna.Test/Serialization/Serializers/ListSerializer.Tests.cs +++ b/Fauna.Test/Serialization/Serializers/ListSerializer.Tests.cs @@ -93,4 +93,19 @@ public void DeserializeSingleValueAsList() var deserialized = Helpers.Deserialize(serializer, s_ctx, wire); Assert.AreEqual(list, deserialized); } + + [Test] + public void RoundTripEmptyArray() + { + var serializer = Serializer.Generate>(s_ctx); + + const string wire = "[]"; + var list = new List(); + + var deserialized = Helpers.Deserialize(serializer, s_ctx, wire); + Assert.AreEqual(list, deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(wire, serialized); + } } diff --git a/Fauna.Test/Serialization/Serializers/NullableStructSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/NullableStructSerializer.Tests.cs new file mode 100644 index 00000000..7f6885be --- /dev/null +++ b/Fauna.Test/Serialization/Serializers/NullableStructSerializer.Tests.cs @@ -0,0 +1,35 @@ +using Fauna.Mapping; +using Fauna.Serialization; +using NUnit.Framework; + +namespace Fauna.Test.Serialization; + +public class NullableStructSerializerTests +{ + private static readonly MappingContext s_ctx = new(); + + [Test] + public void RoundTripNullableStructAsNull() + { + const string wire = "null"; + var serializer = Serializer.Generate(s_ctx, typeof(int?)); + object? deserialized = Helpers.Deserialize(serializer, s_ctx, wire); + Assert.IsNull(deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(wire, serialized); + } + + [Test] + public void RoundTripNullableStructAsValue() + { + const string wire = @"{""@int"":""42""}"; + int? obj = 42; + var serializer = Serializer.Generate(s_ctx, typeof(int?)); + object? deserialized = Helpers.Deserialize(serializer, s_ctx, wire); + Assert.AreEqual(obj, deserialized); + + string serialized = Helpers.Serialize(serializer, s_ctx, deserialized); + Assert.AreEqual(wire, serialized); + } +} diff --git a/Fauna.Test/Serialization/Serializers/PageSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/PageSerializer.Tests.cs new file mode 100644 index 00000000..6ab994bf --- /dev/null +++ b/Fauna.Test/Serialization/Serializers/PageSerializer.Tests.cs @@ -0,0 +1,67 @@ +using Fauna.Mapping; +using Fauna.Serialization; +using Fauna.Types; +using NUnit.Framework; + +namespace Fauna.Test.Serialization; + +public class PageSerializer_Tests +{ + private static readonly MappingContext s_ctx = new(); + + + [Test] + public void DeserializeIntoPageWithPrimitive() + { + const string wire = @"{ + ""@set"": { + ""after"": ""next_page_cursor"", + ""data"": [ + {""@int"":""1""}, + {""@int"":""2""}, + {""@int"":""3""} + ] + } + }"; + + var serializer = Serializer.Generate>(s_ctx); + var deserialized = Helpers.Deserialize(serializer, s_ctx, wire)!; + Assert.AreEqual(new List { 1, 2, 3 }, deserialized.Data); + Assert.AreEqual("next_page_cursor", deserialized.After); + } + + [Test] + public void DeserializeIntoPageWithSingleValue() + { + const string wire = @"""SingleValue"""; + var serializer = Serializer.Generate>(s_ctx); + var deserialized = Helpers.Deserialize(serializer, s_ctx, wire)!; + Assert.AreEqual(new List { "SingleValue" }, deserialized.Data); + Assert.IsNull(deserialized.After); + } + + [Test] + public void DeserializeIntoPageWithUserDefinedClass() + { + const string wire = @"{ + ""@set"": { + ""after"": ""next_page_cursor"", + ""data"": [ + {""first_name"":""Alice"",""last_name"":""Smith"",""age"":{""@int"":""30""}}, + {""first_name"":""Bob"",""last_name"":""Jones"",""age"":{""@int"":""40""}} + ] + } + }"; + + var serializer = Serializer.Generate>(s_ctx); + var deserialized = Helpers.Deserialize(serializer, s_ctx, wire)!; + Assert.AreEqual(2, deserialized.Data.Count); + Assert.AreEqual("Alice", deserialized.Data[0].FirstName); + Assert.AreEqual("Smith", deserialized.Data[0].LastName); + Assert.AreEqual(30, deserialized.Data[0].Age); + Assert.AreEqual("Bob", deserialized.Data[1].FirstName); + Assert.AreEqual("Jones", deserialized.Data[1].LastName); + Assert.AreEqual(40, deserialized.Data[1].Age); + Assert.AreEqual("next_page_cursor", deserialized.After); + } +} diff --git a/Fauna.Test/Serialization/Serializers/SerializerFixtures.cs b/Fauna.Test/Serialization/Serializers/SerializerFixtures.cs deleted file mode 100644 index 34d70c33..00000000 --- a/Fauna.Test/Serialization/Serializers/SerializerFixtures.cs +++ /dev/null @@ -1,596 +0,0 @@ -using Fauna.Serialization; -using Stream = Fauna.Types.Stream; - -namespace Fauna.Test.Serialization; - -public class SerializerFixtures -{ - private static readonly StringSerializer _string = new(); - private static readonly ByteSerializer _byte = new(); - private static readonly SByteSerializer _sbyte = new(); - private static readonly ShortSerializer _short = new(); - private static readonly UShortSerializer _ushort = new(); - private static readonly IntSerializer _int = new(); - private static readonly UIntSerializer _uint = new(); - private static readonly LongSerializer _long = new(); - private static readonly FloatSerializer _float = new(); - private static readonly DoubleSerializer _double = new(); - private static readonly BooleanSerializer _bool = new(); - private static readonly DateOnlySerializer _date = new(); - private static readonly DateTimeSerializer _time = new(); - private static readonly DateTimeOffsetSerializer _offset = new(); - private static readonly StreamSerializer _stream = new(); - - public static object[] TypedSerializerCases = - { - // StringSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _string, - Value = Helpers.StringVal, - Expected = Helpers.StringWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _string, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _string, - Value = Helpers.IntMaxWire, - Throws = "Unexpected token `Int` deserializing with `StringSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // ByteSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _byte, - Value = Helpers.ByteVal, - Expected = Helpers.IntWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _byte, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _byte, - Value = Helpers.LongMaxWire, - Throws = "Unexpected token `Long` deserializing with `ByteSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // SByteSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _sbyte, - Value = Helpers.SByteVal, - Expected = Helpers.IntWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _sbyte, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _sbyte, - Value = Helpers.LongMaxWire, - Throws = "Unexpected token `Long` deserializing with `SByteSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // ShortSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _short, - Value = short.MaxValue, - Expected = Helpers.ShortMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _short, - Value = short.MinValue, - Expected = Helpers.ShortMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _short, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _short, - Value = Helpers.LongMaxWire, - Throws = "Unexpected token `Long` deserializing with `ShortSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // UShortSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _ushort, - Value = ushort.MaxValue, - Expected = Helpers.UShortMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _ushort, - Value = ushort.MinValue, - Expected = Helpers.UShortMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _ushort, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _ushort, - Value = Helpers.LongMaxWire, - Throws = "Unexpected token `Long` deserializing with `UShortSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // IntSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _int, - Value = int.MaxValue, - Expected = Helpers.IntMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _int, - Value = int.MinValue, - Expected = Helpers.IntMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _int, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _int, - Value = Helpers.LongMaxWire, - Throws = "Unexpected token `Long` deserializing with `IntSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // UIntSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _uint, - Value = uint.MaxValue, - Expected = Helpers.UIntMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _uint, - Value = uint.MinValue, - Expected = Helpers.UIntMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _uint, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _uint, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `UIntSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // LongSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _long, - Value = long.MaxValue, - Expected = Helpers.LongMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _long, - Value = long.MinValue, - Expected = Helpers.LongMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _long, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _long, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `LongSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // FloatSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _float, - Value = float.MaxValue, - Expected = Helpers.FloatMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _float, - Value = float.MinValue, - Expected = Helpers.FloatMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _float, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _float, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `FloatSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // DoubleSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = double.MaxValue, - Expected = Helpers.DoubleMaxWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = double.MinValue, - Expected = Helpers.DoubleMinWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = double.NaN, - Expected = Helpers.DoubleNaNWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = double.NegativeInfinity, - Expected = Helpers.DoubleNegInfWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = double.PositiveInfinity, - Expected = Helpers.DoubleInfWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _double, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `DoubleSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // BooleanSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _bool, - Value = true, - Expected = Helpers.TrueWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _bool, - Value = false, - Expected = Helpers.FalseWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _bool, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _bool, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `BooleanSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // DateOnlySerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _date, - Value = Helpers.DateOnlyVal, - Expected = Helpers.DateWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _date, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _date, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `DateOnlySerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // DateTimeSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _time, - Value = Helpers.DateTimeVal, - Expected = Helpers.TimeWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _time, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _time, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `DateTimeSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // DateTimeOffsetSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _offset, - Value = Helpers.DateTimeOffsetVal, - Expected = Helpers.TimeFromOffsetWire, - TestType = Helpers.TestType.Roundtrip - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _offset, - Value = null, - Expected = Helpers.NullWire, - TestType = Helpers.TestType.Serialize - } - }, - new object[] - { - new Helpers.SerializeTest - { - Serializer = _offset, - Value = Helpers.NullWire, - Throws = "Unexpected token `Null` deserializing with `DateTimeOffsetSerializer`", - TestType = Helpers.TestType.Deserialize - } - }, - - // StreamSerializer - new object[] - { - new Helpers.SerializeTest - { - Serializer = _stream, - Value = Helpers.StreamWire, - Expected = Helpers.StreamVal, - TestType = Helpers.TestType.Deserialize - } - } - }; -} diff --git a/Fauna.Test/Serialization/Serializers/TypedSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/TypedSerializer.Tests.cs index 806ba6b6..b5868b86 100644 --- a/Fauna.Test/Serialization/Serializers/TypedSerializer.Tests.cs +++ b/Fauna.Test/Serialization/Serializers/TypedSerializer.Tests.cs @@ -1,16 +1,33 @@ using Fauna.Exceptions; using Fauna.Mapping; +using Fauna.Serialization; using NUnit.Framework; +using Stream = Fauna.Types.Stream; namespace Fauna.Test.Serialization; [TestFixture] public class TypedSerializerTests { - private static readonly MappingContext _ctx = new(); + private static readonly MappingContext s_ctx = new(); + private static readonly StringSerializer _string = new(); + private static readonly ByteSerializer _byte = new(); + private static readonly SByteSerializer _sbyte = new(); + private static readonly ShortSerializer _short = new(); + private static readonly UShortSerializer _ushort = new(); + private static readonly IntSerializer _int = new(); + private static readonly UIntSerializer _uint = new(); + private static readonly LongSerializer _long = new(); + private static readonly FloatSerializer _float = new(); + private static readonly DoubleSerializer _double = new(); + private static readonly BooleanSerializer _bool = new(); + private static readonly DateOnlySerializer _date = new(); + private static readonly DateTimeSerializer _time = new(); + private static readonly DateTimeOffsetSerializer _offset = new(); + private static readonly StreamSerializer _stream = new(); [Test] - [TestCaseSource(typeof(SerializerFixtures), nameof(SerializerFixtures.TypedSerializerCases))] + [TestCaseSource(nameof(TypedSerializerCases))] public void TestTypedSerializers(Helpers.SerializeTest t) { switch (t.TestType) @@ -18,12 +35,12 @@ public void TestTypedSerializers(Helpers.SerializeTest t) case Helpers.TestType.Serialize: if (t.Throws is null) { - var rs = Helpers.Serialize(t.Serializer, _ctx, t.Value); + string rs = Helpers.Serialize(t.Serializer, s_ctx, t.Value); Assert.AreEqual(t.Expected, rs); } else { - var ex = Assert.Throws(() => Helpers.Serialize(t.Serializer, _ctx, t.Value)); + var ex = Assert.Throws(() => Helpers.Serialize(t.Serializer, s_ctx, t.Value)); Assert.That(ex!.Message, Is.EqualTo(t.Throws)); } break; @@ -31,21 +48,21 @@ public void TestTypedSerializers(Helpers.SerializeTest t) case Helpers.TestType.Deserialize: if (t.Throws is null) { - var rd = Helpers.Deserialize(t.Serializer, _ctx, (string)t.Value!); + var rd = Helpers.Deserialize(t.Serializer, s_ctx, (string)t.Value!); Assert.AreEqual(t.Expected, rd); } else { - var ex = Assert.Throws(() => Helpers.Deserialize(t.Serializer, _ctx, (string)t.Value!)); + var ex = Assert.Throws(() => Helpers.Deserialize(t.Serializer, s_ctx, (string)t.Value!)); Assert.That(ex!.Message, Is.EqualTo(t.Throws)); } break; case Helpers.TestType.Roundtrip: - var serialized = Helpers.Serialize(t.Serializer, _ctx, t.Value); + string serialized = Helpers.Serialize(t.Serializer, s_ctx, t.Value); Assert.AreEqual(t.Expected, serialized); - var deserialized = Helpers.Deserialize(t.Serializer, _ctx, serialized); + var deserialized = Helpers.Deserialize(t.Serializer, s_ctx, serialized); Assert.AreEqual(t.Value, deserialized); break; @@ -55,4 +72,576 @@ public void TestTypedSerializers(Helpers.SerializeTest t) } } + public static object[] TypedSerializerCases = + { + // StringSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _string, + Value = Helpers.StringVal, + Expected = Helpers.StringWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _string, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _string, + Value = Helpers.IntMaxWire, + Throws = "Unexpected token `Int` deserializing with `StringSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // ByteSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _byte, + Value = Helpers.ByteVal, + Expected = Helpers.IntWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _byte, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _byte, + Value = Helpers.LongMaxWire, + Throws = "Unexpected token `Long` deserializing with `ByteSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // SByteSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _sbyte, + Value = Helpers.SByteVal, + Expected = Helpers.IntWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _sbyte, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _sbyte, + Value = Helpers.LongMaxWire, + Throws = "Unexpected token `Long` deserializing with `SByteSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // ShortSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _short, + Value = short.MaxValue, + Expected = Helpers.ShortMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _short, + Value = short.MinValue, + Expected = Helpers.ShortMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _short, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _short, + Value = Helpers.LongMaxWire, + Throws = "Unexpected token `Long` deserializing with `ShortSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // UShortSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _ushort, + Value = ushort.MaxValue, + Expected = Helpers.UShortMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _ushort, + Value = ushort.MinValue, + Expected = Helpers.UShortMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _ushort, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _ushort, + Value = Helpers.LongMaxWire, + Throws = "Unexpected token `Long` deserializing with `UShortSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // IntSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _int, + Value = int.MaxValue, + Expected = Helpers.IntMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _int, + Value = int.MinValue, + Expected = Helpers.IntMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _int, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _int, + Value = Helpers.LongMaxWire, + Throws = "Unexpected token `Long` deserializing with `IntSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // UIntSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _uint, + Value = uint.MaxValue, + Expected = Helpers.UIntMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _uint, + Value = uint.MinValue, + Expected = Helpers.UIntMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _uint, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _uint, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `UIntSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // LongSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _long, + Value = long.MaxValue, + Expected = Helpers.LongMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _long, + Value = long.MinValue, + Expected = Helpers.LongMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _long, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _long, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `LongSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // FloatSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _float, + Value = float.MaxValue, + Expected = Helpers.FloatMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _float, + Value = float.MinValue, + Expected = Helpers.FloatMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _float, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _float, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `FloatSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // DoubleSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = double.MaxValue, + Expected = Helpers.DoubleMaxWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = double.MinValue, + Expected = Helpers.DoubleMinWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = double.NaN, + Expected = Helpers.DoubleNaNWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = double.NegativeInfinity, + Expected = Helpers.DoubleNegInfWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = double.PositiveInfinity, + Expected = Helpers.DoubleInfWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _double, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `DoubleSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // BooleanSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _bool, + Value = true, + Expected = Helpers.TrueWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _bool, + Value = false, + Expected = Helpers.FalseWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _bool, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _bool, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `BooleanSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // DateOnlySerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _date, + Value = Helpers.DateOnlyVal, + Expected = Helpers.DateWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _date, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _date, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `DateOnlySerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // DateTimeSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _time, + Value = Helpers.DateTimeVal, + Expected = Helpers.TimeWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _time, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _time, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `DateTimeSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // DateTimeOffsetSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _offset, + Value = Helpers.DateTimeOffsetVal, + Expected = Helpers.TimeFromOffsetWire, + TestType = Helpers.TestType.Roundtrip + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _offset, + Value = null, + Expected = Helpers.NullWire, + TestType = Helpers.TestType.Serialize + } + }, + new object[] + { + new Helpers.SerializeTest + { + Serializer = _offset, + Value = Helpers.NullWire, + Throws = "Unexpected token `Null` deserializing with `DateTimeOffsetSerializer`", + TestType = Helpers.TestType.Deserialize + } + }, + + // StreamSerializer + new object[] + { + new Helpers.SerializeTest + { + Serializer = _stream, + Value = Helpers.StreamWire, + Expected = Helpers.StreamVal, + TestType = Helpers.TestType.Deserialize + } + } + }; } diff --git a/Fauna.Test/Serialization/TestClasses.cs b/Fauna.Test/Serialization/TestClasses.cs index 5a643ad1..bba4ffb7 100644 --- a/Fauna.Test/Serialization/TestClasses.cs +++ b/Fauna.Test/Serialization/TestClasses.cs @@ -7,9 +7,9 @@ namespace Fauna.Test.Serialization; class Person { - public string? FirstName { get; set; } - public string? LastName { get; set; } - public int Age { get; set; } + public string? FirstName { get; init; } + public string? LastName { get; init; } + public int Age { get; init; } public override bool Equals(object? obj) { @@ -29,15 +29,10 @@ public override int GetHashCode() } } -class NullableInt -{ - public int? Val { get; set; } -} - class ClassForDocument { [Id] public string? Id { get; set; } - [Coll] public Module? Coll { get; set; } + [Collection] public Module? Coll { get; set; } [Ts] public DateTime? Ts { get; set; } [Field("user_field")] public string? UserField { get; set; } } @@ -45,15 +40,24 @@ class ClassForDocument class ClassForDocumentClientGeneratedId { [Id(true)] public string? Id { get; set; } - [Coll] public Module? Coll { get; set; } + [Collection] public Module? Coll { get; set; } [Ts] public DateTime? Ts { get; set; } [Field("user_field")] public string? UserField { get; set; } } +class ClassForDocumentWithSpecialNames +{ + [Id] public string? TheId { get; set; } + [Collection] public Module? TheCollection { get; set; } + [Ts] public DateTime? TheTs { get; set; } + [Field("user_field")] public string? UserField { get; set; } +} + + class ClassForUnmapped { [Id] public string? Id { get; set; } - [Coll] public Module? Coll { get; set; } + [Collection] public Module? Coll { get; set; } [Ts] public DateTime? Ts { get; set; } [Field("user_field")] public string? UserField { get; set; } } @@ -174,7 +178,7 @@ class PersonWithDateConflict : IOnlyField class ClassWithDupeFields { [Id] public string? Id { get; set; } - [Coll] public Module? Coll { get; set; } + [Collection] public Module? Coll { get; set; } [Ts] public DateTime? Ts { get; set; } [Field("user_field")] public string? UserField { get; set; } [Field("user_field")] public string? UserField2 { get; set; } @@ -184,7 +188,7 @@ class ClassWithDupeFields class ClassWithFieldNameOverlap { [Id] public string? Id { get; set; } - [Coll] public Module? Coll { get; set; } + [Collection] public Module? Coll { get; set; } [Ts] public DateTime? Ts { get; set; } [Field("user_field")] public string? UserField { get; set; } [Field] public string? user_field { get; set; } @@ -194,7 +198,7 @@ class ClassWithFieldNameOverlap class ClassWithLotsOfFields { [Id] public string? Id { get; set; } - [Coll] public Module? Coll { get; set; } + [Collection] public Module? Coll { get; set; } [Ts] public DateTime? Ts { get; set; } [Field] public DateTime DateTimeField { get; set; } [Field] public DateOnly DateOnlyField { get; set; } diff --git a/Fauna.Test/Types/Document.Tests.cs b/Fauna.Test/Types/Document.Tests.cs deleted file mode 100644 index e000fa76..00000000 --- a/Fauna.Test/Types/Document.Tests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Fauna.Types; -using NUnit.Framework; - -namespace Fauna.Test.Types; - -[TestFixture] -public class DocumentTests -{ - - [Test] - public void Ctor_DocumentNoData() - { - const string id = "id"; - var coll = new Module("Foo"); - var ts = DateTime.Parse("2024-01-01"); - var doc = new Document(id, coll, ts); - Assert.AreEqual(id, doc.Id); - Assert.AreEqual(coll, doc.Collection); - Assert.AreEqual(ts, doc.Ts); - Assert.IsEmpty(doc.Keys); - } - - [Test] - public void Ctor_DocumentWithData() - { - const string id = "id"; - var coll = new Module("Foo"); - var ts = DateTime.Parse("2024-01-01"); - var d = new Dictionary() - { - {"foo", "bar"} - }; - var doc = new Document(id, coll, ts, d); - - Assert.AreEqual(id, doc.Id); - Assert.AreEqual(coll, doc.Collection); - Assert.AreEqual(ts, doc.Ts); - Assert.AreEqual("bar", doc["foo"]); - } - - [Test] - public void DocumentClonesDictionary() - { - var d = new Dictionary() - { - {"foo", "bar"} - }; - var doc = new Document("id", new Module("Foo"), DateTime.Parse("2024-01-01"), d); - - d["foo"] = "baz"; - - Assert.AreEqual("bar", doc["foo"]); - } -} diff --git a/Fauna.Test/Types/NamedDocument.Tests.cs b/Fauna.Test/Types/NamedDocument.Tests.cs deleted file mode 100644 index e4b5efb5..00000000 --- a/Fauna.Test/Types/NamedDocument.Tests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Fauna.Types; -using NUnit.Framework; - -namespace Fauna.Test.Types; - -[TestFixture] -public class NamedDocumentTests -{ - - [Test] - public void Ctor_NamedDocumentNoData() - { - const string name = "name"; - var coll = new Module("Foo"); - var ts = DateTime.Parse("2024-01-01"); - var doc = new NamedDocument(name, coll, ts); - Assert.AreEqual(name, doc.Name); - Assert.AreEqual(coll, doc.Collection); - Assert.AreEqual(ts, doc.Ts); - Assert.IsEmpty(doc.Keys); - } - - [Test] - public void Ctor_NamedDocumentWithData() - { - const string name = "name"; - var coll = new Module("Foo"); - var ts = DateTime.Parse("2024-01-01"); - var d = new Dictionary() - { - {"foo", "bar"} - }; - var doc = new NamedDocument(name, coll, ts, d); - - Assert.AreEqual(name, doc.Name); - Assert.AreEqual(coll, doc.Collection); - Assert.AreEqual(ts, doc.Ts); - Assert.AreEqual("bar", doc["foo"]); - } - - - [Test] - public void NamedDocumentClonesDictionary() - { - var d = new Dictionary() - { - {"foo", "bar"} - }; - var doc = new NamedDocument("name", new Module("Foo"), DateTime.Parse("2024-01-01"), d); - - d["foo"] = "baz"; - - Assert.AreEqual("bar", doc["foo"]); - } -} diff --git a/Fauna/Mapping/Attributes.cs b/Fauna/Mapping/Attributes.cs index 8bc78aec..f0ffb90f 100644 --- a/Fauna/Mapping/Attributes.cs +++ b/Fauna/Mapping/Attributes.cs @@ -20,20 +20,26 @@ public IgnoreAttribute() { } } +public abstract class BaseFieldAttribute : Attribute +{ + public readonly string? Name; + public FieldType Type; + + protected BaseFieldAttribute(string? name, FieldType type) + { + Name = name; + Type = type; + } +} + /// /// Attribute used to specify fields on a Fauna document or struct. /// [AttributeUsage(AttributeTargets.Property)] -public class FieldAttribute : Attribute +public class FieldAttribute : BaseFieldAttribute { - internal readonly string? _name; - - public FieldAttribute() { } - - public FieldAttribute(string name) - { - _name = name; - } + public FieldAttribute() : base(null, FieldType.Field) { } + public FieldAttribute(string name) : base(name, FieldType.Field) { } } /// @@ -41,16 +47,14 @@ public FieldAttribute(string name) /// serialization unless isClientGenerated is set to true. /// [AttributeUsage(AttributeTargets.Property)] -public class IdAttribute : Attribute +public class IdAttribute : BaseFieldAttribute { - internal readonly bool _isClientGenerated; + private const string FieldName = "id"; - public IdAttribute() { } + public IdAttribute() : base(FieldName, FieldType.ServerGeneratedId) { } public IdAttribute(bool isClientGenerated) - { - _isClientGenerated = isClientGenerated; - } + : base(FieldName, isClientGenerated ? FieldType.ClientGeneratedId : FieldType.ServerGeneratedId) { } } /// @@ -58,9 +62,11 @@ public IdAttribute(bool isClientGenerated) /// during serialization. /// [AttributeUsage(AttributeTargets.Property)] -public class CollAttribute : Attribute +public class CollectionAttribute : BaseFieldAttribute { - public CollAttribute() { } + private const string FieldName = "coll"; + + public CollectionAttribute() : base(FieldName, FieldType.Coll) { } } @@ -69,7 +75,9 @@ public CollAttribute() { } /// serialization. /// [AttributeUsage(AttributeTargets.Property)] -public class TsAttribute : Attribute +public class TsAttribute : BaseFieldAttribute { - public TsAttribute() { } + private const string FieldName = "ts"; + + public TsAttribute() : base(FieldName, FieldType.Ts) { } } diff --git a/Fauna/Mapping/FieldInfo.cs b/Fauna/Mapping/FieldInfo.cs index 2334ae3a..346058f8 100644 --- a/Fauna/Mapping/FieldInfo.cs +++ b/Fauna/Mapping/FieldInfo.cs @@ -31,13 +31,13 @@ public sealed class FieldInfo private MappingContext _ctx; private ISerializer? _serializer; - internal FieldInfo(MappingContext ctx, FieldAttribute attr, PropertyInfo prop, FieldType fieldType) + internal FieldInfo(MappingContext ctx, BaseFieldAttribute attr, PropertyInfo prop) { var nullCtx = new NullabilityInfoContext(); var nullInfo = nullCtx.Create(prop); - Name = attr._name ?? FieldName.Canonical(prop.Name); - FieldType = fieldType; + Name = attr.Name ?? FieldName.Canonical(prop.Name); + FieldType = attr.Type; Property = prop; Type = prop.PropertyType; IsNullable = nullInfo.WriteState is NullabilityState.Nullable; diff --git a/Fauna/Mapping/MappingInfo.cs b/Fauna/Mapping/MappingInfo.cs index 4b7b8091..d2937312 100644 --- a/Fauna/Mapping/MappingInfo.cs +++ b/Fauna/Mapping/MappingInfo.cs @@ -40,9 +40,8 @@ internal MappingInfo(MappingContext ctx, Type ty, string? colName = null) { if (prop.GetCustomAttribute() != null) continue; - var attr = prop.GetCustomAttribute() ?? new FieldAttribute(); - var fieldType = GetFieldType(prop); - var info = new FieldInfo(ctx, attr, prop, fieldType); + var attr = prop.GetCustomAttribute() ?? new FieldAttribute(); + var info = new FieldInfo(ctx, attr, prop); if (byName.ContainsKey(info.Name)) throw new ArgumentException($"Duplicate field name {info.Name} in {ty}"); @@ -58,25 +57,4 @@ internal MappingInfo(MappingContext ctx, Type ty, string? colName = null) var serType = typeof(ClassSerializer<>).MakeGenericType(new[] { ty }); ClassSerializer = (ISerializer)Activator.CreateInstance(serType, this)!; } - - private FieldType GetFieldType(PropertyInfo propertyInfo) - { - var idAttr = propertyInfo.GetCustomAttribute(); - if (idAttr != null) - { - return idAttr._isClientGenerated ? FieldType.ClientGeneratedId : FieldType.ServerGeneratedId; - } - - if (propertyInfo.GetCustomAttribute() != null) - { - return FieldType.Ts; - } - - if (propertyInfo.GetCustomAttribute() != null) - { - return FieldType.Coll; - } - - return FieldType.Field; - } } diff --git a/Fauna/Serialization/BaseRefSerializer.cs b/Fauna/Serialization/BaseRefSerializer.cs new file mode 100644 index 00000000..5e925823 --- /dev/null +++ b/Fauna/Serialization/BaseRefSerializer.cs @@ -0,0 +1,167 @@ +using Fauna.Exceptions; +using Fauna.Mapping; +using Fauna.Types; + +namespace Fauna.Serialization; + + +internal class RefSerializer : BaseSerializer> where T : notnull +{ + private readonly BaseRefSerializer _baseRefSerializer; + + public RefSerializer(ISerializer docSerializer) + { + _baseRefSerializer = new BaseRefSerializer(docSerializer); + } + + public override Ref Deserialize(MappingContext context, ref Utf8FaunaReader reader) + { + return (Ref)_baseRefSerializer.Deserialize(context, ref reader); + } + + public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) + { + _baseRefSerializer.Serialize(context, writer, o); + } +} + +internal class NamedRefSerializer : BaseSerializer> where T : notnull +{ + private readonly BaseRefSerializer _baseRefSerializer; + + public NamedRefSerializer(ISerializer docSerializer) + { + _baseRefSerializer = new BaseRefSerializer(docSerializer); + } + + public override NamedRef Deserialize(MappingContext context, ref Utf8FaunaReader reader) + { + return (NamedRef)_baseRefSerializer.Deserialize(context, ref reader); + } + + public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) + { + _baseRefSerializer.Serialize(context, writer, o); + } +} + +internal class BaseRefSerializer : BaseSerializer> where T : notnull +{ + private readonly ISerializer _docSerializer; + + public BaseRefSerializer(ISerializer docSerializer) + { + _docSerializer = docSerializer; + } + + public override BaseRef Deserialize(MappingContext context, ref Utf8FaunaReader reader) + { + return reader.CurrentTokenType switch + { + TokenType.StartRef => DeserializeRefInternal(new BaseRefBuilder(), context, ref reader), + TokenType.StartDocument => DeserializeDocument(new BaseRefBuilder(), context, ref reader), + _ => throw new SerializationException( + $"Unexpected token while deserializing into Ref: {reader.CurrentTokenType}") + }; + } + + public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) + { + switch (o) + { + case null: + writer.WriteNullValue(); + break; + case Ref r: + writer.WriteStartRef(); + writer.WriteString("id", r.Id); + writer.WriteModule("coll", r.Collection); + writer.WriteEndRef(); + break; + case NamedRef r: + writer.WriteStartRef(); + writer.WriteString("name", r.Name); + writer.WriteModule("coll", r.Collection); + writer.WriteEndRef(); + break; + default: + throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); + } + } + + private static BaseRef DeserializeRefInternal(BaseRefBuilder builder, MappingContext context, ref Utf8FaunaReader reader) + { + + while (reader.Read() && reader.CurrentTokenType != TokenType.EndRef) + { + if (reader.CurrentTokenType != TokenType.FieldName) + throw new SerializationException( + $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); + + string fieldName = reader.GetString()!; + reader.Read(); + switch (fieldName) + { + case "id": + builder.Id = reader.GetString(); + break; + case "name": + builder.Name = reader.GetString(); + break; + case "coll": + builder.Collection = reader.GetModule(); + break; + case "cause": + builder.Cause = reader.GetString(); + break; + case "exists": + builder.Exists = reader.GetBoolean(); + break; + default: + throw new SerializationException( + $"Unexpected field while deserializing into Ref: {fieldName}"); + } + } + + return builder.Build(); + } + + public BaseRef DeserializeDocument(BaseRefBuilder builder, MappingContext context, ref Utf8FaunaReader reader) + { + + while (reader.Read() && reader.CurrentTokenType != TokenType.EndDocument) + { + if (reader.CurrentTokenType != TokenType.FieldName) + throw new SerializationException( + $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); + + string fieldName = reader.GetString()!; + reader.Read(); + switch (fieldName) + { + case "id": + builder.Id = reader.GetString(); + break; + case "name": + builder.Name = reader.GetString(); + break; + case "coll": + builder.Collection = reader.GetModule(); + + if (_docSerializer is not IPartialDocumentSerializer cs) + { + throw new SerializationException($"Serializer {_docSerializer.GetType().Name} must implement IPartialDocumentSerializer interface."); + } + + // This assumes ordering on the wire. If name is not null and we're here, then it's a named document so name is a string. + builder.Doc = (T?)cs.DeserializeDocument(context, builder.Id, builder.Name, builder.Collection, ref reader); + break; + } + + // After we deserialize into a doc, we end on the EndDocument a token and do not want to read again + if (reader.CurrentTokenType == TokenType.EndDocument) break; + } + + return builder.Build(); + } +} diff --git a/Fauna/Serialization/ClassSerializer.cs b/Fauna/Serialization/ClassSerializer.cs index a8f0d094..e5f9c977 100644 --- a/Fauna/Serialization/ClassSerializer.cs +++ b/Fauna/Serialization/ClassSerializer.cs @@ -5,15 +5,11 @@ namespace Fauna.Serialization; -internal interface IClassDocumentSerializer : ISerializer -{ - public object DeserializeDocument(MappingContext context, string? id, string? name, ref Utf8FaunaReader reader); -} - -internal class ClassSerializer : BaseSerializer, IClassDocumentSerializer +internal class ClassSerializer : BaseSerializer, IPartialDocumentSerializer { private const string IdField = "id"; private const string NameField = "name"; + private const string CollField = "coll"; private readonly MappingInfo _info; public ClassSerializer(MappingInfo info) @@ -22,11 +18,12 @@ public ClassSerializer(MappingInfo info) _info = info; } - public object DeserializeDocument(MappingContext context, string? id, string? name, ref Utf8FaunaReader reader) + public object DeserializeDocument(MappingContext context, string? id, string? name, Module? coll, ref Utf8FaunaReader reader) { object instance = CreateInstance(); if (id is not null) TrySetId(instance, id); if (name is not null) TrySetName(instance, name); + if (coll is not null) TrySetColl(instance, coll); SetFields(instance, context, ref reader, TokenType.EndDocument); return instance; } @@ -53,7 +50,7 @@ public override T Deserialize(MappingContext context, ref Utf8FaunaReader reader { if (reader.CurrentTokenType != TokenType.FieldName) throw new SerializationException( - $"Unexpected token while deserializing into DocumentRef: {reader.CurrentTokenType}"); + $"Unexpected token while deserializing into Ref: {reader.CurrentTokenType}"); string fieldName = reader.GetString()!; reader.Read(); @@ -201,6 +198,21 @@ private void TrySetName(object instance, string name) } } + private void TrySetColl(object instance, Module coll) + { + if (_info.FieldsByName.TryGetValue(CollField, out var field)) + { + if (field.Type == typeof(Module)) + { + field.Property.SetValue(instance, coll); + } + else + { + throw UnexpectedToken(TokenType.String); + } + } + } + private new SerializationException UnexpectedToken(TokenType tokenType) => new($"Unexpected token while deserializing into class {_info.Type.Name}: {tokenType}"); } diff --git a/Fauna/Serialization/DictionarySerializer.cs b/Fauna/Serialization/DictionarySerializer.cs index 1e67ee69..40eda3a2 100644 --- a/Fauna/Serialization/DictionarySerializer.cs +++ b/Fauna/Serialization/DictionarySerializer.cs @@ -1,9 +1,11 @@ +using System.Text.Json; using Fauna.Exceptions; using Fauna.Mapping; +using Fauna.Types; namespace Fauna.Serialization; -internal class DictionarySerializer : BaseSerializer> +internal class DictionarySerializer : BaseSerializer>, IPartialDocumentSerializer { private readonly ISerializer _elemSerializer; @@ -14,24 +16,16 @@ public DictionarySerializer(ISerializer elemSerializer) public override Dictionary Deserialize(MappingContext context, ref Utf8FaunaReader reader) { - if (reader.CurrentTokenType != TokenType.StartObject) - throw new SerializationException( - $"Unexpected token while deserializing into {typeof(Dictionary)}: {reader.CurrentTokenType}"); - - var dict = new Dictionary(); - - while (reader.Read() && reader.CurrentTokenType != TokenType.EndObject) + switch (reader.CurrentTokenType) { - if (reader.CurrentTokenType != TokenType.FieldName) + case TokenType.StartObject: + return DeserializeInternal(new Dictionary(), TokenType.EndObject, context, ref reader); + case TokenType.StartDocument: + return DeserializeInternal(new Dictionary(), TokenType.EndDocument, context, ref reader); + default: throw new SerializationException( - $"Unexpected token while deserializing field of {typeof(Dictionary)}: {reader.CurrentTokenType}"); - - var fieldName = reader.GetString()!; - reader.Read(); - dict.Add(fieldName, _elemSerializer.Deserialize(context, ref reader)); + $"Unexpected token while deserializing into {typeof(Dictionary)}: {reader.CurrentTokenType}"); } - - return dict; } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) @@ -57,8 +51,39 @@ public override void Serialize(MappingContext context, Utf8FaunaWriter writer, o default: throw new NotImplementedException(); } + } + public object DeserializeDocument(MappingContext context, string? id, string? name, Module? coll, ref Utf8FaunaReader reader) + { + var dict = new Dictionary(); + if (typeof(T) == typeof(object)) + { + if (id != null) dict.Add("id", (T)(object)id); + if (name != null) dict.Add("name", (T)(object)name); + if (coll != null) dict.Add("coll", (T)(object)coll); + } + + return DeserializeInternal(dict, TokenType.EndDocument, context, ref reader); + } + + private Dictionary DeserializeInternal( + Dictionary dict, + TokenType endToken, + MappingContext context, + ref Utf8FaunaReader reader) + { + + while (reader.Read() && reader.CurrentTokenType != endToken) + { + if (reader.CurrentTokenType != TokenType.FieldName) + throw new SerializationException( + $"Unexpected token while deserializing field of {typeof(Dictionary)}: {reader.CurrentTokenType}"); + string fieldName = reader.GetString()!; + reader.Read(); + dict.Add(fieldName, _elemSerializer.Deserialize(context, ref reader)); + } + return dict; } } diff --git a/Fauna/Serialization/DocumentSerializer.cs b/Fauna/Serialization/DocumentSerializer.cs deleted file mode 100644 index 94892505..00000000 --- a/Fauna/Serialization/DocumentSerializer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Types; - -namespace Fauna.Serialization; - -internal class DocumentSerializer : BaseSerializer -{ - - public override Document Deserialize(MappingContext context, ref Utf8FaunaReader reader) - { - return reader.CurrentTokenType switch - { - TokenType.StartRef or TokenType.StartDocument => Deserialize(new InternalDocument(), context, ref reader, EndTokenFor(reader.CurrentTokenType)), - _ => throw new SerializationException( - $"Unexpected token while deserializing into Document: {reader.CurrentTokenType}") - }; - } - - public static Document Deserialize(InternalDocument builder, MappingContext context, ref Utf8FaunaReader reader, TokenType endToken) - { - while (reader.Read() && reader.CurrentTokenType != endToken) - { - if (reader.CurrentTokenType != TokenType.FieldName) - throw new SerializationException( - $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); - - string fieldName = reader.GetString()!; - reader.Read(); - switch (fieldName) - { - case "id": - builder.Id = reader.GetString(); - break; - case "coll": - builder.Coll = reader.GetModule(); - break; - case "ts": - builder.Ts = reader.GetTime(); - break; - case "cause": - builder.Cause = reader.GetString(); - break; - case "exists": - builder.Exists = reader.GetBoolean(); - break; - default: - builder.Data[fieldName] = DynamicSerializer.Singleton.Deserialize(context, ref reader); - break; - } - } - - return (Document)builder.Get(); - } - - public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) - { - switch (o) - { - case null: - writer.WriteNullValue(); - break; - case Document n: - writer.WriteStartRef(); - writer.WriteString("id", n.Id); - writer.WriteModule("coll", n.Collection); - writer.WriteEndRef(); - break; - default: - throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); - } - } -} diff --git a/Fauna/Serialization/DynamicSerializer.cs b/Fauna/Serialization/DynamicSerializer.cs index 0c563f60..e8061d8f 100644 --- a/Fauna/Serialization/DynamicSerializer.cs +++ b/Fauna/Serialization/DynamicSerializer.cs @@ -11,7 +11,7 @@ internal class DynamicSerializer : BaseSerializer private readonly ListSerializer _list; private readonly PageSerializer _page; private readonly DictionarySerializer _dict; - private readonly QuerySerializer _query; + private readonly BaseRefSerializer> _docref; private DynamicSerializer() @@ -19,7 +19,7 @@ private DynamicSerializer() _list = new ListSerializer(this); _page = new PageSerializer(this); _dict = new DictionarySerializer(this); - _query = new QuerySerializer(); + _docref = new BaseRefSerializer>(_dict); } public override object? Deserialize(MappingContext context, ref Utf8FaunaReader reader) => @@ -28,7 +28,7 @@ private DynamicSerializer() TokenType.StartObject => _dict.Deserialize(context, ref reader), TokenType.StartArray => _list.Deserialize(context, ref reader), TokenType.StartPage => _page.Deserialize(context, ref reader), - TokenType.StartRef => DeserializeRefInternal(context, ref reader), + TokenType.StartRef => _docref.Deserialize(context, ref reader), TokenType.StartDocument => DeserializeDocumentInternal(context, ref reader), TokenType.String => reader.GetString(), TokenType.Int => reader.GetInt(), @@ -43,47 +43,9 @@ private DynamicSerializer() $"Unexpected token while deserializing: {reader.CurrentTokenType}"), }; - private static object DeserializeRefInternal(MappingContext context, ref Utf8FaunaReader reader) + private object DeserializeDocumentInternal(MappingContext context, ref Utf8FaunaReader reader) { - reader.Read(); - - if (reader.CurrentTokenType != TokenType.FieldName) - throw new SerializationException( - $"Unexpected token while deserializing @ref: {reader.CurrentTokenType}"); - - string fieldName = reader.GetString()!; - reader.Read(); - - switch (fieldName) - { - case "id": - try - { - return RefSerializer.Deserialize(reader.GetString(), context, ref reader); - } - catch (NullDocumentException e) - { - return new NullDocument(e.Id, null, e.Collection, e.Cause); - } - - case "name": - try - { - return NamedRefSerializer.Deserialize(reader.GetString(), context, ref reader); - } - catch (NullDocumentException e) - { - return new NullDocument(null, e.Name, e.Collection, e.Cause); - } - - default: - throw new SerializationException($"Unexpected field while deserializing @ref: {fieldName}"); - } - } - - private static object DeserializeDocumentInternal(MappingContext context, ref Utf8FaunaReader reader) - { - var builder = new InternalDocument(); + var builder = new BaseRefBuilder>(); while (reader.Read() && reader.CurrentTokenType != TokenType.EndDocument) { if (reader.CurrentTokenType != TokenType.FieldName) @@ -94,56 +56,38 @@ private static object DeserializeDocumentInternal(MappingContext context, ref Ut reader.Read(); - try + switch (fieldName) { - switch (fieldName) - { - // Relies on ordering for doc fields. - case "id": - builder.Id = reader.GetString(); - break; - case "name": - builder.Name = reader.GetString(); - break; - case "coll": - builder.Coll = reader.GetModule(); - - // if we encounter a mapped collection, jump to the class deserializer. - // NB this relies on the fact that docs on the wire always - // start with id and coll. - if (context.TryGetCollection(builder.Coll.Name, out var info)) + // Relies on ordering for doc fields. + case "id": + builder.Id = reader.GetString(); + break; + case "name": + builder.Name = reader.GetString(); + break; + case "coll": + builder.Collection = reader.GetModule(); + + // if we encounter a mapped collection, jump to the class deserializer. + // NB this relies on the fact that docs on the wire always start with id and coll. + if (context.TryGetCollection(builder.Collection.Name, out var info) && info.ClassSerializer is IPartialDocumentSerializer ser) + { + return new BaseRefBuilder { - if (info.ClassSerializer is IClassDocumentSerializer ser) - { - // This assumes ordering on the wire. If name is not null and we're here, then it's a named document so name is a string. - return ser.DeserializeDocument(context, builder.Id, builder.Name, ref reader); - } - } - - if (builder.Id is not null) - { - return DocumentSerializer.Deserialize(builder, context, ref reader, - EndTokenFor(TokenType.StartDocument)); - } - - if (builder.Name is not null) - { - return NamedDocumentSerializer.Deserialize(builder, context, ref reader, - EndTokenFor(TokenType.StartDocument)); - } - - break; - default: - throw new SerializationException($"Unexpected field while deserializing @doc: {fieldName}"); - } - } - catch (NullDocumentException e) - { - return new NullDocument(e.Id, e.Name, e.Collection, e.Cause); + Id = builder.Id, + Name = builder.Name, + Collection = builder.Collection, + Doc = ser.DeserializeDocument(context, builder.Id, builder.Name, builder.Collection, + ref reader) + }.Build(); + } + + builder.Doc = (Dictionary?)_dict.DeserializeDocument(context, builder.Id, builder.Name, builder.Collection, ref reader); + break; } } - throw new SerializationException("Unsupported document"); + return builder.Build(); } /// diff --git a/Fauna/Serialization/IPartialDocumentSerializer.cs b/Fauna/Serialization/IPartialDocumentSerializer.cs new file mode 100644 index 00000000..15269442 --- /dev/null +++ b/Fauna/Serialization/IPartialDocumentSerializer.cs @@ -0,0 +1,9 @@ +using Fauna.Mapping; +using Fauna.Types; + +namespace Fauna.Serialization; + +internal interface IPartialDocumentSerializer : ISerializer +{ + public object DeserializeDocument(MappingContext context, string? id, string? name, Module? coll, ref Utf8FaunaReader reader); +} diff --git a/Fauna/Serialization/InternalDocument.cs b/Fauna/Serialization/InternalDocument.cs deleted file mode 100644 index 374eb994..00000000 --- a/Fauna/Serialization/InternalDocument.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Types; - -namespace Fauna.Serialization; - -public class InternalDocument -{ - public string? Id { get; set; } - public string? Name { get; set; } - public Module? Coll { get; set; } - public bool? Exists { get; set; } - public string? Cause { get; set; } - public DateTime? Ts { get; set; } - public IDictionary Data { get; } = new Dictionary(); - - public object Get() - { - const string unknown = "unknown"; - - if (Exists != null && !Exists.Value) - { - if (Id != null) - { - throw new NullDocumentException(Id, null, Coll ?? new Module(unknown), Cause ?? unknown); - } - - throw new NullDocumentException(null, Name, Coll ?? new Module(unknown), Cause ?? unknown); - } - - if (Id != null && Coll != null && Ts != null) - { - if (Name != null) Data.Add("name", Name); - return new Document(Id, Coll, Ts.Value, Data); - } - - if (Id != null && Coll != null) - { - return new Ref(Id, Coll); - } - - if (Name != null && Coll != null && Ts != null) - { - return new NamedDocument(Name, Coll, Ts.Value, Data); - } - - if (Name != null && Coll != null) - { - return new NamedRef(Name, Coll); - } - - if (Id != null) Data.Add("id", Id); - if (Name != null) Data.Add("name", Name); - if (Coll != null) Data.Add("coll", Coll); - if (Ts != null) Data.Add("ts", Ts.Value); - if (Exists != null) Data.Add("exists", Exists.Value); - if (Cause != null) Data.Add("cause", Cause); - - return Data; - } -} diff --git a/Fauna/Serialization/NamedDocumentSerializer.cs b/Fauna/Serialization/NamedDocumentSerializer.cs deleted file mode 100644 index 31c267c4..00000000 --- a/Fauna/Serialization/NamedDocumentSerializer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Types; - -namespace Fauna.Serialization; - -internal class NamedDocumentSerializer : BaseSerializer -{ - - public override NamedDocument Deserialize(MappingContext context, ref Utf8FaunaReader reader) - { - return reader.CurrentTokenType switch - { - TokenType.StartRef or TokenType.StartDocument => Deserialize(new InternalDocument(), context, ref reader, EndTokenFor(reader.CurrentTokenType)), - _ => throw new SerializationException( - $"Unexpected token while deserializing into NamedDocument: {reader.CurrentTokenType}") - }; - } - - public static NamedDocument Deserialize(InternalDocument builder, MappingContext context, ref Utf8FaunaReader reader, TokenType endToken) - { - while (reader.Read() && reader.CurrentTokenType != endToken) - { - if (reader.CurrentTokenType != TokenType.FieldName) - throw new SerializationException( - $"Unexpected token while deserializing into NamedDocument: {reader.CurrentTokenType}"); - - string fieldName = reader.GetString()!; - reader.Read(); - switch (fieldName) - { - case "name": - builder.Name = reader.GetString(); - break; - case "coll": - builder.Coll = reader.GetModule(); - break; - case "ts": - builder.Ts = reader.GetTime(); - break; - case "cause": - builder.Cause = reader.GetString(); - break; - case "exists": - builder.Exists = reader.GetBoolean(); - break; - default: - builder.Data[fieldName] = DynamicSerializer.Singleton.Deserialize(context, ref reader); - break; - } - } - - return (NamedDocument)builder.Get(); - } - - public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) - { - switch (o) - { - case null: - writer.WriteNullValue(); - break; - case NamedDocument n: - writer.WriteStartRef(); - writer.WriteString("name", n.Name); - writer.WriteModule("coll", n.Collection); - writer.WriteEndRef(); - break; - default: - throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); - } - } -} diff --git a/Fauna/Serialization/NamedRefSerializer.cs b/Fauna/Serialization/NamedRefSerializer.cs deleted file mode 100644 index f81c63e4..00000000 --- a/Fauna/Serialization/NamedRefSerializer.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Types; - -namespace Fauna.Serialization; - -internal class NamedRefSerializer : BaseSerializer -{ - - public override NamedRef Deserialize(MappingContext context, ref Utf8FaunaReader reader) - { - return reader.CurrentTokenType switch - { - TokenType.StartRef => DeserializeInternal(new InternalDocument(), context, ref reader), - _ => throw new SerializationException( - $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}") - }; - } - - public static NamedRef Deserialize(string? name, MappingContext context, ref Utf8FaunaReader reader) - { - InternalDocument builder = new() { Name = name }; - return DeserializeInternal(builder, context, ref reader); - } - - public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) - { - switch (o) - { - case null: - writer.WriteNullValue(); - break; - case NamedRef n: - writer.WriteStartRef(); - writer.WriteString("name", n.Name); - writer.WriteModule("coll", n.Collection); - writer.WriteEndRef(); - break; - default: - throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); - } - } - - private static NamedRef DeserializeInternal(InternalDocument builder, MappingContext context, ref Utf8FaunaReader reader) - { - while (reader.Read() && reader.CurrentTokenType != TokenType.EndRef) - { - if (reader.CurrentTokenType != TokenType.FieldName) - throw new SerializationException( - $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); - - string fieldName = reader.GetString()!; - reader.Read(); - switch (fieldName) - { - case "name": - builder.Name = reader.GetString(); - break; - case "coll": - builder.Coll = reader.GetModule(); - break; - case "cause": - builder.Cause = reader.GetString(); - break; - case "exists": - builder.Exists = reader.GetBoolean(); - break; - default: - throw new SerializationException( - $"Unexpected field while deserializing into NamedRef: {fieldName}"); - } - } - - return (NamedRef)builder.Get(); - } - -} diff --git a/Fauna/Serialization/NullableDocumentSerializer.cs b/Fauna/Serialization/NullableDocumentSerializer.cs deleted file mode 100644 index 342e177e..00000000 --- a/Fauna/Serialization/NullableDocumentSerializer.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Types; - -namespace Fauna.Serialization; - -internal class NullableDocumentSerializer : BaseSerializer> where T : class -{ - private readonly ISerializer _valueSerializer; - - public NullableDocumentSerializer(ISerializer valueSerializer) - { - _valueSerializer = valueSerializer; - } - - public override NullableDocument Deserialize(MappingContext context, ref Utf8FaunaReader reader) - { - if (reader.CurrentTokenType is not (TokenType.StartObject or TokenType.StartRef or TokenType.StartDocument)) - throw new SerializationException( - $"Unexpected token while deserializing into {typeof(NullableDocument)}: {reader.CurrentTokenType}"); - - try - { - var val = _valueSerializer.Deserialize(context, ref reader); - return new NonNullDocument(val); - } - catch (NullDocumentException e) - { - return new NullDocument(e.Id, e.Name, e.Collection, e.Cause); - } - } - - public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) - { - switch (o) - { - case null: - writer.WriteNullValue(); - break; - case NonNullDocument n: - _valueSerializer.Serialize(context, writer, n.Value); - break; - case NullDocument n: - if (n.Id != null) - { - writer.WriteStartRef(); - writer.WriteString("id", n.Id); - writer.WriteModule("coll", n.Collection); - writer.WriteEndRef(); - } - else - { - writer.WriteStartRef(); - writer.WriteString("name", n.Name!); - writer.WriteModule("coll", n.Collection); - writer.WriteEndRef(); - } - break; - default: - throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); - } - } -} diff --git a/Fauna/Serialization/RefSerializer.cs b/Fauna/Serialization/RefSerializer.cs deleted file mode 100644 index 5a4ea9ec..00000000 --- a/Fauna/Serialization/RefSerializer.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Fauna.Exceptions; -using Fauna.Mapping; -using Fauna.Types; - -namespace Fauna.Serialization; - -internal class RefSerializer : BaseSerializer -{ - - public override Ref Deserialize(MappingContext context, ref Utf8FaunaReader reader) - { - return reader.CurrentTokenType switch - { - TokenType.StartRef => DeserializeInternal(new InternalDocument(), context, ref reader), - _ => throw new SerializationException( - $"Unexpected token while deserializing into Ref: {reader.CurrentTokenType}") - }; - } - - public static Ref Deserialize(string? id, MappingContext context, ref Utf8FaunaReader reader) - { - InternalDocument builder = new() { Id = id }; - return DeserializeInternal(builder, context, ref reader); - } - - public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) - { - switch (o) - { - case null: - writer.WriteNullValue(); - break; - case Ref n: - writer.WriteStartRef(); - writer.WriteString("id", n.Id); - writer.WriteModule("coll", n.Collection); - writer.WriteEndRef(); - break; - default: - throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); - } - } - - private static Ref DeserializeInternal(InternalDocument builder, MappingContext context, ref Utf8FaunaReader reader) - { - while (reader.Read() && reader.CurrentTokenType != TokenType.EndRef) - { - if (reader.CurrentTokenType != TokenType.FieldName) - throw new SerializationException( - $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); - - string fieldName = reader.GetString()!; - reader.Read(); - switch (fieldName) - { - case "id": - builder.Id = reader.GetString(); - break; - case "coll": - builder.Coll = reader.GetModule(); - break; - case "cause": - builder.Cause = reader.GetString(); - break; - case "exists": - builder.Exists = reader.GetBoolean(); - break; - default: - throw new SerializationException( - $"Unexpected field while deserializing into NamedRef: {fieldName}"); - } - } - - return (Ref)builder.Get(); - } -} diff --git a/Fauna/Serialization/Serializer.cs b/Fauna/Serialization/Serializer.cs index 8ec10e87..a41e6652 100644 --- a/Fauna/Serialization/Serializer.cs +++ b/Fauna/Serialization/Serializer.cs @@ -39,10 +39,6 @@ public static class Serializer private static readonly BooleanSerializer s_bool = new(); private static readonly ModuleSerializer s_module = new(); private static readonly StreamSerializer s_stream = new(); - private static readonly DocumentSerializer s_doc = new(); - private static readonly NamedDocumentSerializer s_namedDoc = new(); - private static readonly RefSerializer s_docRef = new(); - private static readonly NamedRefSerializer s_namedRef = new(); private static readonly QuerySerializer s_query = new(); private static readonly QueryExprSerializer s_queryExpr = new(); private static readonly QueryLiteralSerializer s_queryLiteral = new(); @@ -97,10 +93,6 @@ public static ISerializer Generate(MappingContext context, Type targetType) if (targetType == typeof(bool)) return s_bool; if (targetType == typeof(Module)) return s_module; if (targetType == typeof(Stream)) return s_stream; - if (targetType == typeof(Document)) return s_doc; - if (targetType == typeof(NamedDocument)) return s_namedDoc; - if (targetType == typeof(Ref)) return s_docRef; - if (targetType == typeof(NamedRef)) return s_namedRef; if (targetType == typeof(Query)) return s_query; if (targetType == typeof(QueryExpr)) return s_queryExpr; if (targetType == typeof(QueryLiteral)) return s_queryLiteral; @@ -125,17 +117,6 @@ public static ISerializer Generate(MappingContext context, Type targetType) throw new ArgumentException($"Unsupported nullable type. Generic arguments > 1: {args}"); } - if (targetType.GetGenericTypeDefinition() == typeof(NullableDocument<>) || - targetType.GetGenericTypeDefinition() == typeof(NonNullDocument<>) || - targetType.GetGenericTypeDefinition() == typeof(NullDocument<>)) - { - var argTypes = targetType.GetGenericArguments(); - var valueType = argTypes[0]; - var valueSerializer = Generate(context, valueType); - var serType = typeof(NullableDocumentSerializer<>).MakeGenericType(new[] { valueType }); - object? ser = Activator.CreateInstance(serType, valueSerializer); - return (ISerializer)ser!; - } if (targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { @@ -177,6 +158,39 @@ public static ISerializer Generate(MappingContext context, Type targetType) return (ISerializer)ser!; } + if (targetType.GetGenericTypeDefinition() == typeof(BaseRef<>)) + { + var docType = targetType.GetGenericArguments()[0]; + var docSerializer = Generate(context, docType); + + var serType = typeof(BaseRefSerializer<>).MakeGenericType(new[] { docType }); + object? ser = Activator.CreateInstance(serType, new[] { docSerializer }); + + return (ISerializer)ser!; + } + + if (targetType.GetGenericTypeDefinition() == typeof(Ref<>)) + { + var docType = targetType.GetGenericArguments()[0]; + var docSerializer = Generate(context, docType); + + var serType = typeof(RefSerializer<>).MakeGenericType(new[] { docType }); + object? ser = Activator.CreateInstance(serType, new[] { docSerializer }); + + return (ISerializer)ser!; + } + + if (targetType.GetGenericTypeDefinition() == typeof(NamedRef<>)) + { + var docType = targetType.GetGenericArguments()[0]; + var docSerializer = Generate(context, docType); + + var serType = typeof(NamedRefSerializer<>).MakeGenericType(new[] { docType }); + object? ser = Activator.CreateInstance(serType, new[] { docSerializer }); + + return (ISerializer)ser!; + } + if (targetType.IsGenericType && targetType.Name.Contains("AnonymousType")) { return DynamicSerializer.Singleton; diff --git a/Fauna/Serialization/Utf8FaunaReader.cs b/Fauna/Serialization/Utf8FaunaReader.cs index 0a237bb4..ff638dee 100644 --- a/Fauna/Serialization/Utf8FaunaReader.cs +++ b/Fauna/Serialization/Utf8FaunaReader.cs @@ -633,13 +633,13 @@ private void HandleEndObject() /// /// Method HandleTaggedString is used to advance through a JSON object that represents a tagged type with a /// a string value. For example: - /// + /// /// * Given { "@int": "123" } /// * Read JSON until JsonTokenType.PropertyName and you've determined it's an int /// * Call HandleTaggedString(TokenType.Int) /// * The underlying JSON reader is advanced until JsonTokenType.EndObject /// * Access the int via GetInt() - /// + /// /// private void HandleTaggedString(TokenType token) { diff --git a/Fauna/Types/BaseDocument.cs b/Fauna/Types/BaseDocument.cs deleted file mode 100644 index 3a28e823..00000000 --- a/Fauna/Types/BaseDocument.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections; - -namespace Fauna.Types; - -/// -/// Represents the base structure of a document. -/// -public class BaseDocument : IReadOnlyDictionary -{ - private readonly Dictionary _data; - - /// - /// Gets the timestamp of the document. - /// - public DateTime Ts { get; } - - /// - /// Gets the collection to which the document belongs. - /// - public Module Collection { get; } - - /// - /// Initializes a new instance of the class with specified collection and timestamp. - /// - /// The collection to which the document belongs. - /// The timestamp of the document. - public BaseDocument(Module coll, DateTime ts) - { - Ts = ts; - Collection = coll; - _data = new Dictionary(); - } - - /// - /// Initializes a new instance of the class with specified collection, timestamp, and initial data. - /// - /// The collection to which the document belongs. - /// The timestamp of the document. - /// Initial data for the document in key-value pairs. - public BaseDocument(Module coll, DateTime ts, IDictionary data) - { - Ts = ts; - Collection = coll; - _data = new Dictionary(data); - } - - /// Returns an enumerator that iterates through the data of the document. - /// An enumerator for the data of the document. - public IEnumerator> GetEnumerator() - { - return _data.GetEnumerator(); - } - - /// Returns an enumerator that iterates through the data of the document. - /// An enumerator for the data of the document. - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Gets the count of key-value pairs contained in the document. - /// - /// The number of key-value pairs. - public int Count => _data.Count; - - - /// Determines whether the Document contains the specified key. - /// The key to locate in the Document. - /// - /// is . - /// - /// if the Document contains an element with the specified key; otherwise, . - public bool ContainsKey(string key) - { - return _data.ContainsKey(key); - } - - /// Gets the value associated with the specified key. - /// The key of the value to get. - /// When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. - /// - /// is . - /// - /// if the Document contains an element with the specified key; otherwise, . - public bool TryGetValue(string key, out object? value) - { - return _data.TryGetValue(key, out value); - } - - /// - /// Gets the value associated with the specified key in the document. - /// - /// The key of the value to get. - /// The value associated with the specified key. - public object? this[string key] => _data[key]; - - /// Gets a collection containing the keys of the data in the document. - /// A collection containing the keys of the data in the document. - public IEnumerable Keys => _data.Keys; - - /// Gets a collection containing the values, excluding properties, of the Document. - /// A collection containing the values, excluding properties, of the Document. - public IEnumerable Values => _data.Values; -} diff --git a/Fauna/Types/BaseRef.cs b/Fauna/Types/BaseRef.cs new file mode 100644 index 00000000..549f4cfa --- /dev/null +++ b/Fauna/Types/BaseRef.cs @@ -0,0 +1,82 @@ +using Fauna.Linq; + +namespace Fauna.Types; + +public abstract class BaseRef +{ + + /// + /// Gets the materialized document represented by the Ref. Is null unless IsLoaded is true + /// and Exists is true. + /// + protected readonly T? Doc; + + /// + /// Gets the cause when exists is false. Is null unless IsLoaded is true and Exists is false. + /// + public string? Cause { get; } + + /// + /// Gets the collection to which the ref belongs. + /// + public Module Collection { get; } + + /// + /// Gets a boolean indicating whether the doc exists. Is null unless IsLoaded is true. + /// + public bool? Exists { get; } + + /// + /// Gets a boolean indicating whether the document represented by the ref has been loaded. + /// + public bool IsLoaded { get; } = false; + + public BaseRef(DataContext.ICollection col) + { + Collection = new Module(col.Name); + } + + + public BaseRef(DataContext.ICollection col, T doc) + { + Collection = new Module(col.Name); + Doc = doc; + IsLoaded = true; + Exists = true; + } + + public BaseRef(DataContext.ICollection col, string cause) + { + Collection = new Module(col.Name); + Exists = false; + Cause = cause; + IsLoaded = true; + } + + public BaseRef(Module coll) + { + Collection = coll; + } + + public BaseRef(Module coll, T doc) + { + Collection = coll; + Exists = true; + Doc = doc; + IsLoaded = true; + } + + public BaseRef(Module coll, string cause) + { + Collection = coll; + Exists = false; + Cause = cause; + IsLoaded = true; + } + + public abstract T Get(); +} + +internal class UnloadedRefException : Exception +{ +} diff --git a/Fauna/Types/BaseRefBuilder.cs b/Fauna/Types/BaseRefBuilder.cs new file mode 100644 index 00000000..b8ebe2da --- /dev/null +++ b/Fauna/Types/BaseRefBuilder.cs @@ -0,0 +1,33 @@ + +namespace Fauna.Types; + +public class BaseRefBuilder +{ + public string? Id { get; set; } + public string? Name { get; set; } + public Module? Collection { get; set; } + public string? Cause { get; set; } + public bool? Exists { get; set; } + public T? Doc { get; set; } + + public BaseRef Build() + { + if (Collection is null) throw new ArgumentNullException(nameof(Collection)); + + if (Id is not null) + { + if (Exists != null && !Exists.Value) return new Ref(Id, Collection, Cause ?? ""); + if (Doc != null) return new Ref(Id, Collection, Doc); + return new Ref(Id, Collection); + } + + if (Name is not null) + { + if (Exists != null && !Exists.Value) return new NamedRef(Name, Collection, Cause ?? ""); + if (Doc != null) return new NamedRef(Name, Collection, Doc); + return new NamedRef(Name, Collection); + } + + throw new ArgumentException("Id and Name cannot both be null"); + } +} diff --git a/Fauna/Types/Document.cs b/Fauna/Types/Document.cs deleted file mode 100644 index 36a81f2c..00000000 --- a/Fauna/Types/Document.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Fauna.Types; - -/// -/// Represents a document. -/// -public sealed class Document : BaseDocument -{ - - /// - /// Gets the string value of the document id. - /// - public string Id { get; } - - /// - /// Initializes a new instance of the Document class with the specified id, coll, and ts. - /// - /// The string value of the document id. - /// The module to which the document belongs. - /// The timestamp of the document. - public Document(string id, Module coll, DateTime ts) : base(coll, ts) - { - Id = id; - } - - /// - /// Initializes a new instance of the Document class with the specified id, coll, ts, and additional data stored - /// as key/value pairs on the instance. - /// - /// The string value of the document id. - /// The module to which the document belongs. - /// The timestamp of the document. - /// Additional data on the document. - public Document(string id, Module coll, DateTime ts, IDictionary data) : base(coll, ts, data) - { - Id = id; - } -} diff --git a/Fauna/Types/NamedDocument.cs b/Fauna/Types/NamedDocument.cs deleted file mode 100644 index 39341fb7..00000000 --- a/Fauna/Types/NamedDocument.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Fauna.Types; - -/// -/// Represents a document that has a "name" instead of an "id". For example, a Role document is represented as a -/// NamedDocument. -/// -public sealed class NamedDocument : BaseDocument -{ - - /// - /// Gets the string value of the document name. - /// - public string Name { get; } - - /// - /// Initializes a new instance of the NamedDocument class with the specified name, coll, and ts. - /// - /// The string value of the document name. - /// The module to which the document belongs. - /// The timestamp of the document. - public NamedDocument(string name, Module coll, DateTime ts) : base(coll, ts) - { - Name = name; - } - - /// - /// Initializes a new instance of the NamedDocument class with the specified name, coll, ts, and additional data stored - /// as key/value pairs on the instance. - /// - /// The string value of the document name. - /// The module to which the document belongs. - /// The timestamp of the document. - /// Additional data on the document. - public NamedDocument(string name, Module coll, DateTime ts, IDictionary data) : base(coll, ts, data) - { - Name = name; - } -} diff --git a/Fauna/Types/NamedRef.cs b/Fauna/Types/NamedRef.cs index f31d5159..07f374e1 100644 --- a/Fauna/Types/NamedRef.cs +++ b/Fauna/Types/NamedRef.cs @@ -1,24 +1,54 @@ +using Fauna.Exceptions; +using Fauna.Linq; + namespace Fauna.Types; + /// /// Represents a document ref that has a "name" instead of an "id". For example, a Role document reference is -/// represented as a NamedDocumentRef. +/// represented as a NamedRef. /// -public class NamedRef +public class NamedRef : BaseRef { - public NamedRef(string name, Module collection) - { - Name = name; - Collection = collection; - } - /// /// Gets the string value of the ref name. /// public string Name { get; } - /// - /// Gets the collection to which the ref belongs. - /// - public Module Collection { get; } + public NamedRef(string name, DataContext.ICollection col) : base(col) + { + Name = name; + } + + public NamedRef(string name, DataContext.ICollection col, T doc) : base(col, doc) + { + Name = name; + } + + public NamedRef(string name, DataContext.ICollection col, string cause) : base(col, cause) + { + Name = name; + } + + public NamedRef(string name, Module col) : base(col) + { + Name = name; + } + + public NamedRef(string name, Module col, string cause) : base(col, cause) + { + Name = name; + } + + public NamedRef(string name, Module col, T doc) : base(col, doc) + { + Name = name; + } + + public override T Get() + { + if (!IsLoaded) throw new UnloadedRefException(); + if (Exists.HasValue && !Exists.Value) throw new NullDocumentException(null, Name, Collection, Cause ?? ""); + return Doc!; + } } diff --git a/Fauna/Types/NullableDocument.cs b/Fauna/Types/NullableDocument.cs deleted file mode 100644 index f53763b3..00000000 --- a/Fauna/Types/NullableDocument.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Fauna.Types; - -/// -/// A wrapper class that allows and user-defined classes -/// to be null references. -/// -/// -public abstract class NullableDocument -{ - /// - /// The wrapped value. - /// - public T? Value { get; } - - public NullableDocument(T? value) - { - Value = value; - } -} - -/// -/// A class representing a null document returned by Fauna. -/// -/// -public class NullDocument : NullableDocument -{ - /// - /// The ID of the null document. - /// - public string? Id { get; } - - /// - /// The Name of the null document if it's a named document. - /// - public string? Name { get; } - - /// - /// The Collection. - /// - public Module Collection { get; } - - /// - /// The Cause for the null document. - /// - public string Cause { get; } - - /// - /// Whether the NullDocument is Named. - /// - public bool IsNamed { get; } - - public NullDocument(string? id, string? name, Module collection, string cause) : base(default) - { - if (id != null && name != null) throw new ArgumentException("Provide an id or a name, but not both."); - - Id = id; - Name = name; - Collection = collection; - Cause = cause; - } -} - - -/// -/// A class wrapping a non-null document returned by Fauna. -/// -/// -public class NonNullDocument : NullableDocument -{ - public NonNullDocument(T value) : base(value) - { - } -} diff --git a/Fauna/Types/Ref.cs b/Fauna/Types/Ref.cs index dc6b05fd..916ece41 100644 --- a/Fauna/Types/Ref.cs +++ b/Fauna/Types/Ref.cs @@ -1,23 +1,53 @@ +using Fauna.Exceptions; +using Fauna.Linq; + namespace Fauna.Types; + /// /// Represents a document ref. /// -public class Ref +public class Ref : BaseRef { - public Ref(string id, Module collection) + /// + /// Gets the string value of the ref ID. + /// + public string Id { get; } + + public Ref(string id, DataContext.ICollection col) : base(col) { Id = id; - Collection = collection; } - /// - /// Gets the string value of the ref id. - /// - public string Id { get; } + public Ref(string id, DataContext.ICollection col, T doc) : base(col, doc) + { + Id = id; + } - /// - /// Gets the collection to which the ref belongs. - /// - public Module Collection { get; } + public Ref(string id, DataContext.ICollection col, string cause) : base(col, cause) + { + Id = id; + } + + public Ref(string id, Module col) : base(col) + { + Id = id; + } + + public Ref(string id, Module col, string cause) : base(col, cause) + { + Id = id; + } + + public Ref(string id, Module col, T doc) : base(col, doc) + { + Id = id; + } + + public override T Get() + { + if (!IsLoaded) throw new UnloadedRefException(); + if (Exists.HasValue && !Exists.Value) throw new NullDocumentException(Id, null, Collection, Cause ?? ""); + return Doc!; + } } diff --git a/README.md b/README.md index 16982607..6c08f2ae 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,9 @@ Fauna.Mapping.Attributes and the Fauna.DataContext class provide the ability to You can use attributes to map a POCO class to a Fauna document or object shape: -`[Id]`: Should only be used once per class and be associated with a field named `Id` that represents the Fauna document ID. It's not encoded unless the isClientGenerated flag is true. -`[Ts]`: Should only be used once per class and be associated with a field named `Ts` that represents the timestamp of a document. It's not encoded. -`[Coll]`: Typically goes unmodeled. Should only be used once per class and be associated with a field named `Coll` that represents the collection field of a document. It will never be encoded. +`[Id]`: Should only be used once per class on a field that represents the Fauna document ID. It's not encoded unless the isClientGenerated flag is true. +`[Ts]`: Should only be used once per class on a field that represents the timestamp of a document. It's not encoded. +`[Collection]`: Typically goes unmodeled. Should only be used once per class on a field that represents the collection field of a document. It will never be encoded. `[Field]`: Can be associated with any field to override its name in Fauna. `[Ignore]`: Can be used to ignore fields during encoding and decoding. @@ -263,30 +263,29 @@ A null document ([NullDoc](https://docs.fauna.com/fauna/current/reference/fql_re Option 1, you can let the driver throw an exception and do something with it. ```csharp try { - await client.QueryAsync(FQL($"Collection.byName('Fake')")) + await client.QueryAsync(FQL($"SomeColl.byId('123')")) } catch (NullDocumentException e) { - Console.WriteLine(e.Id); // "Fake" - Console.WriteLine(e.Collection.Name); // "Collection" + Console.WriteLine(e.Id); // "123" + Console.WriteLine(e.Collection.Name); // "SomeColl" Console.WriteLine(e.Cause); // "not found" } ``` -Option 2, you wrap your expected type in a NullableDocument<>. You can wrap Document, NamedDocument, DocumentRef, NamedDocumentRef, and POCOs. +Option 2, you wrap your expected type in a Ref<> or NamedRef<>. Supported types are Dictionary and POCOs. ```csharp var q = FQL($"Collection.byName('Fake')"); -var r = await client.QueryAsync>(q); -switch (r.Data) -{ - case NullDocument d: - // Handle the null document case - Console.WriteLine(d.Id); // "Fake" - Console.WriteLine(d.Collection.Name); // "Collection" - Console.WriteLine(d.Cause); // "not found" - break; - case NonNullDocument d: - var doc = d.Value!; // NamedDocument - break; +var r = (await client.QueryAsync>>(q)).Data; +if (r.Data.Exists) { + Console.WriteLine(d.Id); // "Fake" + Console.WriteLine(d.Collection.Name); // "Collection" + var doc = r.Get(); // A dictionary with id, coll, ts, and any user-defined fields. +} else { + Console.WriteLine(d.Name); // "Fake" + Console.WriteLine(d.Collection.Name); // "Collection" + Console.WriteLine(d.Cause); // "not found" + r.Get() // this throws a NullDocumentException } + ``` ## Event Streaming From 6f721234e8bb7db6229d6bb4152e71a995945d69 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Wed, 9 Oct 2024 16:59:51 +0200 Subject: [PATCH 2/3] fix: peg gh action to ubuntu 22.04 --- .github/workflows/6.0.x/global.json | 6 ++++++ .github/workflows/8.0.x/global.json | 6 ++++++ .github/workflows/pr_validate.yml | 5 ++--- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/6.0.x/global.json create mode 100644 .github/workflows/8.0.x/global.json diff --git a/.github/workflows/6.0.x/global.json b/.github/workflows/6.0.x/global.json new file mode 100644 index 00000000..2a10edb1 --- /dev/null +++ b/.github/workflows/6.0.x/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "6.0.x", + "rollForward": "latestFeature" + } +} diff --git a/.github/workflows/8.0.x/global.json b/.github/workflows/8.0.x/global.json new file mode 100644 index 00000000..619ea7d9 --- /dev/null +++ b/.github/workflows/8.0.x/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.x", + "rollForward": "latestFeature" + } +} diff --git a/.github/workflows/pr_validate.yml b/.github/workflows/pr_validate.yml index 66b1ea53..3feff46d 100644 --- a/.github/workflows/pr_validate.yml +++ b/.github/workflows/pr_validate.yml @@ -9,7 +9,7 @@ on: jobs: dotnet-test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: dotnet-version: [ '6.0.x', '8.0.x' ] @@ -23,10 +23,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup dotnet ${{ matrix.dotnet-version }} - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.dotnet-version }} - - name: Restore dependencies run: dotnet restore - name: Build From 7980e2fee9c712926de914b9a89b5066b5857126 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Wed, 9 Oct 2024 17:23:36 +0200 Subject: [PATCH 3/3] PR feedback --- .github/workflows/6.0.x/global.json | 6 ------ .github/workflows/8.0.x/global.json | 6 ------ .../Serialization/Serializers/BaseRefSerializer.Tests.cs | 1 - .../Serialization/Serializers/DynamicSerializer.Tests.cs | 2 -- Fauna/Types/BaseRefBuilder.cs | 1 - 5 files changed, 16 deletions(-) delete mode 100644 .github/workflows/6.0.x/global.json delete mode 100644 .github/workflows/8.0.x/global.json diff --git a/.github/workflows/6.0.x/global.json b/.github/workflows/6.0.x/global.json deleted file mode 100644 index 2a10edb1..00000000 --- a/.github/workflows/6.0.x/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "6.0.x", - "rollForward": "latestFeature" - } -} diff --git a/.github/workflows/8.0.x/global.json b/.github/workflows/8.0.x/global.json deleted file mode 100644 index 619ea7d9..00000000 --- a/.github/workflows/8.0.x/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.x", - "rollForward": "latestFeature" - } -} diff --git a/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs index beeea116..2e88e99d 100644 --- a/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs +++ b/Fauna.Test/Serialization/Serializers/BaseRefSerializer.Tests.cs @@ -1,4 +1,3 @@ - using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; diff --git a/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs b/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs index aa1f364d..f2640d62 100644 --- a/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs +++ b/Fauna.Test/Serialization/Serializers/DynamicSerializer.Tests.cs @@ -1,5 +1,3 @@ - -using System.Reflection.Metadata; using Fauna.Mapping; using Fauna.Serialization; using Fauna.Types; diff --git a/Fauna/Types/BaseRefBuilder.cs b/Fauna/Types/BaseRefBuilder.cs index b8ebe2da..4492585a 100644 --- a/Fauna/Types/BaseRefBuilder.cs +++ b/Fauna/Types/BaseRefBuilder.cs @@ -1,4 +1,3 @@ - namespace Fauna.Types; public class BaseRefBuilder