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}" };
}
}