Skip to content

Commit

Permalink
Implement HyperSerializerDelegate for easy expansion on content-types
Browse files Browse the repository at this point in the history
  • Loading branch information
OoLunar committed Sep 25, 2023
1 parent ed5776f commit 4f86c0c
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 68 deletions.
4 changes: 2 additions & 2 deletions benchmarks/Benchmarks/HyperBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ public HyperBenchmarks()
public async Task CleanupAsync() => await _hyperServer.StopAsync();

[WarmupCount(5), Benchmark]
public Task BaseTestAsync() => _client.GetAsync(_localhost);
public Task FullHttpRequest() => _client.GetAsync(_localhost);

[WarmupCount(5), Benchmark(45)]
[WarmupCount(5), Benchmark(47)]
public ValueTask<Result<HyperContext>> ParseHeadersTestAsync()
{
HyperConnection connection = new(new MemoryStream(_headers), _hyperServer);
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/Responders/HelloWorldValueResponder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace HyperSharp.Benchmarks.Responders

public async ValueTask<Result<HyperStatus>> RespondAsync(HyperContext context, CancellationToken cancellationToken = default)
{
await context.RespondAsync(HyperStatus.OK("Hello World!"), cancellationToken);
await context.RespondAsync(HyperStatus.OK("Hello World!"), HyperSerializers.PlainTextAsync, cancellationToken);
return Result.Success(default(HyperStatus));
}
}
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/Responders/OkTaskResponder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace HyperSharp.Benchmarks.Responders

public async Task<Result<HyperStatus>> RespondAsync(HyperContext context, CancellationToken cancellationToken = default)
{
await context.RespondAsync(new HyperStatus(HttpStatusCode.OK), cancellationToken);
await context.RespondAsync(new HyperStatus(HttpStatusCode.OK), HyperSerializers.PlainTextAsync, cancellationToken);
return Result.Success(default(HyperStatus));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/HyperSharp/HyperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public sealed record HyperConfiguration
public IPEndPoint ListeningEndpoint { get; init; }

/// <summary>
/// The default JSON serializer options to use for <see cref="HyperContext.RespondAsync(HyperStatus, JsonSerializerOptions?, CancellationToken)"/>.
/// The default JSON serializer options to use for <see cref="HyperSerializers.JsonAsync(HyperContext, HyperStatus, CancellationToken)"/>.
/// </summary>
public JsonSerializerOptions JsonSerializerOptions { get; init; }

Expand Down
4 changes: 2 additions & 2 deletions src/HyperSharp/HyperServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private async Task HandleConnectionAsync(Ulid id, NetworkStream networkStream)
_ => throw new NotImplementedException("Unimplemented result status, please open a GitHub issue as this is a bug.")
};

await context.Value.RespondAsync(response, Configuration.JsonSerializerOptions, cancellationTokenSource.Token);
await context.Value.RespondAsync(response, HyperSerializers.JsonAsync, cancellationTokenSource.Token);
HyperLogging.HttpResponded(_logger, connection.Id, response, null);
}
}
Expand All @@ -183,7 +183,7 @@ private async Task HandleConnectionAsync(Ulid id, NetworkStream networkStream)
}
else
{
await context.Value!.RespondAsync(HyperStatus.InternalServerError(), Configuration.JsonSerializerOptions);
await context.Value!.RespondAsync(HyperStatus.InternalServerError(), HyperSerializers.JsonAsync);
}

HyperLogging.ConnectionClosing(_logger, connection.Id, null);
Expand Down
66 changes: 6 additions & 60 deletions src/HyperSharp/Protocol/HyperContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Globalization;
using Microsoft.Toolkit.HighPerformance;

namespace HyperSharp.Protocol
Expand All @@ -40,9 +38,6 @@ public class HyperContext

private static readonly byte[] _colonSpace = ": "u8.ToArray();
private static readonly byte[] _newLine = "\r\n"u8.ToArray();
private static readonly byte[] _emptyBody = Array.Empty<byte>();
private static readonly byte[] _contentTextEncoding = "text/plain; charset=utf-8"u8.ToArray();
private static readonly byte[] _contentJsonEncoding = "application/json; charset=utf-8"u8.ToArray();

/// <summary>
/// The HTTP method of the request.
Expand Down Expand Up @@ -109,84 +104,35 @@ public HyperContext(HttpMethod method, Uri route, Version version, HyperHeaderCo
: new Uri(connection.Server.Configuration._host, route);
}

/// <summary>
/// Responds to the request with the specified status in plain text.
/// </summary>
/// <param name="status">The status to respond with.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use when writing the response.</param>
public async Task RespondAsync(HyperStatus status, CancellationToken cancellationToken = default)
{
// Write request line
Connection.StreamWriter.Write<byte>(_httpVersions[Version]);
Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes($"{(int)status.Code} {status.Code}"));
Connection.StreamWriter.Write<byte>(_newLine);

// Serialize body ahead of time due to headers
byte[] content = Encoding.UTF8.GetBytes(status.Body?.ToString() ?? "");

// Write headers
status.Headers.TryAdd("Date", DateTime.UtcNow.ToString("R"));
status.Headers.TryAdd("Content-Length", content.Length.ToString(CultureInfo.InvariantCulture));
status.Headers.UnsafeTryAdd("Content-Type", _contentTextEncoding);
status.Headers.UnsafeTryAdd("Server", Connection.Server.Configuration._serverNameBytes);

foreach ((string headerName, byte[] value) in status.Headers)
{
Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(headerName));
Connection.StreamWriter.Write<byte>(_colonSpace);
Connection.StreamWriter.Write<byte>(value);
Connection.StreamWriter.Write<byte>(_newLine);
}
Connection.StreamWriter.Write<byte>(_newLine);

// Write body
if (content.Length != 0)
{
Connection.StreamWriter.Write<byte>(content);
}

HasResponded = true;
await Connection.StreamWriter.FlushAsync(cancellationToken);
}
/// <inheritdoc cref="RespondAsync(HyperStatus, HyperSerializerDelegate, CancellationToken)"/>
public Task RespondAsync(HyperStatus status, CancellationToken cancellationToken = default) => RespondAsync(status, HyperSerializers.JsonAsync, cancellationToken);

/// <summary>
/// Responds to the request with the specified status, and serializes the body using the specified <see cref="JsonSerializerOptions"/>.
/// Responds to the request with the specified status in plain text.
/// </summary>
/// <param name="status">The status to respond with.</param>
/// <param name="serializerOptions">The <see cref="JsonSerializerOptions"/> to use when serializing the body.</param>
/// <param name="serializerDelegate">The <see cref="HyperSerializerDelegate"/> to use when serializing the body.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use when writing the response.</param>
public async Task RespondAsync(HyperStatus status, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default)
public async Task RespondAsync(HyperStatus status, HyperSerializerDelegate serializerDelegate, CancellationToken cancellationToken = default)
{
// Write request line
Connection.StreamWriter.Write<byte>(_httpVersions[Version]);
Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes($"{(int)status.Code} {status.Code}"));
Connection.StreamWriter.Write<byte>(_newLine);

// Serialize body ahead of time due to headers
byte[] content = status.Body is null
? _emptyBody
: JsonSerializer.SerializeToUtf8Bytes(status.Body, serializerOptions ?? Connection.Server.Configuration.JsonSerializerOptions);

// Write headers
status.Headers.TryAdd("Date", DateTime.UtcNow.ToString("R"));
status.Headers.TryAdd("Content-Length", content.Length.ToString(CultureInfo.InvariantCulture));
status.Headers.UnsafeTryAdd("Content-Type", _contentJsonEncoding);
status.Headers.UnsafeTryAdd("Server", Connection.Server.Configuration._serverNameBytes);

foreach ((string headerName, byte[] value) in status.Headers)
{
Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(headerName));
Connection.StreamWriter.Write<byte>(_colonSpace);
Connection.StreamWriter.Write<byte>(value);
Connection.StreamWriter.Write<byte>(_newLine);
}
Connection.StreamWriter.Write<byte>(_newLine);

// Write body
if (content.Length != 0)
{
Connection.StreamWriter.Write<byte>(content);
}
await serializerDelegate(this, status, cancellationToken);

HasResponded = true;
await Connection.StreamWriter.FlushAsync(cancellationToken);
Expand Down
14 changes: 14 additions & 0 deletions src/HyperSharp/Protocol/HyperSerializerDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;

namespace HyperSharp.Protocol
{
/// <summary>
/// The delegate to be called when serializing the body to the client.
/// </summary>
/// <param name="context">The <see cref="HyperContext"/> for the current request.</param>
/// <param name="status">The returned <see cref="HyperStatus"/> from the responder.</param>
/// <param name="cancellationToken">A cancellation token to stop the serializer.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation, returning a <see cref="bool"/> indicating whether the serializer was successful.</returns>
public delegate ValueTask<bool> HyperSerializerDelegate(HyperContext context, HyperStatus status, CancellationToken cancellationToken = default);
}
41 changes: 41 additions & 0 deletions src/HyperSharp/Protocol/HyperSerializers/JsonAsync.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Toolkit.HighPerformance;

namespace HyperSharp.Protocol
{
public static partial class HyperSerializers
{
private static readonly byte[] _jsonEncodingHeader = "Content-Type: application/json; charset=utf-8\r\nContent-Length: "u8.ToArray();

/// <summary>
/// Serializes the body to the client as JSON using the <see cref="JsonSerializer.SerializeToUtf8Bytes{TValue}(TValue, JsonSerializerOptions?)"/> method with the <see cref="HyperConfiguration.JsonSerializerOptions"/> options.
/// </summary>
/// <remarks>
/// This serializer is the default serializer for HyperSharp.
/// </remarks>
/// <inheritdoc cref="HyperSerializerDelegate"/>
public static ValueTask<bool> JsonAsync(HyperContext context, HyperStatus status, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(status);

// Write Content-Type header and beginning of Content-Length header
context.Connection.StreamWriter.Write<byte>(_jsonEncodingHeader);
byte[] body = JsonSerializer.SerializeToUtf8Bytes(status.Body, context.Connection.Server.Configuration.JsonSerializerOptions);

// Finish the Content-Length header
context.Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(body.Length.ToString())); // TODO: This could probably be done without allocating a string
context.Connection.StreamWriter.Write<byte>(_newLine);

// Write body
context.Connection.StreamWriter.Write<byte>(_newLine);
context.Connection.StreamWriter.Write<byte>(body);

return ValueTask.FromResult(true);
}
}
}
42 changes: 42 additions & 0 deletions src/HyperSharp/Protocol/HyperSerializers/PlainTextAsync.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Toolkit.HighPerformance;

namespace HyperSharp.Protocol
{
/// <summary>
/// Holds a collection of static methods implementing <see cref="HyperSerializerDelegate"/> for the most common of Content-Types.
/// </summary>
public static partial class HyperSerializers
{
private static readonly byte[] _newLine = "\r\n"u8.ToArray();
private static readonly byte[] _contentTypeJsonEncodingHeader = "Content-Type: application/json; charset=utf-8\r\nContent-Length: "u8.ToArray();

/// <summary>
/// Serializes the body to the client as plain text using the <see cref="object.ToString"/> method with the <see cref="Encoding.UTF8"/> encoding.
/// </summary>
/// <inheritdoc cref="HyperSerializerDelegate"/>
public static ValueTask<bool> PlainTextAsync(HyperContext context, HyperStatus status, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(status);

// Write Content-Type header and beginning of Content-Length header
context.Connection.StreamWriter.Write<byte>(_contentTypeJsonEncodingHeader);

byte[] body = Encoding.UTF8.GetBytes(status.Body?.ToString() ?? "");

// Write Content-Length header
context.Connection.StreamWriter.Write<byte>(Encoding.ASCII.GetBytes(body.Length.ToString())); // TODO: This could probably be done without allocating a string
context.Connection.StreamWriter.Write<byte>(_newLine);

// Write body
context.Connection.StreamWriter.Write<byte>(_newLine);
context.Connection.StreamWriter.Write<byte>(body);

return ValueTask.FromResult(true);
}
}
}
5 changes: 4 additions & 1 deletion src/HyperSharp/Setup/HyperConfigurationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ public sealed class HyperConfigurationBuilder
public IPEndPoint ListeningEndpoint { get; set; } = new(IPAddress.Any, 8080);

/// <summary>
/// A list of types that implement <see cref="IResponderBase"/>.
/// A list of types which will be executed when a new HTTP request is received.
/// </summary>
/// <remarks>
/// All types implement <see cref="IResponderBase"/>.
/// </remarks>
public List<Type> Responders { get; set; } = new();

/// <summary>
Expand Down

0 comments on commit 4f86c0c

Please sign in to comment.