diff --git a/Pf2e/Web/Helpers/EntityFieldFormatter.cs b/Pf2e/Web/Helpers/EntityFieldFormatter.cs deleted file mode 100644 index cef4f06..0000000 --- a/Pf2e/Web/Helpers/EntityFieldFormatter.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System.Collections; -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; - -public class EntityFieldFormatter -{ - /// - /// Форматировать значение для отображения. - /// - public string? FormatValue(object? value) - { - if (value == null) - return null; - - Type type = value.GetType(); - - foreach (ValueFormatter formatter in _formatters) - { - if (formatter.CanFormat(type)) - { - return formatter.Format(value); - } - } - - return null; - } - - /// - /// Абстрактный класс форматтера значений. - /// - private abstract class ValueFormatter - { - /// - /// Проверяет, может ли форматтер обработать указанный тип. - /// - /// Тип значения. - /// true, если форматтер может обработать тип, иначе false. - public abstract bool CanFormat(Type type); - - /// - /// Форматирует значение для отображения. - /// - /// Значение для форматирования. - /// Строковое представление отформатированного значения. - public abstract string Format(object value); - } - - /// - /// Форматтер для конкретного типа. - /// - /// Тип значения. - /// Функция форматирования значения. - private class TypeValueFormatter(Func formatter) : ValueFormatter - { - public override bool CanFormat(Type type) => type == typeof(T); - - public override string Format(object value) => formatter(value); - } - - /// - /// Форматтер с пользовательскими функциями проверки и форматирования. - /// - /// Функция проверки типа. - /// Функция форматирования значения. - private class CustomValueFormatter(Func checker, Func formatter) - : ValueFormatter - { - public override bool CanFormat(Type type) => checker(type); - - public override string Format(object value) => formatter(value); - } - - /// - /// Форматтер для коллекций. - /// - private class CollectionFormatter(EntityFieldFormatter fieldFormatter) : 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 items = []; - - foreach (object? item in collection) - { - string? itemDisplay = fieldFormatter.FormatValue(item); - items.Add(itemDisplay ?? "null"); - } - - return items.Count == 0 ? "(empty)" : string.Join("
", items); - } - } - - /// - /// Коллекция форматтеров. - /// - private readonly List _formatters; - - /// - /// Получить описание элемента перечисления из DescriptionAttribute. - /// - /// Элемент перечисления. - /// Описание или имя элемента, если атрибут не найден. - private static string GetEnumDescription(Enum value) - { - Type type = value.GetType(); - string name = Enum.GetName(type, value) ?? value.ToString(); - - System.Reflection.FieldInfo? field = type.GetField(name); - if (field == null) - return value.ToString(); - - if ( - Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) - is DescriptionAttribute attr - ) - { - return attr.Description; - } - - return value.ToString(); - } - - /// - /// Конструктор EntitySerializer. - /// - public EntityFieldFormatter() - { - _formatters = - [ - // Форматтеры для перечислений - new CustomValueFormatter( - (type) => type.IsEnum, - (value) => - { - Enum enumValue = (Enum)value; - string description = GetEnumDescription(enumValue); - return $"{description} ({enumValue})"; - } - ), - // Примитивные типы - new CustomValueFormatter( - (type) => type.IsPrimitive || type == typeof(string) || type == typeof(decimal), - (value) => value.ToString() ?? "null" - ), - // LocaleString - new TypeValueFormatter( - (value) => - { - LocaleString localeString = (LocaleString)value; - List items = []; - foreach (LocaleItem item in localeString) - { - items.Add($"{item.Locale}: {item.Value}"); - } - return string.Join("
", 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 $"{name}"; - } - ), - // EntitySource - new TypeValueFormatter( - (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( - (value) => - { - Keyword keyword = (Keyword)value; - string name = keyword.Name[LocaleType.English]; - return $"{name}"; - } - ), - // KeywordAlias - new TypeValueFormatter( - (value) => - { - KeywordAlias alias = (KeywordAlias)value; - return $"{alias.Value}"; - } - ), - // ExternalTarget - new TypeValueFormatter( - (value) => - { - ExternalTarget target = (ExternalTarget)value; - return $"{target.Name} (Icon: {target.Icon}, Prefix: {target.Prefix})"; - } - ), - // EntityExternal - new TypeValueFormatter( - (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( - (value) => - { - Source source = (Source)value; - string releaseDate = source.ReleaseDate?.ToString("yyyy-MM-dd") ?? "Unknown"; - return $"{source.Name} (Category: {source.Category}, Release: {releaseDate})"; - } - ), - // Коллекции - new CollectionFormatter(this), - ]; - } -} diff --git a/Pf2e/Web/Helpers/EntitySerialization.cs b/Pf2e/Web/Helpers/EntitySerialization.cs new file mode 100644 index 0000000..60ad3de --- /dev/null +++ b/Pf2e/Web/Helpers/EntitySerialization.cs @@ -0,0 +1,395 @@ +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(); + } + } + + /// + /// Сериализует объект в результат сериализации. + /// + /// Объект для сериализации. + /// Результат сериализации. + 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 _builders = []; + + private static EntityBuilder NewBuilder() + where TEntity : class + { + EntityBuilder builder = new(); + _builders.Add(builder); + return builder; + } + + private static void RegisterBuilders() + { + // Добавляем построитель для FullEntity + NewBuilder() + .AddGroup( + "Основная сущность", + builder => + { + builder.AddField(fe => fe.Entity).AddField(fe => fe.SubEntity); + } + ); + + NewBuilder() + .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() + .AddGroup( + "Действие", + builder => + { + builder + .AddField(a => a.Id) + .AddField(a => a.Entity) + .AddField(a => a.Actions) + .AddField(a => a.Category) + .AddField(a => a.Extra); + } + ); + + NewBuilder() + .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() + .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() + .AddGroup( + "Предыстория", + builder => + { + builder + .AddField(b => b.Id) + .AddField(b => b.Entity) + .AddField(b => b.BackgroundBoosts) + .AddField(b => b.BackgroundItems) + .AddField(b => b.BackgroundSkills); + } + ); + + NewBuilder() + .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() + .AddGroup( + "Состояние", + builder => + { + builder + .AddField(c => c.Id) + .AddField(c => c.Entity) + .AddField(c => c.Group) + .AddField(c => c.Extra); + } + ); + + NewBuilder() + .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() + .AddGroup( + "Категория божества", + builder => + { + builder + .AddField(dc => dc.Id) + .AddField(dc => dc.Entity) + .AddField(dc => dc.Description); + } + ); + + NewBuilder() + .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() + .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() + .AddGroup( + "Наследие", + builder => + { + builder + .AddField(h => h.Id) + .AddField(h => h.Entity) + .AddField(h => h.Ancestry) + .UseCommentFrom(h => h.AncestryId); + } + ); + + NewBuilder() + .AddGroup( + "Язык", + builder => + { + builder.AddField(l => l.Id).AddField(l => l.Entity); + } + ); + + NewBuilder() + .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() + .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() + .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() + .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() + .AddGroup( + "Группа подклассов", + builder => + { + builder + .AddField(sg => sg.Id) + .AddField(sg => sg.Entity) + .AddField(sg => sg.Class) + .UseCommentFrom(sg => sg.ClassId) + .AddField(sg => sg.Subclasses); + } + ); + + NewBuilder() + .AddGroup( + "Признак", + builder => + { + builder + .AddField(t => t.Id) + .AddField(t => t.Entity) + .AddField(t => t.Type) + .AddField(t => t.Entities); + } + ); + } +} diff --git a/Pf2e/Web/Helpers/EntitySerializer.cs b/Pf2e/Web/Helpers/EntitySerializer.cs deleted file mode 100644 index 1ce0f39..0000000 --- a/Pf2e/Web/Helpers/EntitySerializer.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Reflection; -using Pf2e.Common; - -namespace Pf2e.Web.Helpers; - -/// -/// Сериализатор сущности для отображения на веб-странице. -/// -public class EntitySerializer -{ - /// - /// Результат сериализации сущности. - /// - public class SerializationResult - { - public List Groups { get; set; } = []; - } - - /// - /// Информация о поле. - /// - 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; - } - - /// - /// Форматтер полей сущности. - /// - public EntityFieldFormatter FieldFormatter { get; } = new(); - - /// - /// Сериализовать FullEntity. - /// - 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); - //} - - return result; - } - - /// - /// Добавить поля объекта в группу. - /// - //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; - - // object? value = property.GetValue(obj); - // FieldBuilderDefinition fieldInfo = CreateFieldInfo(property, value); - // group.Fields.Add(fieldInfo); - // } - //} - - /// - /// Создать FieldBuilderDefinition для свойства. - /// - /// Поле класса. - /// Значение поля. - /// Информация о поле. - private FieldInfo CreateFieldInfo(PropertyInfo property, object? value) - { - string? displayValue = FieldFormatter.FormatValue(value); - - return new() - { - Name = property.Name, - Type = property.PropertyType.Name, - Value = value, - DisplayValue = displayValue ?? "null", - IsImplemented = displayValue is not null, - }; - } -} diff --git a/Pf2e/Web/Helpers/Format/Formatters.cs b/Pf2e/Web/Helpers/Format/Formatters.cs new file mode 100644 index 0000000..81912ee --- /dev/null +++ b/Pf2e/Web/Helpers/Format/Formatters.cs @@ -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 +{ + /// + /// Форматтер для перечислений (enum). + /// + 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(); + } + } + ); + + /// + /// Форматтер для примитивных типов и строк. + /// + public static readonly ValueFormatter PrimitiveFormatter = new CustomValueFormatter( + (type) => type.IsPrimitive || type == typeof(string) || type == typeof(decimal), + (value) => value.ToString() ?? "null" + ); + + /// + /// Форматтер для коллекций. + /// + public static readonly ValueFormatter CollectionFormatter = new CollectionFormatter(); + + /// + /// Форматтеры для стандартных типов из базы данных. + /// + public static readonly List DefaultTypeFormatters = + [ + new TypeValueFormatter( + (value) => + { + LocaleString localeString = (LocaleString)value; + List items = []; + foreach (LocaleItem item in localeString) + { + items.Add($"{item.Locale}: {item.Value}"); + } + return string.Join("
", 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 $"{name}"; + } + ), + // EntitySource + new TypeValueFormatter( + (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( + (value) => + { + Keyword keyword = (Keyword)value; + string name = keyword.Name[LocaleType.English]; + return $"{name}"; + } + ), + // KeywordAlias + new TypeValueFormatter( + (value) => + { + KeywordAlias alias = (KeywordAlias)value; + return $"{alias.Value}"; + } + ), + // ExternalTarget + new TypeValueFormatter( + (value) => + { + ExternalTarget target = (ExternalTarget)value; + return $"{target.Name} (Icon: {target.Icon}, Prefix: {target.Prefix})"; + } + ), + // EntityExternal + new TypeValueFormatter( + (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( + (value) => + { + Source source = (Source)value; + string releaseDate = source.ReleaseDate?.ToString("yyyy-MM-dd") ?? "Unknown"; + return $"{source.Name} (Category: {source.Category}, Release: {releaseDate})"; + } + ), + ]; + + /// + /// Набор всех стандартных форматтеров. + /// + public static readonly List AllFormatters = + [ + .. DefaultTypeFormatters, + EnumFormatter, + PrimitiveFormatter, + CollectionFormatter, + ]; + + /// + /// Добавить пользовательский форматтер в набор всех форматтеров. + /// + /// Добавляемый форматтер. + public static void AddFormatter(ValueFormatter formatter) + { + AllFormatters.Insert(0, formatter); + } + + /// + /// Выбрать подходящий форматтер для типа. + /// + /// Тип значения. + /// Форматтер для типа или null, если подходящего не найдено. + public static ValueFormatter? SelectFormatter(Type type) => + AllFormatters.FirstOrDefault(f => f.CanFormat(type)) ?? null; + + /// + /// Выбрать подходящий форматтер для значения. + /// + /// Значение. + /// Форматтер для значения или null, если значение равно null или подходящего не найдено. + public static ValueFormatter? SelectFormatter(object? value) => + value == null ? null : SelectFormatter(value.GetType()); + + /// + /// Форматировать значение в строку. + /// + /// Форматируемое значение. + /// Строковое представление отформатированного значения. + public static string Format(object? value) => + SelectFormatter(value)?.Format(value!) ?? "[not implemented]"; +} diff --git a/Pf2e/Web/Helpers/Format/ValueFormatter.cs b/Pf2e/Web/Helpers/Format/ValueFormatter.cs new file mode 100644 index 0000000..bba2723 --- /dev/null +++ b/Pf2e/Web/Helpers/Format/ValueFormatter.cs @@ -0,0 +1,88 @@ +using System.Collections; +using static LinqToDB.Reflection.Methods.LinqToDB.Insert; + +namespace Pf2e.Web.Helpers.Format; + +/// +/// Абстрактный класс форматтера значений. +/// +public abstract class ValueFormatter +{ + /// + /// Проверяет, может ли форматтер обработать указанный тип. + /// + /// Тип значения. + /// true, если форматтер может обработать тип, иначе false. + public abstract bool CanFormat(Type type); + + /// + /// Форматирует значение для отображения. + /// + /// Значение для форматирования. + /// Строковое представление отформатированного значения. + public abstract string Format(object value); + + /// + /// Форматирует имя типа для отображения. + /// + /// Тип значения. + /// Строковое представление имени типа. + public virtual string TypeName(Type type) => type.Name; +} + +/// +/// Форматтер для конкретного типа. +/// +/// Тип значения. +/// Функция форматирования значения. +public class TypeValueFormatter(Func formatter) : ValueFormatter +{ + public override bool CanFormat(Type type) => type == typeof(T); + + public override string Format(object value) => formatter(value); +} + +/// +/// Форматтер с пользовательскими функциями проверки и форматирования. +/// +/// Функция проверки типа. +/// Функция форматирования значения. +public class CustomValueFormatter( + Func checker, + Func formatter, + Func? 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); +} + +/// +/// Форматтер для коллекций. +/// +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 items = []; + + foreach (object? item in collection) + { + items.Add(Formatters.Format(item)); + } + + return items.Count == 0 ? "(empty)" : string.Join("
", items); + } + + public override string TypeName(Type type) + { + return $"{type.Name[..^2]}<{string.Join(", ", type.GetGenericArguments().Select(t => t.Name))}>"; + } +} diff --git a/Pf2e/Web/Helpers/PgNameHelper.cs b/Pf2e/Web/Helpers/PgNameHelper.cs index 02db290..a282b9d 100644 --- a/Pf2e/Web/Helpers/PgNameHelper.cs +++ b/Pf2e/Web/Helpers/PgNameHelper.cs @@ -1,19 +1,19 @@ -using System.Reflection; +using System.Reflection; using NpgsqlTypes; using Pf2e.Database.Model.Enum; namespace Pf2e.Web.Helpers { /// - /// Âñïîìîãàòåëüíûé êëàññ äëÿ ïîëó÷åíèÿ èìåí PostgresSQL èç ïåðå÷èñëåíèé. + /// Вспомогательный класс для получения имен PostgresSQL из перечислений. /// public static class PgNameHelper { /// - /// Ïîëó÷àåò èìÿ PostgresSQL äëÿ çàäàííîãî òèïà ñóùíîñòè. + /// Получает имя PostgresSQL для заданного типа сущности. /// - /// Òèï ñóùíîñòè. - /// Ñòðîêîâîå ïðåäñòàâëåíèå èìåíè PostgresSQL. + /// Тип сущности. + /// Строковое представление имени PostgresSQL. public static string GetPgName(EntityType entityType) { FieldInfo? fieldInfo = entityType.GetType().GetField(entityType.ToString()); diff --git a/Pf2e/Web/Helpers/Serialization/EntityBuilder.cs b/Pf2e/Web/Helpers/Serialization/EntityBuilder.cs new file mode 100644 index 0000000..2503120 --- /dev/null +++ b/Pf2e/Web/Helpers/Serialization/EntityBuilder.cs @@ -0,0 +1,144 @@ +using System.Reflection; +using Pf2e.Web.Helpers.Format; + +namespace Pf2e.Web.Helpers.Serialization; + +/// +/// Построитель сериализации для сущности. +/// +/// Тип сущности для сериализации. +public abstract class EntityBuilder(Type type) +{ + /// + /// Тип сущности для сериализации. + /// + public Type EntityType { get; } = type; + + /// + /// Сериализует объект в список групп полей. + /// + /// + /// + public abstract List Compile(object value); +} + +/// +/// Построитель сериализации для типа T. +/// +/// Тип сущности для сериализации. +public class EntityBuilder() : EntityBuilder(typeof(TEntity)) + where TEntity : class +{ + /// + /// Список групп полей. + /// + public List Groups { get; set; } = []; + + /// + /// Добавляет группу полей в построитель сериализации. + /// + /// Название группы. + /// Делегат для настройки группы полей. + /// Текущий экземпляр построителя. + public EntityBuilder AddGroup( + string name, + Action> configure + ) + { + FieldGroupBuilder groupBuilder = new(name); + configure(groupBuilder); + Groups.Add(groupBuilder.Build()); + return this; + } + + /// + /// Добавляет группу полей в построитель сериализации с названием типа. + /// + /// Делегат для настройки группы полей. + /// Текущий экземпляр построителя. + public EntityBuilder AddGroup(Action> configure) + { + return AddGroup(typeof(TEntity).Name, configure); + } + + /// + public override List Compile(object value) + { + return Groups.Select(group => CompileGroup(group, value)).ToList(); + } + + /// + /// Строит группу полей. + /// + /// Готовая группа полей. + 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; + } + + /// + /// Построение поля сериализации. + /// + /// Определение поля. + /// Значение поля. + /// Построенное поле сериализации. + 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, + }; + } +} diff --git a/Pf2e/Web/Helpers/Serialization/FieldDefinition.cs b/Pf2e/Web/Helpers/Serialization/FieldDefinition.cs new file mode 100644 index 0000000..93f1427 --- /dev/null +++ b/Pf2e/Web/Helpers/Serialization/FieldDefinition.cs @@ -0,0 +1,22 @@ +namespace Pf2e.Web.Helpers.Serialization; + +/// +/// Информация о поле. +/// +public class FieldDefinition +{ + /// + /// Название поля. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Тип поля. + /// + public required Type Type { get; set; } + + /// + /// Форматтер значения поля. + /// + public Func? ValueFormatter { get; set; } = null; +} diff --git a/Pf2e/Web/Helpers/Serialization/FieldGroupBuilder.cs b/Pf2e/Web/Helpers/Serialization/FieldGroupBuilder.cs new file mode 100644 index 0000000..f1e3264 --- /dev/null +++ b/Pf2e/Web/Helpers/Serialization/FieldGroupBuilder.cs @@ -0,0 +1,116 @@ +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Pf2e.Web.Helpers.Format; + +namespace Pf2e.Web.Helpers.Serialization; + +/// +/// Построитель группы полей для типа TField. +/// +/// Тип сущности для группы полей. +public class FieldGroupBuilder(string name) + where TField : class +{ + private readonly FieldGroupDefinition _groupDefinition = new() + { + Name = name, + GroupType = typeof(TField), + }; + + /// + /// Добавляет поле в группу. + /// + /// Лямбда-выражение для получения поля. + /// Текущий экземпляр построителя группы полей. + public FieldGroupBuilder AddField(Expression> fieldExpression) + { + // Получаем информацию о свойстве из выражения + MemberExpression? memberExpression = GetMemberExpression(fieldExpression.Body); + if (memberExpression == null) + { + throw new ArgumentException( + "Выражение должно быть доступом к свойству", + nameof(fieldExpression) + ); + } + + PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member; + + // Получаем атрибут CommentAttribute, если он есть + CommentAttribute? commentAttribute = propertyInfo.GetCustomAttribute(); + string fieldName = commentAttribute?.Comment ?? propertyInfo.Name; + Type fieldType = propertyInfo.PropertyType; + + _groupDefinition.Fields.Add(new() { Name = fieldName, Type = fieldType }); + return this; + } + + /// + /// Устанавливает форматтер значения для последнего добавленного поля. + /// + /// Функция форматирования значения. + /// Текущий экземпляр построителя группы полей. + public FieldGroupBuilder SetValueFormatter(Func valueFormatter) + { + _groupDefinition.Fields.Last().ValueFormatter = valueFormatter; + return this; + } + + /// + /// Использует комментарий из другого поля для установки имени последнего добавленного поля. + /// + /// Лямбда-выражение для получения поля с комментарием. + /// Текущий экземпляр построителя группы полей. + public FieldGroupBuilder UseCommentFrom( + Expression> fieldExpression + ) + { + // Получаем информацию о свойстве из выражения + MemberExpression? memberExpression = GetMemberExpression(fieldExpression.Body); + if (memberExpression == null) + { + throw new ArgumentException( + "Выражение должно быть доступом к свойству", + nameof(fieldExpression) + ); + } + + PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member; + + // Получаем атрибут CommentAttribute, если он есть + CommentAttribute? commentAttribute = propertyInfo.GetCustomAttribute(); + if (commentAttribute != null) + { + // Устанавливаем имя последнего добавленного поля + if (_groupDefinition.Fields.Count > 0) + { + _groupDefinition.Fields.Last().Name = commentAttribute.Comment; + } + } + + return this; + } + + /// + /// Завершает построение группы полей и возвращает определение группы. + /// + /// Итоговое определение группы полей. + public FieldGroupDefinition Build() => _groupDefinition; + + /// + /// Получает MemberExpression из тела лямбда-выражения. + /// + /// Тело выражения. + /// MemberExpression или null, если не удалось получить. + private static MemberExpression? GetMemberExpression(Expression expression) + { + return expression switch + { + MemberExpression memberExpression => memberExpression, + UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression => + unaryExpression.Operand as MemberExpression, + _ => null, + }; + } +} diff --git a/Pf2e/Web/Helpers/Serialization/FieldGroupDefinition.cs b/Pf2e/Web/Helpers/Serialization/FieldGroupDefinition.cs new file mode 100644 index 0000000..ddc6883 --- /dev/null +++ b/Pf2e/Web/Helpers/Serialization/FieldGroupDefinition.cs @@ -0,0 +1,22 @@ +namespace Pf2e.Web.Helpers.Serialization; + +/// +/// Группа полей. +/// +public class FieldGroupDefinition +{ + /// + /// Название группы. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Тип класса, к которому относится поля группы. + /// + public Type GroupType { get; set; } = null!; + + /// + /// Список полей в группе. + /// + public List Fields { get; set; } = []; +} diff --git a/Pf2e/Web/Helpers/Serialization/SerializationResult.cs b/Pf2e/Web/Helpers/Serialization/SerializationResult.cs new file mode 100644 index 0000000..8e18074 --- /dev/null +++ b/Pf2e/Web/Helpers/Serialization/SerializationResult.cs @@ -0,0 +1,54 @@ +namespace Pf2e.Web.Helpers.Serialization; + +/// +/// Результат сериализации сущности. +/// +public class SerializationResult +{ + /// + /// Группы полей. + /// + public List Groups { get; set; } = []; +} + +/// +/// Группа полей для сериализации. +/// +public class SerializationGroup +{ + /// + /// Название группы. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Список полей в группе. + /// + public List Fields { get; set; } = []; +} + +/// +/// Поле для сериализации. +/// +public class SerializationField +{ + /// + /// Название поля. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Значение поля в строковом представлении. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Значение поля в строковом представлении. + /// + public string Value { get; set; } = string.Empty; + + /// + /// Флаг, указывающий, реализовано ли поле (true) или нет (false). + /// + public bool IsImplemented { get; set; } = true; +} diff --git a/Pf2e/Web/Helpers/SerializationBuilder.cs b/Pf2e/Web/Helpers/SerializationBuilder.cs deleted file mode 100644 index 9137b70..0000000 --- a/Pf2e/Web/Helpers/SerializationBuilder.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System.Linq.Expressions; -using System.Reflection; - -namespace Pf2e.Web.Helpers; - -/// -/// Построитель сериализации для типа T. -/// -/// Тип сущности для сериализации. -public class SerializationBuilder - where T : class -{ - /// - /// Список групп полей. - /// - public List Groups { get; set; } = []; - - /// - /// Форматтер полей сущности. - /// - protected EntityFieldFormatter FieldFormatter { get; } = new(); - - /// - /// Добавляет группу полей в построитель сериализации. - /// - /// Название группы. - /// Делегат для настройки группы полей. - /// Текущий экземпляр построителя. - public SerializationBuilder AddGroup(string name, Action> configure) - { - FieldGroupBuilder groupBuilder = new(name); - configure(groupBuilder); - Groups.Add(groupBuilder.Build()); - return this; - } - - /// - /// Добавляет группу полей в построитель сериализации с названием типа. - /// - /// Делегат для настройки группы полей. - /// Текущий экземпляр построителя. - public SerializationBuilder AddGroup(Action> configure) - { - return AddGroup(typeof(T).Name, configure); - } - - /// - /// Построитель группы полей для типа TField. - /// - /// Тип сущности для группы полей. - public class FieldGroupBuilder(string name) - where TField : class - { - private readonly FieldGroup _group = new() { Name = name }; - - /// - /// Добавляет поле в группу. - /// - /// Лямбда-выражение для получения поля. - /// Текущий экземпляр построителя группы полей. - public FieldGroupBuilder AddField(Expression> fieldExpression) - { - // Получаем информацию о свойстве из выражения - MemberExpression? memberExpression = GetMemberExpression(fieldExpression.Body); - if (memberExpression == null) - { - throw new ArgumentException( - "Выражение должно быть доступом к свойству", - nameof(fieldExpression) - ); - } - - PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member; - string fieldName = propertyInfo.Name; - Type fieldType = propertyInfo.PropertyType; - - _group.Fields.Add(new() { Name = fieldName, Type = fieldType }); - return this; - } - - /// - /// Устанавливает форматтер значения для последнего добавленного поля. - /// - /// Функция форматирования значения. - /// Текущий экземпляр построителя группы полей. - public FieldGroupBuilder SetValueFormatter(Func valueFormatter) - { - _group.Fields.Last().ValueFormatter = valueFormatter; - return this; - } - - /// - /// Строит группу полей. - /// - /// Готовая группа полей. - public FieldGroup Build() => _group; - - /// - /// Получает MemberExpression из тела лямбда-выражения. - /// - /// Тело выражения. - /// MemberExpression или null, если не удалось получить. - private static MemberExpression? GetMemberExpression(Expression expression) - { - return expression switch - { - MemberExpression memberExpression => memberExpression, - UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression => - unaryExpression.Operand as MemberExpression, - _ => null, - }; - } - } - - /// - /// Группа полей. - /// - public class FieldGroup - { - /// - /// Название группы. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Список полей в группе. - /// - public List Fields { get; set; } = []; - } - - /// - /// Информация о поле. - /// - public class FieldBuilderDefinition - { - /// - /// Название поля. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Тип поля. - /// - public required Type Type { get; set; } - - /// - /// Форматтер значения поля. - /// - public Func? ValueFormatter { get; set; } = null; - } -} diff --git a/Pf2e/Web/Pages/Entity.cshtml b/Pf2e/Web/Pages/Entity.cshtml index 828294c..a8fee1d 100644 --- a/Pf2e/Web/Pages/Entity.cshtml +++ b/Pf2e/Web/Pages/Entity.cshtml @@ -1,5 +1,5 @@ @page "/{type}/{slug}" -@model Pf2e.Web.Pages.EntityModel +@model Pf2e.Web.Pages.Entity @{ ViewData["Title"] = "Entity"; @@ -26,10 +26,10 @@ { НЕ РЕАЛИЗОВАНО: } - @Html.Raw(field.DisplayValue) + @Html.Raw(field.Value) } -} +} \ No newline at end of file diff --git a/Pf2e/Web/Pages/Entity.cshtml.cs b/Pf2e/Web/Pages/Entity.cshtml.cs index e736710..a508e23 100644 --- a/Pf2e/Web/Pages/Entity.cshtml.cs +++ b/Pf2e/Web/Pages/Entity.cshtml.cs @@ -16,22 +16,22 @@ using Pf2e.Web.Helpers; namespace Pf2e.Web.Pages { [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public class EntityModel(Pf2eDbContext context) : PageModel + 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 Pf2e.Web.Helpers.Serialization.SerializationResult SerializationResult { get; set; } = null!; public IActionResult OnGet(string type, string slug) { // Добавим отладочную информацию System.Diagnostics.Debug.WriteLine( - $"EntityModel.OnGet called with type: {type}, slug: {slug}" + $"Entity.OnGet called with type: {type}, slug: {slug}" ); // Логируем в консоль для веб-сервера - Console.WriteLine($"EntityModel.OnGet called with type: {type}, slug: {slug}"); + Console.WriteLine($"Entity.OnGet called with type: {type}, slug: {slug}"); try { @@ -41,7 +41,7 @@ namespace Pf2e.Web.Pages Console.WriteLine($"EntityType resolved to: {entityType}"); // Находим сущность по slug и типу - Entity? entity = _context + Database.Model.Base.Entity? entity = _context .Entities.AsNoTracking() .FirstOrDefault(e => e.Slug.ToLower() == slug.ToLower() && e.Type == entityType @@ -67,7 +67,7 @@ namespace Pf2e.Web.Pages FullEntity = new(_context, entityType, entity.Id); // Сериализуем для отображения - EntitySerializer serializer = new(); + EntitySerialization serializer = new(); SerializationResult = serializer.Serialize(FullEntity); // Set breadcrumbs @@ -76,15 +76,15 @@ namespace Pf2e.Web.Pages { ("Home", "/"), (entityType.ToString(), $"/{typeName}"), - (entity.Name[Pf2e.Database.Model.Common.I18n.LocaleType.English], "") + (entity.Name[Pf2e.Database.Model.Common.I18n.LocaleType.English], ""), }; return Page(); } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Error in EntityModel.OnGet: {ex.Message}"); - Console.WriteLine($"Error in EntityModel.OnGet: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Error in Entity.OnGet: {ex.Message}"); + Console.WriteLine($"Error in Entity.OnGet: {ex.Message}"); return NotFound(); } } diff --git a/Pf2e/Web/Pages/EntityType.cshtml.cs b/Pf2e/Web/Pages/EntityType.cshtml.cs index 4f6be88..ebd0c74 100644 --- a/Pf2e/Web/Pages/EntityType.cshtml.cs +++ b/Pf2e/Web/Pages/EntityType.cshtml.cs @@ -18,7 +18,7 @@ namespace Pf2e.Web.Pages [BindProperty(SupportsGet = true)] public int? PageNum { get; set; } = 1; - public IList Entity { get; set; } = null!; + public IList Entity { get; set; } = null!; public EntityType CurrentType;