From 238d02b93b743fc28b1dd45f9064484a05597180 Mon Sep 17 00:00:00 2001 From: Collin Barrett Date: Tue, 4 Jun 2024 10:53:21 -0500 Subject: [PATCH] =?UTF-8?q?feat(dir):=20=E2=9C=A8=20migrate=20OpenAPI=20&?= =?UTF-8?q?=20SwaggerUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilterLists.Directory.Api/Endpoints.cs | 110 ++++++++++++++---- .../FilterLists.Directory.Api.csproj | 5 + .../OpenApi/OpenApiConfigurationExtension.cs | 38 ++++++ .../OpenApi/OpenApiTags.cs | 106 +++++++++++++++++ .../FilterLists.Directory.Api/Program.cs | 4 + .../Properties/launchSettings.json | 4 +- .../FilterLists.Directory.Application.csproj | 1 + 7 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiConfigurationExtension.cs create mode 100644 services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiTags.cs diff --git a/services/Directory/FilterLists.Directory.Api/Endpoints.cs b/services/Directory/FilterLists.Directory.Api/Endpoints.cs index 60f4f1d6dc..b0f00c70b9 100644 --- a/services/Directory/FilterLists.Directory.Api/Endpoints.cs +++ b/services/Directory/FilterLists.Directory.Api/Endpoints.cs @@ -1,5 +1,8 @@ using FilterLists.Directory.Application.Queries; using MediatR; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using static FilterLists.Directory.Api.OpenApi.OpenApiTags; namespace FilterLists.Directory.Api; @@ -8,43 +11,102 @@ internal static class Endpoints internal static void MapEndpoints(this WebApplication app) { app.MapGet("/languages", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetLanguages.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetLanguages.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [LanguagesTag], + Summary = "Gets the languages targeted by the FilterLists.", + OperationId = nameof(GetLanguages) + }); app.MapGet("/licenses", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetLicenses.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetLicenses.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [LicensesTag], + Summary = "Gets the licenses applied to the FilterLists.", + OperationId = nameof(GetLicenses) + }); app.MapGet("/lists", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetLists.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetLists.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [FilterListsTag], + Summary = "Gets the FilterLists.", + OperationId = nameof(GetLists) + }); app.MapGet("/lists/{id:int}", - async (int id, IMediator mediator, CancellationToken ct) => - await mediator.Send(new GetListDetails.Request(id), ct) - ); + async (int id, IMediator mediator, CancellationToken ct) => + await mediator.Send(new GetListDetails.Request(id), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [FilterListsTag], + Summary = "Gets the details of the FilterList.", + OperationId = nameof(GetListDetails), + Parameters = + [ + new OpenApiParameter + { + Name = "id", + In = ParameterLocation.Path, + Description = "The identifier of the FilterList.", + Required = true, + Example = new OpenApiInteger(1) + } + ] + }); app.MapGet("/maintainers", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetMaintainers.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetMaintainers.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [MaintainersTag], + Summary = "Gets the maintainers of the FilterLists.", + OperationId = nameof(GetMaintainers) + }); app.MapGet("/software", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetSoftware.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetSoftware.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [SoftwareTag], + Summary = "Gets the software that subscribes to the FilterLists.", + OperationId = nameof(GetSoftware) + }); app.MapGet("/syntaxes", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetSyntaxes.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetSyntaxes.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [SyntaxesTag], + Summary = "Gets the syntaxes of the FilterLists.", + OperationId = nameof(GetSyntaxes) + }); app.MapGet("/tags", - (IMediator mediator, CancellationToken ct) => - mediator.CreateStream(new GetTags.Request(), ct) - ); + (IMediator mediator, CancellationToken ct) => + mediator.CreateStream(new GetTags.Request(), ct) + ) + .WithOpenApi(operation => new OpenApiOperation(operation) + { + Tags = [TagsTag], + Summary = "Gets the tags of the FilterLists.", + OperationId = nameof(GetTags) + }); } } \ No newline at end of file diff --git a/services/Directory/FilterLists.Directory.Api/FilterLists.Directory.Api.csproj b/services/Directory/FilterLists.Directory.Api/FilterLists.Directory.Api.csproj index 924f81eb47..37c2372465 100644 --- a/services/Directory/FilterLists.Directory.Api/FilterLists.Directory.Api.csproj +++ b/services/Directory/FilterLists.Directory.Api/FilterLists.Directory.Api.csproj @@ -11,4 +11,9 @@ + + + + + diff --git a/services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiConfigurationExtension.cs b/services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiConfigurationExtension.cs new file mode 100644 index 0000000000..e12852a643 --- /dev/null +++ b/services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiConfigurationExtension.cs @@ -0,0 +1,38 @@ +using Microsoft.OpenApi.Models; +using ConfigurationExtensions = FilterLists.Directory.Application.ConfigurationExtensions; + +namespace FilterLists.Directory.Api.OpenApi; + +internal static class OpenApiConfigurationExtensions +{ + internal static void AddOpenApiGen(this IServiceCollection services) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options => + { + options.SwaggerDoc( + "v1", + new OpenApiInfo + { + Title = "FilterLists Directory API", + Description = "An ASP.NET Core API serving the core FilterList information.", + Version = "v1", + TermsOfService = + new Uri("https://github.com/collinbarrett/FilterLists/blob/main/.github/CODE_OF_CONDUCT.md"), + Contact = new OpenApiContact { Name = "FilterLists", Url = new Uri("https://filterlists.com") }, + License = new OpenApiLicense + { + Name = "MIT License", + Url = new Uri("https://github.com/collinbarrett/FilterLists/blob/main/LICENSE") + } + }); + + // include view model xml comments + var xmlFilename = $"{typeof(ConfigurationExtensions).Assembly.GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + + // include OpenApiTag Description and ExternalDocs + options.DocumentFilter(); + }); + } +} \ No newline at end of file diff --git a/services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiTags.cs b/services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiTags.cs new file mode 100644 index 0000000000..d4e519cb5b --- /dev/null +++ b/services/Directory/FilterLists.Directory.Api/OpenApi/OpenApiTags.cs @@ -0,0 +1,106 @@ +using FilterLists.Directory.Infrastructure.Persistence.Queries.Context; +using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace FilterLists.Directory.Api.OpenApi; + +internal static class OpenApiTags +{ + internal static readonly OpenApiTag LanguagesTag = new() + { + Name = nameof(QueryDbContext.Languages), + Description = "A written form of communication used by sites targeted by a FilterList", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(Language)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(Language)}") + } + }; + + internal static readonly OpenApiTag LicensesTag = new() + { + Name = nameof(QueryDbContext.Licenses), + Description = "A legal document governing the use or redistribution of a FilterList", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(License)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(License)}") + } + }; + + internal static readonly OpenApiTag FilterListsTag = new() + { + Name = nameof(QueryDbContext.FilterLists), + Description = "A text file containing a list of rules for blocking or manipulating internet traffic", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(FilterList)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(FilterList)}") + } + }; + + internal static readonly OpenApiTag MaintainersTag = new() + { + Name = nameof(QueryDbContext.Maintainers), + Description = "An individual, group, or organization who maintains one or more FilterLists", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(Maintainer)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(Maintainer)}") + } + }; + + internal static readonly OpenApiTag SoftwareTag = new() + { + Name = nameof(QueryDbContext.Software), + Description = "An application, browser extension, or other utility that consumes FilterLists", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(Software)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(Software)}") + } + }; + + internal static readonly OpenApiTag SyntaxesTag = new() + { + Name = nameof(QueryDbContext.Syntaxes), + Description = "A named set of rules that govern the format of a FilterList", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(Syntax)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(Syntax)}") + } + }; + + internal static readonly OpenApiTag TagsTag = new() + { + Name = nameof(QueryDbContext.Tags), + Description = + "A generic taxonomy applied to a FilterList to provide information about its contents and/or purpose", + ExternalDocs = new OpenApiExternalDocs + { + Description = $"{nameof(FilterLists)} {nameof(Tag)} Wiki", + Url = new Uri($"https://github.com/collinbarrett/FilterLists/wiki/{nameof(Tag)}") + } + }; + + [UsedImplicitly] + internal class TagDescriptionsDocumentFilter : IDocumentFilter + { + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + swaggerDoc.Tags = + [ + LanguagesTag, + LicensesTag, + FilterListsTag, + MaintainersTag, + SoftwareTag, + SyntaxesTag, + TagsTag + ]; + } + } +} \ No newline at end of file diff --git a/services/Directory/FilterLists.Directory.Api/Program.cs b/services/Directory/FilterLists.Directory.Api/Program.cs index 62c953281b..4ed65eb4b6 100644 --- a/services/Directory/FilterLists.Directory.Api/Program.cs +++ b/services/Directory/FilterLists.Directory.Api/Program.cs @@ -1,4 +1,5 @@ using FilterLists.Directory.Api; +using FilterLists.Directory.Api.OpenApi; using FilterLists.Directory.Application; var builder = WebApplication.CreateBuilder(args); @@ -6,6 +7,7 @@ builder.WebHost.ConfigureKestrel(serverOptions => serverOptions.AddServerHeader = false); builder.AddServiceDefaults(); builder.Services.AddProblemDetails(); +builder.Services.AddOpenApiGen(); builder.AddApplication(); var app = builder.Build(); @@ -13,5 +15,7 @@ app.UseExceptionHandler(); app.MapEndpoints(); app.MapDefaultEndpoints(); +app.UseSwagger(); +app.UseSwaggerUI(); app.Run(); \ No newline at end of file diff --git a/services/Directory/FilterLists.Directory.Api/Properties/launchSettings.json b/services/Directory/FilterLists.Directory.Api/Properties/launchSettings.json index c3363795da..7f605a79e8 100644 --- a/services/Directory/FilterLists.Directory.Api/Properties/launchSettings.json +++ b/services/Directory/FilterLists.Directory.Api/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "lists", + "launchUrl": "swagger/index.html", "applicationUrl": "http://localhost:5444", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -15,7 +15,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "lists", + "launchUrl": "swagger/index.html", "applicationUrl": "https://localhost:7490;http://localhost:5444", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/services/Directory/FilterLists.Directory.Application/FilterLists.Directory.Application.csproj b/services/Directory/FilterLists.Directory.Application/FilterLists.Directory.Application.csproj index 489f452c38..8ff353e91b 100644 --- a/services/Directory/FilterLists.Directory.Application/FilterLists.Directory.Application.csproj +++ b/services/Directory/FilterLists.Directory.Application/FilterLists.Directory.Application.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true