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; /// /// Generates OpenAPI 3.1.0 documents from aggregated microservice schemas. /// public 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 options) { _routingState = routingState; _options = options.Value; } /// 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 connections) { var paths = new JsonObject(); // Group endpoints by path var pathGroups = new Dictionary>(); 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 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 connections) { var tags = new JsonArray(); var seen = new HashSet(); 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; } }