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
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user