using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; namespace GB5Shared.Swagger; /// /// Groups every Swagger operation under its owning module by matching the assembly-name /// prefix baked into NSwag's generated OperationId (e.g. "DevadminSLEndPointsReplicateGetReplicate"), /// with a route-path fallback for non-FastEndpoints actions (e.g. Dapr [Topic] subscriber controllers) /// whose OperationId doesn't carry that prefix. /// /// Stashes the resolved module name in the operation's ExtensionData under - /// read back by to decide whether this operation belongs in a /// given module's document - and sets the VISIBLE tag to "{Module}.{Feature}" (Feature = the next /// path segment after the module, e.g. "MM.BOM", "MM.Scheduling") so Swagger UI groups a module's /// page into per-feature sections instead of one flat list of every endpoint in that module. /// public sealed class ModuleTagProcessor : IOperationProcessor { /// ExtensionData key under which the owning module name is stashed for ModuleScopedProcessor. public const string ModuleKey = "x-gb5-module"; private readonly Dictionary _moduleByAssembly; private readonly Dictionary _pathAliases; public ModuleTagProcessor(Dictionary moduleByAssembly, Dictionary? pathAliases = null) { _moduleByAssembly = moduleByAssembly; _pathAliases = pathAliases ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } public bool Process(OperationProcessorContext context) { var operationId = context.OperationDescription.Operation.OperationId ?? string.Empty; var path = context.OperationDescription.Path?.Trim('/') ?? string.Empty; var segments = path.Split('/'); var firstSegment = segments.Length > 0 ? segments[0] : string.Empty; foreach (var (assemblyName, moduleName) in _moduleByAssembly) { if (operationId.StartsWith(assemblyName, StringComparison.Ordinal)) { Assign(context, moduleName, firstSegment); return true; } } foreach (var moduleName in _moduleByAssembly.Values) { if (string.Equals(firstSegment, moduleName, StringComparison.OrdinalIgnoreCase)) { // The module IS the first segment here (e.g. Dapr [Topic] routes with no feature // sub-path), so group by the next segment instead, if there is one. var feature = segments.Length > 1 ? segments[1] : firstSegment; Assign(context, moduleName, feature); return true; } } if (_pathAliases.TryGetValue(firstSegment, out var aliasedModule)) Assign(context, aliasedModule, firstSegment); return true; } private static void Assign(OperationProcessorContext context, string moduleName, string featureSegment) { var operation = context.OperationDescription.Operation; operation.ExtensionData ??= new Dictionary(); operation.ExtensionData[ModuleKey] = moduleName; var feature = string.IsNullOrWhiteSpace(featureSegment) ? moduleName : featureSegment; operation.Tags = new List { $"{moduleName}.{feature}" }; } }