Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for API browsers (Swagger, Redoc) #591

Merged
merged 6 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading