124 lines
3.9 KiB
C#
124 lines
3.9 KiB
C#
using System.Globalization;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
namespace StellaOps.Router.Gateway.OpenApi;
|
|
|
|
/// <summary>
|
|
/// Endpoints for serving OpenAPI documentation.
|
|
/// </summary>
|
|
public static class OpenApiEndpoints
|
|
{
|
|
private static readonly ISerializer YamlSerializer = new SerializerBuilder()
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Maps OpenAPI endpoints to the application.
|
|
/// </summary>
|
|
public static IEndpointRouteBuilder MapRouterOpenApiEndpoints(this IEndpointRouteBuilder endpoints)
|
|
{
|
|
endpoints.MapGet("/.well-known/openapi", GetOpenApiDiscovery)
|
|
.ExcludeFromDescription();
|
|
|
|
endpoints.MapGet("/openapi.json", GetOpenApiJson)
|
|
.ExcludeFromDescription();
|
|
|
|
endpoints.MapGet("/openapi.yaml", GetOpenApiYaml)
|
|
.ExcludeFromDescription();
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
private static IResult GetOpenApiDiscovery(
|
|
[FromServices] IRouterOpenApiDocumentCache cache,
|
|
HttpContext context)
|
|
{
|
|
var (_, etag, generatedAt) = cache.GetDocument();
|
|
|
|
var discovery = new
|
|
{
|
|
openapi_json = "/openapi.json",
|
|
openapi_yaml = "/openapi.yaml",
|
|
etag,
|
|
generated_at = generatedAt.ToString("O", CultureInfo.InvariantCulture)
|
|
};
|
|
|
|
context.Response.Headers.CacheControl = "public, max-age=60";
|
|
return Results.Ok(discovery);
|
|
}
|
|
|
|
private static IResult GetOpenApiJson(
|
|
[FromServices] IRouterOpenApiDocumentCache cache,
|
|
HttpContext context)
|
|
{
|
|
var (documentJson, etag, _) = cache.GetDocument();
|
|
|
|
// Check If-None-Match header
|
|
if (context.Request.Headers.TryGetValue("If-None-Match", out var ifNoneMatch))
|
|
{
|
|
if (ifNoneMatch == etag)
|
|
{
|
|
context.Response.Headers.ETag = etag;
|
|
context.Response.Headers.CacheControl = "public, max-age=60";
|
|
return Results.StatusCode(304);
|
|
}
|
|
}
|
|
|
|
context.Response.Headers.ETag = etag;
|
|
context.Response.Headers.CacheControl = "public, max-age=60";
|
|
return Results.Content(documentJson, "application/json; charset=utf-8");
|
|
}
|
|
|
|
private static IResult GetOpenApiYaml(
|
|
[FromServices] IRouterOpenApiDocumentCache cache,
|
|
HttpContext context)
|
|
{
|
|
var (documentJson, etag, _) = cache.GetDocument();
|
|
|
|
// Check If-None-Match header
|
|
if (context.Request.Headers.TryGetValue("If-None-Match", out var ifNoneMatch))
|
|
{
|
|
if (ifNoneMatch == etag)
|
|
{
|
|
context.Response.Headers.ETag = etag;
|
|
context.Response.Headers.CacheControl = "public, max-age=60";
|
|
return Results.StatusCode(304);
|
|
}
|
|
}
|
|
|
|
// Convert JSON to YAML
|
|
var jsonNode = JsonNode.Parse(documentJson);
|
|
var yamlContent = ConvertToYaml(jsonNode);
|
|
|
|
context.Response.Headers.ETag = etag;
|
|
context.Response.Headers.CacheControl = "public, max-age=60";
|
|
return Results.Content(yamlContent, "application/yaml; charset=utf-8");
|
|
}
|
|
|
|
private static string ConvertToYaml(JsonNode? node)
|
|
{
|
|
if (node is null)
|
|
return string.Empty;
|
|
|
|
var obj = ConvertJsonNodeToObject(node);
|
|
return YamlSerializer.Serialize(obj);
|
|
}
|
|
|
|
private static object? ConvertJsonNodeToObject(JsonNode? node)
|
|
{
|
|
return node switch
|
|
{
|
|
null => null,
|
|
JsonObject obj => obj.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => ConvertJsonNodeToObject(kvp.Value)),
|
|
JsonArray arr => arr.Select(ConvertJsonNodeToObject).ToList(),
|
|
JsonValue val => val.GetValue<object>(),
|
|
_ => null
|
|
};
|
|
}
|
|
}
|