Skip to content

Commit

Permalink
Rework how field-based parameters are serialized and deserialized (#467)
Browse files Browse the repository at this point in the history
Adds a mechanism that allows to configure how path parameters and form encoded values are read and written. Additional changes:

* Allow DateOnly to be injected and returned
* Controllers may now also return simple data if they would like to
  • Loading branch information
Kaliumhexacyanoferrat committed Mar 12, 2024
1 parent f46397b commit d1533b7
Show file tree
Hide file tree
Showing 38 changed files with 1,067 additions and 671 deletions.
42 changes: 36 additions & 6 deletions Modules/Controllers/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
using GenHTTP.Modules.Layouting.Provider;
using System.Diagnostics.CodeAnalysis;

using System.Diagnostics.CodeAnalysis;
using GenHTTP.Api.Infrastructure;

using GenHTTP.Modules.Controllers.Provider;
using GenHTTP.Modules.Conversion.Formatters;
using GenHTTP.Modules.Conversion.Providers;
using GenHTTP.Modules.Layouting.Provider;
using GenHTTP.Modules.Reflection.Injectors;

namespace GenHTTP.Modules.Controllers
{
Expand All @@ -15,9 +21,11 @@ public static class Extensions
/// <typeparam name="T">The type of the controller used to handle requests</typeparam>
/// <param name="builder">The layout the controller should be added to</param>
/// <param name="path">The path that should be handled by the controller</param>
public static LayoutBuilder AddController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, string path) where T : new()
/// <param name="injectors">Optionally the injectors to be used by this controller</param>
/// <param name="formats">Optionally the formats to be used by this controller</param>
public static LayoutBuilder AddController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, string path, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null) where T : new()
{
builder.Add(path, Controller.From<T>());
builder.Add(path, Controller.From<T>().Configured(injectors, serializers, formatters));
return builder;
}

Expand All @@ -27,9 +35,31 @@ public static class Extensions
/// </summary>
/// <typeparam name="T">The type of the controller used to handle requests</typeparam>
/// <param name="builder">The layout the controller should be added to</param>
public static LayoutBuilder IndexController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder) where T : new()
/// <param name="injectors">Optionally the injectors to be used by this controller</param>
/// <param name="formats">Optionally the formats to be used by this controller</param>
public static LayoutBuilder IndexController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null) where T : new()
{
builder.Add(Controller.From<T>().Configured(injectors, serializers, formatters));
return builder;
}

private static ControllerBuilder<T> Configured<T>(this ControllerBuilder<T> builder, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null) where T : new()
{
builder.Add(Controller.From<T>());
if (injectors != null)
{
builder.Injectors(injectors);
}

if (serializers != null)
{
builder.Serializers(serializers);
}

if (formatters != null)
{
builder.Formatters(formatters);
}

return builder;
}

Expand Down
21 changes: 16 additions & 5 deletions Modules/Controllers/Provider/ControllerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using GenHTTP.Api.Infrastructure;

using GenHTTP.Modules.Conversion;
using GenHTTP.Modules.Conversion.Formatters;
using GenHTTP.Modules.Conversion.Providers;
using GenHTTP.Modules.Reflection;
using GenHTTP.Modules.Reflection.Injectors;
Expand All @@ -14,17 +15,19 @@ namespace GenHTTP.Modules.Controllers.Provider

public sealed class ControllerBuilder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : IHandlerBuilder<ControllerBuilder<T>> where T : new()
{
private IBuilder<SerializationRegistry>? _Formats;
private IBuilder<SerializationRegistry>? _Serializers;

private IBuilder<InjectionRegistry>? _Injection;

private IBuilder<FormatterRegistry>? _Formatters;

private readonly List<IConcernBuilder> _Concerns = new();

#region Functionality

public ControllerBuilder<T> Formats(IBuilder<SerializationRegistry> registry)
public ControllerBuilder<T> Serializers(IBuilder<SerializationRegistry> registry)
{
_Formats = registry;
_Serializers = registry;
return this;
}

Expand All @@ -34,6 +37,12 @@ public ControllerBuilder<T> Injectors(IBuilder<InjectionRegistry>? registry)
return this;
}

public ControllerBuilder<T> Formatters(IBuilder<FormatterRegistry>? registry)
{
_Formatters = registry;
return this;
}

public ControllerBuilder<T> Add(IConcernBuilder concern)
{
_Concerns.Add(concern);
Expand All @@ -42,11 +51,13 @@ public ControllerBuilder<T> Add(IConcernBuilder concern)

public IHandler Build(IHandler parent)
{
var formats = (_Formats ?? Serialization.Default()).Build();
var serializers = (_Serializers ?? Serialization.Default()).Build();

var injectors = (_Injection ?? Injection.Default()).Build();

return Concerns.Chain(parent, _Concerns, (p) => new ControllerHandler<T>(p, formats, injectors));
var formatters = (_Formatters ?? Formatting.Default()).Build();

return Concerns.Chain(parent, _Concerns, (p) => new ControllerHandler<T>(p, serializers, injectors, formatters));
}

#endregion
Expand Down
20 changes: 12 additions & 8 deletions Modules/Controllers/Provider/ControllerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using GenHTTP.Api.Protocol;
using GenHTTP.Api.Routing;

using GenHTTP.Modules.Conversion.Formatters;
using GenHTTP.Modules.Conversion.Providers;
using GenHTTP.Modules.Reflection;
using GenHTTP.Modules.Reflection.Injectors;
Expand All @@ -29,20 +30,23 @@ public sealed class ControllerHandler<[DynamicallyAccessedMembers(DynamicallyAcc

private ResponseProvider ResponseProvider { get; }

private FormatterRegistry Formatting { get; }

#endregion

#region Initialization

public ControllerHandler(IHandler parent, SerializationRegistry formats, InjectionRegistry injection)
public ControllerHandler(IHandler parent, SerializationRegistry serialization, InjectionRegistry injection, FormatterRegistry formatting)
{
Parent = parent;
Formatting = formatting;

ResponseProvider = new(null);
ResponseProvider = new(serialization, formatting);

Provider = new(this, AnalyzeMethods(typeof(T), formats, injection));
Provider = new(this, AnalyzeMethods(typeof(T), serialization, injection, formatting));
}

private IEnumerable<Func<IHandler, MethodHandler>> AnalyzeMethods(Type type, SerializationRegistry formats, InjectionRegistry injection)
private IEnumerable<Func<IHandler, MethodHandler>> AnalyzeMethods(Type type, SerializationRegistry serialization, InjectionRegistry injection, FormatterRegistry formatting)
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
Expand All @@ -52,7 +56,7 @@ public ControllerHandler(IHandler parent, SerializationRegistry formats, Injecti

var path = DeterminePath(method, arguments);

yield return (parent) => new MethodHandler(parent, method, path, () => new T(), annotation, ResponseProvider.GetResponseAsync, formats, injection);
yield return (parent) => new MethodHandler(parent, method, path, () => new T(), annotation, ResponseProvider.GetResponseAsync, serialization, injection, formatting);
}
}

Expand All @@ -75,7 +79,7 @@ private static MethodRouting DeterminePath(MethodInfo method, List<string> argum
}
}

private static List<string> FindPathArguments(MethodInfo method)
private List<string> FindPathArguments(MethodInfo method)
{
var found = new List<string>();

Expand All @@ -85,9 +89,9 @@ private static List<string> FindPathArguments(MethodInfo method)
{
if (parameter.GetCustomAttribute(typeof(FromPathAttribute), true) is not null)
{
if (!parameter.CheckSimple())
if (!parameter.CanFormat(Formatting))
{
throw new InvalidOperationException("Parameters marked as 'FromPath' must be of a simple type (e.g. string or int)");
throw new InvalidOperationException("Parameters marked as 'FromPath' must be formattable (e.g. string or int)");
}

if (parameter.CheckNullable())
Expand Down
23 changes: 4 additions & 19 deletions Modules/Conversion/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System;
using System.Globalization;

using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.Conversion.Formatters;

namespace GenHTTP.Modules.Conversion
{

Expand All @@ -16,7 +17,7 @@ public static class Extensions
/// <param name="value">The value to be converted</param>
/// <param name="type">The target type to convert the value to</param>
/// <returns>The converted value</returns>
public static object? ConvertTo(this string? value, Type type)
public static object? ConvertTo(this string? value, Type type, FormatterRegistry formatters)
{
if (string.IsNullOrEmpty(value))
{
Expand All @@ -38,23 +39,7 @@ public static class Extensions
{
var actualType = Nullable.GetUnderlyingType(type) ?? type;

if (actualType.IsEnum)
{
return Enum.Parse(actualType, value);
}

if (actualType == typeof(bool))
{
if (value == "1" || value == "on") return true;
else if (value == "0" || value == "off") return false;
}

if (actualType == typeof(Guid))
{
return Guid.Parse(value);
}

return Convert.ChangeType(value, actualType, CultureInfo.InvariantCulture);
return formatters.Read(value, actualType);
}
catch (Exception e)
{
Expand Down
29 changes: 29 additions & 0 deletions Modules/Conversion/Formatters/BoolFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace GenHTTP.Modules.Conversion.Formatters
{

public sealed class BoolFormatter : IFormatter
{

public bool CanHandle(Type type) => type == typeof(bool);

public object? Read(string value, Type type)
{
if (value == "1" || value == "on")
{
return true;
}
else if (value == "0" || value == "off")
{
return false;
}

return null;
}

public string? Write(object value, Type type) => ((bool)value) ? "1" : "0";

}

}
33 changes: 33 additions & 0 deletions Modules/Conversion/Formatters/DateOnlyFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Text.RegularExpressions;

namespace GenHTTP.Modules.Conversion.Formatters
{

public sealed class DateOnlyFormatter : IFormatter
{
private static readonly Regex DATE_ONLY_PATTERN = new(@"^([0-9]{4})\-([0-9]{2})\-([0-9]{2})$", RegexOptions.Compiled);

public bool CanHandle(Type type) => type == typeof(DateOnly);

public object? Read(string value, Type type)
{
var match = DATE_ONLY_PATTERN.Match(value);

if (match.Success)
{
var year = int.Parse(match.Groups[1].Value);
var month = int.Parse(match.Groups[2].Value);
var day = int.Parse(match.Groups[3].Value);

return new DateOnly(year, month, day);
}

throw new ArgumentException($"Input does not match the requested format (yyyy-mm-dd): {value}");
}

public string? Write(object value, Type type) => ((DateOnly)value).ToString("yyyy-MM-dd");

}

}
17 changes: 17 additions & 0 deletions Modules/Conversion/Formatters/EnumFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace GenHTTP.Modules.Conversion.Formatters
{

public sealed class EnumFormatter : IFormatter
{

public bool CanHandle(Type type) => type.IsEnum;

public object? Read(string value, Type type) => Enum.Parse(type, value);

public string? Write(object value, Type type) => value.ToString();

}

}
31 changes: 31 additions & 0 deletions Modules/Conversion/Formatters/FormatterBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections.Generic;

using GenHTTP.Api.Infrastructure;

namespace GenHTTP.Modules.Conversion.Formatters
{

public sealed class FormatterBuilder : IBuilder<FormatterRegistry>
{
private readonly List<IFormatter> _Registry = new();

#region Functionality

public FormatterBuilder Add(IFormatter formatter)
{
_Registry.Add(formatter);
return this;
}

public FormatterBuilder Add<T>() where T : IFormatter, new() => Add(new T());

public FormatterRegistry Build()
{
return new FormatterRegistry(_Registry);
}

#endregion

}

}
38 changes: 38 additions & 0 deletions Modules/Conversion/Formatters/FormatterRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace GenHTTP.Modules.Conversion.Formatters
{

public sealed class FormatterRegistry
{

#region Get-/Setters

public IReadOnlyList<IFormatter> Formatters { get; private set; }

#endregion

#region Initialization

public FormatterRegistry(List<IFormatter> formatters)
{
Formatters = formatters;
}

#endregion

#region Functionality

public bool CanHandle(Type type) => Formatters.Any(f => f.CanHandle(type));

public object? Read(string value, Type type) => Formatters.First(f => f.CanHandle(type)).Read(value, type);

public string? Write(object value, Type type) => Formatters.First(f => f.CanHandle(type)).Write(value, type);

#endregion

}

}

0 comments on commit d1533b7

Please sign in to comment.