docs(swagger): 合并展示 SimpleLite 投影 WebApi 接口
通过 SimpleLiteOpenApiDocumentFilter 将 SimpleLite OpenAPI 描述注入 Swagger,开发环境可在 /swagger 统一浏览 /api/sl/projection 全部端点。
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
||||
</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.Configs;
|
||||
using MiGu.Server.Launcher;
|
||||
using MiGu.Server.OpenApi;
|
||||
using Yarp.ReverseProxy.Transforms;
|
||||
|
||||
static string? FindSourceContentRoot(string startDir)
|
||||
@@ -97,7 +98,13 @@ builder.Services.AddControllers()
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
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 输入框,便于手工测带鉴权的端点。
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user