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? configure = null) { var options = app.ApplicationServices.GetService() ?? 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(); 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 = "EonaCat.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); } } }