Skip to content

Commit

Permalink
Merge pull request #545 from PinguApps/544-documents-utils
Browse files Browse the repository at this point in the history
documents utils
  • Loading branch information
pingu2k4 authored Nov 27, 2024
2 parents c85892a + 4de2e4e commit ed37438
Show file tree
Hide file tree
Showing 7 changed files with 1,146 additions and 7 deletions.
275 changes: 275 additions & 0 deletions src/PinguApps.Appwrite.Shared/Converters/DocumentGenericConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
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<TData> : JsonConverter<Document<TData>>
where TData : class, new()
{
public override Document<TData> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? id = null;
string? collectionId = null;
string? databaseId = null;
DateTime? createdAt = null;
DateTime? updatedAt = null;
List<Permission>? permissions = null;
TData data = new();

var dataProperties = new Dictionary<string, object?>();

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;
}

var 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<Permission>), 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<TData>(dataJson, options) ?? new TData();

return new Document<TData>(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<object?> ReadArray(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
var list = new List<object?>();

while (reader.Read())
{
if (reader.TokenType is JsonTokenType.EndArray)
{
break;
}

var item = ReadValue(ref reader, options);
list.Add(item);
}

return list;
}

private Dictionary<string, object?> ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
var dict = new Dictionary<string, object?>();

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, Document<TData> 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 property
if (value.Data is not null)
{
var dataProperties = JsonSerializer.SerializeToElement(value.Data, options);
foreach (var property in dataProperties.EnumerateObject())
{
writer.WritePropertyName(property.Name);
WriteValue(writer, property.Value, options);
}
}

writer.WriteEndObject();
}

internal void WriteValue(Utf8JsonWriter writer, JsonElement element, JsonSerializerOptions options)
{
var dateTimeConverter = new MultiFormatDateTimeConverter();

switch (element.ValueKind)
{
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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TData>
Type dataType = typeToConvert.GetGenericArguments()[0];

// Create a specific generic converter for Doocument<TData>
var converterType = typeof(DocumentGenericConverter<>).MakeGenericType(dataType);
return (JsonConverter?)Activator.CreateInstance(converterType);
}
}
14 changes: 7 additions & 7 deletions src/PinguApps.Appwrite.Shared/Responses/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ namespace PinguApps.Appwrite.Shared.Responses;
/// <param name="Data">Document data</param>
[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<Permission> Permissions,
string Id,
string CollectionId,
string DatabaseId,
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<Permission> Permissions,
[property: JsonExtensionData] Dictionary<string, object?> Data
)
) : DocumentBase(Id, CollectionId, DatabaseId, CreatedAt, UpdatedAt, Permissions)
{
/// <summary>
/// Extract document data by key
Expand Down
26 changes: 26 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/DocumentBase.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// An Appwrite Document object
/// </summary>
/// <param name="Id">Document ID</param>
/// <param name="CollectionId">Collection ID</param>
/// <param name="DatabaseId">Database ID</param>
/// <param name="CreatedAt">Document creation date in ISO 8601 format</param>
/// <param name="UpdatedAt">Document update date in ISO 8601 format</param>
/// <param name="Permissions">Document permissions. <see href="https://appwrite.io/docs/permissions">Learn more about permissions</see></param>
[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<Permission> Permissions
);
29 changes: 29 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/DocumentGeneric.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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;

/// <summary>
/// An Appwrite Document object
/// </summary>
/// <param name="Id">Document ID</param>
/// <param name="CollectionId">Collection ID</param>
/// <param name="DatabaseId">Database ID</param>
/// <param name="CreatedAt">Document creation date in ISO 8601 format</param>
/// <param name="UpdatedAt">Document update date in ISO 8601 format</param>
/// <param name="Permissions">Document permissions. <see href="https://appwrite.io/docs/permissions">Learn more about permissions</see></param>
/// <param name="Data">Document data</param>
[JsonConverter(typeof(DocumentGenericConverterFactory))]
public record Document<TData>(
string Id,
string CollectionId,
string DatabaseId,
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<Permission> Permissions,
TData Data
) : DocumentBase(Id, CollectionId, DatabaseId, CreatedAt, UpdatedAt, Permissions)
where TData : class, new();
Loading

0 comments on commit ed37438

Please sign in to comment.