Отчистка кода

This commit is contained in:
2025-04-15 16:47:40 +04:00
parent 4fec054ac6
commit 414ac03f83
47 changed files with 2192 additions and 2078 deletions
+267
View File
@@ -0,0 +1,267 @@
# Удалите строку ниже, если вы хотите наследовать параметры .editorconfig из каталогов, расположенных выше в иерархии
root = true
# Файлы C#
[*.cs]
#### Основные параметры EditorConfig ####
# Отступы и интервалы
indent_size = 4
indent_style = space
tab_width = 4
# Предпочтения для новых строк
end_of_line = crlf
insert_final_newline = false
#### Действия кода .NET ####
# Члены типа
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Поиск символов
dotnet_search_reference_assemblies = true
#### Рекомендации по написанию кода .NET ####
# Упорядочение Using
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# Предпочтения для this. и Me.
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Параметры использования ключевых слов языка и типов BCL
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Предпочтения для скобок
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Предпочтения модификатора
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Выражения уровень предпочтения
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Предпочтения для полей
dotnet_style_readonly_field = true
# Настройки параметров
dotnet_code_quality_unused_parameters = all
# Параметры подавления
dotnet_remove_unnecessary_suppression_exclusions = none
# Предпочтения для новых строк
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### Рекомендации по написанию кода C# ####
# Предпочтения var
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Члены, заданные выражениями
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Настройки соответствия шаблонов
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Настройки проверки на null
csharp_style_conditional_delegate_call = true
# Предпочтения модификатора
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Предпочтения для блоков кода
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = file_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_top_level_statements = true
# Выражения уровень предпочтения
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# предпочтения для директивы using
csharp_using_directive_placement = outside_namespace
# Предпочтения для новых строк
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### Правила форматирования C# ####
# Предпочтения для новых строк
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Параметры отступов
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Предпочтения для интервалов
csharp_space_after_cast = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Параметры переноса
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
#### Стили именования ####
# Правила именования
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Спецификации символов
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Стили именования
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
[*.{cs,vb}]
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_code_quality_unused_parameters = all:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
+31 -32
View File
@@ -1,41 +1,40 @@
using dotenv.net; using dotenv.net;
using dotenv.net.Utilities; using dotenv.net.Utilities;
namespace Pf2eDataScrapper namespace Pf2eDataScrapper;
/// <summary>
/// Класс настроек.
/// </summary>
internal static class EnvSettings
{ {
/// <summary> /// <summary>
/// Класс настроек. /// Загрузка файла настроек `.env`.
/// </summary> /// </summary>
internal static class EnvSettings public static void Load()
{ {
/// <summary> DotEnv.Load();
/// Загрузка файла настроек `.env`.
/// </summary>
public static void Load()
{
DotEnv.Load();
}
/// <summary>
/// Путь к папке с входными данными.
/// </summary>
public static string DataDirPath => EnvReader.GetStringValue("DATA_DIR");
/// <summary>
/// Папка в входными данными.
/// </summary>
public static DirectoryInfo DataDir => new(DataDirPath);
/// <summary>
/// Путь к папке для сохранения результатов.
/// </summary>
public static string OutputDirPath => EnvReader.GetStringValue("OUTPUT_DIR");
/// <summary>
/// Папка для сохранения результатов.
/// </summary>
public static string OutputDir => new(OutputDirPath);
public static string DatabaseConnection => EnvReader.GetStringValue("DATABASE_CONNECTION");
} }
/// <summary>
/// Путь к папке с входными данными.
/// </summary>
public static string DataDirPath => EnvReader.GetStringValue("DATA_DIR");
/// <summary>
/// Папка в входными данными.
/// </summary>
public static DirectoryInfo DataDir => new(DataDirPath);
/// <summary>
/// Путь к папке для сохранения результатов.
/// </summary>
public static string OutputDirPath => EnvReader.GetStringValue("OUTPUT_DIR");
/// <summary>
/// Папка для сохранения результатов.
/// </summary>
public static string OutputDir => new(OutputDirPath);
public static string DatabaseConnection => EnvReader.GetStringValue("DATABASE_CONNECTION");
} }
+29 -30
View File
@@ -1,42 +1,41 @@
using Pf2eDataScrapper.Importers.Repositories; using Pf2eDataScrapper.Importers.Repositories;
namespace Pf2eDataScrapper.Importers namespace Pf2eDataScrapper.Importers;
/// <summary>
/// Абстрактный класс импортёра
/// </summary>
internal abstract class AbstractImporter
{ {
/// <summary> /// <summary>
/// Абстрактный класс импортёра /// Отображаемое имя импортёра
/// </summary> /// </summary>
internal abstract class AbstractImporter public abstract string Name { get; }
{
/// <summary>
/// Отображаемое имя импортёра
/// </summary>
public abstract string Name { get; }
/// <summary> /// <summary>
/// Короткое имя импортёра /// Короткое имя импортёра
/// </summary> /// </summary>
public abstract string Slug { get; } public abstract string Slug { get; }
public abstract AbstractImporterRepository Repository { get; } public abstract AbstractImporterRepository Repository { get; }
/// <summary> /// <summary>
/// Путь к директории с данными для импортёра /// Путь к директории с данными для импортёра
/// </summary> /// </summary>
public string Dir => Path.Combine(EnvSettings.DataDirPath, Slug); public string Dir => Path.Combine(EnvSettings.DataDirPath, Slug);
/// <summary> /// <summary>
/// Метод подготовки данных для импорта /// Метод подготовки данных для импорта
/// </summary> /// </summary>
public abstract void Prepare(); public abstract void Prepare();
/// <summary> /// <summary>
/// Импорт данных /// Импорт данных
/// </summary> /// </summary>
public abstract void Import(); public abstract void Import();
/// <summary> /// <summary>
/// Построение схем данных /// Построение схем данных
/// </summary> /// </summary>
public abstract void BuildSchemas(); public abstract void BuildSchemas();
}
} }
@@ -1,40 +1,39 @@
using Pf2eDataScrapper.Importers.Repositories; using Pf2eDataScrapper.Importers.Implementations.Aonprd.SchemaBuilder;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.SchemaBuilder; using Pf2eDataScrapper.Importers.Repositories;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd namespace Pf2eDataScrapper.Importers.Implementations.Aonprd;
internal class AonprdImporter : AbstractImporter
{ {
internal class AonprdImporter : AbstractImporter public override string Name => "Archives of Nethys Pathfinder 2e";
public override string Slug => "aonprd";
public override AbstractImporterRepository Repository => new AonprdRepository(this, "https://elasticsearch.aonprd.com");
public AonprdLoader Loader { get; }
public AonprdImporter()
{ {
public override string Name => "Archives of Nethys Pathfinder 2e"; Loader = new AonprdLoader(this);
}
public override string Slug => "aonprd"; public override void Prepare()
{
Repository.Update();
Loader.Prepare();
}
public override AbstractImporterRepository Repository => new AonprdRepository(this, "https://elasticsearch.aonprd.com"); public override void BuildSchemas()
{
AonprdSchemaBuilder builder = new(Loader);
public AonprdLoader Loader { get; } builder.BuildSchemas();
builder.Save(EnvSettings.OutputDir);
}
public AonprdImporter() public override void Import()
{ {
Loader = new AonprdLoader(this); throw new NotImplementedException();
}
public override void Prepare()
{
Repository.Update();
Loader.Prepare();
}
public override void BuildSchemas()
{
var builder = new AonprdSchemaBuilder(Loader);
builder.BuildSchemas();
builder.Save(EnvSettings.OutputDir);
}
public override void Import()
{
throw new NotImplementedException();
}
} }
} }
@@ -1,79 +1,77 @@
using Microsoft.EntityFrameworkCore.Metadata.Internal; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd namespace Pf2eDataScrapper.Importers.Implementations.Aonprd;
internal class AonprdLoader(AonprdImporter importer)
{ {
internal class AonprdLoader (AonprdImporter importer) public AonprdImporter Importer = importer;
public AonprdCompendium Compendium = new();
public int ItemCount => Compendium.Count;
public void Prepare()
{ {
public AonprdImporter Importer = importer; using StreamReader indexFile = new(Path.Combine(Importer.Dir, "index.json"));
JObject indexJson = JObject.Parse(indexFile.ReadToEnd());
public AonprdCompendium Compendium = new(); foreach (KeyValuePair<string, JToken?> kvp in indexJson)
public int ItemCount => Compendium.Count;
public void Prepare()
{ {
using var indexFile = new StreamReader(Path.Combine(Importer.Dir, "index.json")); string file = kvp.Key.ToString();
var indexJson = JObject.Parse(indexFile.ReadToEnd()); JArray items = (JArray) kvp.Value!;
foreach (var kvp in indexJson) foreach (JToken item in items)
{ {
var file = kvp.Key.ToString(); Compendium.AddReference(item.ToString(), file);
var items = (JArray)kvp.Value!;
foreach (var item in items)
{
Compendium.AddReference(item.ToString(), file);
}
} }
} }
}
public IEnumerable<LoaderEntry> Entries() public IEnumerable<LoaderEntry> Entries()
{ {
return Entries(Compendium.Types.Keys.ToArray()); return Entries([.. Compendium.Types.Keys]);
} }
public IEnumerable<LoaderEntry> Entries(string types) public IEnumerable<LoaderEntry> Entries(string types)
{ {
return Entries([types]); return Entries([types]);
} }
public IEnumerable<LoaderEntry> Entries(string[] types) public IEnumerable<LoaderEntry> Entries(string[] types)
{
foreach (string? file in Compendium.Files(types))
{ {
foreach (var file in Compendium.Files(types)) if (file is null)
{ {
if (file is null) continue;
}
using StreamReader fileReader = new(Path.Combine(Importer.Dir, $"{file}.json"));
JArray array = JArray.Parse(fileReader.ReadToEnd());
foreach (JToken item in array)
{
string? id = item.SelectToken("id")?.ToString();
if (id is null)
{ {
continue; continue;
} }
using var fileReader = new StreamReader(Path.Combine(Importer.Dir, $"{file}.json")); LoaderEntry entry = new(id, file, (JObject) item);
var array = JArray.Parse(fileReader.ReadToEnd());
foreach (var item in array) if (types.Contains(entry.Type))
{ {
var id = item.SelectToken("id")?.ToString(); yield return entry;
if (id is null)
{
continue;
}
var entry = new LoaderEntry(id, file, (JObject)item);
if (types.Contains(entry.Type))
{
yield return entry;
}
} }
} }
} }
}
public class LoaderEntry(string id, string file, JObject itemJson) : AonprdReference(id, file) public class LoaderEntry(string id, string file, JObject itemJson) : AonprdReference(id, file)
{ {
public JObject Json { get; } = itemJson; public JObject Json { get; } = itemJson;
}
} }
} }
@@ -2,101 +2,100 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Repositories; using Pf2eDataScrapper.Importers.Repositories;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd namespace Pf2eDataScrapper.Importers.Implementations.Aonprd;
internal class AonprdRepository(AbstractImporter importer, string searchUrl) : AbstractImporterRepository(importer)
{ {
internal class AonprdRepository(AbstractImporter importer, string searchUrl): AbstractImporterRepository(importer) public readonly string SearchUrl = searchUrl;
public int Revision { get; private set; }
private string Load(string url)
{ {
public readonly string SearchUrl = searchUrl; if (!url.StartsWith("http://") && !url.StartsWith("https://"))
public int Revision { get; private set; }
private string Load(string url)
{ {
if (!url.StartsWith("http://") && !url.StartsWith("https://")) url = SearchUrl + url;
{ }
url = SearchUrl + url;
}
using var client = new HttpClient(); using HttpClient client = new();
using var stream = client.GetStreamAsync(url); using Task<Stream> stream = client.GetStreamAsync(url);
using var reader = new StreamReader(stream.Result); using StreamReader reader = new(stream.Result);
return reader.ReadToEnd(); return reader.ReadToEnd();
}
public override void Update()
{
if (CheckRevision())
{
Console.WriteLine($"Данные Aonprd актуальны, текущая ревизия {Revision}.");
return;
}
Console.WriteLine($"Данные Aonprd устарели, обновляем {RepositoryDirPath} до ревизии {Revision}...");
if (Directory.Exists(RepositoryDirPath))
{
Directory.Delete(RepositoryDirPath, true);
}
Directory.CreateDirectory(RepositoryDirPath);
WriteFile("revision.txt", Revision.ToString());
Console.WriteLine("Загрузка индекса...");
string indexContent = Load($"/json-data/aon{Revision}-index.json");
WriteFile("index.json", indexContent);
JObject indexJson = JObject.Parse(indexContent);
Console.WriteLine("Загрузка индексных файлов...");
ProgressBar bar = new(indexJson.Count);
int i = 0;
foreach (KeyValuePair<string, JToken?> kvp in indexJson)
{
string fileName = kvp.Key;
bar.Refresh(i++, fileName);
string fileContent = Load($"/json-data/{fileName}.json");
WriteFile($"{fileName}.json", fileContent);
} }
public override void Update() bar.Refresh(indexJson.Count, "Готово!");
}
/// <summary>
/// Проверка актуальности данных
/// </summary>
/// <returns>true - актуальная ревизия, false - устаревшая/отсутсвует</returns>
private bool CheckRevision()
{
JObject page = JObject.Parse(Load("/aon/_doc/feat-1"));
Revision = Convert.ToInt32(page.SelectToken("_index")?.ToObject<string>()?.Replace("aon", "") ?? throw new Exception("Can't get revision"));
string revisionFile = Path.Combine(RepositoryDirPath, "revision.txt");
if (!File.Exists(revisionFile))
{ {
if (CheckRevision()) return false;
{
Console.WriteLine($"Данные Aonprd актуальны, текущая ревизия {Revision}.");
return;
}
Console.WriteLine($"Данные Aonprd устарели, обновляем {RepositoryDirPath} до ревизии {Revision}...");
if (Directory.Exists(RepositoryDirPath))
{
Directory.Delete(RepositoryDirPath, true);
}
Directory.CreateDirectory(RepositoryDirPath);
WriteFile("revision.txt", Revision.ToString());
Console.WriteLine("Загрузка индекса...");
var indexContent = Load($"/json-data/aon{Revision}-index.json");
WriteFile("index.json", indexContent);
var indexJson = JObject.Parse(indexContent);
Console.WriteLine("Загрузка индексных файлов...");
var bar = new ProgressBar(indexJson.Count);
var i = 0;
foreach (var kvp in indexJson)
{
var fileName = kvp.Key;
bar.Refresh(i++, fileName);
var fileContent = Load($"/json-data/{fileName}.json");
WriteFile($"{fileName}.json", fileContent);
}
bar.Refresh(indexJson.Count, "Готово!");
} }
/// <summary> using StreamReader reader = new(revisionFile);
/// Проверка актуальности данных string lastRevision = reader.ReadToEnd();
/// </summary> return lastRevision == Revision.ToString();
/// <returns>true - актуальная ревизия, false - устаревшая/отсутсвует</returns> }
private bool CheckRevision()
private void WriteFile(string path, string content)
{
string filePath = Path.Combine(RepositoryDirPath, path);
if (File.Exists(filePath))
{ {
var page = JObject.Parse(Load("/aon/_doc/feat-1")); File.Delete(filePath);
Revision = Convert.ToInt32(page.SelectToken("_index")?.ToObject<string>()?.Replace("aon", "") ?? throw new Exception("Can't get revision"));
var revisionFile = Path.Combine(RepositoryDirPath, "revision.txt");
if (!File.Exists(revisionFile))
{
return false;
}
using var reader = new StreamReader(revisionFile);
var lastRevision = reader.ReadToEnd();
return lastRevision == Revision.ToString();
}
private void WriteFile (string path, string content)
{
var filePath = Path.Combine(RepositoryDirPath, path);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
using var writer = new StreamWriter(filePath);
writer.Write(content);
} }
using StreamWriter writer = new(filePath);
writer.Write(content);
} }
} }
@@ -1,28 +1,27 @@
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
internal class AonprdCompendium
{ {
internal class AonprdCompendium public readonly Dictionary<string, AonprdType> Types = [];
public void AddReference(string id, string file)
{ {
public readonly Dictionary<string, AonprdType> Types = []; AonprdReference reference = new(id, file);
public void AddReference(string id, string file) if (Types.TryGetValue(reference.Type, out AonprdType? type))
{ {
AonprdReference reference = new(id, file); type.Add(reference);
}
if (Types.TryGetValue(reference.Type, out var type)) else
{ {
type.Add(reference); Types.Add(reference.Type, new AonprdType(reference));
}
else
{
Types.Add(reference.Type, new AonprdType(reference));
}
} }
public int Count => Types.Values.Sum(x => x.Count);
public IEnumerable<string> Files(IEnumerable<string> types) => Types.Values
.Where(t => types.Contains(t.Type))
.SelectMany(t => t.Files)
.Distinct();
} }
public int Count => Types.Values.Sum(x => x.Count);
public IEnumerable<string> Files(IEnumerable<string> types) => Types.Values
.Where(t => types.Contains(t.Type))
.SelectMany(t => t.Files)
.Distinct();
} }
@@ -1,35 +1,23 @@
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
internal class AonprdReference(string id, string file)
{ {
internal class AonprdReference(string id, string file) public readonly string Id = id;
public readonly string Type = ResolveType(id);
public readonly string File = file;
public static string ResolveType(string id)
{ {
public readonly string Id = id; static string? subType(string type, string cat)
public readonly string Type = ResolveType(id);
public readonly string File = file;
public static string ResolveType(string id)
{ {
static string? subType(string type, string cat) return type.StartsWith($"{cat}-") && type != cat ? type.EndsWith("-bonus") ? $"{cat}-bonus" : $"{cat}-sub" : null;
{
if (type.StartsWith($"{cat}-") && type != cat)
{
if (type.EndsWith("-bonus"))
{
return $"{cat}-bonus";
}
else
{
return $"{cat}-sub";
}
}
return null;
}
var type = string.Join('-', id.Split('-')[0..^1]);
return subType(type, "equipment") ?? subType(type, "weapon") ?? type;
} }
string type = string.Join('-', id.Split('-')[0..^1]);
return subType(type, "equipment") ?? subType(type, "weapon") ?? type;
} }
} }
@@ -1,23 +1,22 @@
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
internal class AonprdType(string type)
{ {
internal class AonprdType(string type) public readonly string Type = type;
public readonly List<AonprdReference> Items = [];
public AonprdType(AonprdReference reference) : this(reference.Type)
{ {
public readonly string Type = type; Items.Add(reference);
public readonly List<AonprdReference> Items = [];
public AonprdType(AonprdReference reference) : this(reference.Type)
{
Items.Add(reference);
}
public void Add(AonprdReference reference)
{
Items.Add(reference);
}
public List<string> Files => [.. Items.Select(x => x.File).Distinct()];
public int Count => Items.Count;
} }
public void Add(AonprdReference reference)
{
Items.Add(reference);
}
public List<string> Files => [.. Items.Select(x => x.File).Distinct()];
public int Count => Items.Count;
} }
@@ -2,86 +2,85 @@
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
using Pf2eDataScrapper.SchemaBuilder; using Pf2eDataScrapper.SchemaBuilder;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.SchemaBuilder namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.SchemaBuilder;
internal class AonprdSchemaBuilder(AonprdLoader loader)
{ {
internal class AonprdSchemaBuilder(AonprdLoader loader) public readonly AonprdLoader Loader = loader;
public readonly AonprdCompendium Compendium = loader.Compendium;
// public readonly AonprdSchemaTypeResolver Resolver = new();
public readonly Dictionary<string, SchemaNode> Schemas = [];
private SchemaNode GetSchema(AonprdLoader.LoaderEntry entry)
{ {
public readonly AonprdLoader Loader = loader; string key = entry.Type;
if (!Schemas.TryGetValue(key, out SchemaNode? value))
public readonly AonprdCompendium Compendium = loader.Compendium;
// public readonly AonprdSchemaTypeResolver Resolver = new();
public readonly Dictionary<string, SchemaNode> Schemas = [];
private SchemaNode GetSchema(AonprdLoader.LoaderEntry entry)
{ {
string key = entry.Type; value = new SchemaNode()
if (!Schemas.TryGetValue(key, out SchemaNode? value))
{ {
value = new SchemaNode() // Resolver = Resolver,
{ Path = key
// Resolver = Resolver, };
Path = key Schemas[key] = value;
}; }
Schemas[key] = value; return value;
} }
return value;
public void BuildSchemas()
{
Console.WriteLine("Building schemas...");
ProgressBar bar = new ProgressBar(Loader.ItemCount);
int i = 0;
foreach (AonprdLoader.LoaderEntry entry in Loader.Entries())
{
bar.Refresh(i++, entry.Type);
GetSchema(entry).AddValue(entry.Json);
} }
public void BuildSchemas() bar.Refresh(Loader.ItemCount, "Done!");
}
public void Save(string targetDir)
{
Console.WriteLine("Dumping schemas...");
targetDir = Path.Combine(targetDir, Loader.Importer.Slug);
ProgressBar bar = new(Schemas.Count);
int i = 0;
foreach (KeyValuePair<string, SchemaNode> kvp in Schemas)
{ {
Console.WriteLine("Building schemas..."); bar.Refresh(i++, kvp.Key);
SaveSchema(targetDir, kvp.Key, kvp.Value);
var bar = new ProgressBar(Loader.ItemCount);
var i = 0;
foreach (var entry in Loader.Entries())
{
bar.Refresh(i++, entry.Type);
GetSchema(entry).AddValue(entry.Json);
}
bar.Refresh(Loader.ItemCount, "Done!");
}
public void Save(string targetDir)
{
Console.WriteLine("Dumping schemas...");
targetDir = Path.Combine(targetDir, Loader.Importer.Slug);
var bar = new ProgressBar(Schemas.Count);
var i = 0;
foreach (var kvp in Schemas)
{
bar.Refresh(i++, kvp.Key);
SaveSchema(targetDir, kvp.Key, kvp.Value);
}
}
protected void SaveSchema(string dir, string name, SchemaNode schema)
{
var invalids = Path.GetInvalidFileNameChars();
var fileName = string.Join("_", schema.Path.Split(invalids, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.');
var path = Path.Combine(dir, fileName + ".yaml");
var dirPath = Path.GetDirectoryName(path);
if (dirPath is not null && !Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
using var writer = new StreamWriter(path);
schema.Beautify();
writer.WriteLine(SchemaNode.FormatYaml(new Dictionary<string, string>()
{
["name"] = name,
["values"] = schema.Values.ToString()
}, schema.Types[0].GetYamlData()));
} }
} }
protected void SaveSchema(string dir, string name, SchemaNode schema)
{
char[] invalids = Path.GetInvalidFileNameChars();
string fileName = string.Join("_", schema.Path.Split(invalids, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.');
string path = Path.Combine(dir, fileName + ".yaml");
string? dirPath = Path.GetDirectoryName(path);
if (dirPath is not null && !Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
using StreamWriter writer = new(path);
schema.Beautify();
writer.WriteLine(SchemaNode.FormatYaml(new Dictionary<string, string>()
{
["name"] = name,
["values"] = schema.Values.ToString()
}, schema.Types[0].GetYamlData()));
}
} }
@@ -1,18 +1,17 @@
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
/// <summary>
/// Тип документов компендиума.
/// </summary>
internal enum CompendiumDocumentType
{ {
/// <summary> Actor,
/// Тип документов компендиума. Adventure,
/// </summary> Cards,
internal enum CompendiumDocumentType Item,
{ JournalEntry,
Actor, Macro,
Adventure, Playlist,
Cards, RollTable,
Item, Scene
JournalEntry,
Macro,
Playlist,
RollTable,
Scene
}
} }
@@ -1,88 +1,87 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
/// <summary>
/// Папка компендиума.
/// </summary>
internal class CompendiumFolder
{ {
/// <summary> /// <summary>
/// Папка компендиума. /// Идентификатор папки.
/// </summary> /// </summary>
internal class CompendiumFolder public readonly string Id;
/// <summary>
/// Название папки.
/// </summary>
public readonly string Name;
/// <summary>
/// Путь к папке.
/// </summary>
public readonly string? Path;
/// <summary>
/// Указатель, что папка содержится в других папках.
/// </summary>
public readonly bool IsContained;
/// <summary>
/// Родительская папка.
/// </summary>
public CompendiumFolder? Parent;
/// <summary>
/// Тип документов в папке.
/// </summary>
public readonly CompendiumDocumentType Type;
/// <summary>
/// Конструктор папки компендиума из JSON-объекта.
/// </summary>
/// <param name="json"></param>
public CompendiumFolder(JObject json)
{ {
/// <summary> if (json.TryGetValue("_id", out JToken? id))
/// Идентификатор папки.
/// </summary>
public readonly string Id;
/// <summary>
/// Название папки.
/// </summary>
public readonly string Name;
/// <summary>
/// Путь к папке.
/// </summary>
public readonly string? Path;
/// <summary>
/// Указатель, что папка содержится в других папках.
/// </summary>
public readonly bool IsContained;
/// <summary>
/// Родительская папка.
/// </summary>
public CompendiumFolder? Parent;
/// <summary>
/// Тип документов в папке.
/// </summary>
public readonly CompendiumDocumentType Type;
/// <summary>
/// Конструктор папки компендиума из JSON-объекта.
/// </summary>
/// <param name="json"></param>
public CompendiumFolder(JObject json)
{ {
if (json.TryGetValue("_id", out var id)) // Вложенная папка через _folders.json
{ Id = id!.ToString();
// Вложенная папка через _folders.json Name = json["name"]!.ToString();
Id = id!.ToString();
Name = json["name"]!.ToString();
Path = null;
IsContained = true;
}
else
{
// Корневой пак в system.json
Id = json["name"]!.ToString();
Name = json["label"]!.ToString();
Path = json["path"]!.ToString();
IsContained = false;
}
Type = (CompendiumDocumentType)Enum.Parse(typeof(CompendiumDocumentType), json["type"]!.ToString());
}
/// <summary>
/// Конструктор папки компендиума.
/// </summary>
/// <param name="id">Идентификатор папки.</param>
/// <param name="name">Название папки.</param>
public CompendiumFolder(string id, string name)
{
Id = id;
Name = name;
Path = null; Path = null;
IsContained = true;
}
else
{
// Корневой пак в system.json
Id = json["name"]!.ToString();
Name = json["label"]!.ToString();
Path = json["path"]!.ToString();
IsContained = false; IsContained = false;
} }
/// <summary> Type = (CompendiumDocumentType) Enum.Parse(typeof(CompendiumDocumentType), json["type"]!.ToString());
/// Строковое представление данных о папки компендиума. }
/// </summary>
/// <returns>Строковое представление данных о папки компендиума.</returns> /// <summary>
public override string ToString() /// Конструктор папки компендиума.
{ /// </summary>
return $"CompendiumFolder([Id={Id}][Type={Type}][Name={Name}][Path={Path}][Parent={Parent?.Name ?? "null"}])"; /// <param name="id">Идентификатор папки.</param>
} /// <param name="name">Название папки.</param>
public CompendiumFolder(string id, string name)
{
Id = id;
Name = name;
Path = null;
IsContained = false;
}
/// <summary>
/// Строковое представление данных о папки компендиума.
/// </summary>
/// <returns>Строковое представление данных о папки компендиума.</returns>
public override string ToString()
{
return $"CompendiumFolder([Id={Id}][Type={Type}][Name={Name}][Path={Path}][Parent={Parent?.Name ?? "null"}])";
} }
} }
@@ -1,28 +1,27 @@
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
/// <summary>
/// Коллекция папок компендиума
/// </summary>
internal class CompendiumFolderCollection : List<CompendiumFolder>
{ {
/// <summary> /// <summary>
/// Коллекция папок компендиума /// Получение папки по идентификатору
/// </summary> /// </summary>
internal class CompendiumFolderCollection : List<CompendiumFolder> /// <param name="id">Искомый идентификатор</param>
/// <returns>Искомая папка</returns>
public CompendiumFolder? GetById(string id)
{ {
/// <summary> return this.FirstOrDefault(x => x.Id == id);
/// Получение папки по идентификатору }
/// </summary>
/// <param name="id">Искомый идентификатор</param>
/// <returns>Искомая папка</returns>
public CompendiumFolder? GetById(string id)
{
return this.FirstOrDefault(x => x.Id == id);
}
/// <summary> /// <summary>
/// Фильтрация требуемых папок компендиума. /// Фильтрация требуемых папок компендиума.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public CompendiumFolderCollection Filter() public CompendiumFolderCollection Filter()
{ {
// return [.. this.FindAll(x => x.Type != CompendiumDocumentType.Actor)]; // return [.. this.FindAll(x => x.Type != CompendiumDocumentType.Actor)];
return [.. this.FindAll(x => x.Type != CompendiumDocumentType.Actor && x.Name == "Conditions")]; return [.. this.FindAll(x => x.Type != CompendiumDocumentType.Actor && x.Name == "Conditions")];
}
} }
} }
@@ -1,170 +1,169 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium; using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry;
/// <summary>
/// Загрузчик данных компендиума
/// </summary>
/// <param name="importer">Импортёр</param>
internal class CompendiumLoader(Pf2EFoundryImporter importer)
{ {
/// <summary> /// <summary>
/// Загрузчик данных компендиума /// Импортёр
/// </summary> /// </summary>
/// <param name="importer">Импортёр</param> public readonly Pf2EFoundryImporter Importer = importer;
internal class CompendiumLoader(Pf2EFoundryImporter importer)
/// <summary>
/// Набор папок компендиума
/// </summary>
public CompendiumFolderCollection Collection = [];
/// <summary>
/// Счётчик папок
/// </summary>
private int _folderCount = 0;
/// <summary>
/// Общее количество элементов в компендиуме
/// </summary>
public int ItemCount { get; protected set; } = 0;
/// <summary>
/// Обработка папок компендиума в поле `packFolders`.
/// </summary>
/// <param name="folders"></param>
/// <param name="parent"></param>
protected void ProcessPackFolders(JArray folders, CompendiumFolder? parent)
{ {
/// <summary> foreach (JToken token in folders)
/// Импортёр
/// </summary>
public readonly Pf2EFoundryImporter Importer = importer;
/// <summary>
/// Набор папок компендиума
/// </summary>
public CompendiumFolderCollection Collection = [];
/// <summary>
/// Счётчик папок
/// </summary>
private int _folderCount = 0;
/// <summary>
/// Общее количество элементов в компендиуме
/// </summary>
public int ItemCount { get; protected set; } = 0;
/// <summary>
/// Обработка папок компендиума в поле `packFolders`.
/// </summary>
/// <param name="folders"></param>
/// <param name="parent"></param>
protected void ProcessPackFolders(JArray folders, CompendiumFolder? parent)
{ {
foreach (var token in folders) JObject obj = (JObject) token;
string id = $"folder-{_folderCount++}";
string name = obj["name"]!.ToString();
CompendiumFolder folder = new(id, name)
{ {
JObject obj = (JObject)token; Parent = parent
};
Collection.Add(folder);
var id = $"folder-{_folderCount++}"; if (obj.TryGetValue("packs", out JToken? packs))
var name = obj["name"]!.ToString(); {
foreach (JToken jToken in packs!)
var folder = new CompendiumFolder(id, name)
{ {
Parent = parent string packId = jToken.ToString();
}; CompendiumFolder? child = Collection.GetById(packId);
Collection.Add(folder);
if (obj.TryGetValue("packs", out var packs)) if (child is not null)
{
foreach (var jToken in packs!)
{ {
var packId = jToken.ToString(); child.Parent = folder;
var child = Collection.GetById(packId);
if (child is not null)
{
child.Parent = folder;
}
} }
} }
if (obj.TryGetValue("folders", out var innerFolders))
{
ProcessPackFolders((JArray)innerFolders!, folder);
}
} }
Collection = Collection.Filter(); if (obj.TryGetValue("folders", out JToken? innerFolders))
{
ProcessPackFolders((JArray) innerFolders!, folder);
}
} }
/// <summary> Collection = Collection.Filter();
/// Подготовка набора папок компендиума }
/// </summary>
public void Prepare() /// <summary>
/// Подготовка набора папок компендиума
/// </summary>
public void Prepare()
{
using StreamReader systemFile = new(Path.Combine(Importer.Dir, "static", "system.json"));
JObject systemData = JObject.Parse(systemFile.ReadToEnd());
foreach (JToken pack in systemData["packs"]!)
{ {
using var systemFile = new StreamReader(Path.Combine(Importer.Dir, "static", "system.json")); CompendiumFolder folder = new((JObject) pack);
var systemData = JObject.Parse(systemFile.ReadToEnd());
foreach (var pack in systemData["packs"]!) Collection.Add(folder);
}
if (systemData.TryGetValue("packFolders", out JToken? packFolders))
{
ProcessPackFolders((JArray) packFolders, null);
}
// Поиск _folders.json
foreach (CompendiumFolder? parent in Collection.ToList())
{
if (parent.Path is null)
{ {
var folder = new CompendiumFolder((JObject)pack); continue;
Collection.Add(folder);
} }
if (systemData.TryGetValue("packFolders", out var packFolders)) string dirPath = Path.Combine(Importer.Dir, parent.Path);
{
ProcessPackFolders((JArray)packFolders, null);
}
// Поиск _folders.json ItemCount += Directory.GetFiles(dirPath, "*.json", SearchOption.AllDirectories)
foreach (var parent in Collection.ToList()) .Length;
foreach (string foldersFilePath in Directory.GetFiles(dirPath, "_folders.json", SearchOption.AllDirectories))
{ {
if (parent.Path is null) ItemCount--;
using StreamReader file = new(foldersFilePath);
JArray data = JArray.Parse(file.ReadToEnd());
foreach (JToken pack in data)
{
CompendiumFolder folder = new((JObject) pack) { Parent = parent };
Collection.Add(folder);
}
}
}
// foreach (var folder in Collection)
// {
// Console.WriteLine(folder);
// }
Collection = Collection.Filter();
Console.WriteLine($"Total items: {ItemCount}");
}
public IEnumerable<LoaderEnrty> Enrties()
{
foreach (CompendiumFolder folder in Collection.FindAll(f => f.Path is not null))
{
string dirPath = Path.Combine(Importer.Dir, folder.Path!);
foreach (string filePath in Directory.GetFiles(dirPath, "*.json", SearchOption.AllDirectories))
{
FileInfo file = new(filePath);
if (file.Name == "_folders.json")
{ {
continue; continue;
} }
var dirPath = Path.Combine(Importer.Dir, parent.Path); using StreamReader reader = new(file.FullName);
JObject itemJson = JObject.Parse(reader.ReadToEnd());
ItemCount += Directory.GetFiles(dirPath, "*.json", SearchOption.AllDirectories) CompendiumFolder? itemFolder = folder;
.Length; if (itemJson.TryGetValue("folder", out JToken? itemJsonFolder))
foreach (var foldersFilePath in Directory.GetFiles(dirPath, "_folders.json", SearchOption.AllDirectories))
{ {
ItemCount--; itemFolder = Collection.GetById(itemJsonFolder.ToString());
using var file = new StreamReader(foldersFilePath);
var data = JArray.Parse(file.ReadToEnd());
foreach (var pack in data)
{
var folder = new CompendiumFolder((JObject)pack) { Parent = parent };
Collection.Add(folder);
}
} }
yield return new LoaderEnrty(file, itemJson, itemFolder ?? folder);
} }
// foreach (var folder in Collection)
// {
// Console.WriteLine(folder);
// }
Collection = Collection.Filter();
Console.WriteLine($"Total items: {ItemCount}");
}
public IEnumerable<LoaderEnrty> Enrties()
{
foreach (var folder in Collection.FindAll(f => f.Path is not null))
{
var dirPath = Path.Combine(Importer.Dir, folder.Path!);
foreach (var filePath in Directory.GetFiles(dirPath, "*.json", SearchOption.AllDirectories))
{
var file = new FileInfo(filePath);
if (file.Name == "_folders.json")
{
continue;
}
using var reader = new StreamReader(file.FullName);
var itemJson = JObject.Parse(reader.ReadToEnd());
var itemFolder = folder;
if (itemJson.TryGetValue("folder", out var itemJsonFolder))
{
itemFolder = Collection.GetById(itemJsonFolder.ToString());
}
yield return new LoaderEnrty(file, itemJson, itemFolder ?? folder);
}
}
}
public class LoaderEnrty(FileInfo file, JObject itemJson, CompendiumFolder folder)
{
public FileInfo File { get; } = file;
public JObject Json { get; } = itemJson;
public CompendiumFolder Folder { get; } = folder;
public string Type { get; } = itemJson["type"]?.ToString()
?? throw new Exception($"Expected to have `type` field in file {file.FullName}");
} }
} }
public class LoaderEnrty(FileInfo file, JObject itemJson, CompendiumFolder folder)
{
public FileInfo File { get; } = file;
public JObject Json { get; } = itemJson;
public CompendiumFolder Folder { get; } = folder;
public string Type { get; } = itemJson["type"]?.ToString()
?? throw new Exception($"Expected to have `type` field in file {file.FullName}");
}
} }
@@ -1,104 +1,97 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NpgsqlTypes; using NpgsqlTypes;
using Pf2eDataScrapper.Model; using Pf2eDataScrapper.Model;
using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum; using Pf2eDataScrapper.Model.Enum;
using System.ComponentModel;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder;
internal abstract class AbstractProcessor(JObject input)
{ {
internal abstract class AbstractProcessor(JObject input) public readonly Pf2eTestContext Context = new();
public readonly JObject Input = input;
protected DataBuilderHelper Helper = DataBuilderHelper.Instance;
public Entity Entity { get; set; } = new();
public abstract EntityType Type { get; }
public abstract IEntity Object { get; }
public virtual void Process()
{ {
public readonly Pf2eTestContext Context = new(); Entity.Id = Helper.NextId;
Entity.Type = Type;
public readonly JObject Input = input; Entity.Name = GetRequired<string>("name");
Entity.Img = GetRequired<string>("img");
Entity.Description = GetRequired<string>("system.description.value");
protected DataBuilderHelper Helper = DataBuilderHelper.Instance; Entity.IsTranslated = false;
Entity.IsChecked = false;
public Entity Entity { get; set; } = new Entity(); Entity.Slug = Helper.GenerateSlug(Entity.Name);
public abstract EntityType Type { get; } Object.Id = Entity.Id;
public abstract IEntity Object { get; } Context.Entities.Add(Entity);
public virtual void Process() // TODO Traits, Alias
}
public void Save()
{
Context.SaveChanges();
}
public 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");
}
public T? GetOptional<T>(string path)
{
JToken? token = Input.SelectToken(path) ?? null;
return token is null ? default! : token.ToObject<T>() ?? default;
}
public T EnumRequired<T>(string path) where T : Enum
{
return EnumOptional<T>(path) ?? throw new Exception($"Can't resolve Enum value for {typeof(T).Name} in path {path}");
}
public T? EnumOptional<T>(string path) where T : Enum
{
string? value = GetOptional<string>(path);
if (value == null)
{ {
Entity.Id = Helper.NextId;
Entity.Type = Type;
Entity.Name = GetRequired<string>("name");
Entity.Img = GetRequired<string>("img");
Entity.Description = GetRequired<string>("system.description.value");
Entity.DescriptionGm = GetOptional<string>("system.description.gm");
Entity.IsTranslated = false;
Entity.IsChecked = false;
Entity.Slug = Helper.GenerateSlug(Entity.Name);
Object.Id = Entity.Id;
Context.Entities.Add(Entity);
// TODO Traits, Alias
}
public void Save()
{
Context.SaveChanges();
}
public T GetRequired<T>(string path)
{
var token = Input.SelectToken(path) ?? throw new NullReferenceException($"'{path}' is null");
return token.ToObject<T>() ?? throw new NullReferenceException($"'{path}' is null");
}
public T? GetOptional<T>(string path)
{
var token = Input.SelectToken(path) ?? null;
if (token is null)
{
return default!;
}
return token.ToObject<T>() ?? default;
}
public T EnumRequired<T>(string path) where T : Enum
{
return EnumOptional<T>(path) ?? throw new Exception($"Can't resolve Enum value for {typeof(T).Name} in path {path}");
}
public T? EnumOptional<T>(string path) where T : Enum
{
string? value = GetOptional<string>(path);
if (value == null)
{
return default;
}
foreach (var field in typeof(T).GetFields())
{
if (Attribute.GetCustomAttribute(field, typeof(PgNameAttribute)) is PgNameAttribute attribute)
{
if (attribute.PgName == value)
{
return (T)field.GetValue(null);
}
}
else
{
if (field.Name == value)
{
return (T)field.GetValue(null);
}
}
}
return default; return default;
} }
foreach (System.Reflection.FieldInfo field in typeof(T).GetFields())
{
if (Attribute.GetCustomAttribute(field, typeof(PgNameAttribute)) is PgNameAttribute attribute)
{
if (attribute.PgName == value)
{
return (T) field.GetValue(null);
}
}
else
{
if (field.Name == value)
{
return (T) field.GetValue(null);
}
}
}
return default;
} }
} }
@@ -1,28 +1,27 @@
using Slugify; using Slugify;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder;
internal class DataBuilderHelper
{ {
internal class DataBuilderHelper public Pf2eDatabase Database = Pf2eDatabase.Instance;
public SlugHelper Slugger = new();
private DataBuilderHelper()
{ {
public Pf2eDatabase Database = Pf2eDatabase.Instance;
public SlugHelper Slugger = new();
private DataBuilderHelper()
{
}
private int _id = 0;
public int NextId { get => ++_id; }
public string GenerateSlug(string name)
{
return Slugger.GenerateSlug(name);
}
public readonly static DataBuilderHelper Instance = new();
} }
private int _id = 0;
public int NextId { get => ++_id; }
public string GenerateSlug(string name)
{
return Slugger.GenerateSlug(name);
}
public static readonly DataBuilderHelper Instance = new();
} }
@@ -3,84 +3,78 @@ using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium; using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
using System.Reflection; using System.Reflection;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder;
internal class PF2eFoundryDataBuilder
{ {
internal class PF2eFoundryDataBuilder public Pf2eDatabase Database = Pf2eDatabase.Instance;
/// <summary>
/// Загрузчик данных.
/// </summary>
public readonly CompendiumLoader Loader;
/// <summary>
/// Коллекция папок компендиума.
/// </summary>
public CompendiumFolderCollection Collection => Loader.Collection;
public Dictionary<string, ConstructorInfo> Processors = [];
public PF2eFoundryDataBuilder(CompendiumLoader loader)
{ {
public Pf2eDatabase Database = Pf2eDatabase.Instance; Loader = loader;
PrepareProcessors();
}
/// <summary> public void PrepareProcessors()
/// Загрузчик данных. {
/// </summary> foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
public readonly CompendiumLoader Loader;
/// <summary>
/// Коллекция папок компендиума.
/// </summary>
public CompendiumFolderCollection Collection => Loader.Collection;
public Dictionary<string, ConstructorInfo> Processors = [];
public PF2eFoundryDataBuilder(CompendiumLoader loader)
{ {
Loader = loader; if (type.GetCustomAttributes(typeof(Pf2eTypeProcessor), true).Length <= 0)
PrepareProcessors();
}
public void PrepareProcessors()
{
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
{ {
if (type.GetCustomAttributes(typeof(Pf2eTypeProcessor), true).Length <= 0) continue;
{
continue;
}
var attribute = type.GetCustomAttribute<Pf2eTypeProcessor>();
Processors.Add(attribute!.Type, type.GetConstructor(new[] { typeof(JObject) })!);
} }
} Pf2eTypeProcessor? attribute = type.GetCustomAttribute<Pf2eTypeProcessor>();
Processors.Add(attribute!.Type, type.GetConstructor([typeof(JObject)])!);
public AbstractProcessor? GetProcessor(string type, JObject input)
{
if (Processors.TryGetValue(type, out var constructor))
{
return (AbstractProcessor)constructor.Invoke([input]);
}
return null;
}
public void TruncateTables()
{
Console.WriteLine("Truncating all old data...");
Database.ExecuteCommand("SELECT f_truncate_tables();");
Console.WriteLine("Truncation completed!");
}
public void Build()
{
Console.WriteLine("Building data...");
var bar = new ProgressBar(Loader.ItemCount);
var i = 0;
foreach (var entry in Loader.Enrties())
{
bar.Refresh(i++, $"{entry.Folder.Name}/{entry.File.Name}");
var processor = GetProcessor($"{entry.Folder.Type}.{entry.Type}", entry.Json);
if (processor is null)
{
continue;
}
processor.Process();
processor.Save();
}
bar.Refresh(Loader.ItemCount, "Done!");
} }
} }
public AbstractProcessor? GetProcessor(string type, JObject input)
{
return Processors.TryGetValue(type, out ConstructorInfo? constructor) ? (AbstractProcessor) constructor.Invoke([input]) : null;
}
public void TruncateTables()
{
Console.WriteLine("Truncating all old data...");
Database.ExecuteCommand("SELECT f_truncate_tables();");
Console.WriteLine("Truncation completed!");
}
public void Build()
{
Console.WriteLine("Building data...");
ProgressBar bar = new(Loader.ItemCount);
int i = 0;
foreach (CompendiumLoader.LoaderEnrty entry in Loader.Enrties())
{
bar.Refresh(i++, $"{entry.Folder.Name}/{entry.File.Name}");
AbstractProcessor? processor = GetProcessor($"{entry.Folder.Type}.{entry.Type}", entry.Json);
if (processor is null)
{
continue;
}
processor.Process();
processor.Save();
}
bar.Refresh(Loader.ItemCount, "Done!");
}
} }
@@ -1,10 +1,9 @@
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder;
{
[AttributeUsage(AttributeTargets.Class)]
internal class Pf2eTypeProcessor(string type, int priority = 0) : Attribute
{
public readonly string Type = type;
public readonly int Priority = priority; [AttributeUsage(AttributeTargets.Class)]
} internal class Pf2eTypeProcessor(string type, int priority = 0) : Attribute
{
public readonly string Type = type;
public readonly int Priority = priority;
} }
@@ -2,32 +2,31 @@
using Pf2eDataScrapper.Model; using Pf2eDataScrapper.Model;
using Pf2eDataScrapper.Model.Enum; using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder.Processors namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.DataBuilder.Processors;
[Pf2eTypeProcessor("Item.condition")]
internal class ConditionProcessor(JObject input) : AbstractProcessor(input)
{ {
[Pf2eTypeProcessor("Item.condition")] public override EntityType Type => EntityType.Condition;
internal class ConditionProcessor(JObject input) : AbstractProcessor(input)
public override Condition Object { get; } = new();
public override void Process()
{ {
public override EntityType Type => EntityType.Condition; base.Process();
public override Condition Object { get; } = new(); JObject extra = new()
public override void Process()
{ {
base.Process(); { "duration", Input.SelectToken("system.duration") },
{ "overrides", Input.SelectToken("system.overrides") },
{ "references", Input.SelectToken("system.references") },
{ "value", Input.SelectToken("system.value") }
};
JObject extra = new() Object.Extra = extra.ToString(Newtonsoft.Json.Formatting.Indented);
{
{ "duration", Input.SelectToken("system.duration") },
{ "overrides", Input.SelectToken("system.overrides") },
{ "references", Input.SelectToken("system.references") },
{ "value", Input.SelectToken("system.value") }
};
Object.Extra = extra.ToString(Newtonsoft.Json.Formatting.Indented); Object.Group = EnumOptional<ConditionGroup>("system.group");
Object.Group = EnumOptional<ConditionGroup>("system.group"); Context.Conditions.Add(Object);
Context.Conditions.Add(Object);
}
} }
} }
@@ -2,66 +2,65 @@
using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.SchemaBuilder; using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.SchemaBuilder;
using Pf2eDataScrapper.Importers.Repositories; using Pf2eDataScrapper.Importers.Repositories;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry;
/// <summary>
/// Импортёр для модуля Pf2e FoundryVTT.
/// </summary>
internal class Pf2EFoundryImporter : AbstractImporter
{ {
/// <summary> /// <summary>
/// Импортёр для модуля Pf2e FoundryVTT. /// Отображаемое имя импортёра.
/// </summary> /// </summary>
internal class Pf2EFoundryImporter : AbstractImporter public override string Name => "Pf2e FoundryVTT module";
/// <summary>
/// Короткое имя импортёра.
/// </summary>
public override string Slug => "pf2e";
public override AbstractImporterRepository Repository => new GitRepository(this, "https://github.com/foundryvtt/pf2e.git");
/// <summary>
/// Загрузчик данных компендиума.
/// </summary>
public CompendiumLoader Loader { get; }
/// <summary>
/// Конструктор импортёра.
/// </summary>
public Pf2EFoundryImporter()
{ {
/// <summary> Loader = new CompendiumLoader(this);
/// Отображаемое имя импортёра. }
/// </summary>
public override string Name => "Pf2e FoundryVTT module";
/// <summary> /// <summary>
/// Короткое имя импортёра. /// Метод подготовки данных для импорта.
/// </summary> /// </summary>
public override string Slug => "pf2e"; public override void Prepare()
{
Loader.Prepare();
}
public override AbstractImporterRepository Repository => new GitRepository(this, "https://github.com/foundryvtt/pf2e.git"); /// <summary>
/// Импорт данных.
/// </summary>
public override void Import()
{
PF2eFoundryDataBuilder builder = new(Loader);
/// <summary> builder.TruncateTables();
/// Загрузчик данных компендиума. builder.Build();
/// </summary> }
public CompendiumLoader Loader { get; }
/// <summary> /// <summary>
/// Конструктор импортёра. /// Построение схем данных
/// </summary> /// </summary>
public Pf2EFoundryImporter() public override void BuildSchemas()
{ {
Loader = new CompendiumLoader(this); CompendiumSchemaBuilder builder = new(Loader);
}
/// <summary> builder.BuildSchemas();
/// Метод подготовки данных для импорта. builder.Save(EnvSettings.OutputDir);
/// </summary>
public override void Prepare()
{
Loader.Prepare();
}
/// <summary>
/// Импорт данных.
/// </summary>
public override void Import()
{
var builder = new PF2eFoundryDataBuilder(Loader);
builder.TruncateTables();
builder.Build();
}
/// <summary>
/// Построение схем данных
/// </summary>
public override void BuildSchemas()
{
var builder = new CompendiumSchemaBuilder(Loader);
builder.BuildSchemas();
builder.Save(EnvSettings.OutputDir);
}
} }
} }
@@ -2,191 +2,185 @@
using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium; using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.Compendium;
using Pf2eDataScrapper.SchemaBuilder; using Pf2eDataScrapper.SchemaBuilder;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.SchemaBuilder namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.SchemaBuilder;
/// <summary>
/// Класс построения схемы компендиумов.
/// </summary>
/// <param name="loader">Загрузчик данных.</param>
internal class CompendiumSchemaBuilder(CompendiumLoader loader)
{ {
/// <summary> /// <summary>
/// Класс построения схемы компендиумов. /// Загрузчик данных.
/// </summary> /// </summary>
/// <param name="loader">Загрузчик данных.</param> public CompendiumLoader Loader = loader;
internal class CompendiumSchemaBuilder(CompendiumLoader loader)
/// <summary>
/// Коллекция папок компендиума.
/// </summary>
public CompendiumFolderCollection Collection = loader.Collection;
/// <summary>
/// Объект для определения типов полей в компендиумах PF2E.
/// </summary>
public CompendiumSchemaTypeResolver Resolver = new();
/// <summary>
/// Схемы данных по папкам.
/// </summary>
public Dictionary<CompendiumFolder, SchemaNode> PackSchemas = [];
/// <summary>
/// Схемы данных по типам.
/// </summary>
public Dictionary<string, SchemaNode> TypeSchemas = [];
private readonly string[] _equipmentTypes = ["equipment", "armor", "backpack", "consumable", "kit", "shield", "treasure", "weapon"];
private SchemaNode GetPackSchema(CompendiumFolder folder)
{ {
/// <summary> if (!PackSchemas.TryGetValue(folder, out SchemaNode? value))
/// Загрузчик данных.
/// </summary>
public CompendiumLoader Loader = loader;
/// <summary>
/// Коллекция папок компендиума.
/// </summary>
public CompendiumFolderCollection Collection = loader.Collection;
/// <summary>
/// Объект для определения типов полей в компендиумах PF2E.
/// </summary>
public CompendiumSchemaTypeResolver Resolver = new();
/// <summary>
/// Схемы данных по папкам.
/// </summary>
public Dictionary<CompendiumFolder, SchemaNode> PackSchemas = [];
/// <summary>
/// Схемы данных по типам.
/// </summary>
public Dictionary<string, SchemaNode> TypeSchemas = [];
private readonly string[] _equipmentTypes = ["equipment", "armor", "backpack", "consumable", "kit", "shield", "treasure", "weapon"];
private SchemaNode GetPackSchema(CompendiumFolder folder)
{ {
if (!PackSchemas.TryGetValue(folder, out SchemaNode? value)) value = new SchemaNode()
{ {
value = new SchemaNode() Resolver = Resolver,
{ Path = folder.Name
Resolver = Resolver, };
Path = folder.Name PackSchemas[folder] = value;
};
PackSchemas[folder] = value;
}
return value;
} }
private SchemaNode GetTypeSchema(string key) return value;
{ }
if (!TypeSchemas.TryGetValue(key, out SchemaNode? value))
{
value = new SchemaNode()
{
Resolver = Resolver,
Path = key
};
TypeSchemas[key] = value;
}
return value; private SchemaNode GetTypeSchema(string key)
{
if (!TypeSchemas.TryGetValue(key, out SchemaNode? value))
{
value = new SchemaNode()
{
Resolver = Resolver,
Path = key
};
TypeSchemas[key] = value;
} }
/// <summary> return value;
/// Получение набора схем, к которым относится проверяемый объект. }
/// </summary>
/// <param name="folder">Папка компендиума, в которой находится объект.</param>
/// <param name="itemType">Указанный тип объекта.</param>
/// <returns></returns>
protected List<SchemaNode> GetItemSchemas(CompendiumFolder folder, string? itemType)
{
var key = folder.Type.ToString() + (itemType is not null ? $".{itemType}" : "");
if (_equipmentTypes.Contains(itemType)) /// <summary>
/// Получение набора схем, к которым относится проверяемый объект.
/// </summary>
/// <param name="folder">Папка компендиума, в которой находится объект.</param>
/// <param name="itemType">Указанный тип объекта.</param>
/// <returns></returns>
protected List<SchemaNode> GetItemSchemas(CompendiumFolder folder, string? itemType)
{
string key = folder.Type.ToString() + (itemType is not null ? $".{itemType}" : "");
return _equipmentTypes.Contains(itemType)
? [GetPackSchema(folder), GetTypeSchema(key), GetTypeSchema("Equipment")]
: [GetPackSchema(folder), GetTypeSchema(key)];
}
/// <summary>
/// Построение схем данных.
/// </summary>
public void BuildSchemas()
{
Console.WriteLine("Building schemas...");
ProgressBar bar = new(Loader.ItemCount);
int i = 0;
foreach (CompendiumLoader.LoaderEnrty entry in Loader.Enrties())
{
bar.Refresh(i++, entry.Folder.Name);
List<SchemaNode> schemas = GetItemSchemas(entry.Folder, entry.Type);
foreach (SchemaNode schema in schemas)
{ {
return [GetPackSchema(folder), GetTypeSchema(key), GetTypeSchema("Equipment")]; schema.AddValue(entry.Json);
}
else
{
return [GetPackSchema(folder), GetTypeSchema(key)];
} }
} }
/// <summary> bar.Refresh(Loader.ItemCount, "Done!");
/// Построение схем данных. }
/// </summary>
public void BuildSchemas() /// <summary>
/// Сохранение данных.
/// </summary>
/// <param name="targetDir">Папка для сохранения.</param>
public void Save(string targetDir)
{
Console.WriteLine("Dumping schemas...");
ProgressBar bar = new(PackSchemas.Count + TypeSchemas.Count);
int i = 0;
targetDir = Path.Combine(targetDir, Loader.Importer.Slug);
if (!Directory.Exists(targetDir))
{ {
Console.WriteLine("Building schemas..."); Directory.CreateDirectory(targetDir);
}
Array.ForEach(Directory.GetFiles(@targetDir), File.Delete);
var bar = new ProgressBar(Loader.ItemCount); // Packs
var i = 0; string packsDir = Path.Combine(targetDir, "packs");
Directory.CreateDirectory(packsDir);
foreach (var entry in Loader.Enrties()) foreach (KeyValuePair<CompendiumFolder, SchemaNode> pair in PackSchemas)
{ {
bar.Refresh(i++, entry.Folder.Name); bar.Refresh(i++, pair.Key.Name);
SaveSchema(packsDir, pair.Key.Name, pair.Value);
var schemas = GetItemSchemas(entry.Folder, entry.Type);
foreach (var schema in schemas)
{
schema.AddValue(entry.Json);
}
}
bar.Refresh(Loader.ItemCount, "Done!");
} }
/// <summary> // Types
/// Сохранение данных. string typesDir = Path.Combine(targetDir, "types");
/// </summary> Directory.CreateDirectory(typesDir);
/// <param name="targetDir">Папка для сохранения.</param> foreach (KeyValuePair<string, SchemaNode> pair in TypeSchemas)
public void Save(string targetDir)
{ {
Console.WriteLine("Dumping schemas..."); bar.Refresh(i++, pair.Key);
SaveSchema(typesDir, pair.Key, pair.Value);
var bar = new ProgressBar(PackSchemas.Count + TypeSchemas.Count);
var i = 0;
targetDir = Path.Combine(targetDir, Loader.Importer.Slug);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
Array.ForEach(Directory.GetFiles(@targetDir), File.Delete);
// Packs
var packsDir = Path.Combine(targetDir, "packs");
Directory.CreateDirectory(packsDir);
foreach (var pair in PackSchemas)
{
bar.Refresh(i++, pair.Key.Name);
SaveSchema(packsDir, pair.Key.Name, pair.Value);
}
// Types
var typesDir = Path.Combine(targetDir, "types");
Directory.CreateDirectory(typesDir);
foreach (var pair in TypeSchemas)
{
bar.Refresh(i++, pair.Key);
SaveSchema(typesDir, pair.Key, pair.Value);
}
Console.WriteLine("Done!");
} }
/// <summary> Console.WriteLine("Done!");
/// Сохранение конкретной схемы. }
/// </summary>
/// <param name="dir">Корневая папка для сохранения этой категории схем.</param> /// <summary>
/// <param name="name">Название сохраняемой схемы.</param> /// Сохранение конкретной схемы.
/// <param name="schema">Сохраняемая схема.</param> /// </summary>
protected void SaveSchema(string dir, string name, SchemaNode schema) /// <param name="dir">Корневая папка для сохранения этой категории схем.</param>
/// <param name="name">Название сохраняемой схемы.</param>
/// <param name="schema">Сохраняемая схема.</param>
protected void SaveSchema(string dir, string name, SchemaNode schema)
{
char[] invalids = Path.GetInvalidFileNameChars();
string fileName = string.Join("_", schema.Path.Split(invalids, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.');
string path = Path.Combine(dir, fileName + ".yaml");
string? dirPath = Path.GetDirectoryName(path);
if (dirPath is not null && !Directory.Exists(dirPath))
{ {
var invalids = Path.GetInvalidFileNameChars(); Directory.CreateDirectory(dirPath);
var fileName = string.Join("_", schema.Path.Split(invalids, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.');
var path = Path.Combine(dir, fileName + ".yaml");
var dirPath = Path.GetDirectoryName(path);
if (dirPath is not null && !Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
using var writer = new StreamWriter(path);
schema.Beautify();
writer.WriteLine(SchemaNode.FormatYaml(new Dictionary<string, string>()
{
["name"] = name,
["values"] = schema.Values.ToString()
}, schema.Types[0].GetYamlData()));
//writer.WriteLine(name);
//writer.WriteLine($"Different values: {schema.Values}");
//writer.WriteLine("=====================================");
//writer.Write(schema.ToString());
} }
using StreamWriter writer = new(path);
schema.Beautify();
writer.WriteLine(SchemaNode.FormatYaml(new Dictionary<string, string>()
{
["name"] = name,
["values"] = schema.Values.ToString()
}, schema.Types[0].GetYamlData()));
//writer.WriteLine(name);
//writer.WriteLine($"Different values: {schema.Values}");
//writer.WriteLine("=====================================");
//writer.Write(schema.ToString());
} }
} }
@@ -2,61 +2,52 @@
using Pf2eDataScrapper.SchemaBuilder; using Pf2eDataScrapper.SchemaBuilder;
using Pf2eDataScrapper.SchemaBuilder.Types; using Pf2eDataScrapper.SchemaBuilder.Types;
namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.SchemaBuilder namespace Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry.SchemaBuilder;
internal class CompendiumSchemaTypeResolver : TypeResolver
{ {
internal class CompendiumSchemaTypeResolver : TypeResolver public class FoundryRulesSchemaType(SchemaNode parent) : IgnoreSchemaType(parent)
{ {
public class FoundryRulesSchemaType(SchemaNode parent) : IgnoreSchemaType(parent) public override string GetTypeName()
{ {
public override string GetTypeName() return "ignored (Foundry rules object)";
{
return "ignored (Foundry rules object)";
}
}
protected readonly List<string> DictionaryPaths = [
"Actor.character.items.[].system.items",
"Actor.character.items.[].system.overlays",
"Item.ancestry.system.items",
"Item.ancestry.system.boosts",
"Item.background.system.boosts",
"Item.background.system.items",
"Item.class.system.items",
"Item.consumable.system.spell.system.damage",
"Item.consumable.system.spell.system.overlays",
"Item.deity.system.spells",
"Item.feat.system.subfeatures.proficiencies",
"Item.kit.system.items",
"Item.kit.system.items.[value].items",
"Item.spell.system.damage",
"Item.spell.system.heightening.damage",
"Item.spell.system.heightening.levels",
"Item.spell.system.heightening.levels.[value].damage",
"Item.spell.system.overlays",
"Item.spell.system.overlays.[value].system.damage",
"Item.spell.system.overlays.[value].system.heightening",
"Item.spell.system.overlays.[value].system.heightening.[value]"
];
/// <summary>
/// Частное определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных. `null`, что бы использовать значение по умолчанию</returns>
protected override AbstractSchemaType? CustomResolve(SchemaNode node, JToken? value)
{
if (CheckPath(node, DictionaryPaths))
{
return new DictionarySchemaType(node);
}
if (node.Path.EndsWith(".rules"))
{
return new FoundryRulesSchemaType(node);
}
return null;
} }
} }
protected readonly List<string> DictionaryPaths = [
"Actor.character.items.[].system.items",
"Actor.character.items.[].system.overlays",
"Item.ancestry.system.items",
"Item.ancestry.system.boosts",
"Item.background.system.boosts",
"Item.background.system.items",
"Item.class.system.items",
"Item.consumable.system.spell.system.damage",
"Item.consumable.system.spell.system.overlays",
"Item.deity.system.spells",
"Item.feat.system.subfeatures.proficiencies",
"Item.kit.system.items",
"Item.kit.system.items.[value].items",
"Item.spell.system.damage",
"Item.spell.system.heightening.damage",
"Item.spell.system.heightening.levels",
"Item.spell.system.heightening.levels.[value].damage",
"Item.spell.system.overlays",
"Item.spell.system.overlays.[value].system.damage",
"Item.spell.system.overlays.[value].system.heightening",
"Item.spell.system.overlays.[value].system.heightening.[value]"
];
/// <summary>
/// Частное определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных. `null`, что бы использовать значение по умолчанию</returns>
protected override AbstractSchemaType? CustomResolve(SchemaNode node, JToken? value)
{
return CheckPath(node, DictionaryPaths)
? new DictionarySchemaType(node)
: node.Path.EndsWith(".rules") ? new FoundryRulesSchemaType(node) : (AbstractSchemaType?) null;
}
} }
@@ -1,13 +1,12 @@
namespace Pf2eDataScrapper.Importers.Repositories namespace Pf2eDataScrapper.Importers.Repositories;
internal abstract class AbstractImporterRepository(AbstractImporter importer)
{ {
internal abstract class AbstractImporterRepository (AbstractImporter importer) public readonly AbstractImporter Importer = importer;
{
public readonly AbstractImporter Importer = importer;
public string RepositoryDirPath => Importer.Dir; public string RepositoryDirPath => Importer.Dir;
public DirectoryInfo RepositoryDir => new(RepositoryDirPath); public DirectoryInfo RepositoryDir => new(RepositoryDirPath);
public abstract void Update(); public abstract void Update();
}
} }
@@ -1,52 +1,50 @@
using System.Data.Common; using System.Diagnostics;
using System.Diagnostics;
namespace Pf2eDataScrapper.Importers.Repositories namespace Pf2eDataScrapper.Importers.Repositories;
/// <summary>
/// Загрузчик данных из git-репозиториев
/// </summary>
/// <param name="repositoryDirPath"></param>
internal class GitRepository(AbstractImporter importer, string url) : AbstractImporterRepository(importer)
{ {
public readonly string Url = url;
/// <summary> /// <summary>
/// Загрузчик данных из git-репозиториев /// Обновление импортёра
/// </summary> /// </summary>
/// <param name="repositoryDirPath"></param> public override void Update()
internal class GitRepository(AbstractImporter importer, string url) : AbstractImporterRepository(importer)
{ {
public readonly string Url = url; string gitPath = Path.Combine(RepositoryDirPath, importer.Slug);
/// <summary> if (Directory.Exists(gitPath))
/// Обновление импортёра
/// </summary>
public override void Update()
{ {
var gitPath = Path.Combine(RepositoryDirPath, importer.Slug); // git pull
Process process = new()
if (Directory.Exists(gitPath))
{ {
// git pull StartInfo = new ProcessStartInfo()
var process = new Process
{ {
StartInfo = new ProcessStartInfo() FileName = "git",
{ WorkingDirectory = gitPath,
FileName = "git", Arguments = "pull",
WorkingDirectory = gitPath, }
Arguments = "pull", };
} process.Start();
}; }
process.Start(); else
} {
else // git clone
Process process = new()
{ {
// git clone StartInfo = new ProcessStartInfo()
var process = new Process
{ {
StartInfo = new ProcessStartInfo() FileName = "git",
{ WorkingDirectory = RepositoryDirPath,
FileName = "git", Arguments = $"clone {Url} {Importer.Slug}",
WorkingDirectory = RepositoryDirPath, }
Arguments = $"clone {Url} {Importer.Slug}", };
} process.Start();
};
process.Start();
}
} }
} }
} }
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
+11 -12
View File
@@ -1,19 +1,18 @@
using NpgsqlTypes; using NpgsqlTypes;
namespace Pf2eDataScrapper.Model.Enum namespace Pf2eDataScrapper.Model.Enum;
public enum EntityRarity
{ {
public enum EntityRarity [PgName("common")]
{ Common,
[PgName("common")]
Common,
[PgName("uncommon")] [PgName("uncommon")]
Uncommon, Uncommon,
[PgName("rare")] [PgName("rare")]
Rare, Rare,
[PgName("unique")] [PgName("unique")]
Unique Unique
}
} }
+8 -9
View File
@@ -1,13 +1,12 @@
using NpgsqlTypes; using NpgsqlTypes;
namespace Pf2eDataScrapper.Model.Enum namespace Pf2eDataScrapper.Model.Enum;
{
public enum TraitType
{
[PgName("regular")]
Regular,
[PgName("tradition")] public enum TraitType
Tradition {
} [PgName("regular")]
Regular,
[PgName("tradition")]
Tradition
} }
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
+5 -6
View File
@@ -1,12 +1,11 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
namespace Pf2eDataScrapper.Model namespace Pf2eDataScrapper.Model;
internal interface IEntity
{ {
internal interface IEntity public int Id { get; set; }
{
public int Id { get; set; }
public Entity Entity { get; set; } public Entity Entity { get; set; }
}
} }
-1
View File
@@ -1,5 +1,4 @@
using Pf2eDataScrapper.Model.Common; using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum;
namespace Pf2eDataScrapper.Model; namespace Pf2eDataScrapper.Model;
+114 -115
View File
@@ -1,123 +1,122 @@
using Npgsql; using Npgsql;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Pf2eDataScrapper namespace Pf2eDataScrapper;
public partial class Pf2eDatabase
{ {
public partial class Pf2eDatabase public NpgsqlConnection Connection;
public readonly string Username;
public readonly string DatabaseName;
private Pf2eDatabase()
{ {
public NpgsqlConnection Connection; Username = UsernamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
DatabaseName = DatabaseNamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
public readonly string Username; Connection = new NpgsqlConnection(EnvSettings.DatabaseConnection);
public readonly string DatabaseName; Connection.Open();
private Pf2eDatabase()
{
Username = UsernamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
DatabaseName = DatabaseNamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
Connection = new NpgsqlConnection(EnvSettings.DatabaseConnection);
Connection.Open();
}
~Pf2eDatabase()
{
Connection.Close();
}
public static Pf2eDatabase Instance { get; } = new();
public NpgsqlTransaction? Transaction { get; private set; }
public void StartTransaction()
{
if (Transaction != null)
{
throw new InvalidOperationException("Transaction already started.");
}
Transaction = Connection.BeginTransaction();
}
public void Commit()
{
if (Transaction == null)
{
throw new InvalidOperationException("No transaction started.");
}
Transaction.Commit();
}
public void ExecuteCommand(string sql)
{
var command = GetCommand(sql);
command.ExecuteNonQuery();
}
public NpgsqlCommand GetCommand(string sql)
{
return new NpgsqlCommand(sql, Connection);
}
/*
public void RebuildDatabase()
{
// Закрываем текущее соединение
Connection.Close();
// Получение имён
var databaseNamePattern = DatabaseNamePattern();
var userConnection = databaseNamePattern.Replace(EnvSettings.DatabaseConnection, ";");
var databaseName = databaseNamePattern.Match(EnvSettings.DatabaseConnection).Groups[1].Value;
var username = UserNamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
// Временное подключение к базе данных
NpgsqlConnection tmpConnection = new(userConnection);
tmpConnection.Open();
// Удаляем базу данных
NpgsqlCommand dropCommand = new($"DROP DATABASE \"{databaseName}\";", tmpConnection);
dropCommand.ExecuteNonQuery();
// Создаём новую базу данных
NpgsqlCommand createCommand = new($@"
CREATE DATABASE {databaseName}
WITH OWNER = {username}
ENCODING = 'UTF8'
CONNECTION LIMIT = -1;
", tmpConnection);
createCommand.ExecuteNonQuery();
string[] permissionCommands = [
$"GRANT TEMPORARY, CONNECT ON DATABASE {databaseName} TO PUBLIC;",
$"GRANT ALL ON DATABASE {databaseName} TO {username} WITH GRANT OPTION;",
$"GRANT ALL ON DATABASE {databaseName} TO postgres;"
];
foreach (var command in permissionCommands)
{
NpgsqlCommand permissionCommand = new(command, tmpConnection);
permissionCommand.ExecuteNonQuery();
}
// Закрываем временное соединение
tmpConnection.Close();
// Заново создаём соединение
Connection = new NpgsqlConnection(EnvSettings.DatabaseConnection);
}
*/
[GeneratedRegex(@"(?:^|;)Database=(.*?)(?:$|;)")]
private static partial Regex DatabaseNamePattern();
[GeneratedRegex(@"(?:^|;)User=(.*?)(?:$|;)")]
private static partial Regex UsernamePattern();
} }
~Pf2eDatabase()
{
Connection.Close();
}
public static Pf2eDatabase Instance { get; } = new();
public NpgsqlTransaction? Transaction { get; private set; }
public void StartTransaction()
{
if (Transaction != null)
{
throw new InvalidOperationException("Transaction already started.");
}
Transaction = Connection.BeginTransaction();
}
public void Commit()
{
if (Transaction == null)
{
throw new InvalidOperationException("No transaction started.");
}
Transaction.Commit();
}
public void ExecuteCommand(string sql)
{
NpgsqlCommand command = GetCommand(sql);
command.ExecuteNonQuery();
}
public NpgsqlCommand GetCommand(string sql)
{
return new NpgsqlCommand(sql, Connection);
}
/*
public void RebuildDatabase()
{
// Закрываем текущее соединение
Connection.Close();
// Получение имён
var databaseNamePattern = DatabaseNamePattern();
var userConnection = databaseNamePattern.Replace(EnvSettings.DatabaseConnection, ";");
var databaseName = databaseNamePattern.Match(EnvSettings.DatabaseConnection).Groups[1].Value;
var username = UserNamePattern().Match(EnvSettings.DatabaseConnection).Groups[1].Value;
// Временное подключение к базе данных
NpgsqlConnection tmpConnection = new(userConnection);
tmpConnection.Open();
// Удаляем базу данных
NpgsqlCommand dropCommand = new($"DROP DATABASE \"{databaseName}\";", tmpConnection);
dropCommand.ExecuteNonQuery();
// Создаём новую базу данных
NpgsqlCommand createCommand = new($@"
CREATE DATABASE {databaseName}
WITH OWNER = {username}
ENCODING = 'UTF8'
CONNECTION LIMIT = -1;
", tmpConnection);
createCommand.ExecuteNonQuery();
string[] permissionCommands = [
$"GRANT TEMPORARY, CONNECT ON DATABASE {databaseName} TO PUBLIC;",
$"GRANT ALL ON DATABASE {databaseName} TO {username} WITH GRANT OPTION;",
$"GRANT ALL ON DATABASE {databaseName} TO postgres;"
];
foreach (var command in permissionCommands)
{
NpgsqlCommand permissionCommand = new(command, tmpConnection);
permissionCommand.ExecuteNonQuery();
}
// Закрываем временное соединение
tmpConnection.Close();
// Заново создаём соединение
Connection = new NpgsqlConnection(EnvSettings.DatabaseConnection);
}
*/
[GeneratedRegex(@"(?:^|;)Database=(.*?)(?:$|;)")]
private static partial Regex DatabaseNamePattern();
[GeneratedRegex(@"(?:^|;)User=(.*?)(?:$|;)")]
private static partial Regex UsernamePattern();
} }
+4 -49
View File
@@ -1,9 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Pf2eDataScrapper.Model; using Pf2eDataScrapper.Model;
using Pf2eDataScrapper.Model.Common;
using Pf2eDataScrapper.Model.Enum; using Pf2eDataScrapper.Model.Enum;
using System.ComponentModel;
using System.Reflection;
namespace Pf2eDataScrapper; namespace Pf2eDataScrapper;
@@ -103,8 +101,10 @@ public partial class Pf2eTestContext : DbContext
public virtual DbSet<Trait> Traits { get; set; } public virtual DbSet<Trait> Traits { get; set; }
#endregion #endregion
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
EnvSettings.Load();
optionsBuilder.UseNpgsql(EnvSettings.DatabaseConnection, optionsBuilder.UseNpgsql(EnvSettings.DatabaseConnection,
builder => builder =>
{ {
@@ -115,50 +115,6 @@ public partial class Pf2eTestContext : DbContext
); );
} }
protected string FromEnum<T>(T enumValue) where T : Enum
{
var enumFieldName = enumValue.ToString();
var field = typeof(T).GetField(enumFieldName);
if (field is null)
{
return enumFieldName;
}
var attribute = field.GetCustomAttribute<DescriptionAttribute>();
return attribute is null ? enumFieldName : attribute.Description;
}
protected T ToEnum<T>(string enumValue) where T : Enum
{
var enumType = typeof(T);
foreach (var field in enumType.GetFields())
{
var attribute = field.GetCustomAttribute<DescriptionAttribute>();
if (attribute is null)
{
if (field.Name == enumValue)
{
return (T)field.GetValue(null)!;
}
}
else
{
if (attribute.Description == enumValue)
{
return (T)field.GetValue(null)!;
}
}
}
throw new ArgumentException($"No matching value found for {enumValue} in {enumType.Name}");
}
protected ValueConverter<T, string> GetEnumConverter<T>() where T : Enum
{
return new ValueConverter<T, string>(
v => FromEnum(v),
v => ToEnum<T>(v));
}
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.UseIdentityByDefaultColumns(); modelBuilder.UseIdentityByDefaultColumns();
@@ -198,8 +154,7 @@ public partial class Pf2eTestContext : DbContext
entity.Property(e => e.Id).HasColumnName("id"); entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Description).HasColumnName("description"); entity.Property(e => e.Description).HasColumnName("description");
entity.Property(e => e.DescriptionGm).HasColumnName("description_gm"); entity.Property(e => e.DescriptionGm).HasColumnName("description_gm");
entity.Property(e => e.Type) entity.Property(e => e.Type).HasColumnName("type");
.HasColumnName("type");
entity.Property(e => e.Img) entity.Property(e => e.Img)
.HasMaxLength(150) .HasMaxLength(150)
.HasColumnName("img"); .HasColumnName("img");
+1 -2
View File
@@ -1,10 +1,9 @@
using Pf2eDataScrapper; using Pf2eDataScrapper;
using Pf2eDataScrapper.Importers.Implementations.Aonprd; using Pf2eDataScrapper.Importers.Implementations.Aonprd;
using Pf2eDataScrapper.Importers.Implementations.Pf2eFoundry;
EnvSettings.Load(); EnvSettings.Load();
var importer = new AonprdImporter(); AonprdImporter importer = new AonprdImporter();
importer.Prepare(); importer.Prepare();
importer.BuildSchemas(); importer.BuildSchemas();
+218 -219
View File
@@ -1,244 +1,243 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.SchemaBuilder.Types; using Pf2eDataScrapper.SchemaBuilder.Types;
namespace Pf2eDataScrapper.SchemaBuilder namespace Pf2eDataScrapper.SchemaBuilder;
/// <summary>
/// Элемент схемы данных.
/// </summary>
internal class SchemaNode
{ {
/// <summary> /// <summary>
/// Элемент схемы данных. /// Отображаемое имя элемента.
/// </summary> /// </summary>
internal class SchemaNode public string Name = "";
/// <summary>
/// Путь к элементу.
/// </summary>
public string Path = "";
/// <summary>
/// Родительский элемент.
/// </summary>
public SchemaNode? Parent = null;
/// <summary>
/// Указание, является ли элемент корневым.
/// </summary>
public bool Root = false;
/// <summary>
/// Корневой элемент схемы данных.
/// </summary>
public SchemaNode RootNode => Root ? this : Parent!.RootNode;
/// <summary>
/// Типы данных, использующиеся в элементе.
/// </summary>
public List<AbstractSchemaType> Types = [];
/// <summary>
/// Указание, может ли этот элемент отсутсвовать в структуре данных.
/// </summary>
public bool IsNullable = false;
/// <summary>
/// Объект для определения типа поля в схеме данных.
/// </summary>
public TypeResolver Resolver = new();
/// <summary>
/// Количество значений, добавленных в тип.
/// </summary>
public int Values { get; protected set; } = 0;
/// <summary>
/// Указание, является ли тип элемента простым.
/// </summary>
public bool IsSimple => Types.Count == 1;
/// <summary>
/// Конструктор корневого элемента.
/// </summary>
public SchemaNode()
{ {
/// <summary> this.Root = true;
/// Отображаемое имя элемента. }
/// </summary>
public string Name = "";
/// <summary> /// <summary>
/// Путь к элементу. /// Конструктор элемента схемы данных.
/// </summary> /// </summary>
public string Path = ""; /// <param name="parent">Родительский элемент.</param>
/// <param name="name">Имя элемента.</param>
public SchemaNode(SchemaNode parent, string name)
{
this.Parent = parent;
this.Name = name;
this.Path = parent.Path + "." + name;
this.Resolver = parent.Resolver;
}
/// <summary> /// <summary>
/// Родительский элемент. /// Добавление нового возможного значения.
/// </summary> /// </summary>
public SchemaNode? Parent = null; /// <param name="value">Добавляемое значение</param>
public void AddValue(JToken? value)
/// <summary> {
/// Указание, является ли элемент корневым. if (value is null || Resolver.TokenType(value) == JTokenType.Null)
/// </summary>
public bool Root = false;
/// <summary>
/// Корневой элемент схемы данных.
/// </summary>
public SchemaNode RootNode => Root ? this : Parent!.RootNode;
/// <summary>
/// Типы данных, использующиеся в элементе.
/// </summary>
public List<AbstractSchemaType> Types = [];
/// <summary>
/// Указание, может ли этот элемент отсутсвовать в структуре данных.
/// </summary>
public bool IsNullable = false;
/// <summary>
/// Объект для определения типа поля в схеме данных.
/// </summary>
public TypeResolver Resolver = new();
/// <summary>
/// Количество значений, добавленных в тип.
/// </summary>
public int Values { get; protected set; } = 0;
/// <summary>
/// Указание, является ли тип элемента простым.
/// </summary>
public bool IsSimple => Types.Count == 1;
/// <summary>
/// Конструктор корневого элемента.
/// </summary>
public SchemaNode()
{ {
this.Root = true; IsNullable = true;
return;
} }
/// <summary> Values++;
/// Конструктор элемента схемы данных.
/// </summary>
/// <param name="parent">Родительский элемент.</param>
/// <param name="name">Имя элемента.</param>
public SchemaNode(SchemaNode parent, string name)
{
this.Parent = parent;
this.Name = name;
this.Path = parent.Path + "." + name;
this.Resolver = parent.Resolver;
}
/// <summary> foreach (AbstractSchemaType type in Types)
/// Добавление нового возможного значения.
/// </summary>
/// <param name="value">Добавляемое значение</param>
public void AddValue(JToken? value)
{ {
if (value is null || Resolver.TokenType(value) == JTokenType.Null) if (type.IsCompatibleValue(value))
{ {
IsNullable = true; type.AddValue(value);
return; return;
} }
Values++;
foreach (var type in Types)
{
if (type.IsCompatibleValue(value))
{
type.AddValue(value);
return;
}
}
var newType = Resolver.Resolve(this, value);
if (newType.IsCompatibleValue(value))
{
Types.Add(newType);
newType.AddValue(value);
}
else
{
var fallbackType = Resolver.Resolve(this, value, true);
Types.Add(fallbackType);
fallbackType.AddValue(value);
}
} }
/// <summary> AbstractSchemaType newType = Resolver.Resolve(this, value);
/// Получение строкового представоения типа элемента.
/// </summary> if (newType.IsCompatibleValue(value))
/// <returns>Строковое представоение типа элемента</returns>
public string GetTypeName()
{ {
if (IsSimple) Types.Add(newType);
{ newType.AddValue(value);
return Types[0].GetTypeName();
}
var typeNames = Types.Select(t => t.GetTypeName()).ToList();
return string.Join(" | ", typeNames);
} }
else
/// <summary>
/// Полное строковое представление элемента.
/// </summary>
/// <returns>Строковое представление элемента.</returns>
public override string ToString()
{ {
var result = new Dictionary<string, string>() AbstractSchemaType fallbackType = Resolver.Resolve(this, value, true);
{ Types.Add(fallbackType);
["type"] = GetTypeName(), fallbackType.AddValue(value);
["path"] = Path,
["is_null"] = IsNullable ? "true" : "false",
["values"] = Values.ToString()
};
if (IsSimple)
{
return FormatYaml(result, Types[0].GetYamlData());
}
if (Types.Count == 0)
{
return FormatYaml(result, new() { ["is_empty"] = "true" });
}
List<string> types = [];
foreach (var type in Types)
{
types.Add(Indent(type?.ToString() ?? ""));
}
return FormatYaml(result, new() { ["types"] = "- " + string.Join("\r\n- ", types) });
}
/// <summary>
/// Подготовка элемента к красивому представлению.
/// </summary>
public void Beautify()
{
foreach (var type in Types)
{
type.Beautify();
}
}
/// <summary>
/// Отступ для строкового представления.
/// </summary>
public const string Tab = " ";
/// <summary>
/// Предел количество отображаемых элементов.
/// </summary>
public const int ElementLimit = 20;
/// <summary>
/// Увеличение отступа строки.
/// </summary>
/// <param name="text">Изменяемая строка.</param>
/// <returns>Строка с добавленными отступами.</returns>
public static string Indent(string text)
{
return text.Replace("\r\n", "\r\n" + Tab);
}
/// <summary>
/// Форматирование данных в YAML.
/// </summary>
/// <param name="input">Словарь данных.</param>
/// <returns>Строка форматированных данных</returns>
public static string FormatYaml(params List<Dictionary<string, string>> input)
{
var result = "";
foreach (var kvp in MergeDictionaries(input))
{
var val = kvp.Value.Contains("\r\n") ? $"\r\n{Tab}{Indent(kvp.Value)}" : kvp.Value;
result += $"{kvp.Key}: {val}\r\n";
}
return result.Trim();
}
public static Dictionary<string, string> MergeDictionaries(params List<Dictionary<string, string>> input)
{
var result = new Dictionary<string, string>();
foreach (var item in input)
{
foreach (var kvp in item)
{
if (result.ContainsKey(kvp.Key))
{
result[kvp.Key] = kvp.Value;
}
else
{
result.Add(kvp.Key, kvp.Value);
}
}
}
return result;
} }
} }
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public string GetTypeName()
{
if (IsSimple)
{
return Types[0].GetTypeName();
}
List<string> typeNames = [.. Types.Select(t => t.GetTypeName())];
return string.Join(" | ", typeNames);
}
/// <summary>
/// Полное строковое представление элемента.
/// </summary>
/// <returns>Строковое представление элемента.</returns>
public override string ToString()
{
Dictionary<string, string> result = new()
{
["type"] = GetTypeName(),
["path"] = Path,
["is_null"] = IsNullable ? "true" : "false",
["values"] = Values.ToString()
};
if (IsSimple)
{
return FormatYaml(result, Types[0].GetYamlData());
}
if (Types.Count == 0)
{
return FormatYaml(result, new() { ["is_empty"] = "true" });
}
List<string> types = [];
foreach (AbstractSchemaType type in Types)
{
types.Add(Indent(type?.ToString() ?? ""));
}
return FormatYaml(result, new() { ["types"] = "- " + string.Join("\r\n- ", types) });
}
/// <summary>
/// Подготовка элемента к красивому представлению.
/// </summary>
public void Beautify()
{
foreach (AbstractSchemaType type in Types)
{
type.Beautify();
}
}
/// <summary>
/// Отступ для строкового представления.
/// </summary>
public const string Tab = " ";
/// <summary>
/// Предел количество отображаемых элементов.
/// </summary>
public const int ElementLimit = 20;
/// <summary>
/// Увеличение отступа строки.
/// </summary>
/// <param name="text">Изменяемая строка.</param>
/// <returns>Строка с добавленными отступами.</returns>
public static string Indent(string text)
{
return text.Replace("\r\n", "\r\n" + Tab);
}
/// <summary>
/// Форматирование данных в YAML.
/// </summary>
/// <param name="input">Словарь данных.</param>
/// <returns>Строка форматированных данных</returns>
public static string FormatYaml(params List<Dictionary<string, string>> input)
{
string result = "";
foreach (KeyValuePair<string, string> kvp in MergeDictionaries(input))
{
string val = kvp.Value.Contains("\r\n") ? $"\r\n{Tab}{Indent(kvp.Value)}" : kvp.Value;
result += $"{kvp.Key}: {val}\r\n";
}
return result.Trim();
}
public static Dictionary<string, string> MergeDictionaries(params List<Dictionary<string, string>> input)
{
Dictionary<string, string> result = [];
foreach (Dictionary<string, string> item in input)
{
foreach (KeyValuePair<string, string> kvp in item)
{
if (result.ContainsKey(kvp.Key))
{
result[kvp.Key] = kvp.Value;
}
else
{
result.Add(kvp.Key, kvp.Value);
}
}
}
return result;
}
} }
+89 -103
View File
@@ -1,125 +1,111 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.SchemaBuilder.Types; using Pf2eDataScrapper.SchemaBuilder.Types;
namespace Pf2eDataScrapper.SchemaBuilder namespace Pf2eDataScrapper.SchemaBuilder;
/// <summary>
/// Класс для определения типа поля в схеме данных.
/// </summary>
internal class TypeResolver
{ {
/// <summary> /// <summary>
/// Класс для определения типа поля в схеме данных. /// Проверка соответствия пути.
/// </summary> /// </summary>
internal class TypeResolver /// <param name="node">Элемент, путь которого проверяется.</param>
/// <param name="path">Проверяемый путь. Допускается использование `*` в местах, где допускается любое значение.</param>
/// <returns>`true`, если `node.Path` соответсвует указанному в `path` значению.</returns>
public bool CheckPath(SchemaNode node, string path)
{ {
/// <summary> if (!path.Contains("*"))
/// Проверка соответствия пути.
/// </summary>
/// <param name="node">Элемент, путь которого проверяется.</param>
/// <param name="path">Проверяемый путь. Допускается использование `*` в местах, где допускается любое значение.</param>
/// <returns>`true`, если `node.Path` соответсвует указанному в `path` значению.</returns>
public bool CheckPath(SchemaNode node, string path)
{ {
if (!path.Contains("*")) return path == node.Path;
{
return path == node.Path;
}
var pathParts = path.Split('.');
var nodeParts = node.Path.Split('.');
if (pathParts.Length > nodeParts.Length)
{
return false;
}
return !pathParts.Where((t, i) => t != "*" && t != nodeParts[i]).Any();
} }
/// <summary> string[] pathParts = path.Split('.');
/// Проверка соответствия пути. string[] nodeParts = node.Path.Split('.');
/// </summary>
/// <param name="node">Элемент, путь которого проверяется.</param> return pathParts.Length > nodeParts.Length ? false : !pathParts.Where((t, i) => t != "*" && t != nodeParts[i]).Any();
/// <param name="paths">Проверяемые пути. Допускается использование `*` в местах, где допускается любое значение.</param> }
/// <returns>`true`, если `node.Path` соответсвует хотя бы одному указанному в `paths` значению.</returns>
public bool CheckPath(SchemaNode node, ICollection<string> paths) /// <summary>
/// Проверка соответствия пути.
/// </summary>
/// <param name="node">Элемент, путь которого проверяется.</param>
/// <param name="paths">Проверяемые пути. Допускается использование `*` в местах, где допускается любое значение.</param>
/// <returns>`true`, если `node.Path` соответсвует хотя бы одному указанному в `paths` значению.</returns>
public bool CheckPath(SchemaNode node, ICollection<string> paths)
{
return paths.Any(path => CheckPath(node, path));
}
/// <summary>
/// Получение типа значения.
/// </summary>
/// <param name="value">Значение.</param>
/// <returns>Типа значения.</returns>
public JTokenType TokenType(JToken? value)
{
return value is null || value.Type is JTokenType.Null or JTokenType.Undefined or JTokenType.None ? JTokenType.Null : value.Type;
}
/// <summary>
/// Частное определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных. `null`, что бы использовать значение по умолчанию</returns>
protected virtual AbstractSchemaType? CustomResolve(SchemaNode node, JToken? value)
{
return null;
}
/// <summary>
/// Определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <param name="disableResolve">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных</returns>
/// <exception cref="ArgumentNullException">
/// Возникает, если у `value` значение идентично `null`
/// (такие значения должны обрабаотываться в `SchemaNode`).
/// </exception>
public AbstractSchemaType Resolve(SchemaNode node, JToken? value, bool disableResolve)
{
if (!disableResolve)
{ {
return paths.Any(path => CheckPath(node, path)); AbstractSchemaType? customType = this.CustomResolve(node, value);
if (customType is not null)
{
return customType;
}
} }
/// <summary> JTokenType type = TokenType(value);
/// Получение типа значения.
/// </summary>
/// <param name="value">Значение.</param>
/// <returns>Типа значения.</returns>
public JTokenType TokenType(JToken? value)
{
if (value is null || value.Type is JTokenType.Null or JTokenType.Undefined or JTokenType.None)
{
return JTokenType.Null;
}
return value.Type; return type == JTokenType.Null
} ? throw new ArgumentNullException(nameof(value))
: type switch
/// <summary>
/// Частное определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных. `null`, что бы использовать значение по умолчанию</returns>
protected virtual AbstractSchemaType? CustomResolve(SchemaNode node, JToken? value)
{
return null;
}
/// <summary>
/// Определение типа поля.
/// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param>
/// <param name="disableResolve">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных</returns>
/// <exception cref="ArgumentNullException">
/// Возникает, если у `value` значение идентично `null`
/// (такие значения должны обрабаотываться в `SchemaNode`).
/// </exception>
public AbstractSchemaType Resolve(SchemaNode node, JToken? value, bool disableResolve)
{
if (!disableResolve)
{
var customType = this.CustomResolve(node, value);
if (customType is not null)
{
return customType;
}
}
var type = TokenType(value);
if (type == JTokenType.Null)
{
throw new ArgumentNullException(nameof(value));
}
return type switch
{ {
JTokenType.Array => new ArraySchemaType(node), JTokenType.Array => new ArraySchemaType(node),
JTokenType.Object => new ObjectSchemaType(node), JTokenType.Object => new ObjectSchemaType(node),
_ => new StaticSchemaType(node, type) _ => new StaticSchemaType(node, type)
}; };
} }
/// <summary> /// <summary>
/// Определение типа поля. /// Определение типа поля.
/// </summary> /// </summary>
/// <param name="node">Поле схемы данных, для которого создаётся тип.</param> /// <param name="node">Поле схемы данных, для которого создаётся тип.</param>
/// <param name="value">Добавляемое значение.</param> /// <param name="value">Добавляемое значение.</param>
/// <returns>Новый тип для схемы данных</returns> /// <returns>Новый тип для схемы данных</returns>
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// Возникает, если у `value` значение идентично `null` /// Возникает, если у `value` значение идентично `null`
/// (такие значения должны обрабаотываться в `SchemaNode`). /// (такие значения должны обрабаотываться в `SchemaNode`).
/// </exception> /// </exception>
public AbstractSchemaType Resolve(SchemaNode node, JToken? value) public AbstractSchemaType Resolve(SchemaNode node, JToken? value)
{ {
return this.Resolve(node, value, false); return this.Resolve(node, value, false);
}
} }
} }
@@ -1,65 +1,64 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.SchemaBuilder.Types namespace Pf2eDataScrapper.SchemaBuilder.Types;
/// <summary>
/// Абстрактный тип представоления данных в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal abstract class AbstractSchemaType(SchemaNode parent)
{ {
/// <summary> /// <summary>
/// Абстрактный тип представоления данных в схеме. /// Элемент схемы данных, к которому относится тип.
/// </summary> /// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param> public SchemaNode Parent = parent;
internal abstract class AbstractSchemaType(SchemaNode parent)
/// <summary>
/// Корневой элемент схемы данных.
/// </summary>
public SchemaNode Root => Parent.RootNode;
/// <summary>
/// Объект для определения типа поля в схеме данных.
/// </summary>
public TypeResolver Resolver => Root.Resolver;
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public abstract bool IsCompatibleValue(JToken value);
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public abstract void AddValue(JToken value);
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public abstract void Beautify();
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public abstract string GetTypeName();
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public abstract Dictionary<string, string> GetYamlData();
/// <summary>
/// Полное строковое представление элемента.
/// </summary>
/// <returns>Строковое представление элемента.</returns>
public override string ToString()
{ {
/// <summary> return SchemaNode.FormatYaml(GetYamlData());
/// Элемент схемы данных, к которому относится тип.
/// </summary>
public SchemaNode Parent = parent;
/// <summary>
/// Корневой элемент схемы данных.
/// </summary>
public SchemaNode Root => Parent.RootNode;
/// <summary>
/// Объект для определения типа поля в схеме данных.
/// </summary>
public TypeResolver Resolver => Root.Resolver;
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public abstract bool IsCompatibleValue(JToken value);
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public abstract void AddValue(JToken value);
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public abstract void Beautify();
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public abstract string GetTypeName();
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public abstract Dictionary<string, string> GetYamlData();
/// <summary>
/// Полное строковое представление элемента.
/// </summary>
/// <returns>Строковое представление элемента.</returns>
public override string ToString()
{
return SchemaNode.FormatYaml(GetYamlData());
}
} }
} }
@@ -1,89 +1,88 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.SchemaBuilder.Types namespace Pf2eDataScrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления массива в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class ArraySchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{ {
/// <summary> /// <summary>
/// Тип представоления массива в схеме. /// Минимальное количество элементов в массиве.
/// </summary> /// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param> public int MinCount = int.MaxValue;
internal class ArraySchemaType(SchemaNode parent) : AbstractSchemaType(parent)
/// <summary>
/// Максимальное количество элементов в массиве.
/// </summary>
public int MaxCount = int.MinValue;
/// <summary>
/// Схема элемента массива.
/// </summary>
protected SchemaNode ElementSchema = new(parent, "[]");
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
/// <summary> return Resolver.TokenType(value) == JTokenType.Array;
/// Минимальное количество элементов в массиве. }
/// </summary>
public int MinCount = int.MaxValue;
/// <summary> /// <summary>
/// Максимальное количество элементов в массиве. /// Добавление нового значения.
/// </summary> /// </summary>
public int MaxCount = int.MinValue; /// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
/// <summary> {
/// Схема элемента массива. if (value.Type != JTokenType.Array)
/// </summary>
protected SchemaNode ElementSchema = new(parent, "[]");
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
return Resolver.TokenType(value) == JTokenType.Array; throw new Exception("Value is not an array, did you forget to call IsCompatibleValue?");
} }
/// <summary> JArray array = (JArray) value;
/// Добавление нового значения.
/// </summary> MinCount = Math.Min(MinCount, array.Count);
/// <param name="value">Новое значение.</param> MaxCount = Math.Max(MaxCount, array.Count);
public override void AddValue(JToken value)
foreach (JToken item in array)
{ {
if (value.Type != JTokenType.Array) ElementSchema.AddValue(item);
{
throw new Exception("Value is not an array, did you forget to call IsCompatibleValue?");
}
var array = (JArray)value;
MinCount = Math.Min(MinCount, array.Count);
MaxCount = Math.Max(MaxCount, array.Count);
foreach (var item in array)
{
ElementSchema.AddValue(item);
}
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
ElementSchema.Beautify();
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return $"array<{ElementSchema.GetTypeName()}>";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new()
{
["min_count"] = MinCount.ToString(),
["max_count"] = MaxCount.ToString(),
["value_type"] = ElementSchema.ToString()
};
} }
} }
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
ElementSchema.Beautify();
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return $"array<{ElementSchema.GetTypeName()}>";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new()
{
["min_count"] = MinCount.ToString(),
["max_count"] = MaxCount.ToString(),
["value_type"] = ElementSchema.ToString()
};
}
} }
@@ -1,77 +1,76 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.SchemaBuilder.Types namespace Pf2eDataScrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления словаря в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class DictionarySchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{ {
/// <summary> /// <summary>
/// Тип представоления словаря в схеме. /// Схема ключей словаря.
/// </summary> /// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param> public SchemaNode KeyNode = new(parent, "[key]");
internal class DictionarySchemaType(SchemaNode parent) : AbstractSchemaType(parent)
/// <summary>
/// Схема значений словаря.
/// </summary>
public SchemaNode ItemNode = new(parent, "[value]");
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
/// <summary> return Resolver.TokenType(value) == JTokenType.Object;
/// Схема ключей словаря. }
/// </summary>
public SchemaNode KeyNode = new(parent, "[key]");
/// <summary> /// <summary>
/// Схема значений словаря. /// Добавление нового значения.
/// </summary> /// </summary>
public SchemaNode ItemNode = new(parent, "[value]"); /// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
JObject obj = (JObject) value;
/// <summary> foreach (KeyValuePair<string, JToken?> pair in obj)
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
return Resolver.TokenType(value) == JTokenType.Object; KeyNode.AddValue(pair.Key);
} ItemNode.AddValue(pair.Value);
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
var obj = (JObject)value;
foreach (var pair in obj)
{
KeyNode.AddValue(pair.Key);
ItemNode.AddValue(pair.Value);
}
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
KeyNode.Beautify();
ItemNode.Beautify();
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return $"dict<{KeyNode.GetTypeName()}, {ItemNode.GetTypeName()}>";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new Dictionary<string, string>()
{
["_key"] = KeyNode.ToString(),
["_value"] = ItemNode.ToString()
};
} }
} }
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
KeyNode.Beautify();
ItemNode.Beautify();
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return $"dict<{KeyNode.GetTypeName()}, {ItemNode.GetTypeName()}>";
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new Dictionary<string, string>()
{
["_key"] = KeyNode.ToString(),
["_value"] = ItemNode.ToString()
};
}
} }
@@ -1,55 +1,54 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.SchemaBuilder.Types namespace Pf2eDataScrapper.SchemaBuilder.Types;
/// <summary>
/// Тип, указывающий, что данные игнорируются.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class IgnoreSchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{ {
/// <summary> /// <summary>
/// Тип, указывающий, что данные игнорируются. /// Проверка, что значение совместимо с текущим типом.
/// </summary> /// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param> /// <param name="value">Проверяемое значение.</param>
internal class IgnoreSchemaType(SchemaNode parent) : AbstractSchemaType(parent) /// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
/// <summary> return true;
/// Проверка, что значение совместимо с текущим типом. }
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{
return true;
}
/// <summary> /// <summary>
/// Добавление нового значения. /// Добавление нового значения.
/// </summary> /// </summary>
/// <param name="value">Новое значение.</param> /// <param name="value">Новое значение.</param>
public override void AddValue(JToken value) public override void AddValue(JToken value)
{ {
} }
/// <summary> /// <summary>
/// Подготовка типа к красивому представлению. /// Подготовка типа к красивому представлению.
/// </summary> /// </summary>
public override void Beautify() public override void Beautify()
{ {
} }
/// <summary> /// <summary>
/// Получение строкового представоения типа элемента. /// Получение строкового представоения типа элемента.
/// </summary> /// </summary>
/// <returns>Строковое представоение типа элемента</returns> /// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName() public override string GetTypeName()
{ {
return "ignore"; return "ignore";
} }
/// <summary> /// <summary>
/// Получение данных для форматирования в YAML. /// Получение данных для форматирования в YAML.
/// </summary> /// </summary>
/// <returns>Словарь данных</returns> /// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData() public override Dictionary<string, string> GetYamlData()
{ {
return []; return [];
}
} }
} }
@@ -1,124 +1,110 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.SchemaBuilder.Types namespace Pf2eDataScrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления объектов в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
internal class ObjectSchemaType(SchemaNode parent) : AbstractSchemaType(parent)
{ {
/// <summary> /// <summary>
/// Тип представоления объектов в схеме. /// Элемнты полей объекта.
/// </summary> /// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param> public List<SchemaNode> Fields = [];
internal class ObjectSchemaType(SchemaNode parent) : AbstractSchemaType(parent)
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
/// <summary> return Resolver.TokenType(value) == JTokenType.Object;
/// Элемнты полей объекта. }
/// </summary>
public List<SchemaNode> Fields = [];
/// <summary> /// <summary>
/// Проверка, что значение совместимо с текущим типом. /// Добавление нового значения.
/// </summary> /// </summary>
/// <param name="value">Проверяемое значение.</param> /// <param name="value">Новое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns> public override void AddValue(JToken value)
public override bool IsCompatibleValue(JToken value) {
JObject obj = (JObject) value;
foreach (SchemaNode field in Fields)
{ {
return Resolver.TokenType(value) == JTokenType.Object; if (!obj.ContainsKey(field.Name))
}
/// <summary>
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
var obj = (JObject)value;
foreach (var field in Fields)
{ {
if (!obj.ContainsKey(field.Name)) field.IsNullable = true;
{
field.IsNullable = true;
}
}
foreach (var pair in obj)
{
var field = Fields.FirstOrDefault(f => f.Name == pair.Key);
if (field is null)
{
field = new SchemaNode(Parent, pair.Key);
Fields.Add(field);
}
field.AddValue(pair.Value);
} }
} }
/// <summary> foreach (KeyValuePair<string, JToken?> pair in obj)
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{ {
Fields.Sort((a, b) => SchemaNode? field = Fields.FirstOrDefault(f => f.Name == pair.Key);
if (field is null)
{ {
if (a.IsNullable && !b.IsNullable) field = new SchemaNode(Parent, pair.Key);
{ Fields.Add(field);
return 0;
}
return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
});
foreach (var field in Fields)
{
field.Beautify();
}
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return "object";
}
/// <summary>
/// Получение данных для форматирования структуры в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public Dictionary<string, string> GetSchemaYaml()
{
var result = new Dictionary<string, string>();
foreach (var field in Fields)
{
var s = field.Name;
if (field.IsNullable)
{
s = $"[{s}]";
}
else
{
s = $"'{s}'";
}
result.Add(s, field.ToString());
} }
return result; field.AddValue(pair.Value);
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new()
{
["schema"] = SchemaNode.FormatYaml(GetSchemaYaml())
};
} }
} }
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
Fields.Sort((a, b) =>
{
return a.IsNullable && !b.IsNullable ? 0 : string.Compare(a.Name, b.Name, StringComparison.Ordinal);
});
foreach (SchemaNode field in Fields)
{
field.Beautify();
}
}
/// <summary>
/// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return "object";
}
/// <summary>
/// Получение данных для форматирования структуры в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public Dictionary<string, string> GetSchemaYaml()
{
Dictionary<string, string> result = new();
foreach (SchemaNode field in Fields)
{
string s = field.Name;
s = field.IsNullable ? $"[{s}]" : $"'{s}'";
result.Add(s, field.ToString());
}
return result;
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
return new()
{
["schema"] = SchemaNode.FormatYaml(GetSchemaYaml())
};
}
} }
@@ -1,102 +1,101 @@
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.SchemaBuilder.Types namespace Pf2eDataScrapper.SchemaBuilder.Types;
/// <summary>
/// Тип представоления простых значений в схеме.
/// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param>
/// <param name="type">Тип значения.</param>
internal class StaticSchemaType(SchemaNode parent, JTokenType type) : AbstractSchemaType(parent)
{ {
/// <summary> /// <summary>
/// Тип представоления простых значений в схеме. /// Тип значения.
/// </summary> /// </summary>
/// <param name="parent">Элемент, к которому добавляется тип.</param> protected JTokenType Type = type;
/// <param name="type">Тип значения.</param>
internal class StaticSchemaType(SchemaNode parent, JTokenType type) : AbstractSchemaType(parent) /// <summary>
/// Набор возможных значений и их количества.
/// </summary>
protected Dictionary<JToken, int> Values = [];
/// <summary>
/// Проверка, что значение совместимо с текущим типом.
/// </summary>
/// <param name="value">Проверяемое значение.</param>
/// <returns>`true`, если значение совместимо с текущим типом.</returns>
public override bool IsCompatibleValue(JToken value)
{ {
/// <summary> return value.Type == Type;
/// Тип значения. }
/// </summary>
protected JTokenType Type = type;
/// <summary> /// <summary>
/// Набор возможных значений и их количества. /// Добавление нового значения.
/// </summary> /// </summary>
protected Dictionary<JToken, int> Values = []; /// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
Values.TryAdd(value, 0);
/// <summary> Values[value]++;
/// Проверка, что значение совместимо с текущим типом. }
/// </summary>
/// <param name="value">Проверяемое значение.</param> /// <summary>
/// <returns>`true`, если значение совместимо с текущим типом.</returns> /// Подготовка типа к красивому представлению.
public override bool IsCompatibleValue(JToken value) /// </summary>
public override void Beautify()
{
if (Type == JTokenType.String)
{ {
return value.Type == Type; Dictionary<JToken, int> newValues = new();
}
/// <summary> foreach (KeyValuePair<JToken, int> pair in Values)
/// Добавление нового значения.
/// </summary>
/// <param name="value">Новое значение.</param>
public override void AddValue(JToken value)
{
Values.TryAdd(value, 0);
Values[value]++;
}
/// <summary>
/// Подготовка типа к красивому представлению.
/// </summary>
public override void Beautify()
{
if (Type == JTokenType.String)
{ {
var newValues = new Dictionary<JToken, int>(); string newValue = "\"" + pair.Key.ToString().Trim().Replace("\"", "\\\"") + "\"";
newValues.TryAdd(newValue, 0);
foreach (var pair in Values) newValues[newValue] += pair.Value;
{
var newValue = "\"" + pair.Key.ToString().Trim().Replace("\"", "\\\"") + "\"";
newValues.TryAdd(newValue, 0);
newValues[newValue] += pair.Value;
}
Values = newValues;
} }
Values = Values.OrderByDescending(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value); Values = newValues;
} }
/// <summary> Values = Values.OrderByDescending(pair => pair.Value).ToDictionary(pair => pair.Key, pair => pair.Value);
/// Получение строкового представоения типа элемента. }
/// </summary>
/// <returns>Строковое представоение типа элемента</returns> /// <summary>
public override string GetTypeName() /// Получение строкового представоения типа элемента.
/// </summary>
/// <returns>Строковое представоение типа элемента</returns>
public override string GetTypeName()
{
return Type.ToString().ToLower();
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
List<string> values = [];
int count = Values.Count;
string prefix = (count == 1) ? "" : "- ";
if (count is <= SchemaNode.ElementLimit and > 0 && (Type != JTokenType.String || !Values.Any(s => s.ToString().Contains('\n'))))
{ {
return Type.ToString().ToLower(); foreach (KeyValuePair<JToken, int> kvp in Values)
}
/// <summary>
/// Получение данных для форматирования в YAML.
/// </summary>
/// <returns>Словарь данных</returns>
public override Dictionary<string, string> GetYamlData()
{
var values = new List<string>();
var count = Values.Count;
var prefix = (count == 1) ? "" : "- ";
if (count is <= SchemaNode.ElementLimit and > 0 && (Type != JTokenType.String || !Values.Any(s => s.ToString().Contains('\n'))))
{ {
foreach (var kvp in Values) values.Add($"{prefix}{kvp.Key} (x{kvp.Value})");
{
values.Add($"{prefix}{kvp.Key} (x{kvp.Value})");
}
} }
return new()
{
["different_values"] = count.ToString() +
((count == Root.Values) ? " (all different)" : "") +
((count == 1) ? " (all same)" : ""),
["variants"] = (values.Count > 0) ? string.Join("\r\n", values) : "[omitted]"
};
} }
return new()
{
["different_values"] = count.ToString() +
((count == Root.Values) ? " (all different)" : "") +
((count == 1) ? " (all same)" : ""),
["variants"] = (values.Count > 0) ? string.Join("\r\n", values) : "[omitted]"
};
} }
} }