Skip to content

Commit

Permalink
Add support for API browsers (Swagger, Redoc) (#591)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaliumhexacyanoferrat authored Dec 15, 2024
1 parent 79b78aa commit 488e804
Show file tree
Hide file tree
Showing 20 changed files with 2,653 additions and 6 deletions.
7 changes: 7 additions & 0 deletions GenHTTP.sln
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenHTTP.Adapters.AspNetCore
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.I18n", "Modules\GenHTTP.Modules.I18n\GenHTTP.Modules.I18n.csproj", "{E17F6CF0-295D-408C-9664-FE18C6E83433}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenHTTP.Modules.ApiBrowsing", "Modules\ApiBrowsing\GenHTTP.Modules.ApiBrowsing.csproj", "{CE7DE343-E16D-46A6-A7F3-DF0CD96617F7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -261,6 +263,10 @@ Global
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E17F6CF0-295D-408C-9664-FE18C6E83433}.Release|Any CPU.Build.0 = Release|Any CPU
{CE7DE343-E16D-46A6-A7F3-DF0CD96617F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE7DE343-E16D-46A6-A7F3-DF0CD96617F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE7DE343-E16D-46A6-A7F3-DF0CD96617F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE7DE343-E16D-46A6-A7F3-DF0CD96617F7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -305,6 +311,7 @@ Global
{7CEE048E-FA6A-4D8C-B6A6-EEEA0B048C54} = {AFBFE61E-0C33-42F6-9370-9F5088EB8633}
{AD7904BC-27BE-4EB5-84BC-62FF32DCBB78} = {C3265C1A-E9A9-45FD-BD24-66DE9C7062F1}
{E17F6CF0-295D-408C-9664-FE18C6E83433} = {23B23225-275E-4F52-8B29-6F44C85B6ACE}
{CE7DE343-E16D-46A6-A7F3-DF0CD96617F7} = {23B23225-275E-4F52-8B29-6F44C85B6ACE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9C67B3AF-0BF6-4E21-8C39-3F74CFCF9632}
Expand Down
24 changes: 24 additions & 0 deletions Modules/ApiBrowsing/ApiBrowser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using GenHTTP.Modules.ApiBrowsing.Common;

namespace GenHTTP.Modules.ApiBrowsing;

/// <summary>
/// Provides graphical, JavaScript based web applications that render an Open API
/// definition so that the API can be explored by users.
/// </summary>
public static class ApiBrowser
{

/// <summary>
/// Creates a handler that will provide a Swagger UI app.
/// </summary>
/// <returns>The newly created handler</returns>
public static BrowserHandlerBuilder SwaggerUI() => new("Swagger", "Swagger UI");

/// <summary>
/// Creates a handler that will provide a Redoc app.
/// </summary>
/// <returns>The newly created handler</returns>
public static BrowserHandlerBuilder Redoc() => new("Redoc", "Redoc");

}
74 changes: 74 additions & 0 deletions Modules/ApiBrowsing/Common/BrowserHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Cottle;
using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;
using GenHTTP.Modules.Basics;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.Pages;
using GenHTTP.Modules.Pages.Rendering;

namespace GenHTTP.Modules.ApiBrowsing.Common;

public sealed class BrowserHandler: IHandler
{

#region Get-/Setters

public IHandler StaticResources { get; }

public TemplateRenderer Template { get; }

public BrowserMetaData MetaData { get; }

#endregion

#region Initialization

public BrowserHandler(string resourceRoot, BrowserMetaData metaData)
{
StaticResources = Resources.From(ResourceTree.FromAssembly($"{resourceRoot}.Static"))
.Build();

Template = Renderer.From(Resource.FromAssembly($"{resourceRoot}.Index.html").Build());

MetaData = metaData;
}

#endregion

#region Functionality

public ValueTask PrepareAsync() => ValueTask.CompletedTask;

public async ValueTask<IResponse?> HandleAsync(IRequest request)
{
if (!request.HasType(RequestMethod.Get, RequestMethod.Head))
{
throw new ProviderException(ResponseStatus.MethodNotAllowed, "Only GET requests are allowed by this handler", (b) => b.Header("Allow", "GET"));
}

if (request.Target.Ended)
{
var config = new Dictionary<Value, Value>
{
["title"] = MetaData.Title,
["url"] = (MetaData.Url ?? "../openapi.json")
};

var content = await Template.RenderAsync(config);

return request.GetPage(content)
.Build();
}

if (request.Target.Current?.Value == "static")
{
request.Target.Advance();
return await StaticResources.HandleAsync(request);
}

return null;
}

#endregion

}
47 changes: 47 additions & 0 deletions Modules/ApiBrowsing/Common/BrowserHandlerBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using GenHTTP.Api.Content;

namespace GenHTTP.Modules.ApiBrowsing.Common;

public class BrowserHandlerBuilder(string resourceRoot, string title) : IHandlerBuilder<BrowserHandlerBuilder>
{
private readonly List<IConcernBuilder> _Concerns = [];

private string? _Url;

private string _Title = title;

/// <summary>
/// Sets the URL of the Open API definition to be consumed (defaults to "../openapi.json").
/// Should be relative to avoid issues with CORS etc.
/// </summary>
/// <param name="url">The URL the application will fetch the Open API definition from</param>
public BrowserHandlerBuilder Url(string url)
{
_Url = url;
return this;
}

/// <summary>
/// Sets the title of the application that will be rendered by the browser (e.g. the title of the tab).
/// </summary>
/// <param name="title">The title of the application to be set</param>
public BrowserHandlerBuilder Title(string title)
{
_Title = title;
return this;
}

public BrowserHandlerBuilder Add(IConcernBuilder concern)
{
_Concerns.Add(concern);
return this;
}

public IHandler Build()
{
var meta = new BrowserMetaData(_Url, _Title);

return Concerns.Chain(_Concerns, new BrowserHandler(resourceRoot, meta));
}

}
3 changes: 3 additions & 0 deletions Modules/ApiBrowsing/Common/BrowserMetaData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace GenHTTP.Modules.ApiBrowsing.Common;

public record BrowserMetaData(string? Url, string Title);
56 changes: 56 additions & 0 deletions Modules/ApiBrowsing/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using GenHTTP.Modules.ApiBrowsing.Common;
using GenHTTP.Modules.Layouting.Provider;

namespace GenHTTP.Modules.ApiBrowsing;

public static class Extensions
{

/// <summary>
/// Creates a Swagger UI application and registers it at the layout.
/// </summary>
/// <param name="layout">The layout to add the application to</param>
/// <param name="segment">The path to make the application available from (defaults to "/swagger/")</param>
/// <param name="url">The URL of the Open API definition to be rendered (defaults to "../openapi.json")</param>
/// <param name="title">The title of the rendered application</param>
/// <returns>The layout once again (builder pattern)</returns>
/// <remarks>
/// There is no auto-detection of Open API definitions provided by the server
/// so the URL provided needs to point to the correct definition to be consumed.
/// Use relative paths to avoid issues with CORS, proxies etc.
/// </remarks>
public static LayoutBuilder AddSwaggerUI(this LayoutBuilder layout, string segment = "swagger", string? url = null, string? title = null)
=> AddBrowser(layout, ApiBrowser.SwaggerUI(), segment, url, title);

/// <summary>
/// Creates a Redoc application and registers it at the layout.
/// </summary>
/// <param name="layout">The layout to add the application to</param>
/// <param name="segment">The path to make the application available from (defaults to "/redoc/")</param>
/// <param name="url">The URL of the Open API definition to be rendered (defaults to "../openapi.json")</param>
/// <param name="title">The title of the rendered application</param>
/// <returns>The layout once again (builder pattern)</returns>
/// <remarks>
/// There is no auto-detection of Open API definitions provided by the server
/// so the URL provided needs to point to the correct definition to be consumed.
/// Use relative paths to avoid issues with CORS, proxies etc.
/// </remarks>
public static LayoutBuilder AddRedoc(this LayoutBuilder layout, string segment = "redoc", string? url = null, string? title = null)
=> AddBrowser(layout, ApiBrowser.Redoc(), segment, url, title);

private static LayoutBuilder AddBrowser(this LayoutBuilder layout, BrowserHandlerBuilder builder, string segment, string? url, string? title)
{
if (url != null)
{
builder.Url(url);
}

if (title != null)
{
builder.Title(title);
}

return layout.Add(segment, builder);
}

}
76 changes: 76 additions & 0 deletions Modules/ApiBrowsing/GenHTTP.Modules.ApiBrowsing.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFrameworks>net8.0;net9.0</TargetFrameworks>

<LangVersion>13.0</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>

<AssemblyVersion>9.4.0.0</AssemblyVersion>
<FileVersion>9.4.0.0</FileVersion>
<Version>9.4.0</Version>

<Authors>Andreas Nägeli</Authors>
<Company/>

<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://genhttp.org/</PackageProjectUrl>

<Description>Serves API browsers such as Swagger UI.</Description>
<PackageTags>HTTP Webserver C# Module Swagger UI API Browser Browsing</PackageTags>

<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591,CS1587,CS1572,CS1573</NoWarn>

<PackageIcon>icon.png</PackageIcon>

</PropertyGroup>

<ItemGroup>

<None Include="..\..\LICENSE" Pack="true" PackagePath="\"/>
<None Include="..\..\Resources\icon.png" Pack="true" PackagePath="\"/>

</ItemGroup>

<ItemGroup>

<ProjectReference Include="..\..\API\GenHTTP.Api.csproj"/>

<ProjectReference Include="..\IO\GenHTTP.Modules.IO.csproj" />

<ProjectReference Include="..\Layouting\GenHTTP.Modules.Layouting.csproj"/>

<ProjectReference Include="..\Pages\GenHTTP.Modules.Pages.csproj" />

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>

</ItemGroup>

<ItemGroup>
<None Remove="Resources\Static\swagger-ui.css" />
<None Remove="Resources\Static\swagger-ui-bundle.js" />
<None Remove="Resources\Static\swagger-ui-standalone-preset.js" />
<None Remove="Resources\Templates\Index.html" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Redoc\Index.html" />
<EmbeddedResource Include="Redoc\Static\redoc-standalone.js" />
<EmbeddedResource Include="Redoc\Static\roboto.css" />
<EmbeddedResource Include="Swagger\Index.html" />
<EmbeddedResource Include="Swagger\Static\swagger-ui-bundle.js" />
<EmbeddedResource Include="Swagger\Static\swagger-ui.css" />
<None Remove="Redoc\Resources\Index.html" />
<None Remove="Redoc\Resources\Static\redoc.standalone.js" />
<None Remove="Redoc\Resources\Static\roboto.css" />
</ItemGroup>

</Project>
20 changes: 20 additions & 0 deletions Modules/ApiBrowsing/Redoc/Index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="./static/roboto.css" rel="stylesheet">

<style>
body \{
margin: 0;
padding: 0;
\}
</style>
</head>
<body>
<redoc spec-url='{url}'></redoc>
<script src="./static/redoc-standalone.js"> </script>
</body>
</html>
1,826 changes: 1,826 additions & 0 deletions Modules/ApiBrowsing/Redoc/Static/redoc-standalone.js

Large diffs are not rendered by default.

Loading

0 comments on commit 488e804

Please sign in to comment.