Skip to content

Commit

Permalink
Merge pull request #291 from PinguApps/287-requsts-nullable-props
Browse files Browse the repository at this point in the history
Created stricter JsonSerialisation rules
  • Loading branch information
pingu2k4 authored Oct 13, 2024
2 parents a978765 + 4ecf688 commit 1d7fc9f
Show file tree
Hide file tree
Showing 66 changed files with 721 additions and 157 deletions.
28 changes: 26 additions & 2 deletions src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using PinguApps.Appwrite.Client.Handlers;
using PinguApps.Appwrite.Client.Internals;
using PinguApps.Appwrite.Shared;
using PinguApps.Appwrite.Shared.Converters;
using Refit;

namespace PinguApps.Appwrite.Client;
Expand All @@ -24,11 +27,13 @@ public static class ServiceCollectionExtensions
/// <returns>The service collection, enabling chaining</returns>
public static IServiceCollection AddAppwriteClient(this IServiceCollection services, string projectId, string endpoint = "https://cloud.appwrite.io/v1", RefitSettings? refitSettings = null)
{
var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddSingleton(new Config(endpoint, projectId));
services.AddTransient<HeaderHandler>();
services.AddTransient<ClientCookieSessionHandler>();

services.AddRefitClient<IAccountApi>(refitSettings)
services.AddRefitClient<IAccountApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler<ClientCookieSessionHandler>();
Expand All @@ -50,10 +55,12 @@ public static IServiceCollection AddAppwriteClient(this IServiceCollection servi
/// <returns>The service collection, enabling chaining</returns>
public static IServiceCollection AddAppwriteClientForServer(this IServiceCollection services, string projectId, string endpoint = "https://cloud.appwrite.io/v1", RefitSettings? refitSettings = null)
{
var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddSingleton(new Config(endpoint, projectId));
services.AddTransient<HeaderHandler>();

services.AddRefitClient<IAccountApi>(refitSettings)
services.AddRefitClient<IAccountApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
Expand All @@ -78,6 +85,23 @@ private static void ConfigureHttpClient(HttpClient client, string endpoint)
client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent());
}

private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings? refitSettings)
{
var settings = refitSettings ?? new RefitSettings();

var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

options.Converters.Add(new IgnoreSdkExcludedPropertiesConverterFactory());

settings.ContentSerializer = new SystemTextJsonContentSerializer(options);

return settings;
}

public static string BuildUserAgent()
{
var dotnetVersion = RuntimeInformation.FrameworkDescription.Replace("Microsoft .NET", ".NET").Trim();
Expand Down
10 changes: 6 additions & 4 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ public App(Client.IAppwriteClient client, Server.Clients.IAppwriteClient server,

public async Task Run(string[] args)
{
var request = new CreateSessionRequest()
_client.SetSession(_session);

var request = new CreateAccountRequest()
{
UserId = "664aac1a00113f82e620",
Secret = "80af6605407a3918cd9bb1796b6bfdc5d4b2dc57dad4677432d902e8bef9ba6f"
Email = "[email protected]",
Password = "MyCoolPassword"
};

var response = await _client.Account.CreateSession(request);
var response = await _client.Account.Create(request);

Console.WriteLine(response.Result.Match(
result => result.ToString(),
Expand Down
26 changes: 24 additions & 2 deletions src/PinguApps.Appwrite.Server/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using PinguApps.Appwrite.Server.Clients;
using PinguApps.Appwrite.Server.Handlers;
using PinguApps.Appwrite.Server.Internals;
using PinguApps.Appwrite.Shared;
using PinguApps.Appwrite.Shared.Converters;
using Refit;

namespace PinguApps.Appwrite.Server;
Expand All @@ -26,15 +29,17 @@ public static class ServiceCollectionExtensions
/// <returns>The service collection, enabling chaining</returns>
public static IServiceCollection AddAppwriteServer(this IServiceCollection services, string projectId, string apiKey, string endpoint = "https://cloud.appwrite.io/v1", RefitSettings? refitSettings = null)
{
var customRefitSettings = AddSerializationConfigToRefitSettings(refitSettings);

services.AddSingleton(new Config(endpoint, projectId, apiKey));
services.AddTransient<HeaderHandler>();

services.AddRefitClient<IAccountApi>(refitSettings)
services.AddRefitClient<IAccountApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);

services.AddRefitClient<IUsersApi>(refitSettings)
services.AddRefitClient<IUsersApi>(customRefitSettings)
.ConfigureHttpClient(x => ConfigureHttpClient(x, endpoint))
.AddHttpMessageHandler<HeaderHandler>()
.ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
Expand All @@ -60,6 +65,23 @@ private static void ConfigureHttpClient(HttpClient client, string endpoint)
client.DefaultRequestHeaders.UserAgent.ParseAdd(BuildUserAgent());
}

private static RefitSettings AddSerializationConfigToRefitSettings(RefitSettings? refitSettings)
{
var settings = refitSettings ?? new RefitSettings();

var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

options.Converters.Add(new IgnoreSdkExcludedPropertiesConverterFactory());

settings.ContentSerializer = new SystemTextJsonContentSerializer(options);

return settings;
}

public static string BuildUserAgent()
{
var dotnetVersion = RuntimeInformation.FrameworkDescription.Replace("Microsoft .NET", ".NET").Trim();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace PinguApps.Appwrite.Shared.Attributes;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class SdkExcludeAttribute : Attribute
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Attributes;

namespace PinguApps.Appwrite.Shared.Converters;

public class IgnoreSdkExcludedPropertiesConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
if (typeToConvert.IsPrimitive ||
typeToConvert.IsEnum ||
typeToConvert == typeof(string) ||
typeToConvert == typeof(decimal) ||
typeToConvert == typeof(DateTime) ||
typeToConvert == typeof(DateTimeOffset) ||
typeToConvert == typeof(TimeSpan) ||
typeToConvert == typeof(Guid) ||
typeToConvert == typeof(object))
{
return false;
}

if (typeof(IEnumerable).IsAssignableFrom(typeToConvert) && typeToConvert != typeof(string))
{
return false;
}

return typeToConvert.IsClass && !typeToConvert.IsAbstract;
}

public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var converterType = typeof(IgnoreSdkExcludedPropertiesConverter<>).MakeGenericType(typeToConvert);

return (JsonConverter)Activator.CreateInstance(converterType)!;
}

private class IgnoreSdkExcludedPropertiesConverter<T> : JsonConverter<T> where T : class
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);

var json = jsonDoc.RootElement.GetRawText();

var newOptions = new JsonSerializerOptions(options);

var converterToRemove = newOptions.Converters.FirstOrDefault(x => x.GetType() == typeof(IgnoreSdkExcludedPropertiesConverterFactory));

if (converterToRemove is not null)
{
newOptions.Converters.Remove(converterToRemove);
}

return JsonSerializer.Deserialize<T>(json, newOptions);
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}

writer.WriteStartObject();

var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(x => x.CanRead && x.GetMethod is not null);

foreach (var prop in properties)
{
if (prop.GetCustomAttribute<SdkExcludeAttribute>() is not null)
continue;

var jsonPropertyNameAttr = prop.GetCustomAttribute<JsonPropertyNameAttribute>();

var jsonPropertyName = jsonPropertyNameAttr is not null
? jsonPropertyNameAttr.Name
: (options.PropertyNamingPolicy?.ConvertName(prop.Name) ?? prop.Name);

var propValue = prop.GetValue(value);

if (propValue is null && options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull)
continue;

writer.WritePropertyName(jsonPropertyName);

var jsonConverterAttr = prop.GetCustomAttribute<JsonConverterAttribute>();
if (jsonConverterAttr is not null && jsonConverterAttr.ConverterType is not null)
{
// Instantiate the specified converter
var converterInstance = (JsonConverter?)Activator.CreateInstance(jsonConverterAttr.ConverterType)!;

// Create a new JsonSerializerOptions instance without the custom converter factory to prevent recursion
var newOptions = new JsonSerializerOptions(options);

// Remove the custom converter factory to prevent it from being invoked again
var converterToRemove = newOptions.Converters
.FirstOrDefault(c => c.GetType() == typeof(IgnoreSdkExcludedPropertiesConverterFactory));

if (converterToRemove != null)
newOptions.Converters.Remove(converterToRemove);

newOptions.Converters.Add(converterInstance);

// Serialize the property value using the specified converter and the new options
JsonSerializer.Serialize(writer, propValue, prop.PropertyType, newOptions);

// Move to the next property after handling with the custom converter
continue;
}

// If no custom converter is specified, serialize normally
JsonSerializer.Serialize(writer, propValue, prop.PropertyType, options);
}

writer.WriteEndObject();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Attributes;
using PinguApps.Appwrite.Shared.Requests.Account.Validators;

namespace PinguApps.Appwrite.Shared.Requests.Account;
Expand All @@ -11,6 +12,7 @@ public class AddAuthenticatorRequest : BaseRequest<AddAuthenticatorRequest, AddA
/// <summary>
/// Type of authenticator. Must be `totp`
/// </summary>
[JsonIgnore]
[JsonPropertyName("type")]
[SdkExclude]
public string Type { get; set; } = "totp";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Attributes;
using PinguApps.Appwrite.Shared.Requests.Account.Validators;

Expand All @@ -14,25 +15,34 @@ public class CreateOauth2SessionRequest : QueryParamBaseRequest<CreateOauth2Sess
/// <para>amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom</para>
/// </summary>
[UrlReplacement("{provider}")]
[JsonPropertyName("provider")]
[SdkExclude]
public string Provider { get; set; } = string.Empty;

/// <summary>
/// URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API
/// </summary>
[QueryParameter("success")]
[JsonPropertyName("success")]
[SdkExclude]
public string? SuccessUri { get; set; }

/// <summary>
/// URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API
/// </summary>
[QueryParameter("failure")]
[JsonPropertyName("failure")]
[SdkExclude]
public string? FailureUri { get; set; }

/// <summary>
/// A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long
/// </summary>
[QueryParameter("scopes[]")]
[JsonPropertyName("scopes")]
[SdkExclude]
public List<string>? Scopes { get; set; }

[JsonIgnore]
protected override string Path => "/account/sessions/oauth2/{provider}";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Attributes;
using PinguApps.Appwrite.Shared.Requests.Account.Validators;

Expand All @@ -14,25 +15,34 @@ public class CreateOauth2TokenRequest : QueryParamBaseRequest<CreateOauth2TokenR
/// <para>amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom</para>
/// </summary>
[UrlReplacement("{provider}")]
[JsonPropertyName("provider")]
[SdkExclude]
public string Provider { get; set; } = string.Empty;

/// <summary>
/// URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API
/// </summary>
[QueryParameter("success")]
[JsonPropertyName("success")]
[SdkExclude]
public string? SuccessUri { get; set; }

/// <summary>
/// URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API
/// </summary>
[QueryParameter("failure")]
[JsonPropertyName("failure")]
[SdkExclude]
public string? FailureUri { get; set; }

/// <summary>
/// A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long
/// </summary>
[QueryParameter("scopes[]")]
[JsonPropertyName("scopes")]
[SdkExclude]
public List<string>? Scopes { get; set; }

[JsonIgnore]
protected override string Path => "/account/tokens/oauth2/{provider}";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Attributes;
using PinguApps.Appwrite.Shared.Requests.Account.Validators;

namespace PinguApps.Appwrite.Shared.Requests.Account;
Expand All @@ -11,7 +12,8 @@ public class DeleteAuthenticatorRequest : BaseRequest<DeleteAuthenticatorRequest
/// <summary>
/// Type of authenticator
/// </summary>
[JsonIgnore]
[JsonPropertyName("type")]
[SdkExclude]
public string Type { get; set; } = "totp";

/// <summary>
Expand Down
Loading

0 comments on commit 1d7fc9f

Please sign in to comment.