Build / Build (push) Successful in 32s
PrivaPub's own types are read through the shared ClientModels context, and a type without generated metadata no longer falls back to reflection silently: it fails at once in development and on the pasture, and is reported on the console elsewhere. Startup outside production checks that every PrivaPub and Mastodon type the client knows has its metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
111 lines
4.7 KiB
C#
111 lines
4.7 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
using decePubClient.Models;
|
|
using decePubClient.Services;
|
|
|
|
using PrivaPub.ClientModels;
|
|
|
|
namespace decePubClient.Helpers
|
|
{
|
|
public static class SUtility
|
|
{
|
|
public const string dateTimeFormat = "dd/MM/yyyy";
|
|
public const string timeFormat = "HH:mm";
|
|
public const string dateTimeFormatExt = "dd/MM/yyyy HH:mm";
|
|
|
|
/// <summary>Marks a response HttpService made up itself (a classified failure), not one the server sent.</summary>
|
|
public const string FailureHeader = "X-DecePub-Failure";
|
|
|
|
/// <summary>
|
|
/// The JSON options for /clientapi payloads and localStorage: web defaults, enums by name as PrivaPub writes them
|
|
/// (numbers still read), ClientJsonContext first, then PrivaPub.ClientModels' own context.
|
|
/// </summary>
|
|
public static JsonSerializerOptions DefaultSerializer { get; } = new(JsonSerializerDefaults.Web)
|
|
{
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
Converters = { new JsonStringEnumConverter() },
|
|
TypeInfoResolverChain = { ClientJsonContext.Default, ClientModelsJsonContext.Default, UnregisteredJson.Instance }
|
|
};
|
|
|
|
/// <summary>The JSON options for PrivaPub's Mastodon API and /oauth: snake_case, MastodonJsonContext first.</summary>
|
|
public static JsonSerializerOptions MastodonSerializer { get; } = new(JsonSerializerDefaults.Web)
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
IgnoreReadOnlyProperties = true,
|
|
TypeInfoResolverChain = { MastodonJsonContext.Default, UnregisteredJson.Instance }
|
|
};
|
|
|
|
/// <summary>What a cancelled request turns into: HTTP 410, which AddResponseError ignores.</summary>
|
|
public static WebResult DefaultTaskCancelledResult =>
|
|
new WebResult().Invalidate("Request cancelled, not an error.", (int)HttpStatusCode.Gone, FailureCodes.Cancelled);
|
|
|
|
public static HttpResponseMessage DefaultTaskCancelled() =>
|
|
ClassifiedFailure(FailureCodes.Cancelled, DefaultTaskCancelledResult.ErrorMessage, HttpStatusCode.Gone);
|
|
|
|
public static HttpResponseMessage DefaultUnauthorized(string message) =>
|
|
ClassifiedFailure(FailureCodes.Unauthorized, message, HttpStatusCode.Unauthorized);
|
|
|
|
/// <summary>A response carrying an invalid WebResult, tagged with FailureHeader so ReadWebResult reads it as is.</summary>
|
|
public static HttpResponseMessage ClassifiedFailure(string failureCode, string message, HttpStatusCode statusCode)
|
|
{
|
|
var content = JsonContent.Create(new WebResult().Invalidate(message, (int)statusCode, failureCode), options: DefaultSerializer);
|
|
content.Headers.Add(FailureHeader, failureCode);
|
|
return new(statusCode) { Content = content };
|
|
}
|
|
|
|
/// <summary>The value a cancelled read returns: default for value types, an empty list for list interfaces.</summary>
|
|
public static T CancellationDefault<T>()
|
|
{
|
|
var type = typeof(T);
|
|
if (type.IsValueType)
|
|
return default;
|
|
if (type.IsInterface && type.IsGenericType &&
|
|
type.GetGenericTypeDefinition() is var definition &&
|
|
(definition == typeof(IEnumerable<>) || definition == typeof(ICollection<>) || definition == typeof(IList<>) ||
|
|
definition == typeof(IReadOnlyCollection<>) || definition == typeof(IReadOnlyList<>)))
|
|
return (T)Activator.CreateInstance(typeof(List<>).MakeGenericType(type.GetGenericArguments()[0]));
|
|
if (type.IsInterface || type.IsAbstract || type.GetConstructor(Type.EmptyTypes) is null)
|
|
return default;
|
|
return Activator.CreateInstance<T>();
|
|
}
|
|
|
|
#region Toasts
|
|
|
|
/// <summary>Shows a failed result: connectivity failures as warnings, the rest as errors; cancellations not at all.</summary>
|
|
public static void AddResponseError(this ToastService toast, WebResult result)
|
|
{
|
|
if (result.StatusCode is (int)HttpStatusCode.Gone)
|
|
return;
|
|
|
|
if (FailureCodes.IsConnectivity(result.ErrorCode))
|
|
{
|
|
toast.Add(ToastSeverity.Warning, result.ErrorMessage);
|
|
return;
|
|
}
|
|
|
|
toast.Add(ToastSeverity.Error, $"{result.StatusCode} - {result.ErrorMessage}");
|
|
}
|
|
|
|
public static void AddResponseError(this ToastService toast, string message) =>
|
|
toast.Add(ToastSeverity.Error, message);
|
|
|
|
public static void AddResponseSuccess(this ToastService toast, string message) =>
|
|
toast.Add(ToastSeverity.Success, message, TimeSpan.FromSeconds(5));
|
|
|
|
public static void AddWarning(this ToastService toast, string message) =>
|
|
toast.Add(ToastSeverity.Warning, message);
|
|
|
|
public static void AddException(this ToastService toast, Exception exception) =>
|
|
toast.Add(ToastSeverity.Error, exception.Message);
|
|
|
|
#endregion
|
|
|
|
public static T IfTrueThen<T>(bool conditionIsTrue, T ifTrue, T ifFalse = default) =>
|
|
conditionIsTrue ? ifTrue : ifFalse;
|
|
}
|
|
}
|