Skip to content

Commit

Permalink
feat: Add initial SwaggerUi support (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
xC0dex authored Jan 25, 2024
1 parent 5c77c38 commit 093abd8
Show file tree
Hide file tree
Showing 32 changed files with 743 additions and 20 deletions.
59 changes: 48 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,71 @@ concurrency:
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
NODE_MODULES_PATH: 'src/APIWeaver.Swagger/node_modules'

jobs:
node-setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Cache Node.js modules
uses: actions/cache@v4
with:
path: ${{ env.NODE_MODULES_PATH }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Restore dependencies
working-directory: src/APIWeaver.Swagger
run: npm ci
- name: Archive node_modules
uses: actions/upload-artifact@v4
with:
name: node-modules
path: ${{ env.NODE_MODULES_PATH }}

build:
runs-on: ubuntu-latest
needs:
- node-setup
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build -c Release --no-restore
- uses: actions/checkout@v4
- name: Download Node.js modules
uses: actions/download-artifact@v4
with:
name: node-modules
path: ${{ env.NODE_MODULES_PATH }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build -c Release --no-restore

analyze:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
needs:
- build
- node-setup
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Analyze and test solution
- name: Download Node.js modules
uses: actions/download-artifact@v4
with:
name: node-modules
path: 'src/APIWeaver.Swagger/node_modules'
- name: Analyze and test solution
uses: highbyte/sonarscan-dotnet@v2.3.1
with:
sonarProjectKey: xC0dex_APIWeaver
sonarProjectName: APIWeaver
sonarProjectName: APIWeaver
sonarOrganization: apiweaver
sonarBeginArguments: /d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml" -d:sonar.cs.vstest.reportsPaths="**/TestResults/*.trx"
dotnetTestArguments: -c Release --logger trx --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
Expand Down
20 changes: 20 additions & 0 deletions APIWeaver.sln
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIWeaver.MinimalApi.Demo",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIWeaver.Swagger", "src\APIWeaver.Swagger\APIWeaver.Swagger.csproj", "{8BA6D753-69C7-4E08-9659-28563A2FBBFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIWeaver.Swagger.Tests", "tests\APIWeaver.Swagger.Tests\APIWeaver.Swagger.Tests.csproj", "{75846F2E-D298-4175-BFB7-1C44C241F4E9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apis", "apis", "{DDE3C770-5DDB-4650-8B68-D753730D3B1B}"
ProjectSection(SolutionItems) = preProject
tests\apis\Directory.Build.props = tests\apis\Directory.Build.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIWeaver.Swagger.Api", "tests\apis\APIWeaver.Swagger.Api\APIWeaver.Swagger.Api.csproj", "{B95DF0A8-54E8-44B8-8416-9D62C3C3E954}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -39,6 +48,9 @@ Global
{05883931-C88D-4196-AAE9-4EA55EF00829} = {9C8C12F5-F67A-4319-94FC-281A1EEFF1B4}
{6688140D-3DBB-422E-B199-4F1FCAB00059} = {BBD49ECA-CB62-4820-B425-296EFCFE7EC2}
{8BA6D753-69C7-4E08-9659-28563A2FBBFB} = {CC045423-814B-40D9-B087-5AC3C8848B09}
{75846F2E-D298-4175-BFB7-1C44C241F4E9} = {9C8C12F5-F67A-4319-94FC-281A1EEFF1B4}
{DDE3C770-5DDB-4650-8B68-D753730D3B1B} = {9C8C12F5-F67A-4319-94FC-281A1EEFF1B4}
{B95DF0A8-54E8-44B8-8416-9D62C3C3E954} = {DDE3C770-5DDB-4650-8B68-D753730D3B1B}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{184B6D75-E412-4F4C-9E98-6128320636C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -57,5 +69,13 @@ Global
{8BA6D753-69C7-4E08-9659-28563A2FBBFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BA6D753-69C7-4E08-9659-28563A2FBBFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BA6D753-69C7-4E08-9659-28563A2FBBFB}.Release|Any CPU.Build.0 = Release|Any CPU
{75846F2E-D298-4175-BFB7-1C44C241F4E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75846F2E-D298-4175-BFB7-1C44C241F4E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75846F2E-D298-4175-BFB7-1C44C241F4E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75846F2E-D298-4175-BFB7-1C44C241F4E9}.Release|Any CPU.Build.0 = Release|Any CPU
{B95DF0A8-54E8-44B8-8416-9D62C3C3E954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B95DF0A8-54E8-44B8-8416-9D62C3C3E954}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B95DF0A8-54E8-44B8-8416-9D62C3C3E954}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B95DF0A8-54E8-44B8-8416-9D62C3C3E954}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\..\src\APIWeaver.Swagger\APIWeaver.Swagger.csproj" />
<ProjectReference Include="..\..\src\APIWeaver\APIWeaver.csproj"/>
</ItemGroup>
<ItemGroup>
Expand Down
12 changes: 12 additions & 0 deletions demo/APIWeaver.MinimalApi.Demo/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
using APIWeaver.Extensions;
using APIWeaver.Swagger.Extensions;
using APIWeaver.Swagger.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiWeaver();

if (builder.Environment.IsDevelopment())
{
builder.Services.Configure<SwaggerUiConfiguration>(x => x.Title = "Hola fresh");
}

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwaggerUi();
}

app.MapGet("/hello-world/{id:guid}", (Guid id) => id)
.Produces(200);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger/index.html",
"applicationUrl": "https://localhost:7113;http://localhost:5299",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
15 changes: 12 additions & 3 deletions src/APIWeaver.Swagger/APIWeaver.Swagger.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\APIWeaver\APIWeaver.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\APIWeaver\APIWeaver.csproj"/>
</ItemGroup>

<ItemGroup>

<EmbeddedResource Include="**/swagger-ui-dist/swagger-ui-bundle.js;**/swagger-ui-dist/swagger-ui-standalone-preset.js;**/swagger-ui-dist/*.css;**/swagger-ui-dist/*.html;**/swagger-ui-dist/*.png;swagger-initializer.js">
<LogicalName>SwaggerUiAssets.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>

</Project>
49 changes: 49 additions & 0 deletions src/APIWeaver.Swagger/Extensions/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using APIWeaver.Swagger.Middleware;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;

namespace APIWeaver.Swagger.Extensions;

/// <summary>
/// Extension methods for <see cref="IApplicationBuilder" />.
/// </summary>
public static class ApplicationBuilderExtensions
{
/// <summary>
/// Configures the application to use Swagger UI.
/// </summary>
/// <param name="appBuilder"><see cref="IApplicationBuilder" />.</param>
/// <param name="options">An action to configure the Swagger UI options.</param>
public static IApplicationBuilder UseSwaggerUi(this IApplicationBuilder appBuilder, Action<SwaggerUiConfiguration>? options = null)
{
var configuredOptions = appBuilder.ApplicationServices.GetRequiredService<IOptions<SwaggerUiConfiguration>>().Value;
options?.Invoke(configuredOptions);
var requestPath = $"/{configuredOptions.RoutePrefix.TrimEnd('/')}";

appBuilder.MapWhen(context => context.Request.Path.StartsWithSegments(requestPath), builder =>
{
builder.UseMiddleware<SwaggerConfigurationMiddleware>();

builder.UseStaticFiles(new StaticFileOptions
{
RequestPath = requestPath,
FileProvider = new EmbeddedFileProvider(typeof(SwaggerConfigurationMiddleware).Assembly, "SwaggerUiAssets")
});

builder.Use((context, next) =>
{
if (!context.Response.HasStarted)
{
context.Response.Redirect($"{requestPath}/index.html");
return Task.CompletedTask;
}

return next();
});
});

return appBuilder;
}
}
8 changes: 8 additions & 0 deletions src/APIWeaver.Swagger/Extensions/PathStringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Http;

namespace APIWeaver.Swagger.Extensions;

internal static class PathStringExtensions
{
public static bool EndsWith(this PathString path, string segment) => path.HasValue && path.Value.EndsWith(segment, StringComparison.OrdinalIgnoreCase);
}
19 changes: 19 additions & 0 deletions src/APIWeaver.Swagger/Extensions/SwaggerOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace APIWeaver.Swagger.Extensions;

/// <summary>
/// Extension methods for <see cref="SwaggerOptions" />.
/// </summary>
public static class SwaggerOptionsExtensions
{
/// <summary>
/// Configures the OpenAPI endpoint for the Swagger UI.
/// </summary>
/// <param name="options"><see cref="SwaggerOptions" />.</param>
/// <param name="name">The name of the OpenAPI document.</param>
/// <param name="url">The URL where the OpenAPI document can be found.</param>
public static SwaggerOptions WithOpenApiEndpoint(this SwaggerOptions options, string name, string url)
{
options.Urls.Add(new Url(name, url));
return options;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace APIWeaver.Swagger.Extensions;

/// <summary>
/// Extension methods for <see cref="SwaggerUiConfiguration" />.
/// </summary>
public static class SwaggerUiConfigurationExtensions
{
/// <summary>
/// Configures OAuth2 options for the Swagger UI.
/// </summary>
/// <param name="swaggerUiConfiguration"><see cref="SwaggerUiConfiguration" />.</param>
/// <param name="authOptions">An action to configure the OAuth2 options.</param>
public static SwaggerUiConfiguration WithOAuth2Options(this SwaggerUiConfiguration swaggerUiConfiguration, Action<OAuth2Options> authOptions)
{
swaggerUiConfiguration.OAuth2Options ??= new OAuth2Options();
authOptions.Invoke(swaggerUiConfiguration.OAuth2Options);
return swaggerUiConfiguration;
}

/// <summary>
/// Configures Swagger options for the Swagger UI.
/// </summary>
/// <param name="swaggerUiConfiguration"><see cref="SwaggerUiConfiguration" />.</param>
/// <param name="swaggerOptions">An action to configure the Swagger options.</param>
public static SwaggerUiConfiguration WithSwaggerOptions(this SwaggerUiConfiguration swaggerUiConfiguration, Action<SwaggerOptions> swaggerOptions)
{
swaggerOptions.Invoke(swaggerUiConfiguration.SwaggerOptions);
return swaggerUiConfiguration;
}
}
12 changes: 12 additions & 0 deletions src/APIWeaver.Swagger/Helper/JsonSerializerHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json;

namespace APIWeaver.Swagger.Helper;

internal static class JsonSerializerHelper
{
public static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
37 changes: 37 additions & 0 deletions src/APIWeaver.Swagger/Middleware/SwaggerConfigurationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Net.Mime;
using APIWeaver.Swagger.Helper;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace APIWeaver.Swagger.Middleware;

internal sealed class SwaggerConfigurationMiddleware(RequestDelegate next)
{

public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Method == HttpMethods.Get && context.Request.Path.EndsWith("configuration.json"))
{
await HandleRequestAsync(context, context.RequestAborted);
return;
}

await next(context);
}

private static async Task HandleRequestAsync(HttpContext context, CancellationToken cancellationToken)
{
var configuration = context.RequestServices.GetRequiredService<IOptions<SwaggerUiConfiguration>>().Value;
if (configuration.SwaggerOptions.Urls.Count == 0)
{
var applicationName = context.RequestServices.GetRequiredService<IWebHostEnvironment>().ApplicationName;
configuration.SwaggerOptions.WithOpenApiEndpoint(applicationName, "https://petstore.swagger.io/v2/swagger.json");
}

var response = context.Response;
response.Headers.ContentType = MediaTypeNames.Application.Json;
await response.WriteAsJsonAsync(configuration, JsonSerializerHelper.SerializerOptions, cancellationToken);
}
}
47 changes: 47 additions & 0 deletions src/APIWeaver.Swagger/Models/OAuth2Options.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace APIWeaver.Swagger.Models;

/// <summary>
/// OAuth configuration for Swagger UI.
/// </summary>
public sealed class OAuth2Options
{
/// <summary>
/// The client ID for your application. You must first create an application at the service provider and obtain this value.
/// </summary>
public string ClientId { get; set; } = null!;

/// <summary>
/// The client secret for your application. You must first create an application at the service provider and obtain this value.
/// </summary>
public string ClientSecret { get; set; } = null!;

/// <summary>
/// The realm of the client application.
/// </summary>
public string Realm { get; set; } = null!;

/// <summary>
/// The application name.
/// </summary>
public string? AppName { get; set; }

/// <summary>
/// The scope separator. The standard is a space (" "), but some services use a comma (",").
/// </summary>
public string ScopeSeparator { get; set; } = " ";

/// <summary>
/// String array of initially selected oauth scopes.
/// </summary>
public IEnumerable<string> Scopes { get; set; } = [];

/// <summary>
/// Additional query parameters added to authorizationUrl and tokenUrl.
/// </summary>
public Dictionary<string, string> AdditionalQueryStringParams { get; set; } = new();

/// <summary>
/// If true, the "Authorize" button will be hidden for operations that do not have any OAuth2 scopes.
/// </summary>
public bool UseBasicAuthenticationWithAccessCodeGrant { get; set; }
}
Loading

0 comments on commit 093abd8

Please sign in to comment.