Skip to content

Commit

Permalink
Merge pull request #19 from bmazzarol/action_result
Browse files Browse the repository at this point in the history
feat(action-result): added builder support for ActionResult [ci]
  • Loading branch information
bmazzarol authored Feb 4, 2023
2 parents 4d125fa + ac8f84f commit b49c490
Show file tree
Hide file tree
Showing 16 changed files with 441 additions and 17 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cd-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
project-name:
- HttpBuildR.Request
- HttpBuildR.Response
- HttpBuildR.ActionResult
steps:
- uses: actions/checkout@v3
- name: Setup .NET
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
project-name:
- HttpBuildR.Request
- HttpBuildR.Response
- HttpBuildR.ActionResult
steps:
- uses: actions/checkout@v3
- name: Setup .NET
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/report-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
project-name:
- HttpBuildR.Request
- HttpBuildR.Response
- HttpBuildR.ActionResult
steps:
- uses: actions/checkout@v3
- name: Setup .NET
Expand Down
72 changes: 72 additions & 0 deletions HttpBuildR.ActionResult.Tests/ActionResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;

namespace HttpBuildR.ActionResult.Tests;

public static class ActionResultTests
{
private static Scenario.Acted<ActionResult<T>, HttpResponse> AsResponse<T>(
this ActionResult<T> actionResult
) =>
actionResult
.ArrangeData()
.Act(async ar =>
{
var actionContext = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
new ActionDescriptor(),
new ModelStateDictionary()
);
await ((IConvertToActionResult)ar).Convert().ExecuteResultAsync(actionContext);
return actionContext.HttpContext.Response;
});

[Fact(DisplayName = "A T can be converted to an OK response")]
public static void Case1() => "this is a test".AsOk().Should().NotBeNull();

[Fact(DisplayName = "A T can be converted to an OK response and cookie")]
public static async Task Case2() =>
await "this is a test"
.AsOk(Cookie.New("a", "c"))
.AsResponse()
.Assert(r => r.StatusCode.Should().Be((int)HttpStatusCode.OK))
.And(r => r.Headers.Should().ContainKey("Set-Cookie").And.ContainValue("a=c; path=/"))
.And(r => new StreamReader(r.Body).ReadToEnd().Should().Be("\"this is a test\""));

[Fact(DisplayName = "A response can be converted to an action response")]
public static async Task Case3() =>
await Resp.BadRequest
.Result()
.WithProblemDetails("a", "a", "a", "A")
.AsAction<string>(Cookie.New("a", "b"))
.AsResponse()
.Assert(r => r.StatusCode.Should().Be((int)HttpStatusCode.BadRequest))
.And(r => r.Headers.Should().ContainKey("Set-Cookie").And.ContainValue("a=b; path=/"))
.And(
r =>
new StreamReader(r.Body)
.ReadToEnd()
.Should()
.Be(
"{\"type\":\"a\",\"title\":\"a\",\"status\":400,\"detail\":\"a\",\"instance\":\"A\"}"
)
);

[Fact(DisplayName = "A response can be converted to an action response with no content")]
public static async Task Case4() =>
await Resp.NotAcceptable
.Result()
.WithHeader("a", "b")
.AsAction<string>()
.AsResponse()
.Assert(r => r.StatusCode.Should().Be((int)HttpStatusCode.NotAcceptable))
.And(r => r.Headers.Should().ContainKey("a").And.ContainValue("b"))
.And(r => r.ContentType.Should().BeNullOrEmpty())
.And(r => r.ContentLength.Should().Be(0L));
}
5 changes: 5 additions & 0 deletions HttpBuildR.ActionResult.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
global using BunsenBurner;
global using FluentAssertions;
global using Xunit;
global using Resp = System.Net.HttpStatusCode;
global using Scenario = BunsenBurner.Scenario<BunsenBurner.Syntax.Aaa>;
35 changes: 35 additions & 0 deletions HttpBuildR.ActionResult.Tests/HttpBuildR.ActionResult.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BunsenBurner" Version="6.2.0" />
<PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="Meziantou.Xunit.ParallelTestFramework" Version="2.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HttpBuildR.ActionResult\HttpBuildR.ActionResult.csproj" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
</ItemGroup>

</Project>
108 changes: 108 additions & 0 deletions HttpBuildR.ActionResult/ActionResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Diagnostics.Contracts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;

// ReSharper disable once CheckNamespace
namespace HttpBuildR;

using Resp = System.Net.HttpStatusCode;

/// <summary>
/// ActionResult builders
/// </summary>
public static class ActionResult
{
private sealed class HttpResponseMessageAction : Microsoft.AspNetCore.Mvc.ActionResult
{
private readonly HttpResponseMessage _response;
private readonly IEnumerable<Cookie> _cookies;

public HttpResponseMessageAction(HttpResponseMessage response, IEnumerable<Cookie> cookies)
{
_response = response;
_cookies = cookies;
}

public override async Task ExecuteResultAsync(ActionContext context)
{
var resp = context.HttpContext.Response;
resp.StatusCode = (int)_response.StatusCode;
foreach (var kvp in _response.Headers.Concat(_response.Content.Headers))
resp.Headers.Add(kvp.Key, new StringValues(kvp.Value.ToArray()));
resp.Body = await _response.Content.ReadAsStreamAsync(
context.HttpContext.RequestAborted
);
resp.ContentType = _response.Content.Headers.ContentType?.ToString() ?? string.Empty;
resp.ContentLength = _response.Content.Headers.ContentLength;
foreach (var cookie in _cookies)
resp.Cookies.Append(cookie.Key, cookie.Value, cookie.Options);
}
}

/// <summary>
/// Converts T to an ActionResult of T
/// </summary>
/// <param name="result">result</param>
/// <param name="cookies">optional cookies</param>
/// <typeparam name="T">some T</typeparam>
/// <returns>action result of T</returns>
[Pure]
public static ActionResult<T> AsOk<T>(this T result, params Cookie[] cookies)
where T : notnull =>
cookies.Length == 0
? new(result)
: Resp.OK.Result().WithJsonContent(result).AsAction<T>(cookies);

/// <summary>
/// Converts a HttpResponseMessage to an ActionResult of T
/// </summary>
/// <param name="response">response</param>
/// <param name="cookies">optional cookies</param>
/// <typeparam name="T">some T</typeparam>
/// <returns>action result of T</returns>
[Pure]
public static ActionResult<T> AsAction<T>(
this HttpResponseMessage response,
params Cookie[] cookies
) => new HttpResponseMessageAction(response, cookies);

/// <summary>
/// Adds problem details as a json response body
/// </summary>
/// <param name="response">response</param>
/// <param name="details">problem details</param>
/// <returns>response</returns>
[Pure]
public static HttpResponseMessage WithProblemDetails(
this HttpResponseMessage response,
ProblemDetails details
) => response.WithJsonContent(details);

/// <summary>
/// Adds problem details as a json response body
/// </summary>
/// <param name="response">response</param>
/// <param name="type">A URI reference [RFC3986] that identifies the problem type</param>
/// <param name="title">A short, human-readable summary of the problem type</param>
/// <param name="detail">A human-readable explanation specific to this occurrence of the problem</param>
/// <param name="instance">A URI reference that identifies the specific occurrence of the problem</param>
/// <returns>response</returns>
[Pure]
public static HttpResponseMessage WithProblemDetails(
this HttpResponseMessage response,
string? type,
string? title,
string? detail,
string? instance
) =>
response.WithProblemDetails(
new ProblemDetails
{
Type = type,
Title = title,
Detail = detail,
Status = (int)response.StatusCode,
Instance = instance
}
);
}
42 changes: 42 additions & 0 deletions HttpBuildR.ActionResult/Cookie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Http;

// ReSharper disable once CheckNamespace
namespace HttpBuildR;

/// <summary>
/// Defines a response cookie
/// </summary>
public readonly struct Cookie
{
/// <summary>
/// Key
/// </summary>
public string Key { get; }

/// <summary>
/// Value
/// </summary>
public string Value { get; }

/// <summary>
/// Cookie options
/// </summary>
public CookieOptions Options { get; }

private Cookie(string key, string value, CookieOptions options)
{
Key = key;
Value = value;
Options = options;
}

/// <summary>
/// Creates a new cookie
/// </summary>
/// <param name="key">key</param>
/// <param name="value">value</param>
/// <param name="options">options</param>
/// <returns>cookie</returns>
public static Cookie New(string key, string value, CookieOptions? options = null) =>
new(key, value, options ?? new CookieOptions());
}
69 changes: 69 additions & 0 deletions HttpBuildR.ActionResult/HttpBuildR.ActionResult.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Authors>Ben Mazzarol</Authors>
</PropertyGroup>

<PropertyGroup>
<PackageId>HttpBuildR.ActionResult</PackageId>
<Title>Http BuildR ActionResult</Title>
<Description>Simple respones builder functions on top of ActionResult!</Description>
<PackageTags>C#, Functional, Fluent, Builder, Response, ActionResult</PackageTags>
<PackageProjectUrl>https://github.com/bmazzarol/Http-BuildR</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Copyright>Copyright (c) Ben Mazzarol. All rights reserved.</Copyright>
<ProjectGuid>d4e22a46-f7cd-400e-85bb-f849038bb67b</ProjectGuid>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IsPackable>True</IsPackable>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>construction-icon.png</PackageIcon>
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisMode>Recommended</AnalysisMode>
<OutputType>library</OutputType>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\construction-icon.png" Pack="true" PackagePath="\" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.51.0.59060">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HttpBuildR.Response\HttpBuildR.Response.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit b49c490

Please sign in to comment.