Files
EonaCat.DoxaApi/DoxaApi/Middleware/ApiDocsMiddleware.cs
T
2026-06-20 10:26:27 +02:00

183 lines
7.3 KiB
C#

using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using EonaCat.DoxaApi.Generation;
using EonaCat.DoxaApi.Interop;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
namespace EonaCat.DoxaApi.Middleware
{
public static class DoxaApiMiddlewareExtensions
{
private static readonly JsonSerializerOptions _writeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
public static IApplicationBuilder UseDoxaApi(this IApplicationBuilder app, Action<DoxaApiOptions>? configure = null)
{
var options = app.ApplicationServices.GetService<DoxaApiOptions>() ?? new DoxaApiOptions();
configure?.Invoke(options);
var prefix = "/" + options.RoutePrefix.Trim('/');
app.Map(prefix + "/doxaApi.json", specApp =>
{
specApp.Run(async context =>
{
var document = GenerateDocument(context, options);
context.Response.ContentType = "application/json; charset=utf-8";
await JsonSerializer.SerializeAsync(context.Response.Body, document, _writeOptions);
});
});
app.Map(prefix + "/openapi.json", oaApp =>
{
oaApp.Run(async context =>
{
var document = GenerateDocument(context, options);
var openApi = OpenApiExporter.Export(document);
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(
openApi.ToJsonString(_writeOptions), Encoding.UTF8);
});
});
app.Map(prefix + "/swagger.json", swApp =>
{
swApp.Run(async context =>
{
var document = GenerateDocument(context, options);
var swagger = SwaggerExporter.Export(document);
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(
swagger.ToJsonString(_writeOptions), Encoding.UTF8);
});
});
app.Map(prefix + "/import", importApp =>
{
importApp.Run(async context =>
{
if (!HttpMethods.IsPost(context.Request.Method))
{
context.Response.StatusCode = 405;
context.Response.Headers["Allow"] = "POST";
await context.Response.WriteAsync("Method Not Allowed - use POST with a JSON body.");
return;
}
if (context.Request.ContentLength == 0 || context.Request.Body is null)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Request body is empty.");
return;
}
try
{
var imported = await OpenApiImporter.ImportAsync(context.Request.Body);
context.Response.ContentType = "application/json; charset=utf-8";
await JsonSerializer.SerializeAsync(context.Response.Body, imported, _writeOptions);
}
catch (NotSupportedException ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync($"Unsupported spec format: {ex.Message}");
}
catch (Exception ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync($"Failed to parse spec: {ex.Message}");
}
});
});
app.Map(prefix, uiApp =>
{
uiApp.Run(async context =>
{
var requestPath = context.Request.Path.Value ?? "";
var assetName = requestPath.Trim('/');
if (string.IsNullOrEmpty(assetName) || assetName == options.RoutePrefix.Trim('/'))
{
assetName = "index.html";
}
var asset = EmbeddedAssetLoader.Load(assetName);
if (asset is null)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Not found");
return;
}
context.Response.ContentType = asset.Value.ContentType;
if (assetName == "index.html")
{
var html = Encoding.UTF8.GetString(asset.Value.Bytes);
html = html.Replace("__DOXA_API_TITLE__", options.Title)
.Replace("__DOXA_API_ACCENT__", options.AccentColor)
.Replace("__DOXA_API_THEME__", options.Theme)
.Replace("__DOXA_API_BASE__", prefix + "/")
.Replace("__DOXA_API_SPEC_PATH__", prefix + "/doxaApi.json");
await context.Response.WriteAsync(html, Encoding.UTF8);
return;
}
await context.Response.Body.WriteAsync(asset.Value.Bytes);
});
});
return app;
}
private static Models.ApiDocument GenerateDocument(HttpContext context, DoxaApiOptions options)
{
var provider = context.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();
var actions = provider.ActionDescriptors.Items;
return new ApiDocumentGenerator(actions, options).Generate();
}
}
internal static class EmbeddedAssetLoader
{
private static readonly Assembly _assembly = typeof(EmbeddedAssetLoader).Assembly;
private static readonly string _prefix = "DoxaApi.UI.Assets.";
public static (byte[] Bytes, string ContentType)? Load(string assetName)
{
var resourceName = _prefix + assetName.Replace('/', '.');
using var stream = _assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
return null;
}
using var ms = new MemoryStream();
stream.CopyTo(ms);
var contentType = Path.GetExtension(assetName) switch
{
".html" => "text/html; charset=utf-8",
".css" => "text/css; charset=utf-8",
".js" => "application/javascript; charset=utf-8",
".svg" => "image/svg+xml",
".png" => "image/png",
".ico" => "image/x-icon",
_ => "application/octet-stream"
};
return (ms.ToArray(), contentType);
}
}
}