up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 18:08:55 +02:00
parent 6e45066e37
commit f1a39c4ce3
234 changed files with 24038 additions and 6910 deletions

View File

@@ -0,0 +1,285 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Models;
namespace StellaOps.Router.Gateway.OpenApi;
/// <summary>
/// Generates OpenAPI 3.1.0 documents from aggregated microservice schemas.
/// </summary>
internal sealed class OpenApiDocumentGenerator : IOpenApiDocumentGenerator
{
private readonly IGlobalRoutingState _routingState;
private readonly OpenApiAggregationOptions _options;
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
public OpenApiDocumentGenerator(
IGlobalRoutingState routingState,
IOptions<OpenApiAggregationOptions> options)
{
_routingState = routingState;
_options = options.Value;
}
/// <inheritdoc />
public string GenerateDocument()
{
var connections = _routingState.GetAllConnections();
var doc = new JsonObject
{
["openapi"] = "3.1.0",
["info"] = GenerateInfo(),
["servers"] = GenerateServers(),
["paths"] = GeneratePaths(connections),
["components"] = GenerateComponents(connections),
["tags"] = GenerateTags(connections)
};
return doc.ToJsonString(JsonOptions);
}
private JsonObject GenerateInfo()
{
var info = new JsonObject
{
["title"] = _options.Title,
["version"] = _options.Version,
["description"] = _options.Description,
["license"] = new JsonObject
{
["name"] = _options.LicenseName
}
};
if (_options.ContactName is not null || _options.ContactEmail is not null)
{
var contact = new JsonObject();
if (_options.ContactName is not null)
contact["name"] = _options.ContactName;
if (_options.ContactEmail is not null)
contact["email"] = _options.ContactEmail;
info["contact"] = contact;
}
return info;
}
private JsonArray GenerateServers()
{
return new JsonArray
{
new JsonObject
{
["url"] = _options.ServerUrl
}
};
}
private JsonObject GeneratePaths(IReadOnlyList<ConnectionState> connections)
{
var paths = new JsonObject();
// Group endpoints by path
var pathGroups = new Dictionary<string, List<(ConnectionState Conn, EndpointDescriptor Endpoint)>>();
foreach (var conn in connections)
{
foreach (var endpoint in conn.Endpoints.Values)
{
if (!pathGroups.TryGetValue(endpoint.Path, out var list))
{
list = [];
pathGroups[endpoint.Path] = list;
}
list.Add((conn, endpoint));
}
}
// Generate path items
foreach (var (path, endpoints) in pathGroups.OrderBy(p => p.Key))
{
var pathItem = new JsonObject();
foreach (var (conn, endpoint) in endpoints)
{
var operation = GenerateOperation(conn, endpoint);
var method = endpoint.Method.ToLowerInvariant();
pathItem[method] = operation;
}
paths[path] = pathItem;
}
return paths;
}
private JsonObject GenerateOperation(ConnectionState conn, EndpointDescriptor endpoint)
{
var operation = new JsonObject
{
["operationId"] = $"{conn.Instance.ServiceName}_{endpoint.Path.Replace("/", "_").Trim('_')}_{endpoint.Method}",
["tags"] = new JsonArray { conn.Instance.ServiceName }
};
// Add documentation from SchemaInfo
if (endpoint.SchemaInfo is not null)
{
if (endpoint.SchemaInfo.Summary is not null)
operation["summary"] = endpoint.SchemaInfo.Summary;
if (endpoint.SchemaInfo.Description is not null)
operation["description"] = endpoint.SchemaInfo.Description;
if (endpoint.SchemaInfo.Deprecated)
operation["deprecated"] = true;
// Override tags if specified
if (endpoint.SchemaInfo.Tags.Count > 0)
{
var tags = new JsonArray();
foreach (var tag in endpoint.SchemaInfo.Tags)
{
tags.Add(tag);
}
operation["tags"] = tags;
}
}
// Add security requirements
var security = ClaimSecurityMapper.GenerateSecurityRequirement(endpoint);
if (security.Count > 0)
{
operation["security"] = security;
}
// Add request body if schema exists
if (endpoint.SchemaInfo?.RequestSchemaId is not null)
{
var schemaRef = $"#/components/schemas/{conn.Instance.ServiceName}_{endpoint.SchemaInfo.RequestSchemaId}";
operation["requestBody"] = new JsonObject
{
["required"] = true,
["content"] = new JsonObject
{
["application/json"] = new JsonObject
{
["schema"] = new JsonObject
{
["$ref"] = schemaRef
}
}
}
};
}
// Add responses
var responses = new JsonObject();
// Success response
var successResponse = new JsonObject
{
["description"] = "Success"
};
if (endpoint.SchemaInfo?.ResponseSchemaId is not null)
{
var schemaRef = $"#/components/schemas/{conn.Instance.ServiceName}_{endpoint.SchemaInfo.ResponseSchemaId}";
successResponse["content"] = new JsonObject
{
["application/json"] = new JsonObject
{
["schema"] = new JsonObject
{
["$ref"] = schemaRef
}
}
};
}
responses["200"] = successResponse;
// Error responses
responses["400"] = new JsonObject { ["description"] = "Bad Request" };
responses["401"] = new JsonObject { ["description"] = "Unauthorized" };
responses["404"] = new JsonObject { ["description"] = "Not Found" };
responses["422"] = new JsonObject { ["description"] = "Validation Error" };
responses["500"] = new JsonObject { ["description"] = "Internal Server Error" };
operation["responses"] = responses;
return operation;
}
private JsonObject GenerateComponents(IReadOnlyList<ConnectionState> connections)
{
var components = new JsonObject();
// Generate schemas with service prefix
var schemas = new JsonObject();
foreach (var conn in connections)
{
foreach (var (schemaId, schemaDef) in conn.Schemas)
{
var prefixedId = $"{conn.Instance.ServiceName}_{schemaId}";
try
{
var schemaNode = JsonNode.Parse(schemaDef.SchemaJson);
if (schemaNode is not null)
{
schemas[prefixedId] = schemaNode;
}
}
catch (JsonException)
{
// Skip invalid schemas
}
}
}
if (schemas.Count > 0)
{
components["schemas"] = schemas;
}
// Generate security schemes
var allEndpoints = connections.SelectMany(c => c.Endpoints.Values);
var securitySchemes = ClaimSecurityMapper.GenerateSecuritySchemes(allEndpoints, _options.TokenUrl);
if (securitySchemes.Count > 0)
{
components["securitySchemes"] = securitySchemes;
}
return components;
}
private JsonArray GenerateTags(IReadOnlyList<ConnectionState> connections)
{
var tags = new JsonArray();
var seen = new HashSet<string>();
foreach (var conn in connections)
{
var serviceName = conn.Instance.ServiceName;
if (seen.Add(serviceName))
{
var tag = new JsonObject
{
["name"] = serviceName,
["description"] = $"{serviceName} microservice (v{conn.Instance.Version})"
};
if (conn.OpenApiInfo?.Description is not null)
{
tag["description"] = conn.OpenApiInfo.Description;
}
tags.Add(tag);
}
}
return tags;
}
}