From e8cfbf5b6f5f43f5942532886bccf4b454ac9f6c Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 4 Jun 2026 17:40:39 +0100 Subject: [PATCH 01/62] WIP --- .github/workflows/pr-gatechecks.yml | 28 +++++++++++ CheersDb.slnx | 7 +++ Directory.Packages.props | 10 ++++ Directory.build.props | 9 ++++ src/CheersDb.Api/CheersDb.Api.csproj | 13 +++++ src/CheersDb.Api/CheersDb.Api.http | 6 +++ .../Controllers/ProducersController.cs | 42 +++++++++++++++++ .../Dtos/GetProducerDetailsDto.cs | 33 +++++++++++++ src/CheersDb.Api/Dtos/LinkDto.cs | 25 ++++++++++ src/CheersDb.Api/Http/LinkRelationships.cs | 47 +++++++++++++++++++ src/CheersDb.Api/Program.cs | 31 ++++++++++++ .../Properties/launchSettings.json | 23 +++++++++ src/CheersDb.Api/appsettings.Development.json | 8 ++++ src/CheersDb.Api/appsettings.json | 9 ++++ 14 files changed, 291 insertions(+) create mode 100644 .github/workflows/pr-gatechecks.yml create mode 100644 CheersDb.slnx create mode 100644 Directory.Packages.props create mode 100644 Directory.build.props create mode 100644 src/CheersDb.Api/CheersDb.Api.csproj create mode 100644 src/CheersDb.Api/CheersDb.Api.http create mode 100644 src/CheersDb.Api/Controllers/ProducersController.cs create mode 100644 src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs create mode 100644 src/CheersDb.Api/Dtos/LinkDto.cs create mode 100644 src/CheersDb.Api/Http/LinkRelationships.cs create mode 100644 src/CheersDb.Api/Program.cs create mode 100644 src/CheersDb.Api/Properties/launchSettings.json create mode 100644 src/CheersDb.Api/appsettings.Development.json create mode 100644 src/CheersDb.Api/appsettings.json diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml new file mode 100644 index 0000000..1f0c672 --- /dev/null +++ b/.github/workflows/pr-gatechecks.yml @@ -0,0 +1,28 @@ +name: PR Gatechecks + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + name: Build solution + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '10.0.x' + + - name: Show .NET info + run: dotnet --info + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release diff --git a/CheersDb.slnx b/CheersDb.slnx new file mode 100644 index 0000000..aa61e83 --- /dev/null +++ b/CheersDb.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..f2c3c1c --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,10 @@ + + + true + + + + + + + \ No newline at end of file diff --git a/Directory.build.props b/Directory.build.props new file mode 100644 index 0000000..912a780 --- /dev/null +++ b/Directory.build.props @@ -0,0 +1,9 @@ + + + 0.0.1 + net10.0 + 14.0 + enable + enable + + \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj new file mode 100644 index 0000000..5fa543f --- /dev/null +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -0,0 +1,13 @@ + + + + true + + + + + + + + + \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.http b/src/CheersDb.Api/CheersDb.Api.http new file mode 100644 index 0000000..d0bbb9b --- /dev/null +++ b/src/CheersDb.Api/CheersDb.Api.http @@ -0,0 +1,6 @@ +@CheersDb.Api_HostAddress = http://localhost:5104 + +GET {{CheersDb.Api_HostAddress}}/producers/1 +Accept: application/json + +### diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs new file mode 100644 index 0000000..058ecfd --- /dev/null +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -0,0 +1,42 @@ +using CheersDb.Api.Dtos; +using CheersDb.Api.Http; +using Microsoft.AspNetCore.Mvc; + +namespace CheersDb.Api.Controllers; + +/// +/// Controller for managing producers in the CheersDb API. +/// +[ApiController] +[Route("[controller]")] +public class ProducersController : ControllerBase +{ + /// + /// Retrieves details for a specific producer by id + /// + /// The id of the producer to retrieve + /// The requested producer details + /// Returns the requested producer in the response body + /// Indicates the requested producer was not found, or the URI is invalid + [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] + [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public IActionResult GetProducerDetails([FromRoute] int id) + { + return Ok(new GetProducerDetailsDto() + { + Id = id, + Name = "Producer Name", + Links = + [ + new() + { + Rel = LinkRelationships.Self, + Href = Url.RouteUrl(nameof(GetProducerDetails), new { id }) ?? string.Empty, + Method = HttpMethod.Get.ToString() + } + ] + }); + } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs new file mode 100644 index 0000000..ab1d2a2 --- /dev/null +++ b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs @@ -0,0 +1,33 @@ +namespace CheersDb.Api.Dtos; + +/// +/// Details about a producer +/// +public class GetProducerDetailsDto +{ + /// + /// The id of the producer + /// + /// 328 + public int? Id { get; init; } + + /// + /// The name of the producer + /// + /// Rye River Brewing + public string? Name { get; init; } + + /// + /// Links related to the producer, such as a self link to retrieve the producer details + /// + /// + /// [ + /// { + /// "rel": "self", + /// "href": "/producers/328", + /// "method": "GET" + /// } + /// ] + /// + public List? Links { get; init; } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Dtos/LinkDto.cs b/src/CheersDb.Api/Dtos/LinkDto.cs new file mode 100644 index 0000000..488a435 --- /dev/null +++ b/src/CheersDb.Api/Dtos/LinkDto.cs @@ -0,0 +1,25 @@ +namespace CheersDb.Api.Dtos; + +/// +/// Represents a hypermedia link in the API response, providing information about the relationship, URL, and HTTP method for the link. +/// +public class LinkDto +{ + /// + /// The relationship of the link to the current resource + /// + /// self + public required string Rel { get; init; } + + /// + /// The URL of the link + /// + /// /producers/1 + public required string Href { get; init; } + + /// + /// The HTTP method for the link + /// + /// GET + public required string Method { get; init; } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Http/LinkRelationships.cs b/src/CheersDb.Api/Http/LinkRelationships.cs new file mode 100644 index 0000000..cc3c862 --- /dev/null +++ b/src/CheersDb.Api/Http/LinkRelationships.cs @@ -0,0 +1,47 @@ +namespace CheersDb.Api.Http; + +/// +/// Defines standard link relationship types for hypermedia links in the API responses, following common conventions for RESTful APIs. +/// +public static class LinkRelationships +{ + /// + /// Indicates an alternate representation of the resource. + /// + public const string Alternate = "alternate"; + + /// + /// Identifies a collection resource (a list of items). + /// + public const string Collection = "collection"; + + /// + /// Identifies an individual item within a collection. + /// + public const string Item = "item"; + + /// + /// A link to the next page or resource in a sequence. + /// + public const string Next = "next"; + + /// + /// A link to the previous page or resource in a sequence. + /// + public const string Prev = "prev"; + + /// + /// A related resource linked to the current resource. + /// + public const string Related = "related"; + + /// + /// A link that points to the resource itself. + /// + public const string Self = "self"; + + /// + /// A link to a parent resource or higher-level context. + /// + public const string Up = "up"; +} \ No newline at end of file diff --git a/src/CheersDb.Api/Program.cs b/src/CheersDb.Api/Program.cs new file mode 100644 index 0000000..e96752c --- /dev/null +++ b/src/CheersDb.Api/Program.cs @@ -0,0 +1,31 @@ +using Scalar.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); + +builder.Services.AddRouting(options => +{ + options.LowercaseUrls = true; + options.LowercaseQueryStrings = true; +}); + +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference("/docs"); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/src/CheersDb.Api/Properties/launchSettings.json b/src/CheersDb.Api/Properties/launchSettings.json new file mode 100644 index 0000000..b120c85 --- /dev/null +++ b/src/CheersDb.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5104", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7277;http://localhost:5104", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/CheersDb.Api/appsettings.Development.json b/src/CheersDb.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/CheersDb.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/CheersDb.Api/appsettings.json b/src/CheersDb.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/CheersDb.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 98eec4ba10327de84e262faf6ef3602e1877d59d Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 09:02:22 +0100 Subject: [PATCH 02/62] WIP --- Directory.Packages.props | 6 +- src/CheersDb.Api/AppSettings.cs | 14 +++++ .../Controllers/ProducersController.cs | 11 ++-- .../ConfigurationManagerExtensions.cs | 28 +++++++++ .../Extensions/OpenApiOptionsExtensions.cs | 63 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 26 ++++++++ .../{LinkRelationships.cs => LinkRels.cs} | 2 +- src/CheersDb.Api/Program.cs | 9 ++- src/CheersDb.Api/appsettings.json | 9 ++- 9 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 src/CheersDb.Api/AppSettings.cs create mode 100644 src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs create mode 100644 src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs create mode 100644 src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs rename src/CheersDb.Api/Http/{LinkRelationships.cs => LinkRels.cs} (96%) diff --git a/Directory.Packages.props b/Directory.Packages.props index f2c3c1c..f5a84fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,8 +3,8 @@ true - - - + + + \ No newline at end of file diff --git a/src/CheersDb.Api/AppSettings.cs b/src/CheersDb.Api/AppSettings.cs new file mode 100644 index 0000000..6787daf --- /dev/null +++ b/src/CheersDb.Api/AppSettings.cs @@ -0,0 +1,14 @@ +using Microsoft.OpenApi; + +namespace CheersDb.Api; + +/// +/// Represents the application settings for the CheersDb API. +/// +public class AppSettings +{ + /// + /// Gets the OpenAPI information for the API, such as title, version, and description. + /// + public OpenApiInfo? OpenApiInfo { get; init; } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index 058ecfd..8400809 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -1,6 +1,7 @@ using CheersDb.Api.Dtos; using CheersDb.Api.Http; using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; namespace CheersDb.Api.Controllers; @@ -9,6 +10,7 @@ namespace CheersDb.Api.Controllers; /// [ApiController] [Route("[controller]")] +[Produces(MediaTypeNames.Application.Json)] public class ProducersController : ControllerBase { /// @@ -16,12 +18,9 @@ public class ProducersController : ControllerBase /// /// The id of the producer to retrieve /// The requested producer details - /// Returns the requested producer in the response body - /// Indicates the requested producer was not found, or the URI is invalid [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] - [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] + [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] public IActionResult GetProducerDetails([FromRoute] int id) { return Ok(new GetProducerDetailsDto() @@ -32,7 +31,7 @@ public IActionResult GetProducerDetails([FromRoute] int id) [ new() { - Rel = LinkRelationships.Self, + Rel = LinkRels.Self, Href = Url.RouteUrl(nameof(GetProducerDetails), new { id }) ?? string.Empty, Method = HttpMethod.Get.ToString() } diff --git a/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs b/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs new file mode 100644 index 0000000..14c9a69 --- /dev/null +++ b/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs @@ -0,0 +1,28 @@ +namespace CheersDb.Api.Extensions; + +/// +/// Provides extension methods for the ConfigurationManager class +/// +public static class ConfigurationManagerExtensions +{ + extension(ConfigurationManager configurationManager) + { + /// + /// Gets the application settings from the configuration manager and validates them + /// + /// A validated AppSettings instance. + /// Thrown when the application settings are invalid. + public AppSettings GetAppSettings() + { + var appSettings = configurationManager.Get(); + + if (appSettings is null) + throw new InvalidOperationException("There was an issue parsing the application settings."); + + if (appSettings.OpenApiInfo is null) + throw new InvalidOperationException($"{nameof(AppSettings.OpenApiInfo)} was not parsed in the application settings."); + + return appSettings; + } + } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs new file mode 100644 index 0000000..9b91c3f --- /dev/null +++ b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; +using System.Net; +using System.Net.Mime; + +namespace CheersDb.Api.Extensions; + +/// +/// Provides extension methods for configuring OpenAPI options in the CheersDb API application. +/// +public static class OpenApiOptionsExtensions +{ + private static readonly string _internalServerErrorResponseKey = ((int)HttpStatusCode.InternalServerError).ToString(); + private static readonly Lazy _internalServerErrorResponseReference = new(() => new OpenApiResponseReference(nameof(HttpStatusCode.InternalServerError))); + + /// + /// Adds a document transformer to the OpenAPI options that applies the provided transformation function to the generated OpenAPI document. + /// + /// The OpenAPI options to configure. + /// The application settings containing the OpenAPI information to be applied to the document. + public static OpenApiOptions ConfigureDocument(this OpenApiOptions options, AppSettings appSettings) + { + return options.AddDocumentTransformer(async (document, context, cancellationToken) => + { + if (appSettings?.OpenApiInfo is not null) + document.Info = appSettings.OpenApiInfo; + + document.Components ??= new OpenApiComponents(); + document.Components.Responses ??= new Dictionary(); + + var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), cancellationToken: cancellationToken); + + var response = new OpenApiResponse + { + Description = "Indicates that an unexpected internal server error has occurred", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference(nameof(ProblemDetails)) + } + } + }; + + document.Components.Responses.Add(nameof(HttpStatusCode.InternalServerError), response); + }); + } + + /// + /// Adds an operation transformer to the OpenAPI options that configures operation responses. + /// + /// The OpenAPI options to configure. + /// The application settings. + public static OpenApiOptions ConfigureOperations(this OpenApiOptions options, AppSettings appSettings) + { + return options.AddOperationTransformer(async (operation, context, cancellationToken) => + { + operation.Responses ??= []; + operation.Responses.Add(_internalServerErrorResponseKey, _internalServerErrorResponseReference.Value); + }); + } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs b/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f5e5de4 --- /dev/null +++ b/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.OpenApi; + +namespace CheersDb.Api.Extensions; + +/// +/// Provides extension methods for configuring services in the CheersDb API application. +/// +public static class ServiceCollectionExtensions +{ + extension(IServiceCollection services) + { + /// + /// Configures OpenAPI for the application using the provided AppSettings + /// + /// + public IServiceCollection AddConfiguredOpenApi(AppSettings appSettings) + { + return services.AddOpenApi(options => + { + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; + options.ConfigureDocument(appSettings); + options.ConfigureOperations(appSettings); + }); + } + } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Http/LinkRelationships.cs b/src/CheersDb.Api/Http/LinkRels.cs similarity index 96% rename from src/CheersDb.Api/Http/LinkRelationships.cs rename to src/CheersDb.Api/Http/LinkRels.cs index cc3c862..777c153 100644 --- a/src/CheersDb.Api/Http/LinkRelationships.cs +++ b/src/CheersDb.Api/Http/LinkRels.cs @@ -3,7 +3,7 @@ /// /// Defines standard link relationship types for hypermedia links in the API responses, following common conventions for RESTful APIs. /// -public static class LinkRelationships +public static class LinkRels { /// /// Indicates an alternate representation of the resource. diff --git a/src/CheersDb.Api/Program.cs b/src/CheersDb.Api/Program.cs index e96752c..b857688 100644 --- a/src/CheersDb.Api/Program.cs +++ b/src/CheersDb.Api/Program.cs @@ -1,6 +1,8 @@ +using CheersDb.Api.Extensions; using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); +var appSettings = builder.Configuration.GetAppSettings(); // Add services to the container. builder.Services.AddControllers(); @@ -11,11 +13,12 @@ options.LowercaseQueryStrings = true; }); -builder.Services.AddOpenApi(); +builder.Services.AddConfiguredOpenApi(appSettings); + +builder.Services.AddSingleton(appSettings); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); @@ -28,4 +31,4 @@ app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/src/CheersDb.Api/appsettings.json b/src/CheersDb.Api/appsettings.json index 10f68b8..00af236 100644 --- a/src/CheersDb.Api/appsettings.json +++ b/src/CheersDb.Api/appsettings.json @@ -5,5 +5,10 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "OpenApiInfo": { + "Title": "CheersDb API", + "Description": "An API for retrieving and altering breweries on the CheersDb platform", + "Version": "v1" + } +} \ No newline at end of file From 0a17e0e4c1d2e2a4de9fb5adf1a62873b6d23d77 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 09:30:13 +0100 Subject: [PATCH 03/62] attempted fix --- .github/workflows/pr-gatechecks.yml | 3 --- src/CheersDb.Api/CheersDb.Api.csproj | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 1f0c672..b45a914 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -18,9 +18,6 @@ jobs: with: dotnet-version: '10.0.x' - - name: Show .NET info - run: dotnet --info - - name: Restore run: dotnet restore diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 5fa543f..2b6952d 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -2,6 +2,7 @@ true + net10.0 From bf9d5fa7c3080aa6f150abe505811e54303fbed4 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 09:35:24 +0100 Subject: [PATCH 04/62] fix --- src/CheersDb.Api/CheersDb.Api.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 2b6952d..549967a 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -2,6 +2,7 @@ true + true net10.0 From 7ae903f3ab89b7a818098cf39a3d35e11b501186 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 11:03:34 +0100 Subject: [PATCH 05/62] fix --- src/CheersDb.Api/CheersDb.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 549967a..1c08b93 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -1,5 +1,5 @@  - + true true From b2948bfe483b91ce8110dff6dd4ef3ea2d72b8b3 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 11:05:46 +0100 Subject: [PATCH 06/62] fix --- .github/workflows/pr-gatechecks.yml | 4 ++-- src/CheersDb.Api/CheersDb.Api.csproj | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index b45a914..397a207 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -19,7 +19,7 @@ jobs: dotnet-version: '10.0.x' - name: Restore - run: dotnet restore + run: dotnet restore .\CheersDb.slnx - name: Build - run: dotnet build --no-restore --configuration Release + run: dotnet build .\CheersDb.slnx --no-restore --configuration Release diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 1c08b93..7392820 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -1,5 +1,4 @@  - true true From f405dcabf0683e29920fbc31dfbf0c9309946323 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 11:57:53 +0100 Subject: [PATCH 07/62] fix --- .github/workflows/pr-gatechecks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 397a207..233a340 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -19,7 +19,7 @@ jobs: dotnet-version: '10.0.x' - name: Restore - run: dotnet restore .\CheersDb.slnx + run: dotnet restore CheersDb.slnx - name: Build - run: dotnet build .\CheersDb.slnx --no-restore --configuration Release + run: dotnet build CheersDb.slnx --no-restore --configuration Release From 374e4b1f4d0cf2cf5646836fb4e76b0c72dcc41b Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:15:53 +0100 Subject: [PATCH 08/62] fix --- src/CheersDb.Api/CheersDb.Api.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 7392820..fdcc999 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -1,6 +1,7 @@  true + enable true net10.0 From 4086100567373b935077a5e425b01c725a19a504 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:19:18 +0100 Subject: [PATCH 09/62] wip --- .github/workflows/pr-gatechecks.yml | 4 ++-- Directory.build.props | 7 ++++--- src/CheersDb.Api/CheersDb.Api.csproj | 3 --- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 233a340..b45a914 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -19,7 +19,7 @@ jobs: dotnet-version: '10.0.x' - name: Restore - run: dotnet restore CheersDb.slnx + run: dotnet restore - name: Build - run: dotnet build CheersDb.slnx --no-restore --configuration Release + run: dotnet build --no-restore --configuration Release diff --git a/Directory.build.props b/Directory.build.props index 912a780..2da26e7 100644 --- a/Directory.build.props +++ b/Directory.build.props @@ -1,9 +1,10 @@ - 0.0.1 - net10.0 + enable 14.0 + true enable - enable + net10.0 + 0.0.1 \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index fdcc999..fccedf8 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -1,9 +1,6 @@  true - enable - true - net10.0 From c296cacca9f0505c16db59c4876060bcabe363e6 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:25:22 +0100 Subject: [PATCH 10/62] fix --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index b45a914..ed59654 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' From c154e76fb8a68cf059641fa128d65f03625a3c58 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:29:30 +0100 Subject: [PATCH 11/62] fix --- CheersDb.sln | Bin 0 -> 2666 bytes CheersDb.slnx | 7 ------- 2 files changed, 7 deletions(-) create mode 100644 CheersDb.sln delete mode 100644 CheersDb.slnx diff --git a/CheersDb.sln b/CheersDb.sln new file mode 100644 index 0000000000000000000000000000000000000000..cc1693ccdaeb38295452f9b39bc278f4fbc00d8a GIT binary patch literal 2666 zcmc(hO>fgc5QgVkB>qDbE=9tY(dkQIA(cD~+sW~cr2^Qk%V&n>V268{Ye zh}hxR;0cM`WZje&@*0_~$@J1>pK>uL+Zp&~ncNWYYn$XYe@i8nN>Yj*$2bRz9+j z69-%YJ73!55VC87BjT$#l&gr28s``*;!s&`o48HbQoaJb_3`Y$+6l2$7P9W}Q(jdP zV&kLLGVchREQdo7X8ZdMRg39|WQV(o&X&vqqBZz>)Jl2K$~MVS^NS1*nzKHq$TSx{ z)g1MSkSd;(o1+{r!`qIs_wbl|n$7$!@3cd-esRu=vU|kSr%&`$dsOf6H@fGNd!ToF z%-Ys|2@AH{s_U0U7;yha@`d+=*h$4WWmY!YeSMVWOZD#4Qu!+|FF>#J&-&Y!Wc?iS z-otjzB%Ok31Cu%nZH)%_)7_O}SUW0PnS2FpY^_afYB7=T^u(On)adeoeB9Nd=sflC z0S`K(FNhfzcq!Mho-AX;EWk(TQ$nV3oUnsH5nA0|lX+fB*mh literal 0 HcmV?d00001 diff --git a/CheersDb.slnx b/CheersDb.slnx deleted file mode 100644 index aa61e83..0000000 --- a/CheersDb.slnx +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - From 84d9c071a21451a405f6049a54e957134f7ae941 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:33:53 +0100 Subject: [PATCH 12/62] fizx --- src/CheersDb.Api/CheersDb.Api.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index fccedf8..7078b53 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -1,4 +1,6 @@  + + true From 16dd9c3f17a6a4f4d905d3e0fcd5601aff0b6545 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:50:07 +0100 Subject: [PATCH 13/62] wip --- .github/workflows/pr-gatechecks.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index ed59654..070d0fb 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -18,6 +18,15 @@ jobs: with: dotnet-version: '10.0.x' + - name: Verify Directory.build.props + run: | + if [ -f "Directory.build.props" ]; then + echo "Directory.build.props found" + cat Directory.build.props + else + echo "ERROR: Directory.build.props not found in root" + fi + - name: Restore run: dotnet restore From bd30ca6ca89a261169b1adc12c15def8e253fd84 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:51:33 +0100 Subject: [PATCH 14/62] wip --- src/CheersDb.Api/CheersDb.Api.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 7078b53..6790110 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -1,5 +1,4 @@  - true From 77b987ae53b0a28dd861f060571c97c813cbce21 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:58:49 +0100 Subject: [PATCH 15/62] wip --- .github/workflows/pr-gatechecks.yml | 9 --------- Directory.build.props => Directory.build1.props | 0 2 files changed, 9 deletions(-) rename Directory.build.props => Directory.build1.props (100%) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 070d0fb..ed59654 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -18,15 +18,6 @@ jobs: with: dotnet-version: '10.0.x' - - name: Verify Directory.build.props - run: | - if [ -f "Directory.build.props" ]; then - echo "Directory.build.props found" - cat Directory.build.props - else - echo "ERROR: Directory.build.props not found in root" - fi - - name: Restore run: dotnet restore diff --git a/Directory.build.props b/Directory.build1.props similarity index 100% rename from Directory.build.props rename to Directory.build1.props From 8cf54fec2ff3fe847201edc62e7648f2dd1c7726 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 15:59:37 +0100 Subject: [PATCH 16/62] wip --- Directory.build1.props => Directory.Build.props | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Directory.build1.props => Directory.Build.props (100%) diff --git a/Directory.build1.props b/Directory.Build.props similarity index 100% rename from Directory.build1.props rename to Directory.Build.props From 2f6da0b947091ceb9b012780844c113cdb46a50a Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:07:27 +0100 Subject: [PATCH 17/62] openapoi --- .github/workflows/pr-gatechecks.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index ed59654..848fd22 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -23,3 +23,9 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release + + - name: OpenAPI Analyzer + uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 + with: + # Change this to your OpenAPI file path + file: obj\Release\net10.0\EndpointInfo\CheersDb.Api.json \ No newline at end of file From 753b3bc6c623b33a0bef567a7ddabd7d2f0a3c3a Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:10:42 +0100 Subject: [PATCH 18/62] fix --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 848fd22..75bce8b 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -28,4 +28,4 @@ jobs: uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: # Change this to your OpenAPI file path - file: obj\Release\net10.0\EndpointInfo\CheersDb.Api.json \ No newline at end of file + file: $GITHUB_WORKSPACE\obj\Release\net10.0\EndpointInfo\CheersDb.Api.json \ No newline at end of file From 674eda857c4e15b6b7328cc19782b336c7c9731d Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:16:24 +0100 Subject: [PATCH 19/62] fix --- .github/workflows/pr-gatechecks.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 75bce8b..33eeb75 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -27,5 +27,4 @@ jobs: - name: OpenAPI Analyzer uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - # Change this to your OpenAPI file path - file: $GITHUB_WORKSPACE\obj\Release\net10.0\EndpointInfo\CheersDb.Api.json \ No newline at end of file + file: ${{ env.GITHUB_WORKSPACE }}\obj\Release\net10.0\EndpointInfo\CheersDb.Api.json \ No newline at end of file From 0ad89caf4a51d4fbc46a6a0dfba4555bb7162e5e Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:19:11 +0100 Subject: [PATCH 20/62] fix --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 33eeb75..cdf4b7f 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -27,4 +27,4 @@ jobs: - name: OpenAPI Analyzer uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - file: ${{ env.GITHUB_WORKSPACE }}\obj\Release\net10.0\EndpointInfo\CheersDb.Api.json \ No newline at end of file + file: /home/runner/work/CheersDb/CheersDb/src/CheersDb.Api/Release/net10.0/EndpointInfo/CheersDb.Api.json \ No newline at end of file From 5e695c6bca38465fd081f3838294fee8c2cd0ad9 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:31:22 +0100 Subject: [PATCH 21/62] fix --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index cdf4b7f..ea99a5d 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -27,4 +27,4 @@ jobs: - name: OpenAPI Analyzer uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - file: /home/runner/work/CheersDb/CheersDb/src/CheersDb.Api/Release/net10.0/EndpointInfo/CheersDb.Api.json \ No newline at end of file + file: /home/runner/work/CheersDb/CheersDb/src/CheersDb.Api/obj/Release/net10.0/EndpointInfo/CheersDb.Api.json \ No newline at end of file From 2806cde3c84f4ce38e5ac5c900f01736fe2734a9 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:41:13 +0100 Subject: [PATCH 22/62] fix --- .github/workflows/pr-gatechecks.yml | 2 +- Directory.Packages.props | 3 +- src/CheersDb.Api/CheersDb.Api.csproj | 7 + src/CheersDb.Api/CheersDb.Api.json | 184 +++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 src/CheersDb.Api/CheersDb.Api.json diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index ea99a5d..a2ab570 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -27,4 +27,4 @@ jobs: - name: OpenAPI Analyzer uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - file: /home/runner/work/CheersDb/CheersDb/src/CheersDb.Api/obj/Release/net10.0/EndpointInfo/CheersDb.Api.json \ No newline at end of file + file: /home/runner/work/CheersDb/CheersDb/src/CheersDb.Api/CheersDb.Api.json \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index f5a84fa..1998b7d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,8 @@ + - + \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 6790110..63b622e 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -2,10 +2,17 @@ true + $(MSBuildProjectDirectory) + true + true + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json new file mode 100644 index 0000000..544ec9d --- /dev/null +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -0,0 +1,184 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "CheersDb API", + "description": "An API for retrieving and altering breweries on the CheersDb platform", + "version": "v1" + }, + "paths": { + "/producers/{id}": { + "get": { + "tags": [ + "Producers" + ], + "summary": "Retrieves details for a specific producer by id", + "operationId": "GetProducerDetails", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the producer to retrieve", + "required": true, + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Returns the requested producer in the response body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetProducerDetailsDto" + } + } + } + }, + "404": { + "description": "Indicates the requested producer was not found, or the URI is invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + } + }, + "components": { + "schemas": { + "GetProducerDetailsDto": { + "type": "object", + "properties": { + "id": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "description": "The id of the producer", + "format": "int32", + "example": 328 + }, + "name": { + "type": [ + "null", + "string" + ], + "description": "The name of the producer", + "example": "Rye River Brewing" + }, + "links": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/LinkDto" + }, + "description": "Links related to the producer, such as a self link to retrieve the producer details", + "example": [ + { + "rel": "self", + "href": "/producers/328", + "method": "GET" + } + ] + } + }, + "description": "Details about a producer" + }, + "LinkDto": { + "required": [ + "rel", + "href", + "method" + ], + "type": "object", + "properties": { + "rel": { + "type": "string", + "description": "The relationship of the link to the current resource", + "example": "self" + }, + "href": { + "type": "string", + "description": "The URL of the link", + "example": "/producers/1" + }, + "method": { + "type": "string", + "description": "The HTTP method for the link", + "example": "GET" + } + }, + "description": "Represents a hypermedia link in the API response, providing information about the relationship, URL, and HTTP method for the link." + }, + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": [ + "null", + "string" + ] + }, + "title": { + "type": [ + "null", + "string" + ] + }, + "status": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" + }, + "detail": { + "type": [ + "null", + "string" + ] + }, + "instance": { + "type": [ + "null", + "string" + ] + } + } + } + }, + "responses": { + "InternalServerError": { + "description": "Indicates that an unexpected internal server error has occurred", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "tags": [ + { + "name": "Producers" + } + ] +} \ No newline at end of file From 15eaa7fcfe7bc25444f304e5eeec9d41320a5ae7 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:48:47 +0100 Subject: [PATCH 23/62] wip --- .github/workflows/pr-gatechecks.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index a2ab570..ff4b865 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -24,7 +24,10 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release + - name: Test + run: echo ${{ env.GITHUB_WORKSPACE }} + - name: OpenAPI Analyzer uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - file: /home/runner/work/CheersDb/CheersDb/src/CheersDb.Api/CheersDb.Api.json \ No newline at end of file + file: ${{ env.GITHUB_WORKSPACE }}/src/CheersDb.Api/CheersDb.Api.json \ No newline at end of file From 06e1c7e4a18b56e704faa33afbe89574e276369c Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 16:53:12 +0100 Subject: [PATCH 24/62] wip --- .github/workflows/pr-gatechecks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index ff4b865..7976d0e 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -25,9 +25,9 @@ jobs: run: dotnet build --no-restore --configuration Release - name: Test - run: echo ${{ env.GITHUB_WORKSPACE }} + run: echo $env.GITHUB_WORKSPACE - name: OpenAPI Analyzer uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - file: ${{ env.GITHUB_WORKSPACE }}/src/CheersDb.Api/CheersDb.Api.json \ No newline at end of file + file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json \ No newline at end of file From 3d830709568aed872bd77329cea88f2ed88b874d Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 17:00:35 +0100 Subject: [PATCH 25/62] wip --- .github/workflows/pr-gatechecks.yml | 52 +++++++++++++++++++++++++--- CheersDb.sln | Bin 2666 -> 0 bytes CheersDb.slnx | 7 ++++ 3 files changed, 54 insertions(+), 5 deletions(-) delete mode 100644 CheersDb.sln create mode 100644 CheersDb.slnx diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 7976d0e..138749f 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -6,7 +6,7 @@ on: jobs: build: - name: Build solution + name: Build Solution runs-on: ubuntu-latest steps: @@ -24,10 +24,52 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release - - name: Test - run: echo $env.GITHUB_WORKSPACE + analyze-openapi: + name: Analyze OpenAPI Spec + runs-on: ubuntu-latest - - name: OpenAPI Analyzer + steps: + - name: Analyze OpenAPI + id: analyze uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: - file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json \ No newline at end of file + file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json + output_format: json + + - name: Display Results + run: | + echo "OpenAPI Analysis Results:" + echo "=========================" + echo "Valid: ${{ steps.analyze.outputs.is_valid }}" + echo "Suggestions: ${{ steps.analyze.outputs.suggestions_count }}" + echo "Operations: ${{ steps.analyze.outputs.operations_count }}" + echo "Paths: ${{ steps.analyze.outputs.paths_count }}" + echo "Schemas: ${{ steps.analyze.outputs.schemas_count }}" + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v9 + with: + script: | + const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); + const comment = `## 🔍 OpenAPI Analysis Results + + **Valid**: ${analysis.is_valid ? '✅' : '❌'} + **Total Suggestions**: ${analysis.suggestions ? Object.values(analysis.suggestions).reduce((total, suggestions) => total + suggestions.length, 0) : 0} + **Operations**: ${analysis.summary ? analysis.summary.operations_count : 0} + **Paths**: ${analysis.summary ? analysis.summary.paths_count : 0} + **Schemas**: ${analysis.summary ? analysis.summary.schemas_count : 0} + + ${analysis.suggestions && Object.keys(analysis.suggestions).length > 0 ? + Object.entries(analysis.suggestions).map(([category, suggestions]) => + `### ${category} (${suggestions.length} issues)\n\n${suggestions.slice(0, 3).map(s => `- ${s}`).join('\n')}${suggestions.length > 3 ? `\n- ... and ${suggestions.length - 3} more` : ''}\n` + ).join('\n') : + '### ✅ No suggestions found! Your OpenAPI specification looks great! 🎉' + }`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); \ No newline at end of file diff --git a/CheersDb.sln b/CheersDb.sln deleted file mode 100644 index cc1693ccdaeb38295452f9b39bc278f4fbc00d8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2666 zcmc(hO>fgc5QgVkB>qDbE=9tY(dkQIA(cD~+sW~cr2^Qk%V&n>V268{Ye zh}hxR;0cM`WZje&@*0_~$@J1>pK>uL+Zp&~ncNWYYn$XYe@i8nN>Yj*$2bRz9+j z69-%YJ73!55VC87BjT$#l&gr28s``*;!s&`o48HbQoaJb_3`Y$+6l2$7P9W}Q(jdP zV&kLLGVchREQdo7X8ZdMRg39|WQV(o&X&vqqBZz>)Jl2K$~MVS^NS1*nzKHq$TSx{ z)g1MSkSd;(o1+{r!`qIs_wbl|n$7$!@3cd-esRu=vU|kSr%&`$dsOf6H@fGNd!ToF z%-Ys|2@AH{s_U0U7;yha@`d+=*h$4WWmY!YeSMVWOZD#4Qu!+|FF>#J&-&Y!Wc?iS z-otjzB%Ok31Cu%nZH)%_)7_O}SUW0PnS2FpY^_afYB7=T^u(On)adeoeB9Nd=sflC z0S`K(FNhfzcq!Mho-AX;EWk(TQ$nV3oUnsH5nA0|lX+fB*mh diff --git a/CheersDb.slnx b/CheersDb.slnx new file mode 100644 index 0000000..aa61e83 --- /dev/null +++ b/CheersDb.slnx @@ -0,0 +1,7 @@ + + + + + + + From ec6bb003704a2e000181ece7a9707f39358f655f Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 17:27:40 +0100 Subject: [PATCH 26/62] wip --- .github/workflows/pr-gatechecks.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 138749f..e6d93c4 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -6,7 +6,7 @@ on: jobs: build: - name: Build Solution + #name: Build Solution runs-on: ubuntu-latest steps: @@ -24,11 +24,11 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Release - analyze-openapi: - name: Analyze OpenAPI Spec - runs-on: ubuntu-latest + #analyze-openapi: + #name: Analyze OpenAPI Spec + #runs-on: ubuntu-latest - steps: + #steps: - name: Analyze OpenAPI id: analyze uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 From c9ef56eb38dfa7ceacf0ed3e601bd474e1526740 Mon Sep 17 00:00:00 2001 From: Peter Breen <46024001+PetesBreenCoding@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:32:30 +0100 Subject: [PATCH 27/62] Update GitHub Action to use github-script@v7 --- .github/workflows/pr-gatechecks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index e6d93c4..04ffc21 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -48,7 +48,7 @@ jobs: - name: Comment on PR if: github.event_name == 'pull_request' - uses: actions/github-script@v9 + uses: actions/github-script@v7 with: script: | const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); @@ -72,4 +72,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: comment - }); \ No newline at end of file + }); From 66754a3f87932d81c1ac588263b28b15ceb7a68f Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 17:36:28 +0100 Subject: [PATCH 28/62] wip --- .github/workflows/pr-gatechecks.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index e6d93c4..28543f6 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -48,7 +48,7 @@ jobs: - name: Comment on PR if: github.event_name == 'pull_request' - uses: actions/github-script@v9 + uses: actions/github-script@v7 with: script: | const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); @@ -60,12 +60,7 @@ jobs: **Paths**: ${analysis.summary ? analysis.summary.paths_count : 0} **Schemas**: ${analysis.summary ? analysis.summary.schemas_count : 0} - ${analysis.suggestions && Object.keys(analysis.suggestions).length > 0 ? - Object.entries(analysis.suggestions).map(([category, suggestions]) => - `### ${category} (${suggestions.length} issues)\n\n${suggestions.slice(0, 3).map(s => `- ${s}`).join('\n')}${suggestions.length > 3 ? `\n- ... and ${suggestions.length - 3} more` : ''}\n` - ).join('\n') : - '### ✅ No suggestions found! Your OpenAPI specification looks great! 🎉' - }`; + github.rest.issues.createComment({ issue_number: context.issue.number, From 4fd146bc1d2c038f6ad5b188ed6af27ea83a5cc8 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 12 Jun 2026 17:40:20 +0100 Subject: [PATCH 29/62] w --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index f708b19..05da868 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -59,7 +59,7 @@ jobs: **Operations**: ${analysis.summary ? analysis.summary.operations_count : 0} **Paths**: ${analysis.summary ? analysis.summary.paths_count : 0} **Schemas**: ${analysis.summary ? analysis.summary.schemas_count : 0} - + `; github.rest.issues.createComment({ From ae5808e0f2a398b3bd49abc3bf0154bb10d2c88e Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 13:20:49 +0100 Subject: [PATCH 30/62] wip --- .github/workflows/pr-gatechecks.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 05da868..c28092d 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -52,16 +52,8 @@ jobs: with: script: | const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); - const comment = `## 🔍 OpenAPI Analysis Results - - **Valid**: ${analysis.is_valid ? '✅' : '❌'} - **Total Suggestions**: ${analysis.suggestions ? Object.values(analysis.suggestions).reduce((total, suggestions) => total + suggestions.length, 0) : 0} - **Operations**: ${analysis.summary ? analysis.summary.operations_count : 0} - **Paths**: ${analysis.summary ? analysis.summary.paths_count : 0} - **Schemas**: ${analysis.summary ? analysis.summary.schemas_count : 0} - `; - - + const comment = `## 🔍 OpenAPI Analysis Results`; + github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, From 63b18aa44b378f4ed97e39b1eed22a2b92535fdc Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 13:22:52 +0100 Subject: [PATCH 31/62] wip --- .github/workflows/pr-gatechecks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index c28092d..bcf487f 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -51,7 +51,6 @@ jobs: uses: actions/github-script@v7 with: script: | - const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); const comment = `## 🔍 OpenAPI Analysis Results`; github.rest.issues.createComment({ From c974bb63123531cb3fe5f479fbf1840a3a62e21a Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 13:40:35 +0100 Subject: [PATCH 32/62] wip --- .github/workflows/pr-gatechecks.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index bcf487f..7b0816b 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -46,16 +46,13 @@ jobs: echo "Paths: ${{ steps.analyze.outputs.paths_count }}" echo "Schemas: ${{ steps.analyze.outputs.schemas_count }}" - - name: Comment on PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 + - name: Save OpenAPI analysis results + run: | + mkdir -p output/openapi-analysis + echo "${{ steps.analyze.outputs }}" > output/openapi-analysis/openapi-analysis.json + + - name: Upload OpenAPI analysis results + uses: actions/upload-artifact@v4 with: - script: | - const comment = `## 🔍 OpenAPI Analysis Results`; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }); + name: openapi-analysis-results + path: output/openapi-analysis/openapi-analysis.json \ No newline at end of file From 08362f688109017951646679bd2b029e9fdbba8d Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 13:55:24 +0100 Subject: [PATCH 33/62] wip --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 7b0816b..d46ff33 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -49,7 +49,7 @@ jobs: - name: Save OpenAPI analysis results run: | mkdir -p output/openapi-analysis - echo "${{ steps.analyze.outputs }}" > output/openapi-analysis/openapi-analysis.json + echo "${{ steps.analyze.outputs.analysis }}" > output/openapi-analysis/openapi-analysis.json - name: Upload OpenAPI analysis results uses: actions/upload-artifact@v4 From 2e8dc846d3d8bf9c03a961d33ca92159e2a44b75 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 14:14:31 +0100 Subject: [PATCH 34/62] wip --- .github/workflows/pr-gatechecks.yml | 41 ++++++++++++----------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index d46ff33..ad032ec 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -10,18 +10,18 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout + - name: 🎛️ Checkout uses: actions/checkout@v4 - - name: Setup .NET + - name: 🛠️ Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - - name: Restore + - name: 🔄 Restore run: dotnet restore - - name: Build + - name: 👷 Build run: dotnet build --no-restore --configuration Release #analyze-openapi: @@ -29,30 +29,23 @@ jobs: #runs-on: ubuntu-latest #steps: - - name: Analyze OpenAPI + - name: 🔍 Analyze OpenAPI id: analyze uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 with: file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json output_format: json - - name: Display Results + - name: 👮 OpenAPI Analysis Quality Gate + shell: pwsh run: | - echo "OpenAPI Analysis Results:" - echo "=========================" - echo "Valid: ${{ steps.analyze.outputs.is_valid }}" - echo "Suggestions: ${{ steps.analyze.outputs.suggestions_count }}" - echo "Operations: ${{ steps.analyze.outputs.operations_count }}" - echo "Paths: ${{ steps.analyze.outputs.paths_count }}" - echo "Schemas: ${{ steps.analyze.outputs.schemas_count }}" - - - name: Save OpenAPI analysis results - run: | - mkdir -p output/openapi-analysis - echo "${{ steps.analyze.outputs.analysis }}" > output/openapi-analysis/openapi-analysis.json - - - name: Upload OpenAPI analysis results - uses: actions/upload-artifact@v4 - with: - name: openapi-analysis-results - path: output/openapi-analysis/openapi-analysis.json \ No newline at end of file + $isValid = ${{ steps.analyze.outputs.is_valid }}; + + if ($isValid) { + Write-Host "✅ OpenAPI analysis passed quality gate checks."; + exit 0; + } + else { + Write-Host "❌ OpenAPI analysis failed quality gate checks."; + exit 1; + } \ No newline at end of file From f693701585ae53882550b35794c9f50a5f9e8857 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 14:16:26 +0100 Subject: [PATCH 35/62] w --- .github/workflows/pr-gatechecks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index ad032ec..963764c 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -39,9 +39,9 @@ jobs: - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh run: | - $isValid = ${{ steps.analyze.outputs.is_valid }}; + $isValid = "${{ steps.analyze.outputs.is_valid }}"; - if ($isValid) { + if ($isValid -eq "true") { Write-Host "✅ OpenAPI analysis passed quality gate checks."; exit 0; } From d85874a625973a085a44def6b9a18d984ec61876 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 14:52:30 +0100 Subject: [PATCH 36/62] a --- .github/workflows/pr-gatechecks.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 963764c..ca435c0 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,12 +40,13 @@ jobs: shell: pwsh run: | $isValid = "${{ steps.analyze.outputs.is_valid }}"; + $suggestionsCount = ${{ steps.analyze.outputs.suggestions_count }}; - if ($isValid -eq "true") { - Write-Host "✅ OpenAPI analysis passed quality gate checks."; + if ($isValid -eq "true" -and $suggestionsCount -eq 0) { + Write-Host -ForegroundColor Green "✔️ OpenAPI analysis passed"; exit 0; } else { - Write-Host "❌ OpenAPI analysis failed quality gate checks."; + Write-Host -ForegroundColor Red "❌ OpenAPI analysis failed"; exit 1; } \ No newline at end of file From 3576b47a5da5459049865b3d86c2489fc4dcf4e1 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 15:16:39 +0100 Subject: [PATCH 37/62] w --- .github/workflows/pr-gatechecks.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index ca435c0..342975d 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -36,6 +36,12 @@ jobs: file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json output_format: json + - name: Display Results + run: | + echo "OpenAPI Analysis Results:" + echo "=========================" + echo ${{ steps.analyze.outputs }} + - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh run: | @@ -43,10 +49,10 @@ jobs: $suggestionsCount = ${{ steps.analyze.outputs.suggestions_count }}; if ($isValid -eq "true" -and $suggestionsCount -eq 0) { - Write-Host -ForegroundColor Green "✔️ OpenAPI analysis passed"; + Write-Host "✔️ OpenAPI analysis passed"; exit 0; } else { - Write-Host -ForegroundColor Red "❌ OpenAPI analysis failed"; + Write-Host "❌ OpenAPI analysis failed"; exit 1; } \ No newline at end of file From e55ef1c016d77860508cc0d223c561100afee016 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 15:22:37 +0100 Subject: [PATCH 38/62] a --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 342975d..78a9543 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,7 +40,7 @@ jobs: run: | echo "OpenAPI Analysis Results:" echo "=========================" - echo ${{ steps.analyze.outputs }} + echo ${{ toJSON(steps.analyze.outputs) }} - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh From 0a06bd0b7ba168b349cffc442b77c36791c8326a Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 15:26:32 +0100 Subject: [PATCH 39/62] w --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 78a9543..020e367 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,7 +40,7 @@ jobs: run: | echo "OpenAPI Analysis Results:" echo "=========================" - echo ${{ toJSON(steps.analyze.outputs) }} + echo "${{ toJSON(steps.analyze.outputs) }}" - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh From e83a1941b7c2bd91e568ab490e243786a684d4c5 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 15:29:05 +0100 Subject: [PATCH 40/62] q --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 020e367..f01c9c1 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,7 +40,7 @@ jobs: run: | echo "OpenAPI Analysis Results:" echo "=========================" - echo "${{ toJSON(steps.analyze.outputs) }}" + echo '${{ toJSON(steps.analyze.outputs) }}' - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh From 97424685fe84820135e83166992eeaa72c2ce1bb Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 15:37:50 +0100 Subject: [PATCH 41/62] w --- .github/workflows/pr-gatechecks.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index f01c9c1..aee5bfb 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -36,23 +36,20 @@ jobs: file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json output_format: json - - name: Display Results - run: | - echo "OpenAPI Analysis Results:" - echo "=========================" - echo '${{ toJSON(steps.analyze.outputs) }}' - - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh run: | $isValid = "${{ steps.analyze.outputs.is_valid }}"; - $suggestionsCount = ${{ steps.analyze.outputs.suggestions_count }}; + $performanceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.performance }}; + $versioningCount = ${{ steps.analyze.outputs.analysis.analysis_categories.versioning }}; + $complianceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.compliance }}; - if ($isValid -eq "true" -and $suggestionsCount -eq 0) { + if ($isValid -eq "true" -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { Write-Host "✔️ OpenAPI analysis passed"; exit 0; } else { Write-Host "❌ OpenAPI analysis failed"; + Write-Host "${{ steps.analyze.outputs.analysis.analysis_categories }}" exit 1; } \ No newline at end of file From a1abe36da1b1f8150d6f3f3728c3db474db3ebb3 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 15:40:42 +0100 Subject: [PATCH 42/62] w --- .github/workflows/pr-gatechecks.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index aee5bfb..6301037 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,9 +40,9 @@ jobs: shell: pwsh run: | $isValid = "${{ steps.analyze.outputs.is_valid }}"; - $performanceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.performance }}; - $versioningCount = ${{ steps.analyze.outputs.analysis.analysis_categories.versioning }}; - $complianceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.compliance }}; + $performanceCount = ${{ steps.analyze.outputs.analysis_categories.performance }}; + $versioningCount = ${{ steps.analyze.outputs.analysis_categories.versioning }}; + $complianceCount = ${{ steps.analyze.outputs.analysis_categories.compliance }}; if ($isValid -eq "true" -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { Write-Host "✔️ OpenAPI analysis passed"; From 8b5505d74c6c4f014e0ae60fb4fcef3658eb06cb Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 17:08:42 +0100 Subject: [PATCH 43/62] a --- .github/workflows/pr-gatechecks.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 6301037..7c08780 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,9 +40,11 @@ jobs: shell: pwsh run: | $isValid = "${{ steps.analyze.outputs.is_valid }}"; - $performanceCount = ${{ steps.analyze.outputs.analysis_categories.performance }}; - $versioningCount = ${{ steps.analyze.outputs.analysis_categories.versioning }}; - $complianceCount = ${{ steps.analyze.outputs.analysis_categories.compliance }}; + $isValid2 = "${{ steps.analyze.outputs.analysis.is_valid }}"; + $blah = "${{ steps.analyze.outputs.analysis.summary.schemas_count }}"; + $performanceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.performance }}; + $versioningCount = ${{ steps.analyze.outputs.analysis.analysis_categories.versioning }}; + $complianceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.compliance }}; if ($isValid -eq "true" -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { Write-Host "✔️ OpenAPI analysis passed"; From 713b21faa2f76dd90c400bc5910826e3daaef826 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 17:10:53 +0100 Subject: [PATCH 44/62] a --- .github/workflows/pr-gatechecks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 7c08780..b691fdf 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,6 +40,7 @@ jobs: shell: pwsh run: | $isValid = "${{ steps.analyze.outputs.is_valid }}"; + $rrr = "${{ steps.analyze.outputs.analysis }}"; $isValid2 = "${{ steps.analyze.outputs.analysis.is_valid }}"; $blah = "${{ steps.analyze.outputs.analysis.summary.schemas_count }}"; $performanceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.performance }}; From c87c9f1cfaee596edfd7f2eebbee31ccc36106de Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 17:16:39 +0100 Subject: [PATCH 45/62] a --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index b691fdf..4747421 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -40,7 +40,7 @@ jobs: shell: pwsh run: | $isValid = "${{ steps.analyze.outputs.is_valid }}"; - $rrr = "${{ steps.analyze.outputs.analysis }}"; + $status = "${{ steps.analyze.outputs.analysis.status }}"; $isValid2 = "${{ steps.analyze.outputs.analysis.is_valid }}"; $blah = "${{ steps.analyze.outputs.analysis.summary.schemas_count }}"; $performanceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.performance }}; From 34bba8e7f7dd0da68b24f6c39e4d5e11bde24562 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 17:23:29 +0100 Subject: [PATCH 46/62] w --- .github/workflows/pr-gatechecks.yml | 36 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 4747421..0f9502b 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -39,20 +39,24 @@ jobs: - name: 👮 OpenAPI Analysis Quality Gate shell: pwsh run: | - $isValid = "${{ steps.analyze.outputs.is_valid }}"; - $status = "${{ steps.analyze.outputs.analysis.status }}"; - $isValid2 = "${{ steps.analyze.outputs.analysis.is_valid }}"; - $blah = "${{ steps.analyze.outputs.analysis.summary.schemas_count }}"; - $performanceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.performance }}; - $versioningCount = ${{ steps.analyze.outputs.analysis.analysis_categories.versioning }}; - $complianceCount = ${{ steps.analyze.outputs.analysis.analysis_categories.compliance }}; - - if ($isValid -eq "true" -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { - Write-Host "✔️ OpenAPI analysis passed"; - exit 0; + $isValid = '${{ steps.analyze.outputs.is_valid }}' + $analysisJson = '${{ steps.analyze.outputs.analysis }}' + + if ([string]::IsNullOrEmpty($analysisJson)) { + Write-Host '❌ OpenAPI analysis produced no analysis output'; + exit 1; + } + + $analysis = $analysisJson | ConvertFrom-Json + $performanceCount = $analysis.analysis_categories.performance + $versioningCount = $analysis.analysis_categories.versioning + $complianceCount = $analysis.analysis_categories.compliance + + if ($isValid -eq 'true' -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { + Write-Host '✔️ OpenAPI analysis passed' + exit 0 + } else { + Write-Host '❌ OpenAPI analysis failed' + Write-Host ($analysis | ConvertTo-Json -Depth 5) + exit 1 } - else { - Write-Host "❌ OpenAPI analysis failed"; - Write-Host "${{ steps.analyze.outputs.analysis.analysis_categories }}" - exit 1; - } \ No newline at end of file From de6c683341d8f429f4e7f9c9e45599ce0666966a Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 17:29:15 +0100 Subject: [PATCH 47/62] w --- .github/workflows/pr-gatechecks.yml | 98 ++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 0f9502b..64ea75d 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -36,27 +36,81 @@ jobs: file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json output_format: json - - name: 👮 OpenAPI Analysis Quality Gate - shell: pwsh + - name: Display Results run: | - $isValid = '${{ steps.analyze.outputs.is_valid }}' - $analysisJson = '${{ steps.analyze.outputs.analysis }}' - - if ([string]::IsNullOrEmpty($analysisJson)) { - Write-Host '❌ OpenAPI analysis produced no analysis output'; - exit 1; - } - - $analysis = $analysisJson | ConvertFrom-Json - $performanceCount = $analysis.analysis_categories.performance - $versioningCount = $analysis.analysis_categories.versioning - $complianceCount = $analysis.analysis_categories.compliance - - if ($isValid -eq 'true' -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { - Write-Host '✔️ OpenAPI analysis passed' - exit 0 - } else { - Write-Host '❌ OpenAPI analysis failed' - Write-Host ($analysis | ConvertTo-Json -Depth 5) - exit 1 + echo "🔍 OpenAPI Analysis Results:" + echo "============================" + echo "✅ Valid: ${{ steps.analyze.outputs.is_valid }}" + echo "📊 Basic Metrics:" + echo " - Operations: ${{ steps.analyze.outputs.operations_count }}" + echo " - Paths: ${{ steps.analyze.outputs.paths_count }}" + echo " - Schemas: ${{ steps.analyze.outputs.schemas_count }}" + echo " - Total Suggestions: ${{ steps.analyze.outputs.suggestions_count }}" + echo "" + echo "🎯 Advanced Analytics:" + echo " - Complexity Score: ${{ steps.analyze.outputs.complexity_score }}" + echo " - Maintainability Score: ${{ steps.analyze.outputs.maintainability_score }}" + echo "" + echo "📋 Analysis Categories:" + echo " - 🔒 Security Issues: ${{ steps.analyze.outputs.security_issues }}" + echo " - ⚡ Performance Issues: ${{ steps.analyze.outputs.performance_issues }}" + echo " - 🏗️ Design Pattern Issues: ${{ steps.analyze.outputs.design_pattern_issues }}" + echo " - 🔄 Versioning Issues: ${{ steps.analyze.outputs.versioning_issues }}" + echo " - 📝 Documentation Issues: ${{ steps.analyze.outputs.documentation_issues }}" + echo " - 🛡️ Compliance Issues: ${{ steps.analyze.outputs.compliance_issues }}" + echo " - 🧪 Testing Recommendations: ${{ steps.analyze.outputs.testing_recommendations }}" + echo " - 📈 Monitoring Recommendations: ${{ steps.analyze.outputs.monitoring_recommendations }}" + echo " - 🔧 Code Generation Opportunities: ${{ steps.analyze.outputs.code_generation_opportunities }}" + echo " - 🏛️ Governance Issues: ${{ steps.analyze.outputs.governance_issues }}" + + - name: Comment on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); + const categories = analysis.analysis_categories || {}; + const analytics = analysis.analytics || {}; + + const comment = `## 🔍 OpenAPI Analysis Results + + **Valid**: ${analysis.is_valid ? '✅' : '❌'} + **Total Suggestions**: ${analysis.suggestions ? Object.values(analysis.suggestions).reduce((total, suggestions) => total + suggestions.length, 0) : 0} + + ### 📊 Basic Metrics + - **Operations**: ${analysis.summary ? analysis.summary.operations_count : 0} + - **Paths**: ${analysis.summary ? analysis.summary.paths_count : 0} + - **Schemas**: ${analysis.summary ? analysis.summary.schemas_count : 0} + + ### 🎯 Advanced Analytics + - **Complexity Score**: ${analytics.complexity_score || 0} + - **Maintainability Score**: ${analytics.maintainability_score || 0}/100 + + ### 📋 Analysis Categories + - **Security Issues**: ${categories.security || 0} + - **Performance Issues**: ${categories.performance || 0} + - **Design Pattern Issues**: ${categories.design_patterns || 0} + - **Versioning Issues**: ${categories.versioning || 0} + - **Documentation Issues**: ${categories.documentation || 0} + - **Compliance Issues**: ${categories.compliance || 0} + - **Testing Recommendations**: ${categories.testing || 0} + - **Monitoring Recommendations**: ${categories.monitoring || 0} + - **Code Generation Opportunities**: ${categories.code_generation || 0} + - **Governance Issues**: ${categories.governance || 0} + + ${analysis.suggestions && Object.keys(analysis.suggestions).length > 0 ? + Object.entries(analysis.suggestions).map(([category, suggestions]) => + `### ${category} (${suggestions.length} issues)\n\n${suggestions.slice(0, 3).map(s => `- ${s}`).join('\n')}${suggestions.length > 3 ? `\n- ... and ${suggestions.length - 3} more` : ''}\n` + ).join('\n') : + '### ✅ No suggestions found! Your OpenAPI specification looks great! 🎉' } + + --- + *Powered by ApyGuard OpenAPI Analyzer with comprehensive best practices analysis*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); \ No newline at end of file From 04aa00ce63d4e61113ecc6086c477b5a3d49779e Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 17 Jun 2026 17:33:50 +0100 Subject: [PATCH 48/62] w --- .github/workflows/pr-gatechecks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 64ea75d..c84a80a 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -68,7 +68,7 @@ jobs: uses: actions/github-script@v7 with: script: | - const analysis = JSON.parse('${{ steps.analyze.outputs.analysis }}'); + const analysis = JSON.parse(`${{ steps.analyze.outputs.analysis }}`); const categories = analysis.analysis_categories || {}; const analytics = analysis.analytics || {}; From 9bd4f3153f62d150830d85c15b67b113a548f124 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 10:20:27 +0100 Subject: [PATCH 49/62] w --- .github/workflows/pr-gatechecks.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index c84a80a..aa1dbb5 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -5,8 +5,10 @@ on: types: [opened, synchronize, reopened] jobs: - build: - #name: Build Solution + pr-gatechecks: + permissions: + pull-requests: write + name: PR Gatechecks runs-on: ubuntu-latest steps: @@ -24,11 +26,6 @@ jobs: - name: 👷 Build run: dotnet build --no-restore --configuration Release - #analyze-openapi: - #name: Analyze OpenAPI Spec - #runs-on: ubuntu-latest - - #steps: - name: 🔍 Analyze OpenAPI id: analyze uses: ApyGuard/apyguard_openapi_analysis@v1.0.9 From 590d0baaf676bcc79f5b89e458f862e91077e818 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 10:57:03 +0100 Subject: [PATCH 50/62] q --- .github/workflows/pr-gatechecks.yml | 58 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index aa1dbb5..143fe88 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -32,35 +32,8 @@ jobs: with: file: ${{ github.workspace }}/src/CheersDb.Api/CheersDb.Api.json output_format: json - - - name: Display Results - run: | - echo "🔍 OpenAPI Analysis Results:" - echo "============================" - echo "✅ Valid: ${{ steps.analyze.outputs.is_valid }}" - echo "📊 Basic Metrics:" - echo " - Operations: ${{ steps.analyze.outputs.operations_count }}" - echo " - Paths: ${{ steps.analyze.outputs.paths_count }}" - echo " - Schemas: ${{ steps.analyze.outputs.schemas_count }}" - echo " - Total Suggestions: ${{ steps.analyze.outputs.suggestions_count }}" - echo "" - echo "🎯 Advanced Analytics:" - echo " - Complexity Score: ${{ steps.analyze.outputs.complexity_score }}" - echo " - Maintainability Score: ${{ steps.analyze.outputs.maintainability_score }}" - echo "" - echo "📋 Analysis Categories:" - echo " - 🔒 Security Issues: ${{ steps.analyze.outputs.security_issues }}" - echo " - ⚡ Performance Issues: ${{ steps.analyze.outputs.performance_issues }}" - echo " - 🏗️ Design Pattern Issues: ${{ steps.analyze.outputs.design_pattern_issues }}" - echo " - 🔄 Versioning Issues: ${{ steps.analyze.outputs.versioning_issues }}" - echo " - 📝 Documentation Issues: ${{ steps.analyze.outputs.documentation_issues }}" - echo " - 🛡️ Compliance Issues: ${{ steps.analyze.outputs.compliance_issues }}" - echo " - 🧪 Testing Recommendations: ${{ steps.analyze.outputs.testing_recommendations }}" - echo " - 📈 Monitoring Recommendations: ${{ steps.analyze.outputs.monitoring_recommendations }}" - echo " - 🔧 Code Generation Opportunities: ${{ steps.analyze.outputs.code_generation_opportunities }}" - echo " - 🏛️ Governance Issues: ${{ steps.analyze.outputs.governance_issues }}" - - - name: Comment on PR + + - name: ✏️ Comment on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: @@ -110,4 +83,29 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: comment - }); \ No newline at end of file + }); + + - name: 🚦 OpenAPI Gatecheck + shell: pwsh + run: | + $isValid = '${{ steps.analyze.outputs.is_valid }}' + $analysisJson = `${{ steps.analyze.outputs.analysis }}` + + if ([string]::IsNullOrEmpty($analysisJson)) { + Write-Host '❌ OpenAPI analysis produced no analysis output'; + exit 1; + } + + $analysis = $analysisJson | ConvertFrom-Json + $performanceCount = $analysis.analysis_categories.performance + $versioningCount = $analysis.analysis_categories.versioning + $complianceCount = $analysis.analysis_categories.compliance + + if ($isValid -eq 'true' -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { + Write-Host '✔️ OpenAPI analysis passed' + exit 0 + } else { + Write-Host '❌ OpenAPI analysis failed' + Write-Host ($analysis | ConvertTo-Json -Depth 5) + exit 1 + } From f80c92957270008acb2817e1673947ec40403077 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 11:25:16 +0100 Subject: [PATCH 51/62] w --- .github/workflows/pr-gatechecks.yml | 35 +++++++++++------------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 143fe88..07da22e 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -34,8 +34,7 @@ jobs: output_format: json - name: ✏️ Comment on PR - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const analysis = JSON.parse(`${{ steps.analyze.outputs.analysis }}`); @@ -86,26 +85,18 @@ jobs: }); - name: 🚦 OpenAPI Gatecheck - shell: pwsh - run: | - $isValid = '${{ steps.analyze.outputs.is_valid }}' - $analysisJson = `${{ steps.analyze.outputs.analysis }}` - - if ([string]::IsNullOrEmpty($analysisJson)) { - Write-Host '❌ OpenAPI analysis produced no analysis output'; - exit 1; - } - - $analysis = $analysisJson | ConvertFrom-Json - $performanceCount = $analysis.analysis_categories.performance - $versioningCount = $analysis.analysis_categories.versioning - $complianceCount = $analysis.analysis_categories.compliance + uses: actions/github-script@v9 + env: + IS_VALID: ${{ github.event.pull_request.title }} + ANALYSIS: ${{ steps.analyze.outputs.analysis }} + with: + script: | + const analysis = JSON.parse(process.env.ANALYSIS); - if ($isValid -eq 'true' -and $performanceCount -eq 0 -and $versioningCount -eq 0 -and $complianceCount -eq 0) { - Write-Host '✔️ OpenAPI analysis passed' - exit 0 + if (process.env.IS_VALID === 'true' && analysis.is_valid && analysis.analysis_categories.performance === 0) { + console.log('✔️ OpenAPI analysis passed'); + process.exit(0); } else { - Write-Host '❌ OpenAPI analysis failed' - Write-Host ($analysis | ConvertTo-Json -Depth 5) - exit 1 + console.error('❌ OpenAPI analysis failed'); + process.exit(1); } From 34e440f6f141bb46911b50259bd22a3e708787ef Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 11:28:18 +0100 Subject: [PATCH 52/62] w --- .github/workflows/pr-gatechecks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 07da22e..97eef44 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -87,13 +87,13 @@ jobs: - name: 🚦 OpenAPI Gatecheck uses: actions/github-script@v9 env: - IS_VALID: ${{ github.event.pull_request.title }} + IS_VALID: ${{ steps.analyze.outputs.is_valid }} ANALYSIS: ${{ steps.analyze.outputs.analysis }} with: script: | const analysis = JSON.parse(process.env.ANALYSIS); - if (process.env.IS_VALID === 'true' && analysis.is_valid && analysis.analysis_categories.performance === 0) { + if (process.env.IS_VALID && analysis.is_valid) { console.log('✔️ OpenAPI analysis passed'); process.exit(0); } else { From 8eceede336c59c010a2e3fb3b59ea86b181e90bc Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 11:52:58 +0100 Subject: [PATCH 53/62] w --- .github/workflows/pr-gatechecks.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 97eef44..083f761 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -87,16 +87,19 @@ jobs: - name: 🚦 OpenAPI Gatecheck uses: actions/github-script@v9 env: - IS_VALID: ${{ steps.analyze.outputs.is_valid }} ANALYSIS: ${{ steps.analyze.outputs.analysis }} with: script: | const analysis = JSON.parse(process.env.ANALYSIS); + const categories = analysis.analysis_categories || {}; + const issueCount = categories.performance + categories.compliance + categories.versioning; - if (process.env.IS_VALID && analysis.is_valid) { + if (analysis.is_valid && issueCount === 0) { console.log('✔️ OpenAPI analysis passed'); process.exit(0); } else { + console.log(`Valid: ${analysis.is_valid}`); + console.log(`Total blocking issues: ${issueCount}`); console.error('❌ OpenAPI analysis failed'); process.exit(1); - } + } \ No newline at end of file From 6f07126aff4831ce1ea1159c52ccded75fb5302d Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 15:24:17 +0100 Subject: [PATCH 54/62] cache --- .github/workflows/pr-gatechecks.yml | 6 ++++-- src/CheersDb.Api/Controllers/ProducersController.cs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-gatechecks.yml b/.github/workflows/pr-gatechecks.yml index 083f761..839e348 100644 --- a/.github/workflows/pr-gatechecks.yml +++ b/.github/workflows/pr-gatechecks.yml @@ -35,9 +35,11 @@ jobs: - name: ✏️ Comment on PR uses: actions/github-script@v9 + env: + ANALYSIS: ${{ steps.analyze.outputs.analysis }} with: script: | - const analysis = JSON.parse(`${{ steps.analyze.outputs.analysis }}`); + const analysis = JSON.parse(process.env.ANALYSIS); const categories = analysis.analysis_categories || {}; const analytics = analysis.analytics || {}; @@ -92,7 +94,7 @@ jobs: script: | const analysis = JSON.parse(process.env.ANALYSIS); const categories = analysis.analysis_categories || {}; - const issueCount = categories.performance + categories.compliance + categories.versioning; + const issueCount = categories.performance + categories.compliance + categories.versioning + categories.documentation; if (analysis.is_valid && issueCount === 0) { console.log('✔️ OpenAPI analysis passed'); diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index 8400809..31295be 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -21,6 +21,7 @@ public class ProducersController : ControllerBase [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] + [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)] public IActionResult GetProducerDetails([FromRoute] int id) { return Ok(new GetProducerDetailsDto() From bf684a8eebf9b445ea3bfd524cda7e0dd65b11ee Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 18 Jun 2026 17:31:13 +0100 Subject: [PATCH 55/62] cache control --- src/CheersDb.Api/CheersDb.Api.json | 22 ++++++ .../Controllers/ProducersController.cs | 1 - .../Extensions/OpenApiComponentsExtensions.cs | 68 +++++++++++++++++ .../Extensions/OpenApiOptionsExtensions.cs | 75 +++++++++---------- .../Extensions/ServiceCollectionExtensions.cs | 2 +- 5 files changed, 127 insertions(+), 41 deletions(-) create mode 100644 src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json index 544ec9d..92f99c2 100644 --- a/src/CheersDb.Api/CheersDb.Api.json +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -29,6 +29,14 @@ "responses": { "200": { "description": "Returns the requested producer in the response body", + "headers": { + "Cache-Control": { + "$ref": "#/components/headers/Cache-Control" + }, + "ETag": { + "$ref": "#/components/headers/ETag" + } + }, "content": { "application/json": { "schema": { @@ -174,6 +182,20 @@ } } } + }, + "headers": { + "Cache-Control": { + "description": "Instructions for caching mechanisms in responses", + "schema": { + "type": "string" + } + }, + "ETag": { + "description": "Indicates the current version of the resource", + "schema": { + "type": "string" + } + } } }, "tags": [ diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index 31295be..8400809 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -21,7 +21,6 @@ public class ProducersController : ControllerBase [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] - [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)] public IActionResult GetProducerDetails([FromRoute] int id) { return Ok(new GetProducerDetailsDto() diff --git a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs new file mode 100644 index 0000000..18ef4b2 --- /dev/null +++ b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.Net.Http.Headers; +using Microsoft.OpenApi; +using System.Net; +using System.Net.Mime; + +namespace CheersDb.Api.Extensions; + +/// +/// Provides extension methods for configuring OpenAPI components in the CheersDb API application. +/// +public static class OpenApiComponentsExtensions +{ + private static readonly Lazy _stringSchema = new(() => new OpenApiSchema { Type = JsonSchemaType.String }); + + extension(OpenApiComponents components) + { + /// + /// Adds a response to the OpenAPI components if it does not already exist. + /// + /// The context of the OpenAPI document transformation. + /// A cancellation token that can be used to cancel the operation. + public async Task ConfigureResponsesAsync(OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + components.Responses ??= new Dictionary(); + + var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), cancellationToken: cancellationToken); + + var response = new OpenApiResponse + { + Description = "Indicates that an unexpected internal server error has occurred", + Content = new Dictionary + { + [MediaTypeNames.Application.Json] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference(nameof(ProblemDetails)) + } + } + }; + + components.Responses.Add(nameof(HttpStatusCode.InternalServerError), response); + } + + /// + /// Configures the OpenAPI components to include a header for caching mechanisms in responses. + /// + public void ConfigureHeaders() + { + components.Headers ??= new Dictionary(); + + var cacheControlHeader = new OpenApiHeader + { + Description = "Instructions for caching mechanisms in responses", + Schema = _stringSchema.Value + }; + + var etagHeader = new OpenApiHeader + { + Description = "Indicates the current version of the resource", + Schema = _stringSchema.Value + }; + + components.Headers.Add(HeaderNames.CacheControl, cacheControlHeader); + components.Headers.Add(HeaderNames.ETag, etagHeader); + } + } +} diff --git a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs index 9b91c3f..122c6ac 100644 --- a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs @@ -1,8 +1,7 @@ -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OpenApi; +using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; using System.Net; -using System.Net.Mime; namespace CheersDb.Api.Extensions; @@ -12,52 +11,50 @@ namespace CheersDb.Api.Extensions; public static class OpenApiOptionsExtensions { private static readonly string _internalServerErrorResponseKey = ((int)HttpStatusCode.InternalServerError).ToString(); + private static readonly Lazy _internalServerErrorResponseReference = new(() => new OpenApiResponseReference(nameof(HttpStatusCode.InternalServerError))); + private static readonly Lazy _cacheControlHeaderReference = new(() => new OpenApiHeaderReference(HeaderNames.CacheControl)); + private static readonly Lazy _etagHeaderReference = new(() => new OpenApiHeaderReference(HeaderNames.ETag)); - /// - /// Adds a document transformer to the OpenAPI options that applies the provided transformation function to the generated OpenAPI document. - /// - /// The OpenAPI options to configure. - /// The application settings containing the OpenAPI information to be applied to the document. - public static OpenApiOptions ConfigureDocument(this OpenApiOptions options, AppSettings appSettings) + extension(OpenApiOptions options) { - return options.AddDocumentTransformer(async (document, context, cancellationToken) => + /// + /// Adds a document transformer to the OpenAPI options that applies the provided transformation function to the generated OpenAPI document. + /// + /// The application settings containing the OpenAPI information to be applied to the document. + public OpenApiOptions ConfigureDocument(AppSettings appSettings) { - if (appSettings?.OpenApiInfo is not null) - document.Info = appSettings.OpenApiInfo; + return options.AddDocumentTransformer(async (document, context, cancellationToken) => + { + if (appSettings?.OpenApiInfo is not null) + document.Info = appSettings.OpenApiInfo; - document.Components ??= new OpenApiComponents(); - document.Components.Responses ??= new Dictionary(); + document.Components ??= new OpenApiComponents(); - var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), cancellationToken: cancellationToken); + await document.Components.ConfigureResponsesAsync(context, cancellationToken); + document.Components.ConfigureHeaders(); + }); + } - var response = new OpenApiResponse + /// + /// Adds an operation transformer to the OpenAPI options that configures operation responses. + /// + public OpenApiOptions ConfigureOperations() + { + return options.AddOperationTransformer(async (operation, context, cancellationToken) => { - Description = "Indicates that an unexpected internal server error has occurred", - Content = new Dictionary - { - [MediaTypeNames.Application.Json] = new OpenApiMediaType - { - Schema = new OpenApiSchemaReference(nameof(ProblemDetails)) - } - } - }; + operation.Responses ??= []; + operation.Responses.Add(_internalServerErrorResponseKey, _internalServerErrorResponseReference.Value); - document.Components.Responses.Add(nameof(HttpStatusCode.InternalServerError), response); - }); - } + operation.Responses.TryGetValue(((int)HttpStatusCode.OK).ToString(), out var okResponse); - /// - /// Adds an operation transformer to the OpenAPI options that configures operation responses. - /// - /// The OpenAPI options to configure. - /// The application settings. - public static OpenApiOptions ConfigureOperations(this OpenApiOptions options, AppSettings appSettings) - { - return options.AddOperationTransformer(async (operation, context, cancellationToken) => - { - operation.Responses ??= []; - operation.Responses.Add(_internalServerErrorResponseKey, _internalServerErrorResponseReference.Value); - }); + if (okResponse is not null && okResponse is OpenApiResponse okResponseConcrete) + { + okResponseConcrete.Headers ??= new Dictionary(); + okResponseConcrete.Headers.Add(HeaderNames.CacheControl, _cacheControlHeaderReference.Value); + okResponseConcrete.Headers.Add(HeaderNames.ETag, _etagHeaderReference.Value); + } + }); + } } } \ No newline at end of file diff --git a/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs b/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs index f5e5de4..0eb99d5 100644 --- a/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public IServiceCollection AddConfiguredOpenApi(AppSettings appSettings) { options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; options.ConfigureDocument(appSettings); - options.ConfigureOperations(appSettings); + options.ConfigureOperations(); }); } } From 60b79f8e8fa3ad5f7394d2e56201343869890fc8 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 19 Jun 2026 12:07:06 +0100 Subject: [PATCH 56/62] w --- src/CheersDb.Api/CheersDb.Api.http | 1 + src/CheersDb.Api/CheersDb.Api.json | 43 +++++++++++++++++++ .../Controllers/ProducersController.cs | 14 ++++-- .../Dtos/GetProducerDetailsDto.cs | 5 +++ .../Extensions/OpenApiComponentsExtensions.cs | 29 ++++++++++--- .../Extensions/OpenApiOptionsExtensions.cs | 38 +++++++++++++--- .../Http/NonStandardHeaderNames.cs | 6 +++ src/CheersDb.Api/appsettings.json | 11 ++++- 8 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 src/CheersDb.Api/Http/NonStandardHeaderNames.cs diff --git a/src/CheersDb.Api/CheersDb.Api.http b/src/CheersDb.Api/CheersDb.Api.http index d0bbb9b..c582075 100644 --- a/src/CheersDb.Api/CheersDb.Api.http +++ b/src/CheersDb.Api/CheersDb.Api.http @@ -4,3 +4,4 @@ GET {{CheersDb.Api_HostAddress}}/producers/1 Accept: application/json ### +http \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json index 92f99c2..24c284b 100644 --- a/src/CheersDb.Api/CheersDb.Api.json +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -3,6 +3,15 @@ "info": { "title": "CheersDb API", "description": "An API for retrieving and altering breweries on the CheersDb platform", + "contact": { + "name": "CheersDb Support", + "url": "https://cheersdb.org/support", + "email": "info@cheersdb.org" + }, + "license": { + "name": "GPL-3.0", + "url": "https://github.com/PetesBreenCoding/CheersDb?tab=GPL-3.0-1-ov-file" + }, "version": "v1" }, "paths": { @@ -35,6 +44,9 @@ }, "ETag": { "$ref": "#/components/headers/ETag" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" } }, "content": { @@ -57,6 +69,14 @@ }, "500": { "$ref": "#/components/responses/InternalServerError" + }, + "429": { + "description": "Indicates that the user has sent too many requests in a given amount of time", + "headers": { + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } } } } @@ -86,6 +106,16 @@ "description": "The name of the producer", "example": "Rye River Brewing" }, + "revision": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "description": "The revision number of the producer", + "format": "int32" + }, "links": { "type": [ "null", @@ -195,6 +225,19 @@ "schema": { "type": "string" } + }, + "Retry-After": { + "description": "Indicates how many seconds the user agent should wait before making a follow-up request", + "schema": { + "type": "number" + } + }, + "X-RateLimit-Limit": { + "description": "Indicates the maximum number of requests that the user is allowed to make in a given amount of time", + "schema": { + "type": "number" + }, + "example": 1000 } } }, diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index 8400809..f648a88 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -1,6 +1,8 @@ using CheersDb.Api.Dtos; using CheersDb.Api.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Net.Http.Headers; using System.Net.Mime; namespace CheersDb.Api.Controllers; @@ -21,21 +23,27 @@ public class ProducersController : ControllerBase [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] + [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)] public IActionResult GetProducerDetails([FromRoute] int id) { - return Ok(new GetProducerDetailsDto() + var producerDetails = new GetProducerDetailsDto() { Id = id, Name = "Producer Name", + Revision = 7, Links = [ - new() + new() { Rel = LinkRels.Self, Href = Url.RouteUrl(nameof(GetProducerDetails), new { id }) ?? string.Empty, Method = HttpMethod.Get.ToString() } ] - }); + }; + + Response.Headers.Append(HeaderNames.ETag, producerDetails.Revision.ToString()); + + return Ok(producerDetails); } } \ No newline at end of file diff --git a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs index ab1d2a2..eccc2c4 100644 --- a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs +++ b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs @@ -17,6 +17,11 @@ public class GetProducerDetailsDto /// Rye River Brewing public string? Name { get; init; } + /// + /// The revision number of the producer + /// + public int? Revision { get; init; } + /// /// Links related to the producer, such as a self link to retrieve the producer details /// diff --git a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs index 18ef4b2..05215d1 100644 --- a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using CheersDb.Api.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; @@ -12,7 +13,8 @@ namespace CheersDb.Api.Extensions; /// public static class OpenApiComponentsExtensions { - private static readonly Lazy _stringSchema = new(() => new OpenApiSchema { Type = JsonSchemaType.String }); + private static readonly OpenApiSchema _stringSchema = new() { Type = JsonSchemaType.String }; + private static readonly OpenApiSchema _numberSchema = new() { Type = JsonSchemaType.Number }; extension(OpenApiComponents components) { @@ -27,7 +29,7 @@ public async Task ConfigureResponsesAsync(OpenApiDocumentTransformerContext cont var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), cancellationToken: cancellationToken); - var response = new OpenApiResponse + var internalServerErrorResonse = new OpenApiResponse { Description = "Indicates that an unexpected internal server error has occurred", Content = new Dictionary @@ -39,7 +41,7 @@ public async Task ConfigureResponsesAsync(OpenApiDocumentTransformerContext cont } }; - components.Responses.Add(nameof(HttpStatusCode.InternalServerError), response); + components.Responses.Add(nameof(HttpStatusCode.InternalServerError), internalServerErrorResonse); } /// @@ -52,17 +54,32 @@ public void ConfigureHeaders() var cacheControlHeader = new OpenApiHeader { Description = "Instructions for caching mechanisms in responses", - Schema = _stringSchema.Value + Schema = _stringSchema }; var etagHeader = new OpenApiHeader { Description = "Indicates the current version of the resource", - Schema = _stringSchema.Value + Schema = _stringSchema + }; + + var retryAfterHeader = new OpenApiHeader + { + Description = "Indicates how many seconds the user agent should wait before making a follow-up request", + Schema = _numberSchema + }; + + var rateLimitLimitHeader = new OpenApiHeader + { + Description = "Indicates the maximum number of requests that the user is allowed to make in a given amount of time", + Example = 1000, + Schema = _numberSchema, }; components.Headers.Add(HeaderNames.CacheControl, cacheControlHeader); components.Headers.Add(HeaderNames.ETag, etagHeader); + components.Headers.Add(HeaderNames.RetryAfter, retryAfterHeader); + components.Headers.Add(NonStandardHeaderNames.XRateLimitLimit, rateLimitLimitHeader); } } } diff --git a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs index 122c6ac..0f81153 100644 --- a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs @@ -1,3 +1,4 @@ +using CheersDb.Api.Http; using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; @@ -11,10 +12,15 @@ namespace CheersDb.Api.Extensions; public static class OpenApiOptionsExtensions { private static readonly string _internalServerErrorResponseKey = ((int)HttpStatusCode.InternalServerError).ToString(); + private static readonly string _tooManyRequestsResponseKey = ((int)HttpStatusCode.TooManyRequests).ToString(); - private static readonly Lazy _internalServerErrorResponseReference = new(() => new OpenApiResponseReference(nameof(HttpStatusCode.InternalServerError))); - private static readonly Lazy _cacheControlHeaderReference = new(() => new OpenApiHeaderReference(HeaderNames.CacheControl)); - private static readonly Lazy _etagHeaderReference = new(() => new OpenApiHeaderReference(HeaderNames.ETag)); + private static readonly OpenApiResponseReference _internalServerErrorResponseReference = new(nameof(HttpStatusCode.InternalServerError)); + private static readonly OpenApiResponse _tooManyRequestsResponse = new() { Description = "Indicates that the user has sent too many requests in a given amount of time" }; + + private static readonly OpenApiHeaderReference _cacheControlHeaderReference = new(HeaderNames.CacheControl); + private static readonly OpenApiHeaderReference _etagHeaderReference = new(HeaderNames.ETag); + private static readonly OpenApiHeaderReference _retryAfterHeaderReference = new(HeaderNames.RetryAfter); + private static readonly OpenApiHeaderReference _rateLimitLimitHeaderReference = new(NonStandardHeaderNames.XRateLimitLimit); extension(OpenApiOptions options) { @@ -44,16 +50,36 @@ public OpenApiOptions ConfigureOperations() return options.AddOperationTransformer(async (operation, context, cancellationToken) => { operation.Responses ??= []; - operation.Responses.Add(_internalServerErrorResponseKey, _internalServerErrorResponseReference.Value); + operation.Responses.Add(_internalServerErrorResponseKey, _internalServerErrorResponseReference); + + var tooManyRequestsResponse = new OpenApiResponse + { + Description = "Indicates that the user has sent too many requests in a given amount of time", + Headers = new Dictionary + { + [HeaderNames.RetryAfter] = _retryAfterHeaderReference + } + }; + + operation.Responses.Add(_tooManyRequestsResponseKey, tooManyRequestsResponse); operation.Responses.TryGetValue(((int)HttpStatusCode.OK).ToString(), out var okResponse); if (okResponse is not null && okResponse is OpenApiResponse okResponseConcrete) { okResponseConcrete.Headers ??= new Dictionary(); - okResponseConcrete.Headers.Add(HeaderNames.CacheControl, _cacheControlHeaderReference.Value); - okResponseConcrete.Headers.Add(HeaderNames.ETag, _etagHeaderReference.Value); + okResponseConcrete.Headers.Add(HeaderNames.CacheControl, _cacheControlHeaderReference); + okResponseConcrete.Headers.Add(HeaderNames.ETag, _etagHeaderReference); + okResponseConcrete.Headers.Add(NonStandardHeaderNames.XRateLimitLimit, _rateLimitLimitHeaderReference); } + + //operation.Responses.TryGetValue(_tooManyRequestsResponseKey, out var tooManyRequestsResponse); + + //if (tooManyRequestsResponse is not null && tooManyRequestsResponse is OpenApiResponse tooManyRequestsResponseConcrete) + //{ + // tooManyRequestsResponseConcrete.Headers ??= new Dictionary(); + // tooManyRequestsResponseConcrete.Headers.Add(HeaderNames.RetryAfter, _retryAfterHeaderReference); + //} }); } } diff --git a/src/CheersDb.Api/Http/NonStandardHeaderNames.cs b/src/CheersDb.Api/Http/NonStandardHeaderNames.cs new file mode 100644 index 0000000..e889d83 --- /dev/null +++ b/src/CheersDb.Api/Http/NonStandardHeaderNames.cs @@ -0,0 +1,6 @@ +namespace CheersDb.Api.Http; + +public static class NonStandardHeaderNames +{ + public const string XRateLimitLimit = "X-RateLimit-Limit"; +} diff --git a/src/CheersDb.Api/appsettings.json b/src/CheersDb.Api/appsettings.json index 00af236..39fb54b 100644 --- a/src/CheersDb.Api/appsettings.json +++ b/src/CheersDb.Api/appsettings.json @@ -9,6 +9,15 @@ "OpenApiInfo": { "Title": "CheersDb API", "Description": "An API for retrieving and altering breweries on the CheersDb platform", - "Version": "v1" + "Version": "v1", + "Contact": { + "Name": "CheersDb Support", + "Email": "info@cheersdb.org", + "Url": "https://cheersdb.org/support" + }, + "License": { + "Name": "GPL-3.0", + "Url": "https://github.com/PetesBreenCoding/CheersDb?tab=GPL-3.0-1-ov-file" + } } } \ No newline at end of file From e79183ab80fcca2ddc4fefd09cb70e8eff33a9a0 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Fri, 19 Jun 2026 12:14:53 +0100 Subject: [PATCH 57/62] wip --- src/CheersDb.Api/CheersDb.Api.json | 20 +++++++++++++++++++ .../Extensions/OpenApiComponentsExtensions.cs | 16 +++++++++++++++ .../Extensions/OpenApiOptionsExtensions.cs | 13 ++++-------- .../Http/NonStandardHeaderNames.cs | 18 ++++++++++++++++- 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json index 24c284b..a09f13e 100644 --- a/src/CheersDb.Api/CheersDb.Api.json +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -47,6 +47,12 @@ }, "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" } }, "content": { @@ -238,6 +244,20 @@ "type": "number" }, "example": 1000 + }, + "X-RateLimit-Remaining": { + "description": "Indicates the number of requests remaining in the current rate limit window", + "schema": { + "type": "number" + }, + "example": 999 + }, + "X-RateLimit-Reset": { + "description": "The number of seconds until the rate limit resets.", + "schema": { + "type": "number" + }, + "example": 60 } } }, diff --git a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs index 05215d1..acb7b2a 100644 --- a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs @@ -76,10 +76,26 @@ public void ConfigureHeaders() Schema = _numberSchema, }; + var rateLimitRemainingHeader = new OpenApiHeader + { + Description = "Indicates the number of requests remaining in the current rate limit window", + Example = 999, + Schema = _numberSchema, + }; + + var rateLimitResetHeader = new OpenApiHeader + { + Description = "The number of seconds until the rate limit resets.", + Example = 60, + Schema = _numberSchema, + }; + components.Headers.Add(HeaderNames.CacheControl, cacheControlHeader); components.Headers.Add(HeaderNames.ETag, etagHeader); components.Headers.Add(HeaderNames.RetryAfter, retryAfterHeader); components.Headers.Add(NonStandardHeaderNames.XRateLimitLimit, rateLimitLimitHeader); + components.Headers.Add(NonStandardHeaderNames.XRateLimitRemaining, rateLimitRemainingHeader); + components.Headers.Add(NonStandardHeaderNames.XRateLimitReset, rateLimitResetHeader); } } } diff --git a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs index 0f81153..d6e6111 100644 --- a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs @@ -15,12 +15,13 @@ public static class OpenApiOptionsExtensions private static readonly string _tooManyRequestsResponseKey = ((int)HttpStatusCode.TooManyRequests).ToString(); private static readonly OpenApiResponseReference _internalServerErrorResponseReference = new(nameof(HttpStatusCode.InternalServerError)); - private static readonly OpenApiResponse _tooManyRequestsResponse = new() { Description = "Indicates that the user has sent too many requests in a given amount of time" }; private static readonly OpenApiHeaderReference _cacheControlHeaderReference = new(HeaderNames.CacheControl); private static readonly OpenApiHeaderReference _etagHeaderReference = new(HeaderNames.ETag); private static readonly OpenApiHeaderReference _retryAfterHeaderReference = new(HeaderNames.RetryAfter); private static readonly OpenApiHeaderReference _rateLimitLimitHeaderReference = new(NonStandardHeaderNames.XRateLimitLimit); + private static readonly OpenApiHeaderReference _rateLimitRemainingHeaderReference = new(NonStandardHeaderNames.XRateLimitRemaining); + private static readonly OpenApiHeaderReference _rateLimitResetHeaderReference = new(NonStandardHeaderNames.XRateLimitReset); extension(OpenApiOptions options) { @@ -71,15 +72,9 @@ public OpenApiOptions ConfigureOperations() okResponseConcrete.Headers.Add(HeaderNames.CacheControl, _cacheControlHeaderReference); okResponseConcrete.Headers.Add(HeaderNames.ETag, _etagHeaderReference); okResponseConcrete.Headers.Add(NonStandardHeaderNames.XRateLimitLimit, _rateLimitLimitHeaderReference); + okResponseConcrete.Headers.Add(NonStandardHeaderNames.XRateLimitRemaining, _rateLimitRemainingHeaderReference); + okResponseConcrete.Headers.Add(NonStandardHeaderNames.XRateLimitReset, _rateLimitResetHeaderReference); } - - //operation.Responses.TryGetValue(_tooManyRequestsResponseKey, out var tooManyRequestsResponse); - - //if (tooManyRequestsResponse is not null && tooManyRequestsResponse is OpenApiResponse tooManyRequestsResponseConcrete) - //{ - // tooManyRequestsResponseConcrete.Headers ??= new Dictionary(); - // tooManyRequestsResponseConcrete.Headers.Add(HeaderNames.RetryAfter, _retryAfterHeaderReference); - //} }); } } diff --git a/src/CheersDb.Api/Http/NonStandardHeaderNames.cs b/src/CheersDb.Api/Http/NonStandardHeaderNames.cs index e889d83..b487f00 100644 --- a/src/CheersDb.Api/Http/NonStandardHeaderNames.cs +++ b/src/CheersDb.Api/Http/NonStandardHeaderNames.cs @@ -1,6 +1,22 @@ namespace CheersDb.Api.Http; +/// +/// Contains constants for non-standard HTTP header names used in the CheersDb API. +/// public static class NonStandardHeaderNames { + /// + /// The X-RateLimit-Limit header name. Indicates the maximum number of requests allowed in a given time period. + /// public const string XRateLimitLimit = "X-RateLimit-Limit"; -} + + /// + /// The X-RateLimit-Remaining header name. Indicates the number of requests remaining in the current rate limit window. + /// + public const string XRateLimitRemaining = "X-RateLimit-Remaining"; + + /// + /// The X-RateLimit-Reset header name. Indicates the number of seconds until the rate limit window resets. + /// + public const string XRateLimitReset = "X-RateLimit-Reset"; +} \ No newline at end of file From 1088f6b9de22c0f0cf3587710cc72e8a6964b250 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 25 Jun 2026 09:55:26 +0100 Subject: [PATCH 58/62] security --- Directory.Packages.props | 3 ++- src/CheersDb.Api/AppSettings.cs | 5 +++++ src/CheersDb.Api/CheersDb.Api.csproj | 1 + src/CheersDb.Api/CheersDb.Api.json | 20 ++++++++++++++++- .../Controllers/ProducersController.cs | 5 ++++- .../Extensions/OpenApiComponentsExtensions.cs | 16 +++++++++++++- .../Extensions/OpenApiOptionsExtensions.cs | 19 +++++++++++++++- .../Extensions/ServiceCollectionExtensions.cs | 2 +- src/CheersDb.Api/Program.cs | 22 +++++++++++++++++++ src/CheersDb.Api/appsettings.json | 8 +++++++ 10 files changed, 95 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1998b7d..68c2fc7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,9 +3,10 @@ true + - + \ No newline at end of file diff --git a/src/CheersDb.Api/AppSettings.cs b/src/CheersDb.Api/AppSettings.cs index 6787daf..c644bc3 100644 --- a/src/CheersDb.Api/AppSettings.cs +++ b/src/CheersDb.Api/AppSettings.cs @@ -11,4 +11,9 @@ public class AppSettings /// Gets the OpenAPI information for the API, such as title, version, and description. /// public OpenApiInfo? OpenApiInfo { get; init; } + + /// + /// Gets the OpenAPI security scheme for the API, which defines the authentication and authorization requirements. + /// + public OpenApiSecurityScheme? OpenApiSecurityScheme { get; init; } } \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.csproj b/src/CheersDb.Api/CheersDb.Api.csproj index 63b622e..7f10935 100644 --- a/src/CheersDb.Api/CheersDb.Api.csproj +++ b/src/CheersDb.Api/CheersDb.Api.csproj @@ -8,6 +8,7 @@ + all diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json index a09f13e..00ec6a1 100644 --- a/src/CheersDb.Api/CheersDb.Api.json +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -84,7 +84,12 @@ } } } - } + }, + "security": [ + { + "bearerAuth": [ ] + } + ] } } }, @@ -259,8 +264,21 @@ }, "example": 60 } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "description": "JWT Bearer token authentication", + "scheme": "Bearer", + "bearerFormat": "JWT" + } } }, + "security": [ + { + "bearerAuth": [ ] + } + ], "tags": [ { "name": "Producers" diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index f648a88..c45c33a 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -1,7 +1,8 @@ using CheersDb.Api.Dtos; using CheersDb.Api.Http; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; using Microsoft.Net.Http.Headers; using System.Net.Mime; @@ -11,6 +12,7 @@ namespace CheersDb.Api.Controllers; /// Controller for managing producers in the CheersDb API. /// [ApiController] +[Authorize(AuthenticationSchemes = "bearerAuth")] [Route("[controller]")] [Produces(MediaTypeNames.Application.Json)] public class ProducersController : ControllerBase @@ -24,6 +26,7 @@ public class ProducersController : ControllerBase [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)] + [Authorize(AuthenticationSchemes = "bearerAuth")] public IActionResult GetProducerDetails([FromRoute] int id) { var producerDetails = new GetProducerDetailsDto() diff --git a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs index acb7b2a..2caf304 100644 --- a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs @@ -1,4 +1,5 @@ using CheersDb.Api.Http; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; @@ -97,5 +98,18 @@ public void ConfigureHeaders() components.Headers.Add(NonStandardHeaderNames.XRateLimitRemaining, rateLimitRemainingHeader); components.Headers.Add(NonStandardHeaderNames.XRateLimitReset, rateLimitResetHeader); } + + /// + /// Configures the OpenAPI components to include a security scheme for JWT Bearer authentication. + /// + /// The OpenAPI security scheme to include. + public void ConfigureSecuritySchemes(OpenApiSecurityScheme? openApiSecurityScheme) + { + if (openApiSecurityScheme is null) + return; + + components.SecuritySchemes ??= new Dictionary(); + components.SecuritySchemes.Add(openApiSecurityScheme.Name ?? JwtBearerDefaults.AuthenticationScheme, openApiSecurityScheme); + } } -} +} \ No newline at end of file diff --git a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs index d6e6111..76fcd4f 100644 --- a/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiOptionsExtensions.cs @@ -1,4 +1,5 @@ using CheersDb.Api.Http; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; @@ -22,6 +23,15 @@ public static class OpenApiOptionsExtensions private static readonly OpenApiHeaderReference _rateLimitLimitHeaderReference = new(NonStandardHeaderNames.XRateLimitLimit); private static readonly OpenApiHeaderReference _rateLimitRemainingHeaderReference = new(NonStandardHeaderNames.XRateLimitRemaining); private static readonly OpenApiHeaderReference _rateLimitResetHeaderReference = new(NonStandardHeaderNames.XRateLimitReset); + private static OpenApiSecurityRequirement? _defaultSecurityRequirement = null; + + private static OpenApiSecurityRequirement BuildDefaultSecurityRequirement(AppSettings? appSettings, OpenApiDocument? document) + { + return _defaultSecurityRequirement ??= new OpenApiSecurityRequirement + { + { new OpenApiSecuritySchemeReference(appSettings?.OpenApiSecurityScheme?.Name ?? JwtBearerDefaults.AuthenticationScheme, document), new List() } + }; + } extension(OpenApiOptions options) { @@ -36,17 +46,21 @@ public OpenApiOptions ConfigureDocument(AppSettings appSettings) if (appSettings?.OpenApiInfo is not null) document.Info = appSettings.OpenApiInfo; + document.Security ??= []; + document.Security.Add(BuildDefaultSecurityRequirement(appSettings, document)); + document.Components ??= new OpenApiComponents(); await document.Components.ConfigureResponsesAsync(context, cancellationToken); document.Components.ConfigureHeaders(); + document.Components.ConfigureSecuritySchemes(appSettings?.OpenApiSecurityScheme); }); } /// /// Adds an operation transformer to the OpenAPI options that configures operation responses. /// - public OpenApiOptions ConfigureOperations() + public OpenApiOptions ConfigureOperations(AppSettings appSettings) { return options.AddOperationTransformer(async (operation, context, cancellationToken) => { @@ -75,6 +89,9 @@ public OpenApiOptions ConfigureOperations() okResponseConcrete.Headers.Add(NonStandardHeaderNames.XRateLimitRemaining, _rateLimitRemainingHeaderReference); okResponseConcrete.Headers.Add(NonStandardHeaderNames.XRateLimitReset, _rateLimitResetHeaderReference); } + + operation.Security ??= []; + operation.Security.Add(BuildDefaultSecurityRequirement(appSettings, context.Document)); }); } } diff --git a/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs b/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs index 0eb99d5..f5e5de4 100644 --- a/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/CheersDb.Api/Extensions/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public IServiceCollection AddConfiguredOpenApi(AppSettings appSettings) { options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; options.ConfigureDocument(appSettings); - options.ConfigureOperations(); + options.ConfigureOperations(appSettings); }); } } diff --git a/src/CheersDb.Api/Program.cs b/src/CheersDb.Api/Program.cs index b857688..c2ec939 100644 --- a/src/CheersDb.Api/Program.cs +++ b/src/CheersDb.Api/Program.cs @@ -1,9 +1,31 @@ using CheersDb.Api.Extensions; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using Scalar.AspNetCore; +using System.Text; var builder = WebApplication.CreateBuilder(args); var appSettings = builder.Configuration.GetAppSettings(); +builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + ValidIssuer = "cheersdb-api", + ValidAudience = "cheersdb-clients", + + IssuerSigningKey = + new SymmetricSecurityKey(Encoding.UTF8.GetBytes("12345")) //TODO add proper key + }; + }); + // Add services to the container. builder.Services.AddControllers(); diff --git a/src/CheersDb.Api/appsettings.json b/src/CheersDb.Api/appsettings.json index 39fb54b..40fcf89 100644 --- a/src/CheersDb.Api/appsettings.json +++ b/src/CheersDb.Api/appsettings.json @@ -19,5 +19,13 @@ "Name": "GPL-3.0", "Url": "https://github.com/PetesBreenCoding/CheersDb?tab=GPL-3.0-1-ov-file" } + }, + "OpenApiSecurityScheme": { + "Name": "bearerAuth", + "Description": "JWT Bearer token authentication", + "Type": "Http", + "Scheme": "Bearer", + "BearerFormat": "JWT", + "In": "Header" } } \ No newline at end of file From 0627dbac465858881106cdf9657fe4e796b9ab97 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Thu, 25 Jun 2026 14:55:23 +0100 Subject: [PATCH 59/62] wi[p --- src/CheersDb.Api/AppSettings.cs | 8 ++++++- src/CheersDb.Api/CheersDb.Api.http | 7 +++--- src/CheersDb.Api/CheersDb.Api.json | 10 +++++---- .../Controllers/ProducersController.cs | 10 ++++----- .../Dtos/GetProducerDetailsDto.cs | 4 ++-- .../ConfigurationManagerExtensions.cs | 12 ++++++---- src/CheersDb.Api/Http/JwtOptions.cs | 22 +++++++++++++++++++ src/CheersDb.Api/Program.cs | 22 +++++++++++-------- src/CheersDb.Api/appsettings.json | 5 +++++ 9 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 src/CheersDb.Api/Http/JwtOptions.cs diff --git a/src/CheersDb.Api/AppSettings.cs b/src/CheersDb.Api/AppSettings.cs index c644bc3..20128e2 100644 --- a/src/CheersDb.Api/AppSettings.cs +++ b/src/CheersDb.Api/AppSettings.cs @@ -1,4 +1,5 @@ -using Microsoft.OpenApi; +using CheersDb.Api.Http; +using Microsoft.OpenApi; namespace CheersDb.Api; @@ -7,6 +8,11 @@ namespace CheersDb.Api; /// public class AppSettings { + /// + /// Gets the JWT auth settings + /// + public JwtOptions? JwtAuth { get; init; } + /// /// Gets the OpenAPI information for the API, such as title, version, and description. /// diff --git a/src/CheersDb.Api/CheersDb.Api.http b/src/CheersDb.Api/CheersDb.Api.http index c582075..58aa54a 100644 --- a/src/CheersDb.Api/CheersDb.Api.http +++ b/src/CheersDb.Api/CheersDb.Api.http @@ -1,7 +1,6 @@ -@CheersDb.Api_HostAddress = http://localhost:5104 +@CheersDb.Api_HostAddress = https://localhost:7277 +@BearerToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJDaGVlcnNEYkFwaSIsImF1ZCI6IkNoZWVyc0RiQXBpQ2xpZW50cyIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTgxMzkyOTI2M30.2GDbHeNP6iESLtZraeT4wFsNs_WB8XoSbr_WEu0Huv8 GET {{CheersDb.Api_HostAddress}}/producers/1 Accept: application/json - -### -http \ No newline at end of file +Authorization: Bearer {{BearerToken}} \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json index 00ec6a1..ebc934f 100644 --- a/src/CheersDb.Api/CheersDb.Api.json +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -20,7 +20,8 @@ "tags": [ "Producers" ], - "summary": "Retrieves details for a specific producer by id", + "summary": "Get producer details", + "description": "Finds a specific producer usiung the passed id and returns the details in the response body", "operationId": "GetProducerDetails", "parameters": [ { @@ -32,7 +33,8 @@ "pattern": "^-?(?:0|[1-9]\\d*)$", "type": "integer", "format": "int32" - } + }, + "example": 24 } ], "responses": { @@ -107,7 +109,7 @@ ], "description": "The id of the producer", "format": "int32", - "example": 328 + "example": 24 }, "name": { "type": [ @@ -139,7 +141,7 @@ "example": [ { "rel": "self", - "href": "/producers/328", + "href": "/producers/24", "method": "GET" } ] diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index c45c33a..f496271 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -1,7 +1,5 @@ using CheersDb.Api.Dtos; using CheersDb.Api.Http; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using System.Net.Mime; @@ -12,21 +10,21 @@ namespace CheersDb.Api.Controllers; /// Controller for managing producers in the CheersDb API. /// [ApiController] -[Authorize(AuthenticationSchemes = "bearerAuth")] [Route("[controller]")] [Produces(MediaTypeNames.Application.Json)] public class ProducersController : ControllerBase { /// - /// Retrieves details for a specific producer by id + /// Get producer details /// - /// The id of the producer to retrieve + /// Finds a specific producer usiung the passed id and returns the details in the response body + /// The id of the producer to retrieve /// The requested producer details + /// GET /producers/24 [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)] - [Authorize(AuthenticationSchemes = "bearerAuth")] public IActionResult GetProducerDetails([FromRoute] int id) { var producerDetails = new GetProducerDetailsDto() diff --git a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs index eccc2c4..88ffbf3 100644 --- a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs +++ b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs @@ -8,7 +8,7 @@ public class GetProducerDetailsDto /// /// The id of the producer /// - /// 328 + /// 24 public int? Id { get; init; } /// @@ -29,7 +29,7 @@ public class GetProducerDetailsDto /// [ /// { /// "rel": "self", - /// "href": "/producers/328", + /// "href": "/producers/24", /// "method": "GET" /// } /// ] diff --git a/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs b/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs index 14c9a69..8ec991b 100644 --- a/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs +++ b/src/CheersDb.Api/Extensions/ConfigurationManagerExtensions.cs @@ -14,14 +14,18 @@ public static class ConfigurationManagerExtensions /// Thrown when the application settings are invalid. public AppSettings GetAppSettings() { - var appSettings = configurationManager.Get(); - - if (appSettings is null) - throw new InvalidOperationException("There was an issue parsing the application settings."); + var appSettings = configurationManager.Get() + ?? throw new InvalidOperationException("There was an issue parsing the application settings."); if (appSettings.OpenApiInfo is null) throw new InvalidOperationException($"{nameof(AppSettings.OpenApiInfo)} was not parsed in the application settings."); + if (string.IsNullOrEmpty(appSettings.OpenApiSecurityScheme?.Name) || string.IsNullOrEmpty(appSettings.OpenApiSecurityScheme?.Scheme)) + throw new InvalidOperationException($"{nameof(AppSettings.OpenApiSecurityScheme)} was not parsed in the application settings."); + + if (string.IsNullOrEmpty(appSettings.JwtAuth?.Key)) + throw new InvalidOperationException($"{nameof(AppSettings.JwtAuth)} was not parsed in the application settings."); + return appSettings; } } diff --git a/src/CheersDb.Api/Http/JwtOptions.cs b/src/CheersDb.Api/Http/JwtOptions.cs new file mode 100644 index 0000000..f236b0d --- /dev/null +++ b/src/CheersDb.Api/Http/JwtOptions.cs @@ -0,0 +1,22 @@ +namespace CheersDb.Api.Http; + +/// +/// Configuration options for JWT (JSON Web Token) authentication in the CheersDb API. +/// +public sealed class JwtOptions +{ + /// + /// Gets or initializes the issuer of the JWT. This is the principal that issued the token. + /// + public string? Issuer { get; init; } + + /// + /// Gets or initializes the intended audience of the JWT. This identifies the recipients that the JWT is intended for. + /// + public string? Audience { get; init; } + + /// + /// Gets or initializes the secret key used to sign and verify the JWT. + /// + public string? Key { get; init; } +} \ No newline at end of file diff --git a/src/CheersDb.Api/Program.cs b/src/CheersDb.Api/Program.cs index c2ec939..3869639 100644 --- a/src/CheersDb.Api/Program.cs +++ b/src/CheersDb.Api/Program.cs @@ -1,5 +1,5 @@ using CheersDb.Api.Extensions; -using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; using Scalar.AspNetCore; using System.Text; @@ -8,7 +8,7 @@ var appSettings = builder.Configuration.GetAppSettings(); builder.Services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddAuthentication(appSettings.OpenApiSecurityScheme!.Scheme!) .AddJwtBearer(options => { options.TokenValidationParameters = new() @@ -18,17 +18,21 @@ ValidateLifetime = true, ValidateIssuerSigningKey = true, - ValidIssuer = "cheersdb-api", - ValidAudience = "cheersdb-clients", + ValidIssuer = appSettings.JwtAuth!.Issuer, + ValidAudience = appSettings.JwtAuth!.Audience, - IssuerSigningKey = - new SymmetricSecurityKey(Encoding.UTF8.GetBytes("12345")) //TODO add proper key + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.JwtAuth!.Key!)) }; }); // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddAuthorizationBuilder() + .SetFallbackPolicy(new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build()); + builder.Services.AddRouting(options => { options.LowercaseUrls = true; @@ -43,14 +47,14 @@ if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); - app.MapScalarApiReference("/docs"); + app.MapOpenApi().AllowAnonymous(); + app.MapScalarApiReference("/docs").AllowAnonymous(); } app.UseHttpsRedirection(); app.UseAuthorization(); -app.MapControllers(); +app.MapControllers().RequireAuthorization(); app.Run(); \ No newline at end of file diff --git a/src/CheersDb.Api/appsettings.json b/src/CheersDb.Api/appsettings.json index 40fcf89..59636c5 100644 --- a/src/CheersDb.Api/appsettings.json +++ b/src/CheersDb.Api/appsettings.json @@ -6,6 +6,11 @@ } }, "AllowedHosts": "*", + "JwtAuth": { + "Issuer": "CheersDbApi", + "Audience": "CheersDbApiClients", + "Key": "sDQailgaaSsl9fnl9eweVwgR9TTY464q" + }, "OpenApiInfo": { "Title": "CheersDb API", "Description": "An API for retrieving and altering breweries on the CheersDb platform", From 73fbfff5b168771e0e202b5458aa03904593d648 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 1 Jul 2026 11:38:01 +0100 Subject: [PATCH 60/62] wip --- Directory.Packages.props | 2 +- src/CheersDb.Api/CheersDb.Api.http | 4 +-- src/CheersDb.Api/CheersDb.Api.json | 35 ++++++------------- .../Controllers/ProducersController.cs | 2 +- .../Dtos/GetProducerDetailsDto.cs | 8 ++--- src/CheersDb.Api/Dtos/ProblemDetailsDto.cs | 19 ++++++++++ .../Extensions/OpenApiComponentsExtensions.cs | 18 +++++++++- src/CheersDb.Api/Program.cs | 6 ++++ .../Properties/launchSettings.json | 14 ++------ 9 files changed, 64 insertions(+), 44 deletions(-) create mode 100644 src/CheersDb.Api/Dtos/ProblemDetailsDto.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 68c2fc7..c66bc70 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.http b/src/CheersDb.Api/CheersDb.Api.http index 58aa54a..ec22e6a 100644 --- a/src/CheersDb.Api/CheersDb.Api.http +++ b/src/CheersDb.Api/CheersDb.Api.http @@ -1,6 +1,6 @@ -@CheersDb.Api_HostAddress = https://localhost:7277 +@CheersDb.Api_HostAddress = https://localhost:8339 @BearerToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJDaGVlcnNEYkFwaSIsImF1ZCI6IkNoZWVyc0RiQXBpQ2xpZW50cyIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTgxMzkyOTI2M30.2GDbHeNP6iESLtZraeT4wFsNs_WB8XoSbr_WEu0Huv8 -GET {{CheersDb.Api_HostAddress}}/producers/1 +GET {{CheersDb.Api_HostAddress}}/producers/24 Accept: application/json Authorization: Bearer {{BearerToken}} \ No newline at end of file diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json index ebc934f..4537c20 100644 --- a/src/CheersDb.Api/CheersDb.Api.json +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -30,7 +30,6 @@ "description": "The id of the producer to retrieve", "required": true, "schema": { - "pattern": "^-?(?:0|[1-9]\\d*)$", "type": "integer", "format": "int32" }, @@ -98,42 +97,32 @@ "components": { "schemas": { "GetProducerDetailsDto": { + "required": [ + "id", + "name", + "revision", + "links" + ], "type": "object", "properties": { "id": { - "pattern": "^-?(?:0|[1-9]\\d*)$", - "type": [ - "null", - "integer", - "string" - ], + "type": "integer", "description": "The id of the producer", "format": "int32", "example": 24 }, "name": { - "type": [ - "null", - "string" - ], + "type": "string", "description": "The name of the producer", "example": "Rye River Brewing" }, "revision": { - "pattern": "^-?(?:0|[1-9]\\d*)$", - "type": [ - "null", - "integer", - "string" - ], + "type": "integer", "description": "The revision number of the producer", "format": "int32" }, "links": { - "type": [ - "null", - "array" - ], + "type": "array", "items": { "$ref": "#/components/schemas/LinkDto" }, @@ -191,11 +180,9 @@ ] }, "status": { - "pattern": "^-?(?:0|[1-9]\\d*)$", "type": [ "null", - "integer", - "string" + "integer" ], "format": "int32" }, diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index f496271..bbef8f8 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -30,7 +30,7 @@ public IActionResult GetProducerDetails([FromRoute] int id) var producerDetails = new GetProducerDetailsDto() { Id = id, - Name = "Producer Name", + Name = "Rye River Brewing Company", Revision = 7, Links = [ diff --git a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs index 88ffbf3..76f62f3 100644 --- a/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs +++ b/src/CheersDb.Api/Dtos/GetProducerDetailsDto.cs @@ -9,18 +9,18 @@ public class GetProducerDetailsDto /// The id of the producer /// /// 24 - public int? Id { get; init; } + public required int Id { get; init; } /// /// The name of the producer /// /// Rye River Brewing - public string? Name { get; init; } + public required string Name { get; init; } /// /// The revision number of the producer /// - public int? Revision { get; init; } + public required int Revision { get; init; } /// /// Links related to the producer, such as a self link to retrieve the producer details @@ -34,5 +34,5 @@ public class GetProducerDetailsDto /// } /// ] /// - public List? Links { get; init; } + public required List Links { get; init; } } \ No newline at end of file diff --git a/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs b/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs new file mode 100644 index 0000000..63e9387 --- /dev/null +++ b/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace CheersDb.Api.Dtos; + +public class ProblemDetailsDto +{ + public string? Type { get; set; } + + public string? Title { get; set; } + + public int? Status { get; set; } + + public string? Detail { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyOrder(-1)] + [JsonPropertyName("instance")] + public string? Instance { get; set; } +} diff --git a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs index 2caf304..5a84905 100644 --- a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs @@ -1,6 +1,7 @@ using CheersDb.Api.Http; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; @@ -28,7 +29,22 @@ public async Task ConfigureResponsesAsync(OpenApiDocumentTransformerContext cont { components.Responses ??= new Dictionary(); - var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), cancellationToken: cancellationToken); + var tmp = new ApiParameterDescription() + { + Name = "test name", + }; + + var tmp1 = new ProblemDetails() + { + Type = "https://example.com/probs/out-of-credit", + Title = "You do not have enough credit.", + Status = StatusCodes.Status403Forbidden, + Detail = "Your current balance is 30, but that costs 50.", + Instance = "/account/12345/msgs/abc" + }; + + tmp1. + var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), tmp, cancellationToken: cancellationToken); var internalServerErrorResonse = new OpenApiResponse { diff --git a/src/CheersDb.Api/Program.cs b/src/CheersDb.Api/Program.cs index 3869639..e6d633c 100644 --- a/src/CheersDb.Api/Program.cs +++ b/src/CheersDb.Api/Program.cs @@ -3,6 +3,7 @@ using Microsoft.IdentityModel.Tokens; using Scalar.AspNetCore; using System.Text; +using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); var appSettings = builder.Configuration.GetAppSettings(); @@ -25,6 +26,11 @@ }; }); +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; +}); + // Add services to the container. builder.Services.AddControllers(); diff --git a/src/CheersDb.Api/Properties/launchSettings.json b/src/CheersDb.Api/Properties/launchSettings.json index b120c85..0a54048 100644 --- a/src/CheersDb.Api/Properties/launchSettings.json +++ b/src/CheersDb.Api/Properties/launchSettings.json @@ -1,20 +1,12 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5104", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "https://localhost:7277;http://localhost:5104", + "launchBrowser": true, + "launchUrl": "https://localhost:8339/openapi/v1.json", + "applicationUrl": "https://localhost:8339", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From c2306b70921de0197786d6b6eb6fce65acca64cf Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 1 Jul 2026 11:43:49 +0100 Subject: [PATCH 61/62] wip --- src/CheersDb.Api/CheersDb.Api.json | 276 ----------------------------- 1 file changed, 276 deletions(-) delete mode 100644 src/CheersDb.Api/CheersDb.Api.json diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json deleted file mode 100644 index 4537c20..0000000 --- a/src/CheersDb.Api/CheersDb.Api.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "openapi": "3.1.1", - "info": { - "title": "CheersDb API", - "description": "An API for retrieving and altering breweries on the CheersDb platform", - "contact": { - "name": "CheersDb Support", - "url": "https://cheersdb.org/support", - "email": "info@cheersdb.org" - }, - "license": { - "name": "GPL-3.0", - "url": "https://github.com/PetesBreenCoding/CheersDb?tab=GPL-3.0-1-ov-file" - }, - "version": "v1" - }, - "paths": { - "/producers/{id}": { - "get": { - "tags": [ - "Producers" - ], - "summary": "Get producer details", - "description": "Finds a specific producer usiung the passed id and returns the details in the response body", - "operationId": "GetProducerDetails", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The id of the producer to retrieve", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - }, - "example": 24 - } - ], - "responses": { - "200": { - "description": "Returns the requested producer in the response body", - "headers": { - "Cache-Control": { - "$ref": "#/components/headers/Cache-Control" - }, - "ETag": { - "$ref": "#/components/headers/ETag" - }, - "X-RateLimit-Limit": { - "$ref": "#/components/headers/X-RateLimit-Limit" - }, - "X-RateLimit-Remaining": { - "$ref": "#/components/headers/X-RateLimit-Remaining" - }, - "X-RateLimit-Reset": { - "$ref": "#/components/headers/X-RateLimit-Reset" - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetProducerDetailsDto" - } - } - } - }, - "404": { - "description": "Indicates the requested producer was not found, or the URI is invalid", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - }, - "429": { - "description": "Indicates that the user has sent too many requests in a given amount of time", - "headers": { - "Retry-After": { - "$ref": "#/components/headers/Retry-After" - } - } - } - }, - "security": [ - { - "bearerAuth": [ ] - } - ] - } - } - }, - "components": { - "schemas": { - "GetProducerDetailsDto": { - "required": [ - "id", - "name", - "revision", - "links" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "The id of the producer", - "format": "int32", - "example": 24 - }, - "name": { - "type": "string", - "description": "The name of the producer", - "example": "Rye River Brewing" - }, - "revision": { - "type": "integer", - "description": "The revision number of the producer", - "format": "int32" - }, - "links": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LinkDto" - }, - "description": "Links related to the producer, such as a self link to retrieve the producer details", - "example": [ - { - "rel": "self", - "href": "/producers/24", - "method": "GET" - } - ] - } - }, - "description": "Details about a producer" - }, - "LinkDto": { - "required": [ - "rel", - "href", - "method" - ], - "type": "object", - "properties": { - "rel": { - "type": "string", - "description": "The relationship of the link to the current resource", - "example": "self" - }, - "href": { - "type": "string", - "description": "The URL of the link", - "example": "/producers/1" - }, - "method": { - "type": "string", - "description": "The HTTP method for the link", - "example": "GET" - } - }, - "description": "Represents a hypermedia link in the API response, providing information about the relationship, URL, and HTTP method for the link." - }, - "ProblemDetails": { - "type": "object", - "properties": { - "type": { - "type": [ - "null", - "string" - ] - }, - "title": { - "type": [ - "null", - "string" - ] - }, - "status": { - "type": [ - "null", - "integer" - ], - "format": "int32" - }, - "detail": { - "type": [ - "null", - "string" - ] - }, - "instance": { - "type": [ - "null", - "string" - ] - } - } - } - }, - "responses": { - "InternalServerError": { - "description": "Indicates that an unexpected internal server error has occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - } - }, - "headers": { - "Cache-Control": { - "description": "Instructions for caching mechanisms in responses", - "schema": { - "type": "string" - } - }, - "ETag": { - "description": "Indicates the current version of the resource", - "schema": { - "type": "string" - } - }, - "Retry-After": { - "description": "Indicates how many seconds the user agent should wait before making a follow-up request", - "schema": { - "type": "number" - } - }, - "X-RateLimit-Limit": { - "description": "Indicates the maximum number of requests that the user is allowed to make in a given amount of time", - "schema": { - "type": "number" - }, - "example": 1000 - }, - "X-RateLimit-Remaining": { - "description": "Indicates the number of requests remaining in the current rate limit window", - "schema": { - "type": "number" - }, - "example": 999 - }, - "X-RateLimit-Reset": { - "description": "The number of seconds until the rate limit resets.", - "schema": { - "type": "number" - }, - "example": 60 - } - }, - "securitySchemes": { - "bearerAuth": { - "type": "http", - "description": "JWT Bearer token authentication", - "scheme": "Bearer", - "bearerFormat": "JWT" - } - } - }, - "security": [ - { - "bearerAuth": [ ] - } - ], - "tags": [ - { - "name": "Producers" - } - ] -} \ No newline at end of file From edeabef331dc8210387af1372773a1a30389f332 Mon Sep 17 00:00:00 2001 From: Peter Breen Date: Wed, 1 Jul 2026 11:58:17 +0100 Subject: [PATCH 62/62] WIP --- src/CheersDb.Api/CheersDb.Api.json | 281 ++++++++++++++++++ .../Controllers/ProducersController.cs | 2 +- src/CheersDb.Api/Dtos/ProblemDetailsDto.cs | 42 ++- .../Extensions/OpenApiComponentsExtensions.cs | 24 +- 4 files changed, 316 insertions(+), 33 deletions(-) create mode 100644 src/CheersDb.Api/CheersDb.Api.json diff --git a/src/CheersDb.Api/CheersDb.Api.json b/src/CheersDb.Api/CheersDb.Api.json new file mode 100644 index 0000000..d92aca7 --- /dev/null +++ b/src/CheersDb.Api/CheersDb.Api.json @@ -0,0 +1,281 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "CheersDb API", + "description": "An API for retrieving and altering breweries on the CheersDb platform", + "contact": { + "name": "CheersDb Support", + "url": "https://cheersdb.org/support", + "email": "info@cheersdb.org" + }, + "license": { + "name": "GPL-3.0", + "url": "https://github.com/PetesBreenCoding/CheersDb?tab=GPL-3.0-1-ov-file" + }, + "version": "v1" + }, + "paths": { + "/producers/{id}": { + "get": { + "tags": [ + "Producers" + ], + "summary": "Get producer details", + "description": "Finds a specific producer usiung the passed id and returns the details in the response body", + "operationId": "GetProducerDetails", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the producer to retrieve", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "example": 24 + } + ], + "responses": { + "200": { + "description": "Returns the requested producer in the response body", + "headers": { + "Cache-Control": { + "$ref": "#/components/headers/Cache-Control" + }, + "ETag": { + "$ref": "#/components/headers/ETag" + }, + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetProducerDetailsDto" + } + } + } + }, + "404": { + "description": "Indicates the requested producer was not found, or the URI is invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + }, + "429": { + "description": "Indicates that the user has sent too many requests in a given amount of time", + "headers": { + "Retry-After": { + "$ref": "#/components/headers/Retry-After" + } + } + } + }, + "security": [ + { + "bearerAuth": [ ] + } + ] + } + } + }, + "components": { + "schemas": { + "GetProducerDetailsDto": { + "required": [ + "id", + "name", + "revision", + "links" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The id of the producer", + "format": "int32", + "example": 24 + }, + "name": { + "type": "string", + "description": "The name of the producer", + "example": "Rye River Brewing" + }, + "revision": { + "type": "integer", + "description": "The revision number of the producer", + "format": "int32" + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkDto" + }, + "description": "Links related to the producer, such as a self link to retrieve the producer details", + "example": [ + { + "rel": "self", + "href": "/producers/24", + "method": "GET" + } + ] + } + }, + "description": "Details about a producer" + }, + "LinkDto": { + "required": [ + "rel", + "href", + "method" + ], + "type": "object", + "properties": { + "rel": { + "type": "string", + "description": "The relationship of the link to the current resource", + "example": "self" + }, + "href": { + "type": "string", + "description": "The URL of the link", + "example": "/producers/1" + }, + "method": { + "type": "string", + "description": "The HTTP method for the link", + "example": "GET" + } + }, + "description": "Represents a hypermedia link in the API response, providing information about the relationship, URL, and HTTP method for the link." + }, + "ProblemDetailsDto": { + "required": [ + "type", + "title", + "status", + "detail" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "A URI reference [RFC3986] that identifies the problem type", + "example": "https://cheersdb.org/error-codes/e100" + }, + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem type", + "example": "Invalid request" + }, + "status": { + "type": "integer", + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem", + "format": "int32", + "example": 400 + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem", + "example": "The request payload is missing the required 'username' field." + }, + "instance": { + "type": [ + "null", + "string" + ], + "description": "A URI reference [RFC3986] that identifies the specific occurrence of the problem", + "example": "https://cheersdb.org/error-codes/e100/instances/12345" + } + }, + "description": "Represents a standardized error response according to RFC 7807 (Problem Details for HTTP APIs)." + } + }, + "responses": { + "InternalServerError": { + "description": "Indicates that an unexpected internal server error has occurred", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + } + }, + "headers": { + "Cache-Control": { + "description": "Instructions for caching mechanisms in responses", + "schema": { + "type": "string" + } + }, + "ETag": { + "description": "Indicates the current version of the resource", + "schema": { + "type": "string" + } + }, + "Retry-After": { + "description": "Indicates how many seconds the user agent should wait before making a follow-up request", + "schema": { + "type": "number" + } + }, + "X-RateLimit-Limit": { + "description": "Indicates the maximum number of requests that the user is allowed to make in a given amount of time", + "schema": { + "type": "number" + }, + "example": 1000 + }, + "X-RateLimit-Remaining": { + "description": "Indicates the number of requests remaining in the current rate limit window", + "schema": { + "type": "number" + }, + "example": 999 + }, + "X-RateLimit-Reset": { + "description": "The number of seconds until the rate limit resets.", + "schema": { + "type": "number" + }, + "example": 60 + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "description": "JWT Bearer token authentication", + "scheme": "Bearer", + "bearerFormat": "JWT" + } + } + }, + "security": [ + { + "bearerAuth": [ ] + } + ], + "tags": [ + { + "name": "Producers" + } + ] +} \ No newline at end of file diff --git a/src/CheersDb.Api/Controllers/ProducersController.cs b/src/CheersDb.Api/Controllers/ProducersController.cs index bbef8f8..f1c2c70 100644 --- a/src/CheersDb.Api/Controllers/ProducersController.cs +++ b/src/CheersDb.Api/Controllers/ProducersController.cs @@ -23,7 +23,7 @@ public class ProducersController : ControllerBase /// GET /producers/24 [HttpGet("{id:int}", Name = nameof(GetProducerDetails))] [ProducesResponseType(typeof(GetProducerDetailsDto), StatusCodes.Status200OK, Description = "Returns the requested producer in the response body")] - [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid")] + [ProducesResponseType(StatusCodes.Status404NotFound, Description = "Indicates the requested producer was not found, or the URI is invalid", Type = typeof(ProblemDetailsDto))] [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)] public IActionResult GetProducerDetails([FromRoute] int id) { diff --git a/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs b/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs index 63e9387..8f7f587 100644 --- a/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs +++ b/src/CheersDb.Api/Dtos/ProblemDetailsDto.cs @@ -1,19 +1,37 @@ -using System.Text.Json.Serialization; - -namespace CheersDb.Api.Dtos; +namespace CheersDb.Api.Dtos; +/// +/// Represents a standardized error response according to RFC 7807 (Problem Details for HTTP APIs). +/// public class ProblemDetailsDto { - public string? Type { get; set; } + /// + /// A URI reference [RFC3986] that identifies the problem type + /// + /// https://cheersdb.org/error-codes/e100 + public required string Type { get; init; } + + /// + /// A short, human-readable summary of the problem type + /// + /// Invalid request + public required string Title { get; init; } - public string? Title { get; set; } + /// + /// The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem + /// + /// 400 + public required int Status { get; init; } - public int? Status { get; set; } - - public string? Detail { get; set; } + /// + /// A human-readable explanation specific to this occurrence of the problem + /// + /// The request payload is missing the required 'username' field. + public required string Detail { get; init; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyOrder(-1)] - [JsonPropertyName("instance")] + /// + /// A URI reference [RFC3986] that identifies the specific occurrence of the problem + /// + /// https://cheersdb.org/error-codes/e100/instances/12345 public string? Instance { get; set; } -} +} \ No newline at end of file diff --git a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs index 5a84905..09b0c42 100644 --- a/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs +++ b/src/CheersDb.Api/Extensions/OpenApiComponentsExtensions.cs @@ -1,7 +1,6 @@ -using CheersDb.Api.Http; +using CheersDb.Api.Dtos; +using CheersDb.Api.Http; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; @@ -29,22 +28,7 @@ public async Task ConfigureResponsesAsync(OpenApiDocumentTransformerContext cont { components.Responses ??= new Dictionary(); - var tmp = new ApiParameterDescription() - { - Name = "test name", - }; - - var tmp1 = new ProblemDetails() - { - Type = "https://example.com/probs/out-of-credit", - Title = "You do not have enough credit.", - Status = StatusCodes.Status403Forbidden, - Detail = "Your current balance is 30, but that costs 50.", - Instance = "/account/12345/msgs/abc" - }; - - tmp1. - var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), tmp, cancellationToken: cancellationToken); + var problemDetailsSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetailsDto), cancellationToken: cancellationToken); var internalServerErrorResonse = new OpenApiResponse { @@ -53,7 +37,7 @@ public async Task ConfigureResponsesAsync(OpenApiDocumentTransformerContext cont { [MediaTypeNames.Application.Json] = new OpenApiMediaType { - Schema = new OpenApiSchemaReference(nameof(ProblemDetails)) + Schema = new OpenApiSchemaReference(nameof(ProblemDetailsDto)) } } };