work
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds attestation templates and default routing for dev/test/bootstrap scenarios.
|
||||
/// </summary>
|
||||
public sealed class AttestationTemplateSeeder : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly ILogger<AttestationTemplateSeeder> _logger;
|
||||
|
||||
public AttestationTemplateSeeder(IServiceProvider services, IHostEnvironment environment, ILogger<AttestationTemplateSeeder> logger)
|
||||
{
|
||||
_services = services ?? throw new ArgumentNullException(nameof(services));
|
||||
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var templateRepo = scope.ServiceProvider.GetService<INotifyTemplateRepository>();
|
||||
var channelRepo = scope.ServiceProvider.GetService<INotifyChannelRepository>();
|
||||
var ruleRepo = scope.ServiceProvider.GetService<INotifyRuleRepository>();
|
||||
|
||||
if (templateRepo is null)
|
||||
{
|
||||
_logger.LogWarning("Template repository not registered; skipping attestation template seed.");
|
||||
return;
|
||||
}
|
||||
|
||||
var contentRoot = _environment.ContentRootPath;
|
||||
var templatesSeeded = await SeedTemplatesAsync(templateRepo, contentRoot, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (templatesSeeded > 0)
|
||||
{
|
||||
_logger.LogInformation("Seeded {TemplateCount} attestation templates from offline bundle.", templatesSeeded);
|
||||
}
|
||||
|
||||
if (channelRepo is null || ruleRepo is null)
|
||||
{
|
||||
_logger.LogWarning("Channel or rule repository not registered; skipping attestation routing seed.");
|
||||
return;
|
||||
}
|
||||
|
||||
var routingSeeded = await SeedRoutingAsync(channelRepo, ruleRepo, contentRoot, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (routingSeeded > 0)
|
||||
{
|
||||
_logger.LogInformation("Seeded default attestation routing (channels + rules).");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public static async Task<int> SeedTemplatesAsync(
|
||||
INotifyTemplateRepository repository,
|
||||
string contentRootPath,
|
||||
ILogger logger,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repository);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var templateDir = LocateAttestationTemplatesPath(contentRootPath);
|
||||
if (templateDir is null)
|
||||
{
|
||||
logger.LogWarning("Attestation templates directory not found under {ContentRoot}; skipping seed.", contentRootPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(templateDir, "*.template.json", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
try
|
||||
{
|
||||
var template = await ToTemplateAsync(file, cancellationToken).ConfigureAwait(false);
|
||||
await repository.UpsertAsync(template, cancellationToken).ConfigureAwait(false);
|
||||
count++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to seed template from {File}.", file);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public static async Task<int> SeedRoutingAsync(
|
||||
INotifyChannelRepository channelRepository,
|
||||
INotifyRuleRepository ruleRepository,
|
||||
string contentRootPath,
|
||||
ILogger logger,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(channelRepository);
|
||||
ArgumentNullException.ThrowIfNull(ruleRepository);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var samplePath = LocateAttestationRulesPath(contentRootPath);
|
||||
if (samplePath is null)
|
||||
{
|
||||
logger.LogWarning("Attestation rules sample not found under {ContentRoot}; skipping routing seed.", contentRootPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(samplePath);
|
||||
var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var tenant = "bootstrap";
|
||||
var channelsElement = doc.RootElement.GetProperty("channels");
|
||||
var rulesElement = doc.RootElement.GetProperty("rules");
|
||||
|
||||
var channels = channelsElement.EnumerateArray()
|
||||
.Select(ToChannel)
|
||||
.ToArray();
|
||||
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
await channelRepository.UpsertAsync(channel with { TenantId = tenant }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var rule in rulesElement.EnumerateArray())
|
||||
{
|
||||
var model = ToRule(rule, tenant);
|
||||
await ruleRepository.UpsertAsync(model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return channels.Length + rulesElement.GetArrayLength();
|
||||
}
|
||||
|
||||
private static NotifyRule ToRule(JsonElement element, string tenant)
|
||||
{
|
||||
var ruleId = element.GetProperty("ruleId").GetString() ?? throw new InvalidOperationException("ruleId missing");
|
||||
var name = element.GetProperty("name").GetString() ?? ruleId;
|
||||
var enabled = element.GetProperty("enabled").GetBoolean();
|
||||
var matchKinds = element.GetProperty("match").GetProperty("eventKinds").EnumerateArray().Select(p => p.GetString() ?? string.Empty).ToArray();
|
||||
|
||||
var actions = element.GetProperty("actions").EnumerateArray().Select(action =>
|
||||
NotifyRuleAction.Create(
|
||||
actionId: action.GetProperty("actionId").GetString() ?? throw new InvalidOperationException("actionId missing"),
|
||||
channel: action.GetProperty("channel").GetString() ?? string.Empty,
|
||||
template: action.GetProperty("template").GetString() ?? string.Empty,
|
||||
enabled: action.TryGetProperty("enabled", out var en) ? en.GetBoolean() : true)).ToArray();
|
||||
|
||||
return NotifyRule.Create(
|
||||
ruleId: ruleId,
|
||||
tenantId: tenant,
|
||||
name: name,
|
||||
match: NotifyRuleMatch.Create(eventKinds: matchKinds),
|
||||
actions: actions,
|
||||
enabled: enabled,
|
||||
description: "Seeded attestation routing rule.");
|
||||
}
|
||||
|
||||
private static NotifyChannel ToChannel(JsonElement element)
|
||||
{
|
||||
var channelId = element.GetProperty("channelId").GetString() ?? throw new InvalidOperationException("channelId missing");
|
||||
var type = ParseEnum<NotifyChannelType>(element.GetProperty("type").GetString(), NotifyChannelType.Custom);
|
||||
var name = element.GetProperty("name").GetString() ?? channelId;
|
||||
var target = element.TryGetProperty("target", out var t) ? t.GetString() : null;
|
||||
var endpoint = element.TryGetProperty("endpoint", out var e) ? e.GetString() : null;
|
||||
var secretRef = element.GetProperty("secretRef").GetString() ?? string.Empty;
|
||||
|
||||
var config = NotifyChannelConfig.Create(
|
||||
secretRef: secretRef,
|
||||
endpoint: endpoint,
|
||||
target: target);
|
||||
|
||||
return NotifyChannel.Create(
|
||||
channelId: channelId,
|
||||
tenantId: element.GetProperty("tenantId").GetString() ?? "bootstrap",
|
||||
name: name,
|
||||
type: type,
|
||||
config: config,
|
||||
description: element.TryGetProperty("description", out var d) ? d.GetString() : null);
|
||||
}
|
||||
|
||||
private static async Task<NotifyTemplate> ToTemplateAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var templateId = root.GetProperty("templateId").GetString() ?? Path.GetFileNameWithoutExtension(path);
|
||||
var tenantId = root.GetProperty("tenantId").GetString() ?? "bootstrap";
|
||||
var channelType = ParseEnum<NotifyChannelType>(root.GetProperty("channelType").GetString(), NotifyChannelType.Custom);
|
||||
var key = root.GetProperty("key").GetString() ?? throw new InvalidOperationException("key missing");
|
||||
var locale = root.GetProperty("locale").GetString() ?? "en-US";
|
||||
var renderMode = ParseEnum<NotifyTemplateRenderMode>(root.GetProperty("renderMode").GetString(), NotifyTemplateRenderMode.Markdown);
|
||||
var format = ParseEnum<NotifyDeliveryFormat>(root.GetProperty("format").GetString(), NotifyDeliveryFormat.Json);
|
||||
var description = root.TryGetProperty("description", out var desc) ? desc.GetString() : null;
|
||||
var body = root.GetProperty("body").GetString() ?? string.Empty;
|
||||
|
||||
var metadata = Enumerable.Empty<KeyValuePair<string, string>>();
|
||||
if (root.TryGetProperty("metadata", out var meta) && meta.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
metadata = meta.EnumerateObject().Select(p => new KeyValuePair<string, string>(p.Name, p.Value.GetString() ?? string.Empty));
|
||||
}
|
||||
|
||||
return NotifyTemplate.Create(
|
||||
templateId: templateId,
|
||||
tenantId: tenantId,
|
||||
channelType: channelType,
|
||||
key: key,
|
||||
locale: locale,
|
||||
body: body,
|
||||
renderMode: renderMode,
|
||||
format: format,
|
||||
description: description,
|
||||
metadata: metadata,
|
||||
createdBy: "seed:attestation");
|
||||
}
|
||||
|
||||
private static string? LocateAttestationTemplatesPath(string contentRootPath)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(contentRootPath, "offline", "notifier", "templates", "attestation"),
|
||||
Path.Combine(contentRootPath, "..", "offline", "notifier", "templates", "attestation")
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(Directory.Exists);
|
||||
}
|
||||
|
||||
private static string? LocateAttestationRulesPath(string contentRootPath)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(contentRootPath, "StellaOps.Notifier.docs", "attestation-rules.sample.json"),
|
||||
Path.Combine(contentRootPath, "..", "StellaOps.Notifier.docs", "attestation-rules.sample.json"),
|
||||
Path.Combine(contentRootPath, "src", "Notifier", "StellaOps.Notifier", "docs", "attestation-rules.sample.json")
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
private static TEnum ParseEnum<TEnum>(string? value, TEnum fallback) where TEnum : struct
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value) && Enum.TryParse<TEnum>(value, ignoreCase: true, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Xml;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds risk templates and default routing for dev/test/bootstrap scenarios.
|
||||
/// </summary>
|
||||
public sealed class RiskTemplateSeeder : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly ILogger<RiskTemplateSeeder> _logger;
|
||||
|
||||
public RiskTemplateSeeder(IServiceProvider services, IHostEnvironment environment, ILogger<RiskTemplateSeeder> logger)
|
||||
{
|
||||
_services = services ?? throw new ArgumentNullException(nameof(services));
|
||||
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var templateRepo = scope.ServiceProvider.GetService<INotifyTemplateRepository>();
|
||||
var channelRepo = scope.ServiceProvider.GetService<INotifyChannelRepository>();
|
||||
var ruleRepo = scope.ServiceProvider.GetService<INotifyRuleRepository>();
|
||||
|
||||
if (templateRepo is null)
|
||||
{
|
||||
_logger.LogWarning("Template repository not registered; skipping risk template seed.");
|
||||
return;
|
||||
}
|
||||
|
||||
var contentRoot = _environment.ContentRootPath;
|
||||
var templatesSeeded = await SeedTemplatesAsync(templateRepo, contentRoot, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (templatesSeeded > 0)
|
||||
{
|
||||
_logger.LogInformation("Seeded {TemplateCount} risk templates from offline bundle.", templatesSeeded);
|
||||
}
|
||||
|
||||
if (channelRepo is null || ruleRepo is null)
|
||||
{
|
||||
_logger.LogWarning("Channel or rule repository not registered; skipping risk routing seed.");
|
||||
return;
|
||||
}
|
||||
|
||||
var routingSeeded = await SeedRoutingAsync(channelRepo, ruleRepo, contentRoot, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (routingSeeded > 0)
|
||||
{
|
||||
_logger.LogInformation("Seeded default risk routing (channels + rules).");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public static async Task<int> SeedTemplatesAsync(
|
||||
INotifyTemplateRepository repository,
|
||||
string contentRootPath,
|
||||
ILogger logger,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(repository);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var templateDir = LocateRiskTemplatesPath(contentRootPath);
|
||||
if (templateDir is null)
|
||||
{
|
||||
logger.LogWarning("Risk templates directory not found under {ContentRoot}; skipping seed.", contentRootPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(templateDir, "*.template.json", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
try
|
||||
{
|
||||
var template = await ToTemplateAsync(file, cancellationToken).ConfigureAwait(false);
|
||||
await repository.UpsertAsync(template, cancellationToken).ConfigureAwait(false);
|
||||
count++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to seed template from {File}.", file);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public static async Task<int> SeedRoutingAsync(
|
||||
INotifyChannelRepository channelRepository,
|
||||
INotifyRuleRepository ruleRepository,
|
||||
string contentRootPath,
|
||||
ILogger logger,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(channelRepository);
|
||||
ArgumentNullException.ThrowIfNull(ruleRepository);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
var samplePath = LocateRiskRulesPath(contentRootPath);
|
||||
if (samplePath is null)
|
||||
{
|
||||
logger.LogWarning("Risk rules sample not found under {ContentRoot}; skipping routing seed.", contentRootPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
await using var stream = File.OpenRead(samplePath);
|
||||
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var tenant = "bootstrap";
|
||||
var channelsElement = doc.RootElement.GetProperty("channels");
|
||||
var rulesElement = doc.RootElement.GetProperty("rules");
|
||||
|
||||
var channels = channelsElement.EnumerateArray()
|
||||
.Select(ToChannel)
|
||||
.ToArray();
|
||||
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
await channelRepository.UpsertAsync(channel with { TenantId = tenant }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var rule in rulesElement.EnumerateArray())
|
||||
{
|
||||
var model = ToRule(rule, tenant);
|
||||
await ruleRepository.UpsertAsync(model, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return channels.Length + rulesElement.GetArrayLength();
|
||||
}
|
||||
|
||||
private static NotifyRule ToRule(JsonElement element, string tenant)
|
||||
{
|
||||
var ruleId = element.GetProperty("ruleId").GetString() ?? throw new InvalidOperationException("ruleId missing");
|
||||
var name = element.GetProperty("name").GetString() ?? ruleId;
|
||||
var enabled = element.TryGetProperty("enabled", out var en) ? en.GetBoolean() : true;
|
||||
var matchKinds = element.GetProperty("match").GetProperty("eventKinds").EnumerateArray().Select(p => p.GetString() ?? string.Empty).ToArray();
|
||||
|
||||
var actions = element.GetProperty("actions").EnumerateArray().Select(action =>
|
||||
NotifyRuleAction.Create(
|
||||
actionId: action.GetProperty("actionId").GetString() ?? throw new InvalidOperationException("actionId missing"),
|
||||
channel: action.GetProperty("channel").GetString() ?? string.Empty,
|
||||
template: action.GetProperty("template").GetString() ?? string.Empty,
|
||||
locale: action.TryGetProperty("locale", out var loc) ? loc.GetString() : null,
|
||||
throttle: action.TryGetProperty("throttle", out var throttle) ? XmlConvert.ToTimeSpan(throttle.GetString() ?? string.Empty) : default,
|
||||
enabled: action.TryGetProperty("enabled", out var en) ? en.GetBoolean() : true)).ToArray();
|
||||
|
||||
return NotifyRule.Create(
|
||||
ruleId: ruleId,
|
||||
tenantId: tenant,
|
||||
name: name,
|
||||
match: NotifyRuleMatch.Create(eventKinds: matchKinds),
|
||||
actions: actions,
|
||||
enabled: enabled,
|
||||
description: "Seeded risk routing rule.");
|
||||
}
|
||||
|
||||
private static NotifyChannel ToChannel(JsonElement element)
|
||||
{
|
||||
var channelId = element.GetProperty("channelId").GetString() ?? throw new InvalidOperationException("channelId missing");
|
||||
var type = ParseEnum<NotifyChannelType>(element.GetProperty("type").GetString(), NotifyChannelType.Custom);
|
||||
var name = element.GetProperty("name").GetString() ?? channelId;
|
||||
var target = element.TryGetProperty("target", out var t) ? t.GetString() : null;
|
||||
var endpoint = element.TryGetProperty("endpoint", out var e) ? e.GetString() : null;
|
||||
var secretRef = element.GetProperty("secretRef").GetString() ?? string.Empty;
|
||||
|
||||
var config = NotifyChannelConfig.Create(
|
||||
secretRef: secretRef,
|
||||
endpoint: endpoint,
|
||||
target: target);
|
||||
|
||||
return NotifyChannel.Create(
|
||||
channelId: channelId,
|
||||
tenantId: element.GetProperty("tenantId").GetString() ?? "bootstrap",
|
||||
name: name,
|
||||
type: type,
|
||||
config: config,
|
||||
description: element.TryGetProperty("description", out var d) ? d.GetString() : null);
|
||||
}
|
||||
|
||||
private static async Task<NotifyTemplate> ToTemplateAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var templateId = root.GetProperty("templateId").GetString() ?? Path.GetFileNameWithoutExtension(path);
|
||||
var tenantId = root.GetProperty("tenantId").GetString() ?? "bootstrap";
|
||||
var channelType = ParseEnum<NotifyChannelType>(root.GetProperty("channelType").GetString(), NotifyChannelType.Custom);
|
||||
var key = root.GetProperty("key").GetString() ?? throw new InvalidOperationException("key missing");
|
||||
var locale = root.GetProperty("locale").GetString() ?? "en-US";
|
||||
var renderMode = ParseEnum<NotifyTemplateRenderMode>(root.GetProperty("renderMode").GetString(), NotifyTemplateRenderMode.Markdown);
|
||||
var format = ParseEnum<NotifyDeliveryFormat>(root.GetProperty("format").GetString(), NotifyDeliveryFormat.Json);
|
||||
var description = root.TryGetProperty("description", out var desc) ? desc.GetString() : null;
|
||||
var body = root.GetProperty("body").GetString() ?? string.Empty;
|
||||
|
||||
var metadata = Enumerable.Empty<KeyValuePair<string, string>>();
|
||||
if (root.TryGetProperty("metadata", out var meta) && meta.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
metadata = meta.EnumerateObject().Select(p => new KeyValuePair<string, string>(p.Name, p.Value.GetString() ?? string.Empty));
|
||||
}
|
||||
|
||||
return NotifyTemplate.Create(
|
||||
templateId: templateId,
|
||||
tenantId: tenantId,
|
||||
channelType: channelType,
|
||||
key: key,
|
||||
locale: locale,
|
||||
body: body,
|
||||
renderMode: renderMode,
|
||||
format: format,
|
||||
description: description,
|
||||
metadata: metadata,
|
||||
createdBy: "seed:risk");
|
||||
}
|
||||
|
||||
private static string? LocateRiskTemplatesPath(string contentRootPath)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(contentRootPath, "offline", "notifier", "templates", "risk"),
|
||||
Path.Combine(contentRootPath, "..", "offline", "notifier", "templates", "risk")
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(Directory.Exists);
|
||||
}
|
||||
|
||||
private static string? LocateRiskRulesPath(string contentRootPath)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(contentRootPath, "StellaOps.Notifier.docs", "risk-rules.sample.json"),
|
||||
Path.Combine(contentRootPath, "..", "StellaOps.Notifier.docs", "risk-rules.sample.json"),
|
||||
Path.Combine(contentRootPath, "src", "Notifier", "StellaOps.Notifier", "StellaOps.Notifier.docs", "risk-rules.sample.json")
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
private static TEnum ParseEnum<TEnum>(string? value, TEnum fallback) where TEnum : struct
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value) && Enum.TryParse<TEnum>(value, ignoreCase: true, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user