6 Commits
25 changed files with 1628 additions and 723 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ public class FullEntity
.Include(d => d.DeitySpells)
.ThenInclude(ds => ds.Spell)
.Include(d => d.DeityWeapons)
.ThenInclude(dw => dw.Weapon)
//.ThenInclude(dw => dw.Weapon)
.Include(d => d.CategoryEntity)
.Include(d => d.Skills)
),
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="DefaultDocumentation" Version="1.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3485.44" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3537.50" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Scrapper\Pf2e.Scrapper.csproj" />
+385
View File
@@ -0,0 +1,385 @@
using Pf2e.Common;
using Pf2e.Database.Model.Base;
using Pf2e.Database.Model.Structure;
using Pf2e.Web.Helpers.Serialization;
using Action = Pf2e.Database.Model.Structure.Action;
namespace Pf2e.Web.Helpers;
public class EntitySerialization
{
public EntitySerialization()
{
if (_builders.Count == 0)
{
RegisterBuilders();
}
}
/// <summary>
/// Сериализует объект в результат сериализации.
/// </summary>
/// <param name="value">Объект для сериализации.</param>
/// <returns>Результат сериализации.</returns>
public SerializationResult Serialize(object value)
{
SerializationResult result = new();
// Находим подходящий построитель для типа объекта
EntityBuilder? builder = _builders.FirstOrDefault(b => b.EntityType == value.GetType());
if (builder != null)
{
result.Groups = builder.Compile(value);
}
else
{
// Если построитель не найден, создаём пустой результат
result.Groups = [];
}
return result;
}
private static List<EntityBuilder> _builders = [];
private static EntityBuilder<TEntity> NewBuilder<TEntity>()
where TEntity : class
{
EntityBuilder<TEntity> builder = new();
_builders.Add(builder);
return builder;
}
private static void RegisterBuilders()
{
NewBuilder<Entity>()
.AddGroup(
"Сущность",
builder =>
{
builder
.AddField(entity => entity.Id)
.AddField(entity => entity.Name)
.AddField(entity => entity.Type)
.AddField(entity => entity.Slug)
.AddField(entity => entity.Traits)
.AddField(entity => entity.Description)
.AddField(entity => entity.IsTranslated)
.SetValueFormatter(v => (v as bool? == true) ? "Да" : "Нет")
.AddField(entity => entity.IsChecked)
.SetValueFormatter(v => (v as bool? == true) ? "Да" : "Нет")
.AddField(entity => entity.IsLegacy)
.SetValueFormatter(v => (v as bool? == true) ? "Да" : "Нет")
.AddField(entity => entity.Keywords)
.AddField(entity => entity.Sources)
.AddField(entity => entity.LegacyEntities)
.AddField(entity => entity.RemasterEntities);
}
);
NewBuilder<Action>()
.AddGroup(
"Действие",
builder =>
{
builder
.AddField(a => a.Id)
.AddField(a => a.Entity)
.AddField(a => a.Actions)
.AddField(a => a.Category)
.AddField(a => a.Extra);
}
);
NewBuilder<Ancestry>()
.AddGroup(
"Родословная",
builder =>
{
builder
.AddField(a => a.Id)
.AddField(a => a.Entity)
.AddField(a => a.Size)
.AddField(a => a.Hp)
.AddField(a => a.Speed)
.AddField(a => a.VisionEnum)
.AddField(a => a.AncestryBoosts)
.AddField(a => a.AncestryItems)
.AddField(a => a.AncestryLanguages)
.AddField(a => a.Heritages);
}
);
NewBuilder<Archetype>()
.AddGroup(
"Архетип",
builder =>
{
builder
.AddField(a => a.Id)
.AddField(a => a.Entity)
.AddField(a => a.Level)
.AddField(a => a.Extra)
.AddField(a => a.DedicationFeat)
.UseCommentFrom(a => a.DedicationFeatId)
.AddField(a => a.Feats);
}
);
NewBuilder<Background>()
.AddGroup(
"Предыстория",
builder =>
{
builder
.AddField(b => b.Id)
.AddField(b => b.Entity)
.AddField(b => b.BackgroundBoosts)
.AddField(b => b.BackgroundItems)
.AddField(b => b.BackgroundSkills);
}
);
NewBuilder<Class>()
.AddGroup(
"Класс",
builder =>
{
builder
.AddField(c => c.Id)
.AddField(c => c.Entity)
.AddField(c => c.Hp)
.AddField(c => c.Spellcasting)
.AddField(c => c.Tradition)
.AddField(c => c.AdditionalTrainedSkills)
.AddField(c => c.ClassKeyAbilities)
.AddField(c => c.ClassProgressions)
.AddField(c => c.ClassTrainings)
.AddField(c => c.SubclassGroups);
}
);
NewBuilder<Condition>()
.AddGroup(
"Состояние",
builder =>
{
builder
.AddField(c => c.Id)
.AddField(c => c.Entity)
.AddField(c => c.Group)
.AddField(c => c.Extra);
}
);
NewBuilder<Deity>()
.AddGroup(
"Божество",
builder =>
{
builder
.AddField(d => d.Id)
.AddField(d => d.Entity)
.AddField(d => d.Type)
.AddField(d => d.Category)
.AddField(d => d.Fonts)
.AddField(d => d.Sanctification)
.AddField(d => d.SanctificationMust)
.AddField(d => d.Edicts)
.AddField(d => d.Anathema)
.AddField(d => d.ReligiousSymbol)
.AddField(d => d.SacredAnimal)
.AddField(d => d.AreaOfConcern)
.AddField(d => d.DeityAttributes)
.AddField(d => d.DeityDomains)
.AddField(d => d.DeitySpells)
.AddField(d => d.DeityWeapons)
.AddField(d => d.CategoryEntity)
.UseCommentFrom(d => d.CategoryId)
.AddField(d => d.Skills);
}
);
NewBuilder<DeityCategory>()
.AddGroup(
"Категория божества",
builder =>
{
builder
.AddField(dc => dc.Id)
.AddField(dc => dc.Entity)
.AddField(dc => dc.Description);
}
);
NewBuilder<Domain>()
.AddGroup(
"Домен",
builder =>
{
builder
.AddField(d => d.Id)
.AddField(d => d.Entity)
.AddField(d => d.DomainSpell)
.UseCommentFrom(d => d.DomainSpellId)
.AddField(d => d.AdvancedDomainSpell)
.UseCommentFrom(d => d.AdvancedDomainSpellId)
.AddField(d => d.ApocryphalSpell)
.UseCommentFrom(d => d.ApocryphalSpellId)
.AddField(d => d.AdvancedApocryphalSpell)
.UseCommentFrom(d => d.AdvancedApocryphalSpellId)
.AddField(d => d.DeityDomains);
}
);
NewBuilder<Feat>()
.AddGroup(
"Способность",
builder =>
{
builder
.AddField(f => f.Id)
.AddField(f => f.Entity)
.AddField(f => f.Category)
.AddField(f => f.Level)
.AddField(f => f.Actions)
.AddField(f => f.Extra)
.AddField(f => f.Archetypes)
.AddField(f => f.FeatPrerequisites);
}
);
NewBuilder<Heritage>()
.AddGroup(
"Наследие",
builder =>
{
builder
.AddField(h => h.Id)
.AddField(h => h.Entity)
.AddField(h => h.Ancestry)
.UseCommentFrom(h => h.AncestryId);
}
);
NewBuilder<Language>()
.AddGroup(
"Язык",
builder =>
{
builder.AddField(l => l.Id).AddField(l => l.Entity);
}
);
NewBuilder<Rule>()
.AddGroup(
"Правило",
builder =>
{
builder
.AddField(r => r.Id)
.AddField(r => r.Entity)
.AddField(r => r.Order)
.AddField(r => r.Parent)
.UseCommentFrom(r => r.ParentId)
.AddField(r => r.Children)
.AddField(r => r.Breadcrumbs);
}
);
NewBuilder<Ritual>()
.AddGroup(
"Ритуал",
builder =>
{
builder
.AddField(r => r.Id)
.AddField(r => r.Entity)
.AddField(r => r.Level)
.AddField(r => r.CastTime)
.AddField(r => r.Duration)
.AddField(r => r.Elements)
.AddField(r => r.Range)
.AddField(r => r.PrimaryCheckRank)
.AddField(r => r.PrimaryCheckSuffix)
.AddField(r => r.SecondaryCasters)
.AddField(r => r.SecondaryCastersMax)
.AddField(r => r.SecondaryCastersSuffix)
.AddField(r => r.RangeString)
.AddField(r => r.Target)
.AddField(r => r.Cost)
.AddField(r => r.Requirement)
.AddField(r => r.Area)
.AddField(r => r.Checks);
}
);
NewBuilder<Spell>()
.AddGroup(
"Заклинание",
builder =>
{
builder
.AddField(s => s.Id)
.AddField(s => s.Entity)
.AddField(s => s.Level)
.AddField(s => s.IsFocus)
.AddField(s => s.Actions)
.AddField(s => s.Duration)
.AddField(s => s.Range)
.AddField(s => s.Traditions)
.AddField(s => s.Elements)
.AddField(s => s.Trigger)
.AddField(s => s.Target)
.AddField(s => s.Cost)
.AddField(s => s.Requirement)
.AddField(s => s.SpellAreas)
.AddField(s => s.SpellDefenses);
}
);
NewBuilder<Subclass>()
.AddGroup(
"Подкласс",
builder =>
{
builder
.AddField(s => s.Id)
.AddField(s => s.Entity)
.AddField(s => s.Group)
.UseCommentFrom(s => s.GroupId)
.AddField(s => s.Additional)
.AddField(s => s.Extra)
.AddField(s => s.Entities)
.AddField(s => s.Trainings);
}
);
NewBuilder<SubclassGroup>()
.AddGroup(
"Группа подклассов",
builder =>
{
builder
.AddField(sg => sg.Id)
.AddField(sg => sg.Entity)
.AddField(sg => sg.Class)
.UseCommentFrom(sg => sg.ClassId)
.AddField(sg => sg.Subclasses);
}
);
NewBuilder<Trait>()
.AddGroup(
"Признак",
builder =>
{
builder
.AddField(t => t.Id)
.AddField(t => t.Entity)
.AddField(t => t.Type)
.AddField(t => t.Entities);
}
);
}
}
-247
View File
@@ -1,247 +0,0 @@
using System.Collections;
using System.Reflection;
using Pf2e.Common;
using Pf2e.Database.Model;
using Pf2e.Database.Model.Base;
using Pf2e.Database.Model.Common.I18n;
using Pf2e.Database.Model.Enum;
namespace Pf2e.Web.Helpers;
/// <summary>
/// Сериализатор сущности для отображения на веб-странице.
/// </summary>
public class EntitySerializer
{
/// <summary>
/// Результат сериализации сущности.
/// </summary>
public class SerializationResult
{
public List<FieldGroup> Groups { get; set; } = [];
}
/// <summary>
/// Группа полей.
/// </summary>
public class FieldGroup
{
public string Name { get; set; } = string.Empty;
public List<FieldInfo> Fields { get; set; } = [];
}
/// <summary>
/// Информация о поле.
/// </summary>
public class FieldInfo
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public object? Value { get; set; }
public string DisplayValue { get; set; } = string.Empty;
public bool IsImplemented { get; set; } = true;
}
/// <summary>
/// Сериализовать FullEntity.
/// </summary>
public SerializationResult Serialize(FullEntity fullEntity)
{
SerializationResult result = new();
// Группа для основных полей Entity
FieldGroup entityGroup = new() { Name = "Основные поля" };
AddFieldsToGroup(entityGroup, fullEntity.Entity);
result.Groups.Add(entityGroup);
// Группа для полей IEntity (SubEntity)
if (fullEntity.SubEntity != null)
{
FieldGroup subEntityGroup = new()
{
Name = "Поля " + fullEntity.SubEntity.GetType().Name,
};
AddFieldsToGroup(subEntityGroup, fullEntity.SubEntity);
result.Groups.Add(subEntityGroup);
}
// Группа для ссылок
FieldGroup referencesGroup = new() { Name = "Ссылки" };
AddReferencesToGroup(referencesGroup, fullEntity);
result.Groups.Add(referencesGroup);
return result;
}
/// <summary>
/// Добавить поля объекта в группу.
/// </summary>
private void AddFieldsToGroup(FieldGroup group, object obj)
{
Type type = obj.GetType();
PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo property in properties)
{
// Пропускаем навигационные свойства Entity
if (property.Name == "Entity")
continue;
FieldInfo fieldInfo = new()
{
Name = property.Name,
Type = GetTypeDisplayName(property.PropertyType),
Value = property.GetValue(obj),
};
// Обработка значения для отображения
var (displayValue, isImplemented) = FormatValue(fieldInfo.Value);
fieldInfo.DisplayValue = displayValue;
fieldInfo.IsImplemented = isImplemented;
group.Fields.Add(fieldInfo);
}
}
/// <summary>
/// Добавить ссылки в группу.
/// </summary>
private void AddReferencesToGroup(FieldGroup group, FullEntity fullEntity)
{
// Добавляем информацию о связанных сущностях
Entity entity = fullEntity.Entity;
// Traits
if (entity.Traits?.Any() == true)
{
var (displayValue, isImplemented) = FormatValue(entity.Traits);
group.Fields.Add(
new FieldInfo
{
Name = "Traits",
Type = "ICollection<Trait>",
Value = entity.Traits,
DisplayValue = displayValue,
IsImplemented = isImplemented,
}
);
}
// Keywords
if (entity.Keywords?.Any() == true)
{
var (displayValue, isImplemented) = FormatValue(entity.Keywords);
group.Fields.Add(
new FieldInfo
{
Name = "Keywords",
Type = "ICollection<Keyword>",
Value = entity.Keywords,
DisplayValue = displayValue,
IsImplemented = isImplemented,
}
);
}
// Sources
if (entity.Sources?.Any() == true)
{
var (displayValue, isImplemented) = FormatValue(entity.Sources);
group.Fields.Add(
new FieldInfo
{
Name = "Sources",
Type = "ICollection<EntitySource>",
Value = entity.Sources,
DisplayValue = displayValue,
IsImplemented = isImplemented,
}
);
}
// Externals
if (entity.Externals?.Any() == true)
{
var (displayValue, isImplemented) = FormatValue(entity.Externals);
group.Fields.Add(
new()
{
Name = "Externals",
Type = "ICollection<EntityExternal>",
Value = entity.Externals,
DisplayValue = displayValue,
IsImplemented = isImplemented,
}
);
}
}
/// <summary>
/// Получить отображаемое имя типа.
/// </summary>
private string GetTypeDisplayName(Type type)
{
if (type.IsEnum)
return "enum";
return type.Name;
}
/// <summary>
/// Форматировать значение для отображения.
/// </summary>
private (string displayValue, bool isImplemented) FormatValue(object? value)
{
if (value == null)
return ("null", true);
Type type = value.GetType();
// Обработка перечислений
if (type.IsEnum)
{
Enum enumValue = (Enum)value;
return ($"{enumValue} ({(int)(object)enumValue})", true);
}
// Обработка примитивных типов
if (type.IsPrimitive || type == typeof(string) || type == typeof(decimal))
{
return (value.ToString() ?? "null", true);
}
// Обработка IEntity
if (typeof(IEntity).IsAssignableFrom(type))
{
var entity = (IEntity)value;
var entityType = entity.Entity.Type;
var slug = entity.Entity.Slug;
var name = entity.Entity.Name[LocaleType.English];
return ($"<a href=\"/Entity/{entityType}/{slug}\">{name}</a>", true);
}
// Обработка коллекций
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
{
var collection = (IEnumerable)value;
var items = new List<string>();
bool allImplemented = true;
foreach (var item in collection)
{
var (itemDisplay, itemImplemented) = FormatValue(item);
items.Add(itemDisplay);
if (!itemImplemented)
allImplemented = false;
}
if (items.Count == 0)
return ("(empty)", true);
return (string.Join(", ", items), allImplemented);
}
// Для остальных сложных объектов
return ($"[{type.Name}] - НЕ РЕАЛИЗОВАНО", false);
}
}
+196
View File
@@ -0,0 +1,196 @@
using System.ComponentModel;
using Pf2e.Database.Model;
using Pf2e.Database.Model.Base.Keywords;
using Pf2e.Database.Model.Base.Source;
using Pf2e.Database.Model.Base.Targets;
using Pf2e.Database.Model.Common.I18n;
using Pf2e.Database.Model.Enum;
namespace Pf2e.Web.Helpers.Format;
public static class Formatters
{
/// <summary>
/// Форматтер для перечислений (enum).
/// </summary>
public static readonly ValueFormatter EnumFormatter = new CustomValueFormatter(
(type) => type.IsEnum,
(value) =>
{
Enum enumValue = (Enum)value;
string description = GetEnumDescription(enumValue);
return $"{description} ({enumValue})";
string GetEnumDescription(Enum v)
{
Type type = v.GetType();
string name = Enum.GetName(type, v) ?? v.ToString();
System.Reflection.FieldInfo? field = type.GetField(name);
if (field == null)
return v.ToString();
if (
Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute))
is DescriptionAttribute attr
)
{
return attr.Description;
}
return v.ToString();
}
}
);
/// <summary>
/// Форматтер для примитивных типов и строк.
/// </summary>
public static readonly ValueFormatter PrimitiveFormatter = new CustomValueFormatter(
(type) => type.IsPrimitive || type == typeof(string) || type == typeof(decimal),
(value) => value.ToString() ?? "null"
);
/// <summary>
/// Форматтер для коллекций.
/// </summary>
public static readonly ValueFormatter CollectionFormatter = new CollectionFormatter();
/// <summary>
/// Форматтеры для стандартных типов из базы данных.
/// </summary>
public static readonly List<ValueFormatter> DefaultTypeFormatters =
[
new TypeValueFormatter<LocaleString>(
(value) =>
{
LocaleString localeString = (LocaleString)value;
List<string> items = [];
foreach (LocaleItem item in localeString)
{
items.Add($"{item.Locale}: {item.Value}");
}
return string.Join("<br/>", items);
}
),
// IEntity
new CustomValueFormatter(
(type) => typeof(IEntity).IsAssignableFrom(type),
(value) =>
{
IEntity entity = (IEntity)value;
EntityType entityType = entity.Entity.Type;
string typeName = PgNameHelper.GetPgName(entityType);
string slug = entity.Entity.Slug;
string name = entity.Entity.Name[LocaleType.English];
return $"<a href=\"/{typeName}/{slug}\">{name}</a>";
}
),
// EntitySource
new TypeValueFormatter<EntitySource>(
(value) =>
{
EntitySource entitySource = (EntitySource)value;
string sourceName = entitySource.Source.Name;
string suffix = !string.IsNullOrEmpty(entitySource.Suffix)
? $" ({entitySource.Suffix})"
: "";
string isRemaster = entitySource.IsRemaster ? " [Remaster]" : "";
return $"{sourceName}{suffix}{isRemaster}";
}
),
// Keyword
new TypeValueFormatter<Keyword>(
(value) =>
{
Keyword keyword = (Keyword)value;
string name = keyword.Name[LocaleType.English];
return $"{name}";
}
),
// KeywordAlias
new TypeValueFormatter<KeywordAlias>(
(value) =>
{
KeywordAlias alias = (KeywordAlias)value;
return $"{alias.Value}";
}
),
// ExternalTarget
new TypeValueFormatter<ExternalTarget>(
(value) =>
{
ExternalTarget target = (ExternalTarget)value;
return $"{target.Name} (Icon: {target.Icon}, Prefix: {target.Prefix})";
}
),
// EntityExternal
new TypeValueFormatter<EntityExternal>(
(value) =>
{
EntityExternal entityExternal = (EntityExternal)value;
string targetName = entityExternal.Target?.Name ?? "Unknown";
string externalId = !string.IsNullOrEmpty(entityExternal.ExternalId)
? $" ID: {entityExternal.ExternalId}"
: "";
string suffix = !string.IsNullOrEmpty(entityExternal.Suffix)
? $" Suffix: {entityExternal.Suffix}"
: "";
return $"{targetName}{externalId}{suffix}";
}
),
// Source
new TypeValueFormatter<Source>(
(value) =>
{
Source source = (Source)value;
string releaseDate = source.ReleaseDate?.ToString("yyyy-MM-dd") ?? "Unknown";
return $"{source.Name} (Category: {source.Category}, Release: {releaseDate})";
}
),
];
/// <summary>
/// Набор всех стандартных форматтеров.
/// </summary>
public static readonly List<ValueFormatter> AllFormatters =
[
.. DefaultTypeFormatters,
EnumFormatter,
PrimitiveFormatter,
CollectionFormatter,
];
/// <summary>
/// Добавить пользовательский форматтер в набор всех форматтеров.
/// </summary>
/// <param name="formatter">Добавляемый форматтер.</param>
public static void AddFormatter(ValueFormatter formatter)
{
AllFormatters.Insert(0, formatter);
}
/// <summary>
/// Выбрать подходящий форматтер для типа.
/// </summary>
/// <param name="type">Тип значения.</param>
/// <returns>Форматтер для типа или <c>null</c>, если подходящего не найдено.</returns>
public static ValueFormatter? SelectFormatter(Type type) =>
AllFormatters.FirstOrDefault(f => f.CanFormat(type)) ?? null;
/// <summary>
/// Выбрать подходящий форматтер для значения.
/// </summary>
/// <param name="value">Значение.</param>
/// <returns>Форматтер для значения или <c>null</c>, если значение равно <c>null</c> или подходящего не найдено.</returns>
public static ValueFormatter? SelectFormatter(object? value) =>
value == null ? null : SelectFormatter(value.GetType());
/// <summary>
/// Форматировать значение в строку.
/// </summary>
/// <param name="value">Форматируемое значение.</param>
/// <returns>Строковое представление отформатированного значения.</returns>
public static string Format(object? value) =>
SelectFormatter(value)?.Format(value!) ?? "[not implemented]";
}
+88
View File
@@ -0,0 +1,88 @@
using System.Collections;
using static LinqToDB.Reflection.Methods.LinqToDB.Insert;
namespace Pf2e.Web.Helpers.Format;
/// <summary>
/// Абстрактный класс форматтера значений.
/// </summary>
public abstract class ValueFormatter
{
/// <summary>
/// Проверяет, может ли форматтер обработать указанный тип.
/// </summary>
/// <param name="type">Тип значения.</param>
/// <returns><c>true</c>, если форматтер может обработать тип, иначе <c>false</c>.</returns>
public abstract bool CanFormat(Type type);
/// <summary>
/// Форматирует значение для отображения.
/// </summary>
/// <param name="value">Значение для форматирования.</param>
/// <returns>Строковое представление отформатированного значения.</returns>
public abstract string Format(object value);
/// <summary>
/// Форматирует имя типа для отображения.
/// </summary>
/// <param name="type">Тип значения.</param>
/// <returns>Строковое представление имени типа.</returns>
public virtual string TypeName(Type type) => type.Name;
}
/// <summary>
/// Форматтер для конкретного типа.
/// </summary>
/// <typeparam name="T">Тип значения.</typeparam>
/// <param name="formatter">Функция форматирования значения.</param>
public class TypeValueFormatter<T>(Func<object, string> formatter) : ValueFormatter
{
public override bool CanFormat(Type type) => type == typeof(T);
public override string Format(object value) => formatter(value);
}
/// <summary>
/// Форматтер с пользовательскими функциями проверки и форматирования.
/// </summary>
/// <param name="checker">Функция проверки типа.</param>
/// <param name="formatter">Функция форматирования значения.</param>
public class CustomValueFormatter(
Func<Type, bool> checker,
Func<object, string> formatter,
Func<Type, string>? typeName = null
) : ValueFormatter
{
public override bool CanFormat(Type type) => checker(type);
public override string Format(object value) => formatter(value);
public override string TypeName(Type type) => typeName?.Invoke(type) ?? base.TypeName(type);
}
/// <summary>
/// Форматтер для коллекций.
/// </summary>
public class CollectionFormatter() : ValueFormatter
{
public override bool CanFormat(Type type) =>
typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string);
public override string Format(object value)
{
IEnumerable collection = (IEnumerable)value;
List<string> items = [];
foreach (object? item in collection)
{
items.Add(Formatters.Format(item));
}
return items.Count == 0 ? "(empty)" : string.Join("<br/>", items);
}
public override string TypeName(Type type)
{
return $"{type.Name[..^2]}<{string.Join(", ", type.GetGenericArguments().Select(t => t.Name))}>";
}
}
+24
View File
@@ -0,0 +1,24 @@
using System.Reflection;
using NpgsqlTypes;
using Pf2e.Database.Model.Enum;
namespace Pf2e.Web.Helpers
{
/// <summary>
/// Вспомогательный класс для получения имен PostgresSQL из перечислений.
/// </summary>
public static class PgNameHelper
{
/// <summary>
/// Получает имя PostgresSQL для заданного типа сущности.
/// </summary>
/// <param name="entityType">Тип сущности.</param>
/// <returns>Строковое представление имени PostgresSQL.</returns>
public static string GetPgName(EntityType entityType)
{
FieldInfo? fieldInfo = entityType.GetType().GetField(entityType.ToString());
PgNameAttribute? pgNameAttribute = fieldInfo?.GetCustomAttribute<PgNameAttribute>();
return pgNameAttribute?.PgName ?? entityType.ToString().ToLower();
}
}
}
@@ -0,0 +1,144 @@
using System.Reflection;
using Pf2e.Web.Helpers.Format;
namespace Pf2e.Web.Helpers.Serialization;
/// <summary>
/// Построитель сериализации для сущности.
/// </summary>
/// <param name="type">Тип сущности для сериализации.</param>
public abstract class EntityBuilder(Type type)
{
/// <summary>
/// Тип сущности для сериализации.
/// </summary>
public Type EntityType { get; } = type;
/// <summary>
/// Сериализует объект в список групп полей.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public abstract List<SerializationGroup> Compile(object value);
}
/// <summary>
/// Построитель сериализации для типа T.
/// </summary>
/// <typeparam name="TEntity">Тип сущности для сериализации.</typeparam>
public class EntityBuilder<TEntity>() : EntityBuilder(typeof(TEntity))
where TEntity : class
{
/// <summary>
/// Список групп полей.
/// </summary>
public List<FieldGroupDefinition> Groups { get; set; } = [];
/// <summary>
/// Добавляет группу полей в построитель сериализации.
/// </summary>
/// <param name="name">Название группы.</param>
/// <param name="configure">Делегат для настройки группы полей.</param>
/// <returns>Текущий экземпляр построителя.</returns>
public EntityBuilder<TEntity> AddGroup(
string name,
Action<FieldGroupBuilder<TEntity>> configure
)
{
FieldGroupBuilder<TEntity> groupBuilder = new(name);
configure(groupBuilder);
Groups.Add(groupBuilder.Build());
return this;
}
/// <summary>
/// Добавляет группу полей в построитель сериализации с названием типа.
/// </summary>
/// <param name="configure">Делегат для настройки группы полей.</param>
/// <returns>Текущий экземпляр построителя.</returns>
public EntityBuilder<TEntity> AddGroup(Action<FieldGroupBuilder<TEntity>> configure)
{
return AddGroup(typeof(TEntity).Name, configure);
}
/// <inheritdoc />
public override List<SerializationGroup> Compile(object value)
{
return Groups.Select(group => CompileGroup(group, value)).ToList();
}
/// <summary>
/// Строит группу полей.
/// </summary>
/// <returns>Готовая группа полей.</returns>
public SerializationGroup CompileGroup(FieldGroupDefinition groupDefinition, object value)
{
SerializationGroup result = new() { Name = groupDefinition.Name };
foreach (FieldDefinition field in groupDefinition.Fields)
{
PropertyInfo? propertyInfo = groupDefinition.GroupType.GetProperty(field.Name);
if (propertyInfo == null)
continue;
object? fieldValue = propertyInfo.GetValue(value);
result.Fields.Add(CompileField(field, fieldValue));
}
return result;
}
/// <summary>
/// Построение поля сериализации.
/// </summary>
/// <param name="field">Определение поля.</param>
/// <param name="value">Значение поля.</param>
/// <returns>Построенное поле сериализации.</returns>
private SerializationField CompileField(FieldDefinition field, object? value)
{
if (value is null)
{
return new()
{
Name = field.Name,
Type = field.Type.Name,
Value = "null",
IsImplemented = true,
};
}
if (field.ValueFormatter is not null)
{
return new()
{
Name = field.Name,
Type = field.Type.Name,
Value = field.ValueFormatter(value),
IsImplemented = true,
};
}
ValueFormatter? formatter = Formatters.SelectFormatter(value);
if (formatter is not null)
{
return new()
{
Name = field.Name,
Type = field.Type.Name,
Value = formatter.Format(value),
IsImplemented = true,
};
}
return new()
{
Name = field.Name,
Type = field.Type.Name,
Value = value.ToString() ?? "null",
IsImplemented = false,
};
}
}
@@ -0,0 +1,22 @@
namespace Pf2e.Web.Helpers.Serialization;
/// <summary>
/// Информация о поле.
/// </summary>
public class FieldDefinition
{
/// <summary>
/// Название поля.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Тип поля.
/// </summary>
public required Type Type { get; set; }
/// <summary>
/// Форматтер значения поля.
/// </summary>
public Func<object, string>? ValueFormatter { get; set; } = null;
}
@@ -0,0 +1,116 @@
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Pf2e.Web.Helpers.Format;
namespace Pf2e.Web.Helpers.Serialization;
/// <summary>
/// Построитель группы полей для типа TField.
/// </summary>
/// <typeparam name="TField">Тип сущности для группы полей.</typeparam>
public class FieldGroupBuilder<TField>(string name)
where TField : class
{
private readonly FieldGroupDefinition _groupDefinition = new()
{
Name = name,
GroupType = typeof(TField),
};
/// <summary>
/// Добавляет поле в группу.
/// </summary>
/// <param name="fieldExpression">Лямбда-выражение для получения поля.</param>
/// <returns>Текущий экземпляр построителя группы полей.</returns>
public FieldGroupBuilder<TField> AddField(Expression<Func<TField, object>> fieldExpression)
{
// Получаем информацию о свойстве из выражения
MemberExpression? memberExpression = GetMemberExpression(fieldExpression.Body);
if (memberExpression == null)
{
throw new ArgumentException(
"Выражение должно быть доступом к свойству",
nameof(fieldExpression)
);
}
PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member;
// Получаем атрибут CommentAttribute, если он есть
CommentAttribute? commentAttribute = propertyInfo.GetCustomAttribute<CommentAttribute>();
string fieldName = commentAttribute?.Comment ?? propertyInfo.Name;
Type fieldType = propertyInfo.PropertyType;
_groupDefinition.Fields.Add(new() { Name = fieldName, Type = fieldType });
return this;
}
/// <summary>
/// Устанавливает форматтер значения для последнего добавленного поля.
/// </summary>
/// <param name="valueFormatter">Функция форматирования значения.</param>
/// <returns>Текущий экземпляр построителя группы полей.</returns>
public FieldGroupBuilder<TField> SetValueFormatter(Func<object, string> valueFormatter)
{
_groupDefinition.Fields.Last().ValueFormatter = valueFormatter;
return this;
}
/// <summary>
/// Использует комментарий из другого поля для установки имени последнего добавленного поля.
/// </summary>
/// <param name="fieldExpression">Лямбда-выражение для получения поля с комментарием.</param>
/// <returns>Текущий экземпляр построителя группы полей.</returns>
public FieldGroupBuilder<TField> UseCommentFrom(
Expression<Func<TField, object>> fieldExpression
)
{
// Получаем информацию о свойстве из выражения
MemberExpression? memberExpression = GetMemberExpression(fieldExpression.Body);
if (memberExpression == null)
{
throw new ArgumentException(
"Выражение должно быть доступом к свойству",
nameof(fieldExpression)
);
}
PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member;
// Получаем атрибут CommentAttribute, если он есть
CommentAttribute? commentAttribute = propertyInfo.GetCustomAttribute<CommentAttribute>();
if (commentAttribute != null)
{
// Устанавливаем имя последнего добавленного поля
if (_groupDefinition.Fields.Count > 0)
{
_groupDefinition.Fields.Last().Name = commentAttribute.Comment;
}
}
return this;
}
/// <summary>
/// Завершает построение группы полей и возвращает определение группы.
/// </summary>
/// <returns>Итоговое определение группы полей.</returns>
public FieldGroupDefinition Build() => _groupDefinition;
/// <summary>
/// Получает MemberExpression из тела лямбда-выражения.
/// </summary>
/// <param name="expression">Тело выражения.</param>
/// <returns>MemberExpression или null, если не удалось получить.</returns>
private static MemberExpression? GetMemberExpression(Expression expression)
{
return expression switch
{
MemberExpression memberExpression => memberExpression,
UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression =>
unaryExpression.Operand as MemberExpression,
_ => null,
};
}
}
@@ -0,0 +1,22 @@
namespace Pf2e.Web.Helpers.Serialization;
/// <summary>
/// Группа полей.
/// </summary>
public class FieldGroupDefinition
{
/// <summary>
/// Название группы.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Тип класса, к которому относится поля группы.
/// </summary>
public Type GroupType { get; set; } = null!;
/// <summary>
/// Список полей в группе.
/// </summary>
public List<FieldDefinition> Fields { get; set; } = [];
}
@@ -0,0 +1,63 @@
namespace Pf2e.Web.Helpers.Serialization;
/// <summary>
/// Результат сериализации сущности.
/// </summary>
public class SerializationResult
{
/// <summary>
/// Группы полей.
/// </summary>
public List<SerializationGroup> Groups { get; set; } = [];
/// <summary>
/// Расширяет текущий результат сериализации, добавляя группы из другого результата.
/// </summary>
/// <param name="other">Другой результат сериализации.</param>
public void ExtendWith(SerializationResult other)
{
Groups.AddRange(other.Groups);
}
}
/// <summary>
/// Группа полей для сериализации.
/// </summary>
public class SerializationGroup
{
/// <summary>
/// Название группы.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Список полей в группе.
/// </summary>
public List<SerializationField> Fields { get; set; } = [];
}
/// <summary>
/// Поле для сериализации.
/// </summary>
public class SerializationField
{
/// <summary>
/// Название поля.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Значение поля в строковом представлении.
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// Значение поля в строковом представлении.
/// </summary>
public string Value { get; set; } = string.Empty;
/// <summary>
/// Флаг, указывающий, реализовано ли поле (true) или нет (false).
/// </summary>
public bool IsImplemented { get; set; } = true;
}
+9 -11
View File
@@ -1,5 +1,5 @@
@page
@model Pf2e.Web.Pages.EntityModel
@page "/{type}/{slug}"
@model Pf2e.Web.Pages.Entity
@{
ViewData["Title"] = "Entity";
@@ -17,21 +17,19 @@
<dl class="row">
@foreach (var field in group.Fields)
{
<dt class="col-sm-3">@field.Name</dt>
<dt class="col-sm-3">
@field.Name
<span class="text-muted">[@field.Type]</span>
</dt>
<dd class="col-sm-9">
<span class="text-muted">[@field.Type]</span>
@if (field.IsImplemented)
{
@Html.Raw(field.DisplayValue)
}
else
@if (!field.IsImplemented)
{
<span class="badge bg-warning text-dark">НЕ РЕАЛИЗОВАНО:</span>
@Html.Raw(field.DisplayValue)
}
@Html.Raw(field.Value)
</dd>
}
</dl>
</div>
</div>
}
}
+45 -20
View File
@@ -12,39 +12,64 @@ using Pf2e.Database.Model.Base;
using Pf2e.Database.Model.Enum;
using Pf2e.Database.Model.Structure;
using Pf2e.Web.Helpers;
using Pf2e.Web.Helpers.Serialization;
namespace Pf2e.Web.Pages
{
public class EntityModel(Pf2eDbContext context) : PageModel
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class Entity(Pf2eDbContext context) : PageModel
{
private readonly Pf2eDbContext _context = context;
public FullEntity FullEntity { get; set; } = null!;
public EntitySerializer.SerializationResult SerializationResult { get; set; } = null!;
public SerializationResult SerializationResult { get; set; } = null!;
public IActionResult OnGet(string type, string slug)
{
// Получаем тип сущности
EntityType entityType = EntityTypeModel.Enum<EntityType>(type);
// Находим сущность по slug и типу
Entity? entity = _context.Entities.FirstOrDefault(e =>
e.Slug == slug && e.Type == entityType
);
if (entity == null)
try
{
EntityType entityType = EntityTypeModel.Enum<EntityType>(type);
// Находим сущность по slug и типу
Database.Model.Base.Entity? entity = _context
.Entities.AsNoTracking()
.FirstOrDefault(e =>
e.Slug.ToLower() == slug.ToLower() && e.Type == entityType
);
if (entity == null)
{
return NotFound();
}
// Создаём FullEntity
FullEntity = new(_context, entityType, entity.Id);
// Сериализуем для отображения
EntitySerialization serializer = new();
SerializationResult = serializer.Serialize(FullEntity.Entity);
if (FullEntity.SubEntity is not null)
{
SerializationResult.ExtendWith(serializer.Serialize(FullEntity.SubEntity));
}
// Set breadcrumbs
string typeName = PgNameHelper.GetPgName(entityType);
ViewData["Breadcrumbs"] = new List<(string, string)>
{
("Home", "/"),
(entityType.ToString(), $"/{typeName}"),
(entity.Name[Pf2e.Database.Model.Common.I18n.LocaleType.English], ""),
};
return Page();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error in Entity.OnGet: {ex.Message}");
Console.WriteLine($"Error in Entity.OnGet: {ex.Message}");
return NotFound();
}
// Создаём FullEntity
FullEntity = new(_context, entity);
// Сериализуем для отображения
EntitySerializer serializer = new();
SerializationResult = serializer.Serialize(FullEntity);
return Page();
}
}
}
+13 -7
View File
@@ -1,4 +1,4 @@
@page
@page "/{type}"
@using Pf2e.Database.Model.Common.I18n
@model EntityTypeModel
@@ -45,7 +45,10 @@
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
<a asp-page="./Entity" asp-route-type="@item.Type" asp-route-slug="@item.Slug">
@{
var typeName = Pf2e.Web.Helpers.PgNameHelper.GetPgName(item.Type);
}
<a href="/@typeName/@item.Slug">
@Html.DisplayFor(modelItem => item.Name[LocaleType.English])
</a>
</td>
@@ -75,14 +78,17 @@
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
<a class="page-link" asp-page="./EntityType" asp-route-type="@Model.CurrentType" asp-route-pageNum="@(Model.CurrentPage - 1)">Previous</a>
@{
var currentTypeName = Pf2e.Web.Helpers.PgNameHelper.GetPgName(Model.CurrentType);
}
<a class="page-link" href="/@currentTypeName?pageNum=@(Model.CurrentPage - 1)">Previous</a>
</li>
@foreach (var num in Model.Pagination)
{
@if (num == Model.CurrentPage)
{
<li class="page-item active">
<a class="page-link" asp-page="./EntityType" asp-route-type="@Model.CurrentType" asp-route-pageNum="@(num)">@num</a>
<a class="page-link" href="/@currentTypeName?pageNum=@(num)">@num</a>
</li>
}
else if (num == -1)
@@ -94,12 +100,12 @@
else
{
<li class="page-item">
<a class="page-link" asp-page="./EntityType" asp-route-type="@Model.CurrentType" asp-route-pageNum="@(num)">@num</a>
<a class="page-link" href="/@currentTypeName?pageNum=@(num)">@num</a>
</li>
}
}
<li class="page-item @(Model.CurrentPage == Model.LastPage ? "disabled" : "")">
<a class="page-link" asp-page="./EntityType" asp-route-type="@Model.CurrentType" asp-route-pageNum="@(Model.CurrentPage + 1)">Next</a>
<a class="page-link" href="/@currentTypeName?pageNum=@(Model.CurrentPage + 1)">Next</a>
</li>
</ul>
</nav>
</nav>
+42 -15
View File
@@ -12,19 +12,13 @@ using Pf2e.Database.Model.Enum;
namespace Pf2e.Web.Pages
{
public class EntityTypeModel : PageModel
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class EntityTypeModel(Pf2eDbContext context) : PageModel
{
private readonly Pf2eDbContext _context;
public EntityTypeModel(Pf2eDbContext context)
{
_context = context;
}
[BindProperty(SupportsGet = true)]
public int? PageNum { get; set; }
public int? PageNum { get; set; } = 1;
public IList<Entity> Entity { get; set; } = default!;
public IList<Database.Model.Base.Entity> Entity { get; set; } = null!;
public EntityType CurrentType;
@@ -34,29 +28,57 @@ namespace Pf2e.Web.Pages
public const int PerPage = 30;
public void OnGet(string type)
public IActionResult OnGet(string type)
{
CurrentType = Enum<EntityType>(type);
// Добавим отладочную информацию
System.Diagnostics.Debug.WriteLine($"EntityTypeModel.OnGet called with type: {type}");
int count = _context.Entities.Count(entity => entity.Type == CurrentType);
try
{
CurrentType = Enum<EntityType>(type);
System.Diagnostics.Debug.WriteLine($"CurrentType resolved to: {CurrentType}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error resolving enum: {ex.Message}");
return NotFound();
}
int count = context.Entities.Count(entity => entity.Type == CurrentType);
System.Diagnostics.Debug.WriteLine($"Count for {CurrentType}: {count}");
LastPage = (int)Math.Ceiling(count / (double)PerPage);
Entity = _context
Entity = context
.Entities.Where(entity => entity.Type == CurrentType)
.Skip((CurrentPage - 1) * PerPage)
.Take(PerPage)
.ToList();
System.Diagnostics.Debug.WriteLine($"Returning page with {Entity.Count} entities");
// Set breadcrumbs
ViewData["Breadcrumbs"] = new List<(string, string)>
{
("Home", "/"),
(CurrentType.ToString(), ""),
};
return Page();
}
/// <summary>
/// Преобразование значения из JSON в перечисление.
/// </summary>
/// <param name="value">Значение, которое нужно преобразовать</param>
/// <returns>Объект типа `T`, если преобразование успешно, иначе выбрасывает исключение</returns>
/// <returns>Объект типа <c>T</c>, если преобразование успешно, иначе выбрасывает исключение</returns>
public static T Enum<T>(string value)
where T : Enum
{
System.Diagnostics.Debug.WriteLine(
$"Trying to resolve enum value: {value} for type {typeof(T)}"
);
foreach (System.Reflection.FieldInfo field in typeof(T).GetFields())
{
List<string> variants = [field.Name];
@@ -69,8 +91,13 @@ namespace Pf2e.Web.Pages
variants.Add(pgAttribute.PgName);
}
System.Diagnostics.Debug.WriteLine(
$"Checking field: {field.Name}, variants: {string.Join(", ", variants)}"
);
if (variants.Contains(value))
{
System.Diagnostics.Debug.WriteLine($"Matched field: {field.Name}");
return (T)field.GetValue(null)!;
}
}
+5 -2
View File
@@ -21,7 +21,10 @@
{
<tr>
<td>
<a asp-page="./EntityType" asp-route-type="@row.Key">
@{
var typeName = Pf2e.Web.Helpers.PgNameHelper.GetPgName(row.Key);
}
<a href="/@typeName">
@row.Key
</a>
</td>
@@ -29,4 +32,4 @@
</tr>
}
</tbody>
</table>
</table>
+5
View File
@@ -24,5 +24,10 @@ public class IndexModel : PageModel
Data = Context
.Entities.GroupBy(e => e.Type)
.ToDictionary(entities => entities.Key, entities => entities.Count());
ViewData["Breadcrumbs"] = new List<(string, string)>
{
("Home", "")
};
}
}
+5
View File
@@ -13,6 +13,11 @@ public class PrivacyModel : PageModel
public void OnGet()
{
ViewData["Breadcrumbs"] = new List<(string, string)>
{
("Home", "/"),
("Privacy Policy", "")
};
}
}
+22
View File
@@ -32,6 +32,28 @@
</nav>
</header>
<div class="container">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
@if (ViewData["Breadcrumbs"] != null)
{
@foreach (var crumb in (List<(string, string)>)ViewData["Breadcrumbs"]!)
{
if (!string.IsNullOrEmpty(crumb.Item2))
{
<li class="breadcrumb-item"><a href="@crumb.Item2">@crumb.Item1</a></li>
}
else
{
<li class="breadcrumb-item active" aria-current="page">@crumb.Item1</li>
}
}
}
else
{
<li class="breadcrumb-item active" aria-current="page">Home</li>
}
</ol>
</nav>
<main role="main" class="pb-3">
@RenderBody()
</main>
+1
View File
@@ -9,6 +9,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="dotenv.net" Version="4.0.0" />
<PackageReference Include="Markdig" Version="0.42.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+2 -2
View File
@@ -13,12 +13,12 @@ builder.Services.AddDbContext<Pf2eDbContext>(options =>
options.UseNpgsql(EnvReader.GetStringValue("DATABASE_CONNECTION"))
);
var app = builder.Build();
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
//app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}