Files
EonaCat.DoxaApi/DoxaApi/Middleware/DoxaApiMiddlewareExtensions.cs
T

271 lines
11 KiB
C#

using System.Reflection;
using System.Text;
using EonaCat.DoxaApi.Generation;
using EonaCat.DoxaApi.Interop;
using EonaCat.DoxaApi.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using EonaCat.Json;
using EonaCat.Json.Linq;
namespace EonaCat.DoxaApi.Middleware
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
public static class DoxaApiMiddlewareExtensions
{
private static readonly JsonSerializerSettings _writeOptions = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};
public static IApplicationBuilder UseDoxaApi(this IApplicationBuilder app, Action<DoxaApiOptions>? configure = null)
{
var options = app.ApplicationServices.GetService<DoxaApiOptions>() ?? new DoxaApiOptions();
var metricsCollector = app.ApplicationServices.GetService<MetricsCollector>();
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";
var json = JsonHelper.ToJson(document, _writeOptions);
await context.Response.WriteAsync(json, Encoding.UTF8);
});
});
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";
var json = JsonHelper.ToJson(openApi, _writeOptions);
await context.Response.WriteAsync(json, 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";
var json = JsonHelper.ToJson(swagger, _writeOptions);
await context.Response.WriteAsync(json, 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";
var json = JsonHelper.ToJson(imported, _writeOptions);
await context.Response.WriteAsync(json, Encoding.UTF8);
}
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}");
}
});
});
if (options.EnableMockServer)
{
app.Map(prefix + "/mock", mockApp =>
{
mockApp.Run(async context =>
{
var document = GenerateDocument(context, options);
var operationId = context.Request.Query["operationId"].FirstOrDefault();
ApiEndpoint? endpoint = null;
foreach (var group in document.Groups)
{
endpoint = group.Endpoints.FirstOrDefault(e => e.OperationId == operationId);
if (endpoint is not null)
{
break;
}
}
if (endpoint is null)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Unknown operationId. Pass ?operationId=<id> matching an entry in the spec.");
return;
}
var successResponse = endpoint.Responses.FirstOrDefault(r => r.StatusCode.StartsWith('2'))
?? endpoint.Responses.FirstOrDefault();
var mockValue = MockResponseGenerator.Generate(successResponse?.Schema, document);
context.Response.StatusCode = int.TryParse(successResponse?.StatusCode, out var code) ? code : 200;
context.Response.ContentType = "application/json; charset=utf-8";
context.Response.Headers["X-DoxaApi-Mock"] = "true";
var json = JsonHelper.ToJson(mockValue, _writeOptions);
await context.Response.WriteAsync(json, Encoding.UTF8);
});
});
}
if (options.EnableConsole)
{
app.Map(prefix + "/console", consoleApp =>
{
consoleApp.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 ConsoleRequest body.");
return;
}
await ConsoleProxy.HandleAsync(context, _writeOptions);
});
});
}
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);
});
});
// Add analytics endpoint if metrics collector is registered
if (metricsCollector != null)
{
app.Map(prefix + "/analytics", analyticsApp =>
{
analyticsApp.Run(async context =>
{
if (!HttpMethods.IsGet(context.Request.Method))
{
context.Response.StatusCode = 405;
context.Response.Headers["Allow"] = "GET";
await context.Response.WriteAsync("Method Not Allowed - use GET");
return;
}
var dashboard = metricsCollector.GetDashboard();
context.Response.ContentType = "application/json; charset=utf-8";
var json = JsonHelper.ToJson(dashboard, _writeOptions);
await context.Response.WriteAsync(json, Encoding.UTF8);
});
});
}
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 = "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);
}
}
}