Глобальный рефакторинг пространств имён

This commit is contained in:
2025-10-02 13:29:30 +04:00
parent d23d81fb53
commit 2fa94851cb
3176 changed files with 27146 additions and 30584 deletions
@@ -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 { }
+237
View File
@@ -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);
}
}