using System.Globalization;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace StellaOps.Router.Gateway.OpenApi;
///
/// Endpoints for serving OpenAPI documentation.
///
public static class OpenApiEndpoints
{
private static readonly ISerializer YamlSerializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
///
/// Maps OpenAPI endpoints to the application.
///
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