Initial version
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user