Skip to content

Commit

Permalink
Added basic Swagger UI (+Swashbuckle) documentation. #4
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed May 5, 2024
1 parent fbb3183 commit dfa8204
Show file tree
Hide file tree
Showing 66 changed files with 1,085 additions and 133 deletions.
35 changes: 35 additions & 0 deletions docs/decisions/0140-openapi-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# OpenApi Docs

* status: accepted
* date: 2024-05-02
* deciders: jezzsantos

# Context and Problem Statement

Adding OpenAPI documentation to the project will help developers better understand the API and how to use it.

In .net8.0 there is partial support for OpenAPI by ASP.NET Core, but it is not as feature rich as the Swashbuckle or NSwag libraries.
That wil ultimately change in .net9.0 as Swashbuckle has gone out of support. As per [this announcement](https://github.com/dotnet/aspnetcore/issues/54599)

We need the following capabilities.

1. Provide a developer fo the API some documentation about the API.
2. Enables the developer to try out the API with a built-in tool
3. Allows the backend to publish to other tools that can derive code from the API documentation. For example, JS/TS classes to be consumed by the WebsiteHost

## Considered Options

The options are:

1. Swashbuckle
2. NSwag
3. AspNetCore OpenAPI

## Decision Outcome

`Swashbuckle`

- It is mostly supported in .net8.0.
- It does the basics of what we need now, with some custom extensions
- NSwag has serious issues with correctly generating "multipart/form-data" data requests
- We will switch to .net9.0 when released
9 changes: 9 additions & 0 deletions docs/design-principles/0140-developer-tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,12 @@ Specific class properties:

TBD

## Swagger UI

> a.k.a OpenAPI documentation
We provide Swagger UI tooling for both the frontend and backend APIs.

For Backend API: you can find Swagger UI a the root URL of the site `/`

From Frontend API: you can find the swagger UI at : `/swagger`
2 changes: 1 addition & 1 deletion src/ImagesInfrastructure.IntegrationTests/ImagesApiSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
using Application.Resources.Shared;
using Common.Extensions;
using FluentAssertions;
using Infrastructure.Web.Api.Common;
using Infrastructure.Web.Api.Common.Extensions;
using Infrastructure.Web.Api.Interfaces;
using Infrastructure.Web.Api.Operations.Shared.Images;
using Infrastructure.Web.Interfaces.Clients;
using IntegrationTesting.WebApi.Common;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Common.Recording;
using FluentAssertions;
using Infrastructure.Shared.ApplicationServices.External;
using Infrastructure.Web.Api.Common;
using Infrastructure.Web.Api.Interfaces;
using IntegrationTesting.WebApi.Common;
using Microsoft.Extensions.DependencyInjection;
using UnitTesting.Common;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#if TESTINGONLY
using Infrastructure.Web.Api.Common;
using Infrastructure.Web.Api.Interfaces;
using Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith;
using Infrastructure.Web.Interfaces.Clients;
using Flag = Common.FeatureFlags.Flag;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FluentAssertions;
using Infrastructure.Web.Api.Common.Endpoints;
using Infrastructure.Web.Api.Common.Pipeline;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -228,6 +229,41 @@ public async Task WhenInvokeAsyncWithNullValueIResultResponse_ThenReturnsJsonCon
.StatusCode.Should().Be(200);
}

[Fact]
public async Task WhenInvokeAsyncWithNakedObjectResponseAndAcceptEverything_ThenReturnsJson()
{
var response = new TestResponse();
var httpContext = SetupHttpContext(FormatMechanism.AcceptHeader(HttpContentTypes.Everything));
var context = new DefaultEndpointFilterInvocationContext(httpContext.Object);
var next = new EndpointFilterDelegate(_ => new ValueTask<object?>(response));

var result = await _filter.InvokeAsync(context, next);

result.Should().BeOfType<JsonHttpResult<object>>();
result.As<JsonHttpResult<object>>()
.StatusCode.Should().Be((int)HttpStatusCode.OK);
result.As<JsonHttpResult<object>>()
.Value.Should().Be(response);
}

[Fact]
public async Task WhenInvokeAsyncWithIResultResponseAndAcceptEverything_ThenReturnsJson()
{
var payload = new TestResponse();
var response = Results.Ok(payload);
var httpContext = SetupHttpContext(FormatMechanism.AcceptHeader(HttpContentTypes.Everything));
var context = new DefaultEndpointFilterInvocationContext(httpContext.Object);
var next = new EndpointFilterDelegate(_ => new ValueTask<object?>(response));

var result = await _filter.InvokeAsync(context, next);

result.Should().BeOfType<JsonHttpResult<object>>();
result.As<JsonHttpResult<object>>()
.StatusCode.Should().Be((int)HttpStatusCode.OK);
result.As<JsonHttpResult<object>>()
.Value.Should().Be(payload);
}

[Fact]
public async Task WhenInvokeAsyncWithNakedObjectResponseAndAcceptJson_ThenReturnsJson()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FluentAssertions;
using Infrastructure.Web.Api.Common.Endpoints;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using Xunit;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FluentAssertions;
using Infrastructure.Web.Api.Common.Extensions;
using Infrastructure.Web.Api.Interfaces;
using Xunit;

namespace Infrastructure.Web.Api.Common.UnitTests.Extensions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Common;
using Common.Extensions;
using FluentAssertions;
using Infrastructure.Web.Api.Interfaces;
using JetBrains.Annotations;
using UnitTesting.Common;
using Xunit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Common.Extensions;
using Infrastructure.Web.Api.Common.Extensions;
using Infrastructure.Web.Api.Common.Pipeline;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -110,6 +111,11 @@ public class ContentNegotiationFilter : IEndpointFilter

if (accepts.HasAny())
{
if (accepts.ContainsIgnoreCase(HttpContentTypes.Everything))
{
return NegotiatedMimeType.Json;
}

if (accepts.ContainsIgnoreCase(HttpContentTypes.Json))
{
return NegotiatedMimeType.Json;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Application.Common;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

Expand Down
11 changes: 11 additions & 0 deletions src/Infrastructure.Web.Api.Common/Extensions/ErrorExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Common;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;

namespace Infrastructure.Web.Api.Common.Extensions;

Expand Down Expand Up @@ -28,4 +30,13 @@ public static HttpError ToHttpError(this Error error)

return new HttpError(code, error.Message);
}

/// <summary>
/// Converts the specified error to a <see cref="IResult" /> with a problem detail
/// </summary>
public static ProblemHttpResult ToProblem(this Error error)
{
var httpError = error.ToHttpError();
return (ProblemHttpResult)Results.Problem(statusCode: (int)httpError.Code, detail: httpError.Message);
}
}
38 changes: 13 additions & 25 deletions src/Infrastructure.Web.Api.Common/Extensions/HandlerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using Common;
using Common.Extensions;
using Infrastructure.Web.Api.Interfaces;
Expand Down Expand Up @@ -159,36 +160,23 @@ private static IResult ToResult<TResponse>(this PostResult<TResponse> postResult
{
var response = postResult.Response;
var location = postResult.Location;
switch (method)
{
case OperationMethod.Get:
case OperationMethod.Search:
return Results.Ok(response);

case OperationMethod.Post:
{
return location.HasValue()
? Results.Created(location, response)
: Results.Ok(response);
}

case OperationMethod.PutPatch:
return Results.Accepted(null, response);
var options =
new ResponseCodeOptions(response is not EmptyResponse, location.HasValue());
var statusCode = method.ToHttpMethod().GetDefaultResponseCode(options);

case OperationMethod.Delete:
var hasResponse = response is not EmptyResponse;
return hasResponse
? Results.Accepted(null, response)
: Results.NoContent();

default:
return Results.Ok(response);
}
return statusCode switch
{
HttpStatusCode.OK => Results.Ok(response),
HttpStatusCode.Accepted => Results.Accepted(null, response),
HttpStatusCode.Created => Results.Created(location, response),
HttpStatusCode.NoContent => Results.NoContent(),
_ => Results.Ok(response)
};
}

private static IResult ToResult(this Error error)
{
var httpError = error.ToHttpError();
return Results.Problem(statusCode: (int)httpError.Code, detail: httpError.Message);
return error.ToProblem();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Common.Extensions;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using HttpHeaders = Infrastructure.Web.Api.Interfaces.HttpHeaders;

namespace Infrastructure.Web.Api.Common.Extensions;

Expand Down Expand Up @@ -50,8 +51,6 @@ public static Optional<string> GetAPIKeyAuth(this HttpRequest request)
/// <summary>
/// Returns the values of the BASIC authentication from the request (if any)
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static (Optional<string> Username, Optional<string> Password) GetBasicAuth(this HttpRequest request)
{
var fromBasicAuth = AuthenticationHeaderValue.TryParse(request.Headers.Authorization, out var result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Application.Common;
using Common.Extensions;
using Infrastructure.Web.Api.Common.Endpoints;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace Infrastructure.Web.Api.Common.Extensions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Infrastructure.Web.Api.Interfaces;

namespace Infrastructure.Web.Api.Common.Extensions;

public static class OperationMethodExtensions
{
/// <summary>
/// Converts the <see cref="OperationMethod" /> to an appropriate <see cref="HttpMethod" />
/// </summary>
public static HttpMethod ToHttpMethod(this OperationMethod method)
{
return method switch
{
OperationMethod.Get => HttpMethod.Get,
OperationMethod.Search => HttpMethod.Get,
OperationMethod.Post => HttpMethod.Post,
OperationMethod.PutPatch => HttpMethod.Put,
OperationMethod.Delete => HttpMethod.Delete,
_ => HttpMethod.Get
};
}
}
41 changes: 41 additions & 0 deletions src/Infrastructure.Web.Api.Common/Extensions/ResponseExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Net;
using Infrastructure.Web.Api.Interfaces;

namespace Infrastructure.Web.Api.Common.Extensions;

public static class ResponseExtensions
{
/// <summary>
/// Returns the default <see cref="HttpStatusCode" /> for the specified <see cref="responseType" />
/// and <see cref="options" />
/// </summary>
public static HttpStatusCode GetDefaultResponseCode(this HttpMethod method, ResponseCodeOptions options)
{
if (method == HttpMethod.Get)
{
return HttpStatusCode.OK;
}

if (method == HttpMethod.Post)
{
return options.HasLocation
? HttpStatusCode.Created
: HttpStatusCode.OK;
}

if (method == HttpMethod.Put
|| method == HttpMethod.Patch)
{
return HttpStatusCode.Accepted;
}

if (method == HttpMethod.Delete)
{
return options.HasContent
? HttpStatusCode.Accepted
: HttpStatusCode.NoContent;
}

return HttpStatusCode.OK;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Net;
using FluentValidation.Results;
using Infrastructure.Web.Api.Common.Validation;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -23,13 +22,11 @@ public static ProblemDetails ToRfc7807(this ValidationResult result, string requ
.ToList();
var firstMessage = result.Errors.Select(error => error.ErrorMessage)
.First();
var firstCode = result.Errors.Select(error => error.ErrorCode)
.First();

var details = new ProblemDetails
{
Type = firstCode,
Title = ValidationResources.ValidationBehavior_ErrorTitle,
Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5",
Title = "Bad Request",
Status = (int)HttpStatusCode.BadRequest,
Detail = firstMessage,
Instance = requestUrl
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Xml;
using Infrastructure.Web.Api.Common.Extensions;
using Infrastructure.Web.Api.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit dfa8204

Please sign in to comment.