From 231485d0182e6a61054d24b7bc333a51f52719e6 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Tue, 26 Nov 2024 19:25:14 +0000 Subject: [PATCH 1/3] Partial implementation of GenericDocument response object - Requires JsonConverter to be completed --- .../Converters/DocumentGenericConverter.cs | 229 ++++++++++++++++++ .../Responses/Document.cs | 14 +- .../Responses/DocumentBase.cs | 26 ++ .../Responses/DoocumentGeneric.cs | 28 +++ 4 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs create mode 100644 src/PinguApps.Appwrite.Shared/Responses/DocumentBase.cs create mode 100644 src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs diff --git a/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs new file mode 100644 index 00000000..d4361b09 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Responses; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Converters; + +public class DocumentGenericConverter : JsonConverter> + where TData : class, new() +{ + public override Doocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? id = null; + string? collectionId = null; + string? databaseId = null; + DateTime? createdAt = null; + DateTime? updatedAt = null; + List? permissions = null; + TData data = new(); + + var dataProperties = new Dictionary(); + + var dateTimeConverter = new MultiFormatDateTimeConverter(); + var permissionListConverter = new PermissionListConverter(); + + if (reader.TokenType is not JsonTokenType.StartObject) + { + throw new JsonException("Expected StartObject token"); + } + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + string propertyName = reader.GetString()!; + + reader.Read(); + + switch (propertyName) + { + case "$id": + id = reader.GetString(); + break; + case "$collectionId": + collectionId = reader.GetString(); + break; + case "$databaseId": + databaseId = reader.GetString(); + break; + case "$createdAt": + createdAt = dateTimeConverter.Read(ref reader, typeof(DateTime), options); + break; + case "$updatedAt": + updatedAt = dateTimeConverter.Read(ref reader, typeof(DateTime), options); + break; + case "$permissions": + permissions = permissionListConverter.Read(ref reader, typeof(List), options); + break; + default: + var value = ReadValue(ref reader, options); + dataProperties[propertyName] = value; + break; + } + } + + if (id is null) + { + throw new JsonException("Unable to find a value for Id"); + } + + if (collectionId is null) + { + throw new JsonException("Unable to find a value for CollectionId"); + } + + if (databaseId is null) + { + throw new JsonException("Unable to find a value for DatabaseId"); + } + + if (createdAt is null) + { + throw new JsonException("Unable to find a value for CreatedAt"); + } + + if (updatedAt is null) + { + throw new JsonException("Unable to find a value for UpdatedAt"); + } + + if (permissions is null) + { + throw new JsonException("Unable to find a value for Permissions"); + } + + // Deserialize the remaining properties into TData + var dataJson = JsonSerializer.Serialize(dataProperties, options); + data = JsonSerializer.Deserialize(dataJson, options) ?? new TData(); + + return new Doocument(id, collectionId, databaseId, createdAt.Value, updatedAt.Value, permissions, data); + } + + internal object? ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + var str = reader.GetString(); + + if ((DateTime.TryParse(str, out var dateTime))) + { + return dateTime; + } + return str; + + case JsonTokenType.Number: + if (reader.TryGetInt64(out var longValue)) + { + return longValue; + } + return reader.GetSingle(); + + case JsonTokenType.True: + case JsonTokenType.False: + return reader.GetBoolean(); + + case JsonTokenType.Null: + return null; + + case JsonTokenType.StartArray: + return ReadArray(ref reader, options); + + case JsonTokenType.StartObject: + return ReadObject(ref reader, options); + + default: + throw new JsonException($"Unsupported token type: {reader.TokenType}"); + } + } + + private IReadOnlyCollection ReadArray(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var list = new List(); + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndArray) + { + break; + } + + var item = ReadValue(ref reader, options); + list.Add(item); + } + + return list; + } + + private Dictionary ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var dict = new Dictionary(); + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + break; + } + + var propertyName = reader.GetString()!; + + reader.Read(); + + var value = ReadValue(ref reader, options); + + dict[propertyName] = value; + } + + return dict; + } + + public override void Write(Utf8JsonWriter writer, Doocument value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WriteString("$id", value.Id); + writer.WriteString("$collectionId", value.CollectionId); + writer.WriteString("$databaseId", value.DatabaseId); + + // Use MultiFormatDateTimeConverter for DateTime properties + var dateTimeConverter = new MultiFormatDateTimeConverter(); + + writer.WritePropertyName("$createdAt"); + dateTimeConverter.Write(writer, value.CreatedAt, options); + + writer.WritePropertyName("$updatedAt"); + dateTimeConverter.Write(writer, value.UpdatedAt, options); + + writer.WritePropertyName("$permissions"); + JsonSerializer.Serialize(writer, value.Permissions, options); + + // Serialize the data object into individual properties. + var dataProperties = JsonSerializer.SerializeToElement(value.Data, options); + foreach (var property in dataProperties.EnumerateObject()) + { + writer.WritePropertyName(property.Name); + WriteValue(writer, property, options); + } + + writer.WriteEndObject(); + } + + private void WriteValue(Utf8JsonWriter writer, JsonProperty property, JsonSerializerOptions options) + { + // Handle null values + if (property.Value.ValueKind is JsonValueKind.Null) + { + writer.WriteNullValue(); + return; + } + + // TODO - Complete the implementation using DocumentConverter as a guide + } +} diff --git a/src/PinguApps.Appwrite.Shared/Responses/Document.cs b/src/PinguApps.Appwrite.Shared/Responses/Document.cs index 5e822dd3..0723c37b 100644 --- a/src/PinguApps.Appwrite.Shared/Responses/Document.cs +++ b/src/PinguApps.Appwrite.Shared/Responses/Document.cs @@ -19,14 +19,14 @@ namespace PinguApps.Appwrite.Shared.Responses; /// Document data [JsonConverter(typeof(DocumentConverter))] public record Document( - [property: JsonPropertyName("$id")] string Id, - [property: JsonPropertyName("$collectionId")] string CollectionId, - [property: JsonPropertyName("$databaseId")] string DatabaseId, - [property: JsonPropertyName("$createdAt"), JsonConverter(typeof(MultiFormatDateTimeConverter))] DateTime CreatedAt, - [property: JsonPropertyName("$updatedAt"), JsonConverter(typeof(MultiFormatDateTimeConverter))] DateTime UpdatedAt, - [property: JsonPropertyName("$permissions"), JsonConverter(typeof(PermissionReadOnlyListConverter))] IReadOnlyList Permissions, + string Id, + string CollectionId, + string DatabaseId, + DateTime CreatedAt, + DateTime UpdatedAt, + IReadOnlyList Permissions, [property: JsonExtensionData] Dictionary Data -) +) : DocumentBase(Id, CollectionId, DatabaseId, CreatedAt, UpdatedAt, Permissions) { /// /// Extract document data by key diff --git a/src/PinguApps.Appwrite.Shared/Responses/DocumentBase.cs b/src/PinguApps.Appwrite.Shared/Responses/DocumentBase.cs new file mode 100644 index 00000000..15ea69c7 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Responses/DocumentBase.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Converters; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Responses; + +/// +/// An Appwrite Document object +/// +/// Document ID +/// Collection ID +/// Database ID +/// Document creation date in ISO 8601 format +/// Document update date in ISO 8601 format +/// Document permissions. Learn more about permissions +[JsonConverter(typeof(DocumentConverter))] +public abstract record DocumentBase( + [property: JsonPropertyName("$id")] string Id, + [property: JsonPropertyName("$collectionId")] string CollectionId, + [property: JsonPropertyName("$databaseId")] string DatabaseId, + [property: JsonPropertyName("$createdAt"), JsonConverter(typeof(MultiFormatDateTimeConverter))] DateTime CreatedAt, + [property: JsonPropertyName("$updatedAt"), JsonConverter(typeof(MultiFormatDateTimeConverter))] DateTime UpdatedAt, + [property: JsonPropertyName("$permissions"), JsonConverter(typeof(PermissionReadOnlyListConverter))] IReadOnlyList Permissions +); diff --git a/src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs b/src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs new file mode 100644 index 00000000..a5c7542a --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Responses; + +/// +/// An Appwrite Document object +/// +/// Document ID +/// Collection ID +/// Database ID +/// Document creation date in ISO 8601 format +/// Document update date in ISO 8601 format +/// Document permissions. Learn more about permissions +/// Document data +[JsonConverter(typeof(DocumentGenericConverter))] +public record Doocument( + string Id, + string CollectionId, + string DatabaseId, + DateTime CreatedAt, + DateTime UpdatedAt, + IReadOnlyList Permissions, + TData Data +) : DocumentBase(Id, CollectionId, DatabaseId, CreatedAt, UpdatedAt, Permissions) + where TData : class, new(); From 7f732fb10e4b578ebf323bc97c790a61d7416b43 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Wed, 27 Nov 2024 23:04:20 +0000 Subject: [PATCH 2/3] Added tests for the new converters etc --- .../Converters/DocumentGenericConverter.cs | 80 +- .../DocumentGenericConverterFactory.cs | 24 + ...DoocumentGeneric.cs => DocumentGeneric.cs} | 5 +- .../DocumentGenericConverterFactoryTests.cs | 99 +++ .../DocumentGenericConverterTests.cs | 687 ++++++++++++++++++ 5 files changed, 876 insertions(+), 19 deletions(-) create mode 100644 src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverterFactory.cs rename src/PinguApps.Appwrite.Shared/Responses/{DoocumentGeneric.cs => DocumentGeneric.cs} (88%) create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterFactoryTests.cs create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs diff --git a/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs index d4361b09..b11a585d 100644 --- a/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs +++ b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs @@ -7,10 +7,10 @@ namespace PinguApps.Appwrite.Shared.Converters; -public class DocumentGenericConverter : JsonConverter> +public class DocumentGenericConverter : JsonConverter> where TData : class, new() { - public override Doocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override Document Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { string? id = null; string? collectionId = null; @@ -37,7 +37,7 @@ public override Doocument Read(ref Utf8JsonReader reader, Type typeToConv break; } - string propertyName = reader.GetString()!; + var propertyName = reader.GetString()!; reader.Read(); @@ -102,7 +102,7 @@ public override Doocument Read(ref Utf8JsonReader reader, Type typeToConv var dataJson = JsonSerializer.Serialize(dataProperties, options); data = JsonSerializer.Deserialize(dataJson, options) ?? new TData(); - return new Doocument(id, collectionId, databaseId, createdAt.Value, updatedAt.Value, permissions, data); + return new Document(id, collectionId, databaseId, createdAt.Value, updatedAt.Value, permissions, data); } internal object? ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options) @@ -184,7 +184,7 @@ public override Doocument Read(ref Utf8JsonReader reader, Type typeToConv return dict; } - public override void Write(Utf8JsonWriter writer, Doocument value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Document value, JsonSerializerOptions options) { writer.WriteStartObject(); @@ -204,26 +204,72 @@ public override void Write(Utf8JsonWriter writer, Doocument value, JsonSe writer.WritePropertyName("$permissions"); JsonSerializer.Serialize(writer, value.Permissions, options); - // Serialize the data object into individual properties. - var dataProperties = JsonSerializer.SerializeToElement(value.Data, options); - foreach (var property in dataProperties.EnumerateObject()) + // Serialize the Data property + if (value.Data is not null) { - writer.WritePropertyName(property.Name); - WriteValue(writer, property, options); + var dataProperties = JsonSerializer.SerializeToElement(value.Data, options); + foreach (var property in dataProperties.EnumerateObject()) + { + writer.WritePropertyName(property.Name); + WriteValue(writer, property.Value, options); + } } writer.WriteEndObject(); } - private void WriteValue(Utf8JsonWriter writer, JsonProperty property, JsonSerializerOptions options) + internal void WriteValue(Utf8JsonWriter writer, JsonElement element, JsonSerializerOptions options) { - // Handle null values - if (property.Value.ValueKind is JsonValueKind.Null) + var dateTimeConverter = new MultiFormatDateTimeConverter(); + + switch (element.ValueKind) { - writer.WriteNullValue(); - return; + case JsonValueKind.String: + var stringValue = element.GetString(); + if (DateTime.TryParse(stringValue, out var dateTimeValue)) + { + // Write DateTime using the MultiFormatDateTimeConverter + dateTimeConverter.Write(writer, dateTimeValue, options); + } + else + { + writer.WriteStringValue(stringValue); + } + break; + case JsonValueKind.Number: + if (element.TryGetInt32(out var intValue)) + writer.WriteNumberValue(intValue); + else if (element.TryGetInt64(out var longValue)) + writer.WriteNumberValue(longValue); + else if (element.TryGetDouble(out var doubleValue)) + writer.WriteNumberValue(doubleValue); + break; + case JsonValueKind.True: + case JsonValueKind.False: + writer.WriteBooleanValue(element.GetBoolean()); + break; + case JsonValueKind.Null: + writer.WriteNullValue(); + break; + case JsonValueKind.Array: + writer.WriteStartArray(); + foreach (var item in element.EnumerateArray()) + { + WriteValue(writer, item, options); + } + writer.WriteEndArray(); + break; + case JsonValueKind.Object: + writer.WriteStartObject(); + foreach (var property in element.EnumerateObject()) + { + writer.WritePropertyName(property.Name); + WriteValue(writer, property.Value, options); + } + writer.WriteEndObject(); + break; + case JsonValueKind.Undefined: + throw new JsonException("Cannot serialize undefined JsonElement"); } - - // TODO - Complete the implementation using DocumentConverter as a guide } } diff --git a/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverterFactory.cs b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverterFactory.cs new file mode 100644 index 00000000..da178a5f --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverterFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Responses; + +namespace PinguApps.Appwrite.Shared.Converters; +public class DocumentGenericConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + // Ensure the type is a generic type of Document<> + return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Document<>); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + // Extract the TData type from Doocument + Type dataType = typeToConvert.GetGenericArguments()[0]; + + // Create a specific generic converter for Doocument + var converterType = typeof(DocumentGenericConverter<>).MakeGenericType(dataType); + return (JsonConverter?)Activator.CreateInstance(converterType); + } +} diff --git a/src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs b/src/PinguApps.Appwrite.Shared/Responses/DocumentGeneric.cs similarity index 88% rename from src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs rename to src/PinguApps.Appwrite.Shared/Responses/DocumentGeneric.cs index a5c7542a..e5465118 100644 --- a/src/PinguApps.Appwrite.Shared/Responses/DoocumentGeneric.cs +++ b/src/PinguApps.Appwrite.Shared/Responses/DocumentGeneric.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Converters; using PinguApps.Appwrite.Shared.Utils; namespace PinguApps.Appwrite.Shared.Responses; @@ -15,8 +16,8 @@ namespace PinguApps.Appwrite.Shared.Responses; /// Document update date in ISO 8601 format /// Document permissions. Learn more about permissions /// Document data -[JsonConverter(typeof(DocumentGenericConverter))] -public record Doocument( +[JsonConverter(typeof(DocumentGenericConverterFactory))] +public record Document( string Id, string CollectionId, string DatabaseId, diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterFactoryTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterFactoryTests.cs new file mode 100644 index 00000000..2110449e --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterFactoryTests.cs @@ -0,0 +1,99 @@ +using System.Text.Json; +using PinguApps.Appwrite.Shared.Converters; +using PinguApps.Appwrite.Shared.Responses; + +namespace PinguApps.Appwrite.Shared.Tests.Converters; +public class DocumentGenericConverterFactoryTests +{ + private readonly DocumentGenericConverterFactory _factory; + + public DocumentGenericConverterFactoryTests() + { + _factory = new DocumentGenericConverterFactory(); + } + + public class TestData + { + public string? Field1 { get; set; } + } + + [Fact] + public void CanConvert_DocumentGenericType_ReturnsTrue() + { + // Arrange + var typeToConvert = typeof(Document); + + // Act + var result = _factory.CanConvert(typeToConvert); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanConvert_NonGenericType_ReturnsFalse() + { + // Arrange + var typeToConvert = typeof(TestData); + + // Act + var result = _factory.CanConvert(typeToConvert); + + // Assert + Assert.False(result); + } + + [Fact] + public void CanConvert_DifferentGenericType_ReturnsFalse() + { + // Arrange + var typeToConvert = typeof(List); + + // Act + var result = _factory.CanConvert(typeToConvert); + + // Assert + Assert.False(result); + } + + [Fact] + public void CreateConverter_DocumentGenericType_ReturnsConverter() + { + // Arrange + var typeToConvert = typeof(Document); + var options = new JsonSerializerOptions(); + + // Act + var converter = _factory.CreateConverter(typeToConvert, options); + + // Assert + Assert.NotNull(converter); + Assert.IsType>(converter); + } + + [Fact] + public void CreateConverter_NonGenericType_ThrowsException() + { + // Arrange + var typeToConvert = typeof(TestData); + var options = new JsonSerializerOptions(); + + // Act & Assert + Assert.Throws(() => _factory.CreateConverter(typeToConvert, options)); + } + + [Fact] + public void CreateConverter_DifferentGenericType_ReturnsConverterWithFirstGenericArgument() + { + // Arrange + var typeToConvert = typeof(List); + var options = new JsonSerializerOptions(); + + // Act + var converter = _factory.CreateConverter(typeToConvert, options); + + // Assert + Assert.NotNull(converter); + Assert.IsType>(converter); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs new file mode 100644 index 00000000..08579d4f --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs @@ -0,0 +1,687 @@ +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using PinguApps.Appwrite.Shared.Converters; +using PinguApps.Appwrite.Shared.Enums; +using PinguApps.Appwrite.Shared.Responses; +using PinguApps.Appwrite.Shared.Utils; + +namespace PinguApps.Appwrite.Shared.Tests.Converters; +public class DocumentGenericConverterTests +{ + private readonly JsonSerializerOptions _options; + + public DocumentGenericConverterTests() + { + _options = new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = { new DocumentGenericConverter() } + }; + } + + public class TestData + { + public string? Field1 { get; set; } + public int Field2 { get; set; } + public bool Field3 { get; set; } + public DateTime? Field4 { get; set; } + public List? Field5 { get; set; } + public Dictionary? Field6 { get; set; } + public float? FloatField { get; set; } + public long? LongField { get; set; } + public double? DoubleField { get; set; } + } + + [Fact] + public void Read_ValidJson_ReturnsDocument() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"", + ""Field2"": 42, + ""Field3"": true, + ""Field4"": ""2020-10-15T06:38:00.000+00:00"", + ""Field5"": [""item1"", ""item2""], + ""Field6"": { ""key1"": ""value1"", ""key2"": 2 } + }"; + + var document = JsonSerializer.Deserialize>(json, _options); + + Assert.NotNull(document); + Assert.Equal("1", document.Id); + Assert.Equal("col1", document.CollectionId); + Assert.Equal("db1", document.DatabaseId); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00"), document.CreatedAt); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00"), document.UpdatedAt); + Assert.Single(document.Permissions); + Assert.Equal(PermissionType.Read, document.Permissions[0].PermissionType); + Assert.Equal(RoleType.Any, document.Permissions[0].RoleType); + + Assert.NotNull(document.Data); + Assert.Equal("value1", document.Data.Field1); + Assert.Equal(42, document.Data.Field2); + Assert.True(document.Data.Field3); + Assert.Equal(DateTime.Parse("2020-10-15T06:38:00.000+00:00"), document.Data.Field4); + Assert.Equal(new List { "item1", "item2" }, document.Data.Field5); + + Assert.NotNull(document.Data.Field6); + var field6 = document.Data.Field6!; + Assert.Equal(2, field6.Count); + + // Extract values from JsonElement + Assert.True(field6.ContainsKey("key1")); + Assert.Equal("value1", field6["key1"]?.ToString()); + + Assert.True(field6.ContainsKey("key2")); + Assert.Equal(2, Convert.ToInt32(field6["key2"]?.ToString())); + } + + [Fact] + public void Read_InvalidJson_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""invalid-date"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Read_MissingRequiredFields_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Write_ValidDocument_WritesJson() + { + var testData = new TestData + { + Field1 = "value1", + Field2 = 42, + Field3 = true, + Field4 = DateTime.Parse("2020-10-15T06:38:00.000+00:00"), + Field5 = ["item1", "item2"], + Field6 = new Dictionary { { "key1", "value1" }, { "key2", 2 } } + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.Parse("2020-10-15T06:38:00.000+00:00"), + DateTime.Parse("2020-10-15T06:38:00.000+00:00"), + [Permission.Read().Any()], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + var expectedJson = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"", + ""Field2"": 42, + ""Field3"": true, + ""Field4"": ""2020-10-15T06:38:00.000+00:00"", + ""Field5"": [""item1"", ""item2""], + ""Field6"": { ""key1"": ""value1"", ""key2"": 2 }, + ""FloatField"": null, + ""LongField"": null, + ""DoubleField"": null + }".ReplaceLineEndings("").Replace(" ", ""); + + Assert.Equal(JsonDocument.Parse(expectedJson).RootElement.ToString(), JsonDocument.Parse(json).RootElement.ToString()); + } + + [Fact] + public void Write_NullData_WritesJsonWithNoDataProperties() + { + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [Permission.Read().Any()], + null! + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"$id\"", json); + Assert.DoesNotContain("\"Field1\"", json); + } + + [Fact] + public void Read_NullProperty_InsertedIntoData() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": null + }"; + + var document = JsonSerializer.Deserialize>(json, _options); + + Assert.NotNull(document); + Assert.NotNull(document.Data); + Assert.Null(document.Data.Field1); + } + + [Fact] + public void Write_NullValue_SerializesCorrectly() + { + var testData = new TestData + { + Field1 = null + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [Permission.Read().Any()], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"Field1\":null", json); + } + + //[Fact] + //public void ReadValue_UnsupportedTokenType_ThrowsJsonException() + //{ + // var json = @" + // { + // ""$id"": ""1"", + // ""$collectionId"": ""col1"", + // ""$databaseId"": ""db1"", + // ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + // ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + // ""$permissions"": [""read(\""any\"")""], + // ""unsupported"": /** comment */ + // }"; + + // Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + //} + + [Fact] + public void Write_CustomObject_SerializesUsingJsonSerializer() + { + var testData = new TestData + { + Field6 = new Dictionary + { + { "nestedObject", new { Prop1 = "value1", Prop2 = 2 } } + } + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [Permission.Read().Any()], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"Prop1\":\"value1\"", json); + Assert.Contains("\"Prop2\":2", json); + } + + [Fact] + public void Read_InvalidJsonTokenType_ThrowsJsonException() + { + var json = @" + [ + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + } + ]"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Write_DateTimeValue_SerializesCorrectly() + { + var testData = new TestData + { + Field4 = DateTime.Parse("2020-10-15T06:38:00.000+00:00") + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"Field4\":\"2020-10-15T06:38:00.000+00:00\"", json); + } + + [Fact] + public void Write_NullDataProperty_WritesNull() + { + var testData = new TestData + { + Field5 = null + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"Field5\":null", json); + } + + [Fact] + public void Write_BooleanValue_SerializesCorrectly() + { + var testData = new TestData + { + Field3 = true + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"Field3\":true", json); + } + + [Fact] + public void Write_NumberValues_SerializesCorrectly() + { + var testData = new TestData + { + Field2 = 123 + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"Field2\":123", json); + } + + [Fact] + public void Read_MissingId_ThrowsJsonException() + { + var json = @" + { + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Read_MissingCollectionId_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Read_MissingDatabaseId_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Read_MissingCreatedAt_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Read_MissingUpdatedAt_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Read_MissingPermissions_ThrowsJsonException() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""Field1"": ""value1"" + }"; + + Assert.Throws(() => JsonSerializer.Deserialize>(json, _options)); + } + + [Fact] + public void Write_NullDocumentData_SerializesCorrectly() + { + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + null! + ); + + var json = JsonSerializer.Serialize(document, _options); + + Assert.Contains("\"$id\"", json); + Assert.DoesNotContain("\"Field1\"", json); + } + + // Custom converter that returns null during deserialization + public class NullReturningConverter : JsonConverter where T : class + { + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Always return null + reader.Skip(); // Skip the current value to avoid infinite loops + return null; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Write null + writer.WriteNullValue(); + } + } + + [Fact] + public void Read_DataDeserializationReturnsNull_DataSetToNewInstance() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""Field1"": ""value1"", + ""Field2"": 42 + }"; + + // Create new options with the NullReturningConverter for TestData + var optionsWithNullConverter = new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = + { + new NullReturningConverter(), + new DocumentGenericConverter(), + new MultiFormatDateTimeConverter(), + new PermissionListConverter() + } + }; + + var document = JsonSerializer.Deserialize>(json, optionsWithNullConverter); + + Assert.NotNull(document); + Assert.NotNull(document.Data); + // Since data deserialization returned null, Data should be set to new TData() + // So Data's properties should have default values + Assert.Null(document.Data.Field1); + Assert.Equal(0, document.Data.Field2); + } + + [Fact] + public void ReadValue_UnsupportedTokenType_ThrowsJsonException() + { + var json = @"{ + ""Field1"": /* Comment */ ""value1"" + }"; + + var readerOptions = new JsonReaderOptions + { + CommentHandling = JsonCommentHandling.Allow + }; + + var bytes = Encoding.UTF8.GetBytes(json); + + var reader = new Utf8JsonReader(bytes, readerOptions); + + var converter = new DocumentGenericConverter(); + + // Read the StartObject token + reader.Read(); // JsonTokenType.StartObject + + // Read the PropertyName token + reader.Read(); // JsonTokenType.PropertyName + + var propertyName = reader.GetString()!; + + // Read the Comment token + reader.Read(); // JsonTokenType.Comment + + // At this point, reader.TokenType is Comment, which is not handled in ReadValue + // Calling ReadValue should now hit the default case and throw JsonException + try + { + converter.ReadValue(ref reader, _options); + Assert.Fail("Did not throw JsonException"); + } + catch (JsonException) + { + } + } + + [Fact] + public void ReadValue_FloatNumber_ReturnsSingle() + { + var json = @" + { + ""$id"": ""1"", + ""$collectionId"": ""col1"", + ""$databaseId"": ""db1"", + ""$createdAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$updatedAt"": ""2020-10-15T06:38:00.000+00:00"", + ""$permissions"": [""read(\""any\"")""], + ""FloatField"": 1.23 + }"; + + var document = JsonSerializer.Deserialize>(json, _options); + + Assert.NotNull(document); + Assert.NotNull(document.Data); + Assert.Equal(1.23f, document.Data.FloatField); + } + + [Fact] + public void WriteValue_UndefinedValueKind_CallsJsonSerializer() + { + var converter = new DocumentGenericConverter(); + + // Create a default-initialized JsonElement (ValueKind is Undefined) + JsonElement undefinedElement = default; + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + // Since JsonSerializer.Serialize will throw an exception when trying to serialize an undefined JsonElement, + // we can expect an InvalidOperationException + Assert.Throws(() => converter.WriteValue(writer, undefinedElement, _options)); + } + + [Fact] + public void WriteValue_LongNumber_WritesLongValue() + { + var testData = new TestData + { + LongField = (long)int.MaxValue + 1 // Value larger than int.MaxValue + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + // Verify that the LongField is serialized correctly + var jsonDoc = JsonDocument.Parse(json); + Assert.True(jsonDoc.RootElement.TryGetProperty("LongField", out var longFieldElement)); + Assert.Equal(JsonValueKind.Number, longFieldElement.ValueKind); + Assert.Equal((long)int.MaxValue + 1, longFieldElement.GetInt64()); + } + + [Fact] + public void WriteValue_DoubleNumber_WritesDoubleValue() + { + var testData = new TestData + { + DoubleField = 1.23e20 // A large double value + }; + + var document = new Document( + "1", + "col1", + "db1", + DateTime.UtcNow, + DateTime.UtcNow, + [], + testData + ); + + var json = JsonSerializer.Serialize(document, _options); + + // Verify that the DoubleField is serialized correctly + var jsonDoc = JsonDocument.Parse(json); + Assert.True(jsonDoc.RootElement.TryGetProperty("DoubleField", out var doubleFieldElement)); + Assert.Equal(JsonValueKind.Number, doubleFieldElement.ValueKind); + Assert.Equal(1.23e20, doubleFieldElement.GetDouble()); + } + +} From 4de2e4e8310e6dadba92d6411d3ab1af948190f3 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Wed, 27 Nov 2024 23:06:49 +0000 Subject: [PATCH 3/3] Update tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com> --- .../Converters/DocumentGenericConverterTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs index 08579d4f..9a18580c 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Converters/DocumentGenericConverterTests.cs @@ -683,5 +683,4 @@ public void WriteValue_DoubleNumber_WritesDoubleValue() Assert.Equal(JsonValueKind.Number, doubleFieldElement.ValueKind); Assert.Equal(1.23e20, doubleFieldElement.GetDouble()); } - }