docs(swagger): 合并展示 SimpleLite 投影 WebApi 接口
通过 SimpleLiteOpenApiDocumentFilter 将 SimpleLite OpenAPI 描述注入 Swagger,开发环境可在 /swagger 统一浏览 /api/sl/projection 全部端点。
This commit is contained in:
@@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="data\.gitkeep" Condition="Exists('data\.gitkeep')" />
|
<None Include="data\.gitkeep" Condition="Exists('data\.gitkeep')" />
|
||||||
|
<Content Include="OpenApi\simplelite-projection.json" Link="OpenApi\simplelite-projection.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('OpenApi\simplelite-projection.json')" />
|
||||||
|
<Content Include="..\..\Simple\SimpleLite\Docs\openapi\simplelite-projection.json" Link="OpenApi\simplelite-projection.json" CopyToOutputDirectory="PreserveNewest" Condition="Exists('..\..\Simple\SimpleLite\Docs\openapi\simplelite-projection.json')" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace MiGu.Server.OpenApi;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 SimpleLite EmbedIO WebApi 的 OpenAPI 描述合并进 MiGu.Server Swagger 文档。
|
||||||
|
/// 源文件:Simple/SimpleLite/Docs/openapi/simplelite-projection.json
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SimpleLiteOpenApiDocumentFilter : IDocumentFilter
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, OperationType> VerbMap = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["get"] = OperationType.Get,
|
||||||
|
["post"] = OperationType.Post,
|
||||||
|
["put"] = OperationType.Put,
|
||||||
|
["patch"] = OperationType.Patch,
|
||||||
|
["delete"] = OperationType.Delete,
|
||||||
|
["head"] = OperationType.Head,
|
||||||
|
["options"] = OperationType.Options,
|
||||||
|
["trace"] = OperationType.Trace
|
||||||
|
};
|
||||||
|
|
||||||
|
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||||
|
{
|
||||||
|
var json = TryLoadJson();
|
||||||
|
if (json == null) return;
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("tags", out var tagsEl) && tagsEl.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
swaggerDoc.Tags ??= new List<OpenApiTag>();
|
||||||
|
foreach (var tag in tagsEl.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!tag.TryGetProperty("name", out var nameEl)) continue;
|
||||||
|
var name = nameEl.GetString();
|
||||||
|
if (string.IsNullOrEmpty(name) || swaggerDoc.Tags.Any(t => t.Name == name)) continue;
|
||||||
|
var desc = tag.TryGetProperty("description", out var d) ? d.GetString() : null;
|
||||||
|
swaggerDoc.Tags.Add(new OpenApiTag { Name = name, Description = desc });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("paths", out var pathsEl)) return;
|
||||||
|
|
||||||
|
foreach (var pathProp in pathsEl.EnumerateObject())
|
||||||
|
{
|
||||||
|
var fullPath = pathProp.Name.StartsWith("/api/sl", StringComparison.Ordinal)
|
||||||
|
? pathProp.Name
|
||||||
|
: "/api/sl" + pathProp.Name;
|
||||||
|
|
||||||
|
var pathItem = new OpenApiPathItem();
|
||||||
|
foreach (var opProp in pathProp.Value.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!VerbMap.TryGetValue(opProp.Name, out var verb)) continue;
|
||||||
|
pathItem.Operations[verb] = ParseOperation(opProp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathItem.Operations.Count > 0)
|
||||||
|
swaggerDoc.Paths[fullPath] = pathItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenApiOperation ParseOperation(JsonElement el)
|
||||||
|
{
|
||||||
|
var op = new OpenApiOperation
|
||||||
|
{
|
||||||
|
Summary = el.TryGetProperty("summary", out var s) ? s.GetString() : null,
|
||||||
|
Description = el.TryGetProperty("description", out var d) ? d.GetString() : null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (el.TryGetProperty("tags", out var tags) && tags.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var t in tags.EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = t.GetString();
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
op.Tags.Add(new OpenApiTag { Name = name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.TryGetProperty("parameters", out var parameters) && parameters.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var p in parameters.EnumerateArray())
|
||||||
|
{
|
||||||
|
var param = new OpenApiParameter
|
||||||
|
{
|
||||||
|
Name = p.TryGetProperty("name", out var n) ? n.GetString() : null,
|
||||||
|
In = p.TryGetProperty("in", out var loc) ? ParameterLocationFrom(loc.GetString()) : null,
|
||||||
|
Required = p.TryGetProperty("required", out var req) && req.GetBoolean(),
|
||||||
|
Description = p.TryGetProperty("description", out var pd) ? pd.GetString() : null
|
||||||
|
};
|
||||||
|
if (p.TryGetProperty("schema", out var schema))
|
||||||
|
param.Schema = ParseSchema(schema);
|
||||||
|
op.Parameters.Add(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.TryGetProperty("requestBody", out var body))
|
||||||
|
op.RequestBody = ParseRequestBody(body);
|
||||||
|
|
||||||
|
if (el.TryGetProperty("responses", out var responses))
|
||||||
|
{
|
||||||
|
foreach (var resp in responses.EnumerateObject())
|
||||||
|
{
|
||||||
|
op.Responses[resp.Name] = new OpenApiResponse
|
||||||
|
{
|
||||||
|
Description = resp.Value.TryGetProperty("description", out var rd)
|
||||||
|
? rd.GetString() ?? ""
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenApiRequestBody? ParseRequestBody(JsonElement el)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty("content", out var content)) return null;
|
||||||
|
var body = new OpenApiRequestBody();
|
||||||
|
foreach (var ct in content.EnumerateObject())
|
||||||
|
{
|
||||||
|
var media = new OpenApiMediaType();
|
||||||
|
if (ct.Value.TryGetProperty("schema", out var schema))
|
||||||
|
media.Schema = ParseSchema(schema);
|
||||||
|
body.Content[ct.Name] = media;
|
||||||
|
}
|
||||||
|
return body.Content.Count > 0 ? body : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OpenApiSchema ParseSchema(JsonElement el)
|
||||||
|
{
|
||||||
|
var schema = new OpenApiSchema();
|
||||||
|
if (el.TryGetProperty("type", out var t)) schema.Type = t.GetString();
|
||||||
|
if (el.TryGetProperty("description", out var d)) schema.Description = d.GetString();
|
||||||
|
if (el.TryGetProperty("$ref", out var r))
|
||||||
|
{
|
||||||
|
var refId = r.GetString()?.TrimStart('#', '/');
|
||||||
|
if (!string.IsNullOrEmpty(refId))
|
||||||
|
schema.Reference = new OpenApiReference { Id = refId, Type = ReferenceType.Schema };
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParameterLocation? ParameterLocationFrom(string? loc) => loc switch
|
||||||
|
{
|
||||||
|
"query" => ParameterLocation.Query,
|
||||||
|
"path" => ParameterLocation.Path,
|
||||||
|
"header" => ParameterLocation.Header,
|
||||||
|
"cookie" => ParameterLocation.Cookie,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? TryLoadJson()
|
||||||
|
{
|
||||||
|
foreach (var candidate in ResolveCandidatePaths())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
return File.ReadAllText(candidate);
|
||||||
|
}
|
||||||
|
catch { /* next */ }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ResolveCandidatePaths()
|
||||||
|
{
|
||||||
|
var roots = new[] { AppContext.BaseDirectory, Directory.GetCurrentDirectory() }
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var root in roots)
|
||||||
|
{
|
||||||
|
yield return Path.GetFullPath(Path.Combine(root, "OpenApi", "simplelite-projection.json"));
|
||||||
|
yield return Path.GetFullPath(Path.Combine(root, "..", "..", "..", "Simple", "SimpleLite", "Docs", "openapi", "simplelite-projection.json"));
|
||||||
|
yield return Path.GetFullPath(Path.Combine(root, "..", "..", "Simple", "SimpleLite", "Docs", "openapi", "simplelite-projection.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models;
|
|||||||
using MiGu.Server.Auth;
|
using MiGu.Server.Auth;
|
||||||
using MiGu.Server.Configs;
|
using MiGu.Server.Configs;
|
||||||
using MiGu.Server.Launcher;
|
using MiGu.Server.Launcher;
|
||||||
|
using MiGu.Server.OpenApi;
|
||||||
using Yarp.ReverseProxy.Transforms;
|
using Yarp.ReverseProxy.Transforms;
|
||||||
|
|
||||||
static string? FindSourceContentRoot(string startDir)
|
static string? FindSourceContentRoot(string startDir)
|
||||||
@@ -97,7 +98,13 @@ builder.Services.AddControllers()
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddSwaggerGen(c =>
|
||||||
{
|
{
|
||||||
c.SwaggerDoc("v1", new() { Title = "MiGu.Server", Version = "v1", Description = "Simple-FR 平台后端骨架(含 YARP 反代 SimpleLite 8222)。" });
|
c.SwaggerDoc("v1", new()
|
||||||
|
{
|
||||||
|
Title = "MiGu.Server + SimpleLite",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "咪咕平台后端 API,以及经 YARP 反代的 SimpleLite 数据 WebApi(标签 SimpleLite/*)。详见 Simple/SimpleLite/Docs/MIGU-API.md。"
|
||||||
|
});
|
||||||
|
c.DocumentFilter<SimpleLiteOpenApiDocumentFilter>();
|
||||||
// Swagger 里挂 Bearer 输入框,便于手工测带鉴权的端点。
|
// Swagger 里挂 Bearer 输入框,便于手工测带鉴权的端点。
|
||||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user