Глобальный рефакторинг пространств имён
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Абстрактный аттрибут обработчика.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
internal class AbstractProcessorAttribute(string name) : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Название обработчика.
|
||||
/// </summary>
|
||||
public string Name { get; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Описание обработчика.
|
||||
/// </summary>
|
||||
public string Description { get; init; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Группа обработчиков, к которому относится обработчик.
|
||||
/// </summary>
|
||||
public ProcessorGroupEnum Group { get; init; } = ProcessorGroupEnum.General;
|
||||
|
||||
/// <summary>
|
||||
/// Процессор включён по умолчанию.
|
||||
/// </summary>
|
||||
public bool Active { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Разрешить выполнять процессов в параллельном режиме
|
||||
/// </summary>
|
||||
public bool Parallel { get; init; } = false;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Общий аттрибут обработчика.
|
||||
/// </summary>
|
||||
/// <param name="name">Название обработчика.</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
internal class ProcessorAttribute(string name) : AbstractProcessorAttribute(name) { }
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Аттрибут обработчика набор элементов.
|
||||
/// </summary>
|
||||
/// <param name="name">Название обработчика.</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
internal class ProcessorCollectionAttribute(string name) : AbstractProcessorAttribute(name)
|
||||
{
|
||||
/// <summary>
|
||||
/// Название типов, который обрабатывает этот оброботчик.
|
||||
/// <c>null</c> - обработать все типы.
|
||||
/// </summary>
|
||||
public required string? InnerType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип используемого компендиума.
|
||||
/// </summary>
|
||||
public required ProcessorCompendiumEnum CompendiumType { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Аттрибут зависимостей обработчика.
|
||||
/// </summary>
|
||||
/// <param name="dependencies">Коллекция названий обработчиков, которые должны быть выполнены до этого оброботчика.</param>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ProcessorDependenciesAttribute(string[] dependencies) : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Коллекция названий обработчиков, которые должны быть выполнены до этого оброботчика.
|
||||
/// </summary>
|
||||
public ICollection<string> Dependencies { get; } = dependencies;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
internal class ProcessorFactoryAttribute(string name) : AbstractProcessorAttribute(name)
|
||||
{
|
||||
/// <summary>
|
||||
/// Тип обрабатывающего процессора
|
||||
/// </summary>
|
||||
public required Type ProcessorType { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Перечисление для групп обработчиков.
|
||||
/// </summary>
|
||||
public enum ProcessorGroupEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// Общий обработчик.
|
||||
/// </summary>
|
||||
[Description("Общие процессоры")]
|
||||
General,
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик для подготовки данных с сайта <see href="https://aonprd.com/">Archives of Nethys Pathfinder 2e</see>.
|
||||
/// </summary>
|
||||
[Description("Архивы Нефиса: Подготовка")]
|
||||
AonprdPreparation,
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик основных сущностей с сайта <see href="https://aonprd.com/">Archives of Nethys Pathfinder 2e</see>.
|
||||
/// </summary>
|
||||
[Description("Архивы Нефиса: Основные сущности")]
|
||||
AonprdEntities,
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик подклассов с сайта <see href="https://aonprd.com/">Archives of Nethys Pathfinder 2e</see>.
|
||||
/// </summary>
|
||||
[Description("Архивы Нефиса: Подклассы")]
|
||||
AonprdSubclasses,
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик для завершения обработки данных с сайта <see href="https://aonprd.com/">Archives of Nethys Pathfinder 2e</see>.
|
||||
/// </summary>
|
||||
[Description("Архивы Нефиса: Завершение")]
|
||||
AonprdFinalization,
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик для подготовки данных из модуля <see href="https://github.com/foundryvtt/pf2e">pf2e</see> для FoundryVTT.
|
||||
/// </summary>
|
||||
[Description("Foundry EN: Подготовка")]
|
||||
FoundryEnPreparation,
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик основных сущностей из модуля <see href="https://github.com/foundryvtt/pf2e">pf2e</see> для FoundryVTT.
|
||||
/// </summary>
|
||||
[Description("Foundry EN: Основные сущности")]
|
||||
FoundryEnEntities,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Перечисление для типов компендиумов обработчиков.
|
||||
/// </summary>
|
||||
public enum ProcessorCompendiumEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// Компедиум Архивов Нефиса.
|
||||
/// </summary>
|
||||
Aonprd,
|
||||
|
||||
/// <summary>
|
||||
/// Компедиум Foundry EN.
|
||||
/// </summary>
|
||||
FoundryEn,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Перечисление для типов фабрик обработчиков.
|
||||
/// </summary>
|
||||
public enum ProcessorFactoryTypeEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// Простая фабрика одиночного обработчика.
|
||||
/// </summary>
|
||||
Simple,
|
||||
|
||||
/// <summary>
|
||||
/// Фабрика для создания обработчиков, которые могут обрабатывать типы данных компедиума.
|
||||
/// </summary>
|
||||
Compendium,
|
||||
|
||||
/// <summary>
|
||||
/// Фабрика для создания обработчиков, которые могут обрабатывать итераторы данных.
|
||||
/// </summary>
|
||||
Iterator,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Расширение для перечисления <see cref="ProcessorGroupEnum"/>.
|
||||
/// </summary>
|
||||
public static class ProcessorGroupEnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает описание группы обработчика из атрибута <see cref="DescriptionAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="group">Перечисление группы обработчика.</param>
|
||||
/// <returns>Строка с описанием группы обработчика.</returns>
|
||||
public static string GetDescription(this ProcessorGroupEnum group)
|
||||
{
|
||||
return group
|
||||
.GetType()
|
||||
.GetField(group.ToString())
|
||||
?.GetCustomAttribute<DescriptionAttribute>()
|
||||
?.Description ?? group.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using LinqToDB;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Pf2e.Scrapper.Processors.Attributes;
|
||||
using Pf2e.Scrapper.Processors.Definitions;
|
||||
using Pf2e.Scrapper.Processors.Factory;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Collection;
|
||||
|
||||
/// <summary>
|
||||
/// Класс с коллекцией обработчиков.
|
||||
/// </summary>
|
||||
internal class ProcessorCollection
|
||||
{
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Коллекция всех доступных обработчиков.
|
||||
/// </summary>
|
||||
public ICollection<ProcessorRecord> AllItems { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Коллекция обработчиков, которые будут обрабатываться.
|
||||
/// Для изменения необходимо использовать <see cref="FilterProcessors"/>.
|
||||
/// </summary>
|
||||
public List<ProcessorRecord> Items { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Обеспечение доступа к элементам обработчиков.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public ProcessorRecord this[string name] => AllItems.First(item => item.Name == name);
|
||||
|
||||
/// <summary>
|
||||
/// Конструктор класса <see cref="ProcessorCollection"/>.
|
||||
/// </summary>
|
||||
public ProcessorCollection(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
foreach (Type type in assembly.GetTypes())
|
||||
{
|
||||
ProcessType(type, type.GetCustomAttribute<AbstractProcessorAttribute>());
|
||||
}
|
||||
|
||||
CheckDependencies();
|
||||
FilterProcessors([]);
|
||||
}
|
||||
|
||||
private void ProcessType(Type type, AbstractProcessorAttribute? attribute)
|
||||
{
|
||||
switch (attribute)
|
||||
{
|
||||
case null:
|
||||
break;
|
||||
|
||||
case ProcessorAttribute processorAttribute:
|
||||
ProcessType(type, processorAttribute);
|
||||
break;
|
||||
|
||||
case ProcessorCollectionAttribute processorCollectionAttribute:
|
||||
ProcessType(type, processorCollectionAttribute);
|
||||
break;
|
||||
|
||||
case ProcessorFactoryAttribute processorFactoryAttribute:
|
||||
ProcessType(type, processorFactoryAttribute);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new(
|
||||
$"Неизвестная реализация класса {typeof(AbstractProcessorAttribute)} - {attribute.GetType()}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessType(Type type, ProcessorAttribute? attribute)
|
||||
{
|
||||
if (attribute is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type.IsAssignableTo(typeof(IProcessor)))
|
||||
{
|
||||
throw new(
|
||||
$"Процессор {type}, которому установлен аттрибут {attribute.GetType()}, не представляет интерфейс {typeof(IProcessor)}"
|
||||
);
|
||||
}
|
||||
|
||||
ProcessorDependenciesAttribute? dependenciesAttribute =
|
||||
type.GetCustomAttribute<ProcessorDependenciesAttribute>();
|
||||
|
||||
ProcessorRecord item = new()
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Type = type,
|
||||
Description = attribute.Description,
|
||||
Group = attribute.Group,
|
||||
Dependencies = dependenciesAttribute?.Dependencies ?? [],
|
||||
Active = attribute.Active,
|
||||
Parallel = attribute.Parallel,
|
||||
};
|
||||
|
||||
item.Factory = ActivatorUtilities.CreateInstance<SimpleProcessorFactory>(
|
||||
ServiceProvider,
|
||||
item
|
||||
);
|
||||
|
||||
AllItems.Add(item);
|
||||
}
|
||||
|
||||
private void ProcessType(Type type, ProcessorCollectionAttribute? attribute)
|
||||
{
|
||||
if (attribute is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type.IsAssignableTo(typeof(AbstractInputProcessor)))
|
||||
{
|
||||
throw new(
|
||||
$"Процессор {type}, которому установлен аттрибут {attribute.GetType()}, не реализует класс {typeof(AbstractInputProcessor)}."
|
||||
);
|
||||
}
|
||||
|
||||
ProcessorDependenciesAttribute? dependenciesAttribute =
|
||||
type.GetCustomAttribute<ProcessorDependenciesAttribute>();
|
||||
|
||||
ProcessorRecord item = new()
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Type = type,
|
||||
Description = attribute.Description,
|
||||
Group = attribute.Group,
|
||||
Dependencies = dependenciesAttribute?.Dependencies ?? [],
|
||||
Active = attribute.Active,
|
||||
Parallel = attribute.Parallel,
|
||||
};
|
||||
|
||||
CompendiumProcessorFactory factory =
|
||||
ActivatorUtilities.CreateInstance<CompendiumProcessorFactory>(ServiceProvider, item);
|
||||
|
||||
factory.CompendiumType = attribute.CompendiumType;
|
||||
factory.InnerType = attribute.InnerType;
|
||||
|
||||
item.Factory = factory;
|
||||
|
||||
AllItems.Add(item);
|
||||
}
|
||||
|
||||
private void ProcessType(Type type, ProcessorFactoryAttribute? attribute)
|
||||
{
|
||||
if (attribute is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type.IsAssignableTo(typeof(AbstractProcessorFactory)))
|
||||
{
|
||||
throw new(
|
||||
$"Процессор {type}, которому установлен аттрибут {attribute.GetType()}, не реализует класс {typeof(AbstractProcessorFactory)}."
|
||||
);
|
||||
}
|
||||
|
||||
ProcessorDependenciesAttribute? dependenciesAttribute =
|
||||
type.GetCustomAttribute<ProcessorDependenciesAttribute>();
|
||||
|
||||
ProcessorRecord item = new()
|
||||
{
|
||||
Name = attribute.Name,
|
||||
Type = attribute.ProcessorType,
|
||||
Description = attribute.Description,
|
||||
Group = attribute.Group,
|
||||
Dependencies = dependenciesAttribute?.Dependencies ?? [],
|
||||
Active = attribute.Active,
|
||||
Parallel = attribute.Parallel,
|
||||
Factory = (AbstractProcessorFactory)
|
||||
ActivatorUtilities.CreateInstance(ServiceProvider, type),
|
||||
};
|
||||
|
||||
AllItems.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверка, что все зависимости всех обработчиков могут быть удовлетворены.
|
||||
/// </summary>
|
||||
private void CheckDependencies()
|
||||
{
|
||||
foreach (ProcessorRecord item in Items)
|
||||
{
|
||||
foreach (string dependency in item.Dependencies)
|
||||
{
|
||||
if (Items.All(i => i.Name != dependency))
|
||||
{
|
||||
throw new($"Dependency '{dependency}' for processor '{item.Name}' not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сортировать обработчики в соответсвии со всеми зависимостями.
|
||||
/// </summary>
|
||||
public void SortItems()
|
||||
{
|
||||
Items.ForEach(item => item.Priority = -1);
|
||||
|
||||
Dictionary<string, int> priorities = [];
|
||||
|
||||
while (priorities.Count < Items.Count)
|
||||
{
|
||||
foreach (ProcessorRecord item in Items)
|
||||
{
|
||||
if (item.Priority >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IEnumerable<int> dependencyPriorities = item.Dependencies.Select(d =>
|
||||
priorities.GetValueOrDefault(d, -1)
|
||||
);
|
||||
|
||||
if (dependencyPriorities.Any(p => p == -1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
item.Priority = Math.Max(
|
||||
dependencyPriorities.Any() ? dependencyPriorities.Max() + 1 : 1,
|
||||
(int)item.Group * 100
|
||||
);
|
||||
priorities.Add(item.Name, item.Priority);
|
||||
}
|
||||
}
|
||||
|
||||
Items.Sort((a, b) => a.Priority - b.Priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ограничить список используемых обработчиков по названию.
|
||||
/// Автоматически включает все обработчики, от которых зависят указанные в аргументе обработчики.
|
||||
/// </summary>
|
||||
/// <param name="processorNames">Коллекция названиий требуемых обработчиков.</param>
|
||||
public void FilterProcessors(ICollection<string> processorNames)
|
||||
{
|
||||
if (processorNames.Count == 0)
|
||||
{
|
||||
Items = [.. AllItems];
|
||||
SortItems();
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, bool> nameFlags = processorNames.ToDictionary(name => name, _ => false);
|
||||
|
||||
while (nameFlags.Values.Any(v => !v))
|
||||
{
|
||||
foreach (
|
||||
KeyValuePair<string, bool> pair in nameFlags.Where(pair => !pair.Value).ToList()
|
||||
)
|
||||
{
|
||||
ProcessorRecord item = AllItems.First(item => item.Name == pair.Key);
|
||||
|
||||
foreach (string dependency in item.Dependencies)
|
||||
{
|
||||
nameFlags.TryAdd(dependency, false);
|
||||
}
|
||||
|
||||
nameFlags[pair.Key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
Items = AllItems.Where(item => nameFlags.ContainsKey(item.Name)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить список типов, которые обрабатываются в компендиуме.
|
||||
/// </summary>
|
||||
/// <param name="compendium">Тип компендиума.</param>
|
||||
/// <returns>Набор типов.</returns>
|
||||
public ICollection<string> GetTypesOfCompendium(ProcessorCompendiumEnum compendium) =>
|
||||
Items
|
||||
.Where(item =>
|
||||
item.Factory.GetType().IsAssignableTo(typeof(CompendiumProcessorFactory))
|
||||
)
|
||||
.Where(item =>
|
||||
(item.Factory as CompendiumProcessorFactory)!.CompendiumType == compendium
|
||||
)
|
||||
.Select(item => (item.Factory as CompendiumProcessorFactory)!.InnerType)
|
||||
.OfType<string>()
|
||||
.ToList();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pf2e.Scrapper.Processors.Attributes;
|
||||
using Pf2e.Scrapper.Processors.Factory;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Collection;
|
||||
|
||||
/// <summary>
|
||||
/// Элемент коллекции обработчиков.
|
||||
/// </summary>
|
||||
internal class ProcessorRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Название обработчика.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип класса обработчика.
|
||||
/// </summary>
|
||||
public required Type Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Описание обработчика.
|
||||
/// </summary>
|
||||
public string Description { get; init; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Фабрика обработчиков.
|
||||
/// </summary>
|
||||
public AbstractProcessorFactory Factory { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Группа обработчика.
|
||||
/// </summary>
|
||||
public required ProcessorGroupEnum Group { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Коллекция названий обработчиков, после которых должен выполняться обработчик.
|
||||
/// </summary>
|
||||
public ICollection<string> Dependencies { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Процессор включён по умолчанию.
|
||||
/// </summary>
|
||||
public bool Active { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Приоритет выполнения обработчика.
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Разрешить выполнять процессов в параллельном режиме
|
||||
/// </summary>
|
||||
public bool Parallel { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Definitions;
|
||||
|
||||
/// <summary>
|
||||
/// Абстрактный класс-обработчик данных, полученных из Архивов Нефиса.
|
||||
/// Класс предназначен для обработки данных с помощью различных обработчиков,
|
||||
/// для которых указан атрибут <see cref="ProcessorAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="input">Входные данные обработчика.</param>
|
||||
/// <seealso cref="ProcessorAttribute"/>
|
||||
internal abstract class AbstractInputProcessor(ProcessorInput input) : IProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Входные данные обработчика.
|
||||
/// </summary>
|
||||
public ProcessorInput Input { get; } = input;
|
||||
|
||||
/// <summary>
|
||||
/// Основной метод обработки данных.
|
||||
/// </summary>
|
||||
public abstract void Process();
|
||||
|
||||
/// <summary>
|
||||
/// Метод для сохранения изменений в базе данных.
|
||||
/// </summary>
|
||||
public abstract void Save();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Pf2e.Scrapper.Processors.Attributes;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Definitions;
|
||||
|
||||
/// <summary>
|
||||
/// Абстрактный класс-обработчик данных, полученных из Архивов Нефиса.
|
||||
/// Класс предназначен для обработки данных с помощью различных обработчиков,
|
||||
/// для которых указан атрибут <see cref="ProcessorAttribute"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="ProcessorAttribute"/>
|
||||
internal abstract class AbstractSimpleProcessor : IProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Основной метод обработки данных.
|
||||
/// </summary>
|
||||
public abstract void Process();
|
||||
|
||||
/// <summary>
|
||||
/// Метод для сохранения изменений в базе данных.
|
||||
/// </summary>
|
||||
public abstract void Save();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Pf2e.Scrapper.Processors.Definitions;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс для обработчиков данных.
|
||||
/// </summary>
|
||||
public interface IProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Основной метод обработки данных.
|
||||
/// </summary>
|
||||
void Process();
|
||||
|
||||
/// <summary>
|
||||
/// Метод для сохранения изменений в базе данных.
|
||||
/// </summary>
|
||||
void Save();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pf2e.Scrapper.Processors.Definitions;
|
||||
using Pf2e.Scrapper.Processors.Collection;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// Абстрактная фабрика создания процессоров.
|
||||
/// </summary>
|
||||
internal abstract class AbstractProcessorFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Подготовить фабрику к работе.
|
||||
/// </summary>
|
||||
public virtual void Prepare() { }
|
||||
|
||||
/// <summary>
|
||||
/// Количество элеметов для итерирования.
|
||||
/// </summary>
|
||||
/// <returns>Количество элеметов</returns>
|
||||
public abstract int Count();
|
||||
|
||||
/// <summary>
|
||||
/// Итератор процессоров.
|
||||
/// </summary>
|
||||
/// <returns>Пара значений: [0] - описание элемента процессор, [1] - процессор.</returns>
|
||||
public abstract IEnumerable<Tuple<string, IProcessor>> GetEnumerator();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Pf2e.Scrapper.Importers;
|
||||
using Pf2e.Scrapper.Importers.Implementations.Aonprd.Compendium;
|
||||
using Pf2e.Scrapper.Importers.Implementations.FoundryEn.Compendium;
|
||||
using Pf2e.Scrapper.Processors.Attributes;
|
||||
using Pf2e.Scrapper.Processors.Collection;
|
||||
using Pf2e.Scrapper.Processors.Definitions;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Factory;
|
||||
|
||||
internal class CompendiumProcessorFactory(ProcessorRecord item, IServiceProvider serviceProvider)
|
||||
: AbstractProcessorFactory
|
||||
{
|
||||
public ProcessorRecord Item { get; } = item;
|
||||
public IServiceProvider ServiceProvider { get; } = serviceProvider;
|
||||
|
||||
public ProcessorCompendiumEnum? CompendiumType { get; set; } = null;
|
||||
public string? InnerType { get; set; } = null;
|
||||
|
||||
public AbstractCompendium Compendium { get; private set; } = null!;
|
||||
public ICollection<string> Types { get; private set; } = [];
|
||||
|
||||
public override void Prepare()
|
||||
{
|
||||
if (CompendiumType is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(CompendiumType));
|
||||
}
|
||||
|
||||
base.Prepare();
|
||||
|
||||
ProcessorCollection collection =
|
||||
ServiceProvider.GetService<ProcessorCollection>() ?? throw new();
|
||||
|
||||
Compendium =
|
||||
CompendiumType switch
|
||||
{
|
||||
ProcessorCompendiumEnum.Aonprd => ServiceProvider.GetService<AonprdCompendium>(),
|
||||
ProcessorCompendiumEnum.FoundryEn =>
|
||||
ServiceProvider.GetService<FoundryEnCompendium>(),
|
||||
_ => (AbstractCompendium?)null,
|
||||
} ?? throw new($"Не могу выбрать импортёр для группы {Item.Group}");
|
||||
|
||||
Types = InnerType is null
|
||||
? collection.GetTypesOfCompendium(CompendiumType ?? throw new())
|
||||
: [InnerType];
|
||||
}
|
||||
|
||||
public override int Count() => Compendium.Count(Types);
|
||||
|
||||
public override IEnumerable<Tuple<string, IProcessor>> GetEnumerator()
|
||||
{
|
||||
foreach ((AbstractCompendiumItem compendiumItem, JObject json) in Compendium.Iterate(Types))
|
||||
{
|
||||
ProcessorInput processorInput = ActivatorUtilities.CreateInstance<ProcessorInput>(
|
||||
ServiceProvider,
|
||||
compendiumItem.Id,
|
||||
json
|
||||
);
|
||||
|
||||
yield return new(
|
||||
compendiumItem.Id,
|
||||
(IProcessor)
|
||||
ActivatorUtilities.CreateInstance(ServiceProvider, Item.Type, processorInput)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Pf2e.Scrapper.Processors.Collection;
|
||||
using Pf2e.Scrapper.Processors.Definitions;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.Factory;
|
||||
|
||||
internal class SimpleProcessorFactory(ProcessorRecord item, IServiceProvider serviceProvider)
|
||||
: AbstractProcessorFactory
|
||||
{
|
||||
public ProcessorRecord Item { get; } = item;
|
||||
public IServiceProvider ServiceProvider { get; } = serviceProvider;
|
||||
|
||||
public override int Count() => 1;
|
||||
|
||||
public override IEnumerable<Tuple<string, IProcessor>> GetEnumerator()
|
||||
{
|
||||
yield return new(
|
||||
Item.Description,
|
||||
(IProcessor)ActivatorUtilities.CreateInstance(ServiceProvider, Item.Type)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Pf2e.Scrapper.Processors.Attributes;
|
||||
using Pf2e.Scrapper.Processors.Definitions;
|
||||
using Pf2e.Database;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors.General;
|
||||
|
||||
[Processor("truncate", Description = "Отчистка таблиц БД", Group = ProcessorGroupEnum.General)]
|
||||
internal class DatabaseTruncateProcessor : IProcessor
|
||||
{
|
||||
public void Process()
|
||||
{
|
||||
using Pf2eDbContext context = new();
|
||||
string _ = context.Entities.Truncate() + "\n" + context.TruncateAll();
|
||||
}
|
||||
|
||||
public void Save() { }
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using Pf2e.Scrapper.Helpers;
|
||||
using Pf2e.Scrapper.Processors.Collection;
|
||||
using Pf2e.Scrapper.Processors.Definitions;
|
||||
using Pf2e.Scrapper.Processors.Factory;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// Класс, выполняющий обработку коллекции обработчиков.
|
||||
/// </summary>
|
||||
/// <param name="collection">[DI] <inheritdoc cref="ProcessorCollection"/></param>
|
||||
/// <param name="progress">[DI] <inheritdoc cref="ProcessProgress"/></param>
|
||||
/// <param name="settings">[DI] <inheritdoc cref="ApplicationSettings"/></param>
|
||||
internal sealed class ProcessorExecutor(
|
||||
ProcessorCollection collection,
|
||||
ProcessProgress progress,
|
||||
ApplicationSettings settings
|
||||
)
|
||||
{
|
||||
/// <inheritdoc cref="ProcessorCollection"/>
|
||||
public ProcessorCollection Collection { get; } = collection;
|
||||
|
||||
/// <inheritdoc cref="ProcessProgress"/>
|
||||
public ProcessProgress Progress { get; } = progress;
|
||||
|
||||
/// <inheritdoc cref="ApplicationSettings"/>
|
||||
public ApplicationSettings Settings { get; } = settings;
|
||||
|
||||
/// <summary>
|
||||
/// Запускает обработку коллекции обработчиков.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
Collection.SortItems();
|
||||
|
||||
Progress.Processors.Reset(Collection.Items.Count);
|
||||
|
||||
StartItem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Текущий индекс обрабатываемого элемента в коллекции.
|
||||
/// </summary>
|
||||
private int _currentIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Текущая фабрика процессоров, которая используется для обработки.
|
||||
/// </summary>
|
||||
private AbstractProcessorFactory CurrentFactory { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Инициирует обработку текущего элемента в коллекции.
|
||||
/// </summary>
|
||||
/// <remarks>Этот метод извлекает текущий элемент из коллекции, подготавливает связанную
|
||||
/// фабрику и запускает фоновый обработчик для обработки задач элемента. Обработка
|
||||
/// выполняется параллельно, если конфигурация элемента это позволяет, со степенью
|
||||
/// параллелизма, определяемой настройками приложения. После завершения обработки метод
|
||||
/// переходит к следующему шагу рабочего процесса.</remarks>
|
||||
private void StartItem()
|
||||
{
|
||||
ProcessorRecord item = Collection.Items[_currentIndex];
|
||||
|
||||
Progress.Processors.Start(item.Description);
|
||||
Progress.Progress(Progress.Processors.ToString());
|
||||
|
||||
BackgroundWorker worker = new();
|
||||
|
||||
CurrentFactory = item.Factory;
|
||||
CurrentFactory.Prepare();
|
||||
|
||||
worker.DoWork += (_, _) =>
|
||||
{
|
||||
Parallel.ForEach(
|
||||
IterateProcessors(),
|
||||
new() { MaxDegreeOfParallelism = item.Parallel ? Settings.MaxThreads : 1 },
|
||||
RunProcessor
|
||||
);
|
||||
};
|
||||
|
||||
worker.RunWorkerCompleted += (_, _) => Continue();
|
||||
|
||||
worker.RunWorkerAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запускает указанный процессор для выполнения операций обработки и сохранения.
|
||||
/// </summary>
|
||||
/// <remarks>Если процессор выдает <see cref="SkipItemException"/>, сообщение об исключении
|
||||
/// регистрируется как информационный прогресс. Если выдается <see cref="SilentSkipItemException"/>,
|
||||
/// оно молча игнорируется.</remarks>
|
||||
/// <param name="processor">Экземпляр процессора, выполняющий операции обработки и сохранения.</param>
|
||||
/// <param name="state">Экземпляр <see cref="ParallelLoopState"/>, который можно использовать для управления выполнением
|
||||
/// параллельного цикла.</param>
|
||||
/// <param name="arg3">Длинное значение, представляющее текущий индекс итерации или связанные данные для процессора.</param>
|
||||
private void RunProcessor(IProcessor processor, ParallelLoopState state, long arg3)
|
||||
{
|
||||
try
|
||||
{
|
||||
processor.Process();
|
||||
processor.Save();
|
||||
}
|
||||
catch (SkipItemException exception)
|
||||
{
|
||||
Progress.Information(exception.Message);
|
||||
}
|
||||
catch (SilentSkipItemException)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переходит к обработке следующего элемента в коллекции или завершает процесс, если все элементы были обработаны.
|
||||
/// </summary>
|
||||
/// <remarks>Этот метод увеличивает текущий индекс и определяет, начинать ли обработку следующего элемента или завершить
|
||||
/// процесс. Если текущий индекс превышает количество элементов в коллекции, процесс помечается как завершенный.</remarks>
|
||||
private void Continue()
|
||||
{
|
||||
Progress.Processors.End();
|
||||
_currentIndex++;
|
||||
|
||||
if (_currentIndex < Collection.Items.Count)
|
||||
{
|
||||
StartItem();
|
||||
}
|
||||
else
|
||||
{
|
||||
Progress.Processors.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет итерацию по всем процессорам, предоставленным текущей фабрикой.
|
||||
/// </summary>
|
||||
/// <remarks>Этот метод выдает каждый процессор последовательно, позволяя вызывающему
|
||||
/// обрабатывать их по одному за раз. Отслеживание прогресса обновляется для каждого
|
||||
/// процессора во время итерации.</remarks>
|
||||
/// <returns>Перечисляемая коллекция экземпляров <see cref="IProcessor" />,
|
||||
/// представляющая процессоры, доступные в текущей фабрике.</returns>
|
||||
private IEnumerable<IProcessor> IterateProcessors()
|
||||
{
|
||||
Progress.Local.Reset(CurrentFactory.Count());
|
||||
|
||||
foreach ((string title, IProcessor processor) in CurrentFactory.GetEnumerator())
|
||||
{
|
||||
Progress.Local.Start(title);
|
||||
|
||||
yield return processor;
|
||||
|
||||
Progress.Local.End();
|
||||
}
|
||||
|
||||
Progress.Local.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Исключение, показывающее, что обработка сущности пропускается как устаревшая.
|
||||
/// </summary>
|
||||
/// <param name="name">Название сущности.</param>
|
||||
public class LegacyItemException(string name) : SkipItemException(name, "Устаревший элемент") { }
|
||||
|
||||
/// <summary>
|
||||
/// Исключение, показывающее, что обработка сущности пропускается.
|
||||
/// </summary>
|
||||
/// <param name="name">Название сущности.</param>
|
||||
/// <param name="reason">Причина пропуска.</param>
|
||||
public class SkipItemException(string name, string reason)
|
||||
: Exception($"Пропускаю элемент '{name}', указана причина: {reason}.") { }
|
||||
|
||||
/// <summary>
|
||||
/// Исключение, показывающее, что обработка сущности пропускается без вывода сообщения.
|
||||
/// </summary>
|
||||
public class SilentSkipItemException : Exception { }
|
||||
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NpgsqlTypes;
|
||||
using Pf2e.Database.Model.Common.I18n;
|
||||
|
||||
namespace Pf2e.Scrapper.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// Класс для работы с JSON-объектом.
|
||||
/// </summary>
|
||||
/// <param name="name">Название объекта, ассоциированного с вводом.</param>
|
||||
/// <param name="input">JSON-объект, который будет использоваться для доступа к данным.</param>
|
||||
/// <param name="enumSolvers">[DI] Набор интерфейсов, разрешающих значения перечислений.</param>
|
||||
internal class ProcessorInput(string name, JObject input, IEnumerable<IEnumSolver> enumSolvers)
|
||||
{
|
||||
/// <summary>
|
||||
/// Название объекта, ассоциированного с вводом.
|
||||
/// </summary>
|
||||
public string Name { get; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Входящий JSON-объект, который содержит данные для обработки.
|
||||
/// </summary>
|
||||
public JObject Input { get; } = input;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейсы, разрешающие значения перечислений.
|
||||
/// </summary>
|
||||
public IEnumerable<IEnumSolver> EnumSolvers { get; } = enumSolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Значение из JSON по заданному пути, которое обязательно должно существовать.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON.</param>
|
||||
/// <returns>Объект типа `T` или `null`, если не удалось найти значение.</returns>
|
||||
/// <exception cref="NullReferenceException">Если значение не найдено.</exception>
|
||||
public virtual T GetRequired<T>(string path)
|
||||
{
|
||||
JToken token =
|
||||
Input.SelectToken(path) ?? throw new NullReferenceException($"'{path}' is null");
|
||||
|
||||
return token.ToObject<T>() ?? throw new NullReferenceException($"'{path}' is null");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Значение из JSON по заданному пути, которое может не существовать.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON</param>
|
||||
/// <returns>Объект типа `T` или `null`, если значение не найдено</returns>
|
||||
public virtual T? GetOptional<T>(string path)
|
||||
{
|
||||
JToken? token = Input.SelectToken(path) ?? null;
|
||||
|
||||
return (token is null || token.Type == JTokenType.Null)
|
||||
? default!
|
||||
: token.ToObject<T>() ?? default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Значение из JSON по заданному пути с возможностью предоставить значение по умолчанию.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON.</param>
|
||||
/// <param name="defaultValue">Значение, которое возвращается, если значение не найдено.</param>
|
||||
/// <returns>Объект типа `T` или `defaultValue`, если значение не найдено.</returns>
|
||||
public virtual T GetOptional<T>(string path, T defaultValue)
|
||||
{
|
||||
JToken? token = Input.SelectToken(path) ?? null;
|
||||
if (token is null || token.Type == JTokenType.Null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return token.ToObject<T>() ?? defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Преобразование значения из JSON по заданному пути в перечисление с использованием `PgName` атрибута.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON.</param>
|
||||
/// <returns>Перечисление T или выбрасывает исключение, если значение не соответствует жодному варианту.</returns>
|
||||
public virtual T EnumRequired<T>(string path)
|
||||
where T : Enum
|
||||
{
|
||||
string value =
|
||||
GetOptional<string>(path)
|
||||
?? throw new NullReferenceException($"Value in path {path} null or not exists");
|
||||
|
||||
return Enum<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Преобразование значения из JSON по заданному пути в перечисление с возможностью предоставить значение по умолчанию.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON</param>
|
||||
/// <param name="defaultValue">Значение, которое возвращается, если значение не найдено.</param>
|
||||
/// <returns>Значение перечисления T или defaultValue</returns>
|
||||
public virtual T EnumOptional<T>(string path, T defaultValue)
|
||||
where T : Enum
|
||||
{
|
||||
string? value = GetOptional<string>(path);
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Enum<T>(value);
|
||||
}
|
||||
catch (InvalidEnumStringException)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Преобразование значения из JSON по заданному пути в строку локализации для заданного типа локализации.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON.</param>
|
||||
/// <param name="type">Тип локализации.</param>
|
||||
/// <returns>Объект <see cref="LocaleString"/> с добавленным значением для заданного типа.</returns>
|
||||
public virtual LocaleString GetRequiredLocale(string path, LocaleType type)
|
||||
{
|
||||
return new() { [type] = GetRequired<string>(path) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Преобразование значения из JSON по заданному пути в строку локализации для заданного типа локализации.
|
||||
/// </summary>
|
||||
/// <param name="path">Путь в JSON.</param>
|
||||
/// <param name="type">Тип локализации.</param>
|
||||
/// <returns>Объект <see cref="LocaleString"/> или null, если значение не найдено.</returns>
|
||||
public virtual LocaleString? GetOptionalLocale(string path, LocaleType type)
|
||||
{
|
||||
string? value = GetOptional<string>(path);
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new() { [type] = value };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Преобразование значения из JSON в перечисление.
|
||||
/// </summary>
|
||||
/// <param name="value">Значение, которое нужно преобразовать</param>
|
||||
/// <returns>Объект типа `T`, если преобразование успешно, иначе выбрасывает исключение</returns>
|
||||
public virtual T Enum<T>(string value)
|
||||
where T : Enum
|
||||
{
|
||||
IEnumerable<EnumSolver<T>> solvers = EnumSolvers
|
||||
.Where(solver => solver.GetType().IsAssignableTo(typeof(EnumSolver<T>)))
|
||||
.Cast<EnumSolver<T>>();
|
||||
|
||||
foreach (EnumSolver<T> solver in solvers)
|
||||
{
|
||||
T? result = solver.SolveEnum(value);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (System.Reflection.FieldInfo field in typeof(T).GetFields())
|
||||
{
|
||||
List<string> variants = [field.Name];
|
||||
|
||||
if (
|
||||
Attribute.GetCustomAttribute(field, typeof(PgNameAttribute))
|
||||
is PgNameAttribute pgAttribute
|
||||
)
|
||||
{
|
||||
variants.Add(pgAttribute.PgName);
|
||||
}
|
||||
|
||||
if (variants.Contains(value))
|
||||
{
|
||||
return (T)field.GetValue(null)!;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidEnumStringException(typeof(T), value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создание контрольной суммы объекта.
|
||||
/// </summary>
|
||||
/// <param name="isLowercase">Использовать шестнадцатеричные цифры в нижнем регистре.</param>
|
||||
/// <returns>Контрольная сумма объекта.</returns>
|
||||
public string Md5(bool isLowercase = false)
|
||||
{
|
||||
byte[] byteHash = MD5.HashData(Encoding.UTF8.GetBytes(Input.ToString()));
|
||||
string hash = Convert.ToHexString(byteHash).Replace("-", "");
|
||||
return isLowercase ? hash.ToLower() : hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Исключение при неверном значении для перечисленния.
|
||||
/// </summary>
|
||||
/// <param name="type">Тип перечисленния.</param>
|
||||
/// <param name="value">Некорректное значение.</param>
|
||||
public class InvalidEnumStringException(Type type, string value)
|
||||
: Exception($"Invalid value '{value}' for enum type {type.FullName}.") { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс для решения значений перечислений.
|
||||
/// </summary>
|
||||
internal interface IEnumSolver { }
|
||||
|
||||
/// <summary>
|
||||
/// Абстрактный класс для разрешения значений перечислений.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">Тип перечисления, который будет разрешаться.</typeparam>
|
||||
public abstract class EnumSolver<TEnum> : IEnumSolver
|
||||
where TEnum : Enum
|
||||
{
|
||||
/// <summary>
|
||||
/// Словарь, содержащий данные для разрешения значений перечислений.
|
||||
/// </summary>
|
||||
public abstract Dictionary<string, TEnum> Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Метод для разрешения значения перечисления по строковому вводу.
|
||||
/// </summary>
|
||||
/// <param name="input">Строка, представляющая значение перечисления.</param>
|
||||
/// <returns>Элемент перечисления <typeparamref name="TEnum"/>, если найден; иначе <c>null</c>.</returns>
|
||||
public TEnum? SolveEnum(string input)
|
||||
{
|
||||
return Data.GetValueOrDefault(input);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user