Skip to content

Commit

Permalink
Add result type which allows to do response modifications (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaliumhexacyanoferrat authored Jan 4, 2024
1 parent 6738b6d commit 1e19149
Show file tree
Hide file tree
Showing 50 changed files with 540 additions and 204 deletions.
58 changes: 2 additions & 56 deletions API/Protocol/IResponseBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,85 +1,31 @@
using System;

using GenHTTP.Api.Infrastructure;
using GenHTTP.Api.Infrastructure;

namespace GenHTTP.Api.Protocol
{

/// <summary>
/// Allows to configure a HTTP response to be send.
/// </summary>
public interface IResponseBuilder : IBuilder<IResponse>
public interface IResponseBuilder : IBuilder<IResponse>, IResponseModification<IResponseBuilder>
{

/// <summary>
/// The request the response belongs to.
/// </summary>
IRequest Request { get; }

/// <summary>
/// Specifies the HTTP status code of the response.
/// </summary>
/// <param name="status">The HTTP status code of the response</param>
IResponseBuilder Status(ResponseStatus status);

/// <summary>
/// Specifies the HTTP status code of the response.
/// </summary>
/// <param name="status">The status code of the response</param>
/// <param name="reason">The reason phrase of the response (such as "Not Found" for 404)</param>
IResponseBuilder Status(int status, string reason);

/// <summary>
/// Sets the given header field on the response. Changing HTTP
/// protocol headers may cause incorrect behavior.
/// </summary>
/// <param name="key">The name of the header to be set</param>
/// <param name="value">The value of the header field</param>
IResponseBuilder Header(string key, string value);

/// <summary>
/// Sets the expiration date of the response.
/// </summary>
/// <param name="expiryDate">The expiration date of the response</param>
IResponseBuilder Expires(DateTime expiryDate);

/// <summary>
/// Sets the point in time when the requested resource has been
/// modified last.
/// </summary>
/// <param name="modificationDate">The point in time when the requested resource has been modified last</param>
IResponseBuilder Modified(DateTime modificationDate);

/// <summary>
/// Adds the given cookie to the response.
/// </summary>
/// <param name="cookie">The cookie to be added</param>
IResponseBuilder Cookie(Cookie cookie);

/// <summary>
/// Specifies the content to be sent to the client.
/// </summary>
/// <param name="content">The content to be send to the client</param>
IResponseBuilder Content(IResponseContent content);

/// <summary>
/// Specifies the content type of this response.
/// </summary>
/// <param name="contentType">The content type of this response</param>
IResponseBuilder Type(FlexibleContentType contentType);

/// <summary>
/// Specifies the length of the content stream, if known.
/// </summary>
/// <param name="length">The length of the content stream</param>
IResponseBuilder Length(ulong length);

/// <summary>
/// Sets the encoding of the content.
/// </summary>
/// <param name="encoding">The encoding of the content</param>
IResponseBuilder Encoding(string encoding);

}

}
68 changes: 68 additions & 0 deletions API/Protocol/IResponseModification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;

namespace GenHTTP.Api.Protocol
{

/// <summary>
/// The protocol allowing to manipulate the response sent by
/// the server.
/// </summary>
/// <typeparam name="T">The type of builder used as a return value</typeparam>
public interface IResponseModification<out T>
{

/// <summary>
/// Specifies the HTTP status code of the response.
/// </summary>
/// <param name="status">The HTTP status code of the response</param>
T Status(ResponseStatus status);

/// <summary>
/// Specifies the HTTP status code of the response.
/// </summary>
/// <param name="status">The status code of the response</param>
/// <param name="reason">The reason phrase of the response (such as "Not Found" for 404)</param>
T Status(int status, string reason);

/// <summary>
/// Sets the given header field on the response. Changing HTTP
/// protocol headers may cause incorrect behavior.
/// </summary>
/// <param name="key">The name of the header to be set</param>
/// <param name="value">The value of the header field</param>
T Header(string key, string value);

/// <summary>
/// Sets the expiration date of the response.
/// </summary>
/// <param name="expiryDate">The expiration date of the response</param>
T Expires(DateTime expiryDate);

/// <summary>
/// Sets the point in time when the requested resource has been
/// modified last.
/// </summary>
/// <param name="modificationDate">The point in time when the requested resource has been modified last</param>
T Modified(DateTime modificationDate);

/// <summary>
/// Adds the given cookie to the response.
/// </summary>
/// <param name="cookie">The cookie to be added</param>
T Cookie(Cookie cookie);

/// <summary>
/// Specifies the content type of this response.
/// </summary>
/// <param name="contentType">The content type of this response</param>
T Type(FlexibleContentType contentType);

/// <summary>
/// Sets the encoding of the content.
/// </summary>
/// <param name="encoding">The encoding of the content</param>
T Encoding(string encoding);

}

}
3 changes: 2 additions & 1 deletion API/Protocol/ResponseStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ public struct FlexibleResponseStatus
{ ResponseStatus.InsufficientStorage, "Insufficient Storage" },
{ ResponseStatus.LoopDetected, "Loop Detected" },
{ ResponseStatus.NotExtended, "Not Extended" },
{ ResponseStatus.NetworkAuthenticationRequired, "Network Authentication Required" }
{ ResponseStatus.NetworkAuthenticationRequired, "Network Authentication Required" },
{ ResponseStatus.Processing, "Processing" }
};

private static readonly Dictionary<int, ResponseStatus> CODE_MAPPING = MAPPING.Keys.ToDictionary((k) => (int)k, (k) => k);
Expand Down
2 changes: 1 addition & 1 deletion Modules/Controllers/Provider/ControllerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private IEnumerable<Func<IHandler, MethodHandler>> AnalyzeMethods(Type type, Ser

var path = DeterminePath(method, arguments);

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

Expand Down
2 changes: 1 addition & 1 deletion Modules/Functional/Provider/InlineHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private IEnumerable<Func<IHandler, MethodHandler>> AnalyzeMethods(List<InlineFun

var target = function.Delegate.Target ?? throw new InvalidOperationException("Delegate target must not be null");

yield return (parent) => new MethodHandler(parent, function.Delegate.Method, path, () => target, function.Configuration, ResponseProvider.GetResponse, formats, injection);
yield return (parent) => new MethodHandler(parent, function.Delegate.Method, path, () => target, function.Configuration, ResponseProvider.GetResponseAsync, formats, injection);
}
}

Expand Down
27 changes: 27 additions & 0 deletions Modules/Reflection/Adjustments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

using GenHTTP.Api.Protocol;

namespace GenHTTP.Modules.Reflection
{

internal static class Adjustments
{

/// <summary>
/// Allows to chain the execution of the given adjustments into
/// the given response builder.
/// </summary>
/// <param name="builder">The response builder to be adjusted</param>
/// <param name="adjustments">The adjustments to be executed (if any)</param>
/// <returns>The response builder to be chained</returns>
internal static IResponseBuilder Adjust(this IResponseBuilder builder, Action<IResponseBuilder>? adjustments)
{
adjustments?.Invoke(builder);

return builder;
}

}

}
6 changes: 3 additions & 3 deletions Modules/Reflection/GenHTTP.Modules.Reflection.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591,CS1587,CS1572,CS1573</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591,CS1587,CS1572,CS1573</NoWarn>

<PackageIcon>icon.png</PackageIcon>

Expand All @@ -43,7 +43,7 @@

<ProjectReference Include="..\..\API\GenHTTP.Api.csproj" />

<ProjectReference Include="..\IO\GenHTTP.Modules.IO.csproj" />
<ProjectReference Include="..\IO\GenHTTP.Modules.IO.csproj" />
<ProjectReference Include="..\Conversion\GenHTTP.Modules.Conversion.csproj" />

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
Expand Down
27 changes: 27 additions & 0 deletions Modules/Reflection/IResultWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using GenHTTP.Api.Protocol;

namespace GenHTTP.Modules.Reflection
{

/// <summary>
/// Allows the framework to unwrap <see cref="Result{T}" />
/// instances.
/// </summary>
internal interface IResultWrapper
{

/// <summary>
/// The actual result to be returned.
/// </summary>
object? Payload { get; }

/// <summary>
/// Performs the configured modifications to the response
/// on the given builder.
/// </summary>
/// <param name="builder">The response builder to manipulate</param>
void Apply(IResponseBuilder builder);

}

}
6 changes: 3 additions & 3 deletions Modules/Reflection/MethodHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public sealed class MethodHandler : IHandler

private Func<object> InstanceProvider { get; }

private Func<IRequest, IHandler, object?, ValueTask<IResponse?>> ResponseProvider { get; }
private Func<IRequest, IHandler, object?, Action<IResponseBuilder>?, ValueTask<IResponse?>> ResponseProvider { get; }

private SerializationRegistry Serialization { get; }

Expand All @@ -60,7 +60,7 @@ public sealed class MethodHandler : IHandler
#region Initialization

public MethodHandler(IHandler parent, MethodInfo method, MethodRouting routing, Func<object> instanceProvider, IMethodConfiguration metaData,
Func<IRequest, IHandler, object?, ValueTask<IResponse?>> responseProvider, SerializationRegistry serialization, InjectionRegistry injection)
Func<IRequest, IHandler, object?, Action<IResponseBuilder>?, ValueTask<IResponse?>> responseProvider, SerializationRegistry serialization, InjectionRegistry injection)
{
Parent = parent;

Expand Down Expand Up @@ -88,7 +88,7 @@ public MethodHandler(IHandler parent, MethodInfo method, MethodRouting routing,

var result = Invoke(arguments);

return await ResponseProvider(request, this, await UnwrapAsync(result));
return await ResponseProvider(request, this, await UnwrapAsync(result), null);
}

private async ValueTask<object?[]> GetArguments(IRequest request)
Expand Down
23 changes: 18 additions & 5 deletions Modules/Reflection/ResponseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,31 @@ public ResponseProvider(SerializationRegistry? serialization)

#region Functionality

public async ValueTask<IResponse?> GetResponse(IRequest request, IHandler handler, object? result)
public async ValueTask<IResponse?> GetResponseAsync(IRequest request, IHandler handler, object? result, Action<IResponseBuilder>? adjustments = null)
{
// no result = 204
if (result is null)
{
return request.Respond()
.Status(ResponseStatus.NoContent)
.Adjust(adjustments)
.Build();
}

var type = result.GetType();

// unwrap the result if applicable
if (typeof(IResultWrapper).IsAssignableFrom(type))
{
var wrapped = (IResultWrapper)result;

return await GetResponseAsync(request, handler, wrapped.Payload, (b) => wrapped.Apply(b)).ConfigureAwait(false);
}

// response returned by the method
if (result is IResponseBuilder responseBuilder)
{
return responseBuilder.Build();
return responseBuilder.Adjust(adjustments).Build();
}

if (result is IResponse response)
Expand Down Expand Up @@ -79,6 +88,7 @@ public ResponseProvider(SerializationRegistry? serialization)
var downloadResponse = request.Respond()
.Content(download, () => download.CalculateChecksumAsync())
.Type(ContentType.ApplicationForceDownload)
.Adjust(adjustments)
.Build();

return downloadResponse;
Expand All @@ -92,6 +102,7 @@ public ResponseProvider(SerializationRegistry? serialization)
return request.Respond()
.Content(result.ToString() ?? string.Empty)
.Type(ContentType.TextPlain)
.Adjust(adjustments)
.Build();
}

Expand All @@ -103,12 +114,14 @@ public ResponseProvider(SerializationRegistry? serialization)
throw new ProviderException(ResponseStatus.UnsupportedMediaType, "Requested format is not supported");
}

var serializedResult = await serializer.SerializeAsync(request, result).ConfigureAwait(false);
var serializedResult = await serializer.SerializeAsync(request, result)
.ConfigureAwait(false);

return serializedResult.Build();
return serializedResult.Adjust(adjustments)
.Build();
}

throw new ProviderException(ResponseStatus.InternalServerError, "Result type must be one of: IHandlerBuilder, IHandler, IResponseBuilder, IResponse");
throw new ProviderException(ResponseStatus.InternalServerError, "Result type must be one of: IHandlerBuilder, IHandler, IResponseBuilder, IResponse, Stream");
}

#endregion
Expand Down
Loading

0 comments on commit 1e19149

Please sign in to comment.