Переработанная версия ядра
All checks were successful
CI / build-test (push) Successful in 42s

This commit is contained in:
2025-12-05 12:57:05 +03:00
parent ee175a35a0
commit d817417a69
81 changed files with 2335 additions and 1453 deletions

View File

@@ -0,0 +1,59 @@
namespace BotPages.Core.Routing;
using System.Text.RegularExpressions;
/// <summary>
/// Реестр команд, доступных из любого места.
/// </summary>
internal sealed class CommandsRegistry
{
private readonly List<(Regex pattern, Func<PageContext, CancellationToken, Task> handler)> _commands = new();
/// <summary>
/// Зарегистрировать команду, ведущую на страницу.
/// </summary>
public CommandsRegistry Map<TPage>(string commandTemplate) where TPage : Page
{
var pattern = ToRegex(commandTemplate);
_commands.Add((pattern, (ctx, ct) => ctx.Navigation.GoToAsync<TPage>(ctx, ct)));
return this;
}
/// <summary>
/// Зарегистрировать команду с кастомным обработчиком.
/// </summary>
public CommandsRegistry Map(string commandTemplate, Func<PageContext, CancellationToken, Task> handler)
{
var pattern = ToRegex(commandTemplate);
_commands.Add((pattern, handler));
return this;
}
/// <summary>
/// Попробовать выполнить команду.
/// </summary>
public bool TryDispatch(PageContext ctx, string command, CancellationToken ct, out Task? task)
{
foreach (var (pattern, handler) in _commands)
{
if (pattern.IsMatch(command))
{
task = handler(ctx, ct);
return true;
}
}
task = null;
return false;
}
private static Regex ToRegex(string template)
{
// Простейшее преобразование шаблона: "/open {page} {id?}" -> Regex
var escaped = Regex.Escape(template)
.Replace("\\{", "{").Replace("\\}", "}");
var pattern = "^" + escaped
.Replace("{page}", "(?<page>\\S+)")
.Replace("{id?}", "(?<id>\\S+)?") + "$";
return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
}

View File

@@ -0,0 +1,18 @@
namespace BotPages.Core.Routing;
/// <summary>
/// Атрибут для декларативного указания маршрута страницы.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class RouteAttribute : Attribute
{
/// <summary>
/// Шаблон маршрута.
/// </summary>
public string Template { get; }
/// <summary>
/// Создать атрибут маршрута.
/// </summary>
public RouteAttribute(string template) => Template = template;
}

View File

@@ -0,0 +1,29 @@
namespace BotPages.Core.Routing;
/// <summary>
/// Реестр маршрутов страниц.
/// </summary>
internal sealed class RoutesRegistry
{
private readonly Dictionary<string, Type> _routes = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Зарегистрировать маршрут для страницы.
/// </summary>
public void Map<TPage>(string template) where TPage : Page
{
if (_routes.ContainsKey(template))
throw new InvalidOperationException($"Route '{template}' is already mapped.");
_routes[template] = typeof(TPage);
}
/// <summary>
/// Найти страницу по маршруту.
/// </summary>
public Type? Resolve(string template) => _routes.TryGetValue(template, out var t) ? t : null;
/// <summary>
/// Получить снимок всех маршрутов.
/// </summary>
public IReadOnlyDictionary<string, Type> Snapshot() => _routes;
}