diff --git a/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs b/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs index 471d29ad..c9ea9353 100644 --- a/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs +++ b/PARR.Core/Services/Shortcodes/Handlers/ParentUnitByFieldValueShortcodeHandler.cs @@ -6,17 +6,19 @@ using System.Text.RegularExpressions; namespace PARR.Core.Services.Shortcodes.Handlers; /// -/// Шорткод %РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:ИМЯ_АТРИБУТА_ФИЛЬТРА:МАСКА_ЗНАЧЕНИЯ:ИМЯ_АТРИБУТА_РЕЗУЛЬТАТА% -/// Возвращает значение атрибута первого ближайшего родителя, у которого значение атрибута-фильтра подходит под маску +/// Шорткод %РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:АТРИБУТ1:МАСКА1&АТРИБУТ2:МАСКА2>>АТРИБУТ_РЕЗУЛЬТАТА% +/// Возвращает значение атрибута первого ближайшего родителя, удовлетворяющего всем условиям фильтрации. +/// При неверном синтаксисе выбрасывает InvalidOperationException. /// internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler { - private readonly ILogger logger; + private readonly ILogger _logger; public int Order => 56; + // Паттерн: захватываем все содержимое между ключевым словом и конечным % private static readonly Regex _pattern = new( - @"%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:([^:]+):([^:]+):([^%]+)%", + @"%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ:(?[^%]+)%", RegexOptions.Compiled | RegexOptions.IgnoreCase); public Regex Pattern => _pattern; @@ -25,7 +27,7 @@ internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler public ParentUnitByFieldValueShortcodeHandler(ILogger logger) { - this.logger = logger; + _logger = logger; } public Task ResolveAsync(string input, TemplateForShortcode template, string caller, CancellationToken ct = default) @@ -33,67 +35,130 @@ internal class ParentUnitByFieldValueShortcodeHandler : IShortcodeHandler var matches = Pattern.Matches(input); if (matches.Count == 0) return Task.FromResult(input); - var uniqueRequests = matches.Cast() - .Select(m => new - { - FullMatch = m.Value, - FilterField = m.Groups[1].Value.Trim(), - Mask = m.Groups[2].Value.Trim(), - ResultField = m.Groups[3].Value.Trim() - }) - .GroupBy(x => new { x.FilterField, x.Mask, x.ResultField }) - .Select(g => g.First()) + var uniquePayloads = matches.Cast() + .Select(m => m.Groups["payload"].Value.Trim()) + .Distinct() .ToList(); - var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); + var resolvedValues = new Dictionary(); - foreach (var request in uniqueRequests) + foreach (var payload in uniquePayloads) { - resolved[request.FullMatch] = GetFieldValue(template, request.FilterField, request.Mask, request.ResultField, caller); + // Здесь может быть выброшено исключение при неверном синтаксисе + var result = ProcessPayload(template, payload, caller); + resolvedValues[payload] = result; } - var result = input; + var output = input; foreach (Match m in matches) { - if (resolved.TryGetValue(m.Value, out var val)) - result = result.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase); + var payload = m.Groups["payload"].Value.Trim(); + if (resolvedValues.TryGetValue(payload, out var val)) + { + output = output.Replace(m.Value, val, StringComparison.OrdinalIgnoreCase); + } } - return Task.FromResult(result); + return Task.FromResult(output); } - private string GetFieldValue(TemplateForShortcode template, string filterField, string mask, string resultField, string caller) + private string ProcessPayload(TemplateForShortcode template, string payload, string caller) { + // 1. Проверка наличия разделителя '>>' + var separatorIndex = payload.IndexOf(">>", StringComparison.Ordinal); + + if (separatorIndex <= 0 || separatorIndex >= payload.Length - 2) + { + throw new InvalidOperationException( + $"Неверный синтаксис шорткода '%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ...'. " + + $"Отсутствует или расположен неправильно разделитель '>>' в выражении: '{payload}'. " + + $"Ожидаемый формат: АТРИБУТ:МАСКА>>АТРИБУТ_РЕЗУЛЬТАТА"); + } + + string conditionsStr = payload.Substring(0, separatorIndex).Trim(); + string resultFieldName = payload.Substring(separatorIndex + 2).Trim(); + + // 2. Валидация имени результирующего поля + if (string.IsNullOrEmpty(resultFieldName)) + { + throw new InvalidOperationException( + $"Неверный синтаксис шорткода '%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ...'. " + + $"Имя результирующего атрибута после '>>' не может быть пустым. Выражение: '{payload}'"); + } + + // 3. Парсинг условий + var conditions = ParseConditions(conditionsStr); + + if (conditions.Count == 0) + { + throw new InvalidOperationException( + $"Неверный синтаксис шорткода '%РОД_ЭК_ПЕРВЫЙ_ПО_ЗНАЧЕНИЮ...'. " + + $"Не найдено ни одного валидного условия фильтрации перед '>>'. Выражение: '{payload}'"); + } + + // 4. Поиск подходящего родителя (Бизнес-логика: если не нашли - это НЕ ошибка синтаксиса, а отсутствие данных) if (template.ParentUnits == null || template.ParentUnits.Count == 0) { - logger.LogDebug("[{Caller}] Родителей нет. Возвращаю 'Нет данных'.", caller); + _logger.LogDebug("[{Caller}] Список родителей пуст для шаблона.", caller); return "Нет данных"; } var matchingParent = template.ParentUnits.FirstOrDefault(p => { - if (!p.FieldValues.TryGetValue(filterField, out var currentValue)) - return false; + foreach (var cond in conditions) + { + if (!p.FieldValues.TryGetValue(cond.AttributeName, out var currentValue)) + return false; - return MatchesMask(currentValue, mask); + if (!MatchesMask(currentValue, cond.Mask)) + return false; + } + return true; }); if (matchingParent == null) { - logger.LogDebug("[{Caller}] Не найден родитель с '{Filter}' по маске '{Mask}'. Возвращаю 'Нет данных'.", - caller, filterField, mask); + _logger.LogDebug("[{Caller}] Не найден родитель, подходящий под условия: {Payload}", caller, payload); return "Нет данных"; } - if (matchingParent.FieldValues.TryGetValue(resultField, out var value)) + // 5. Извлечение значения + if (matchingParent.FieldValues.TryGetValue(resultFieldName, out var finalValue)) { - return value; + return finalValue; } - logger.LogDebug("[{Caller}] Родитель найден, но атрибут '{Field}' отсутствует. Возвращаю 'Нет данных'.", caller, resultField); + _logger.LogDebug("[{Caller}] Родитель найден, но атрибут '{Field}' отсутствует у него.", caller, resultFieldName); return "Нет данных"; } + private List<(string AttributeName, string Mask)> ParseConditions(string raw) + { + var list = new List<(string, string)>(); + if (string.IsNullOrEmpty(raw)) return list; + + var pairs = raw.Split('&', StringSplitOptions.RemoveEmptyEntries); + + foreach (var pair in pairs) + { + var parts = pair.Split(':', 2); + + // Строгая проверка формата пары "Ключ:Значение" + if (parts.Length != 2 || + string.IsNullOrWhiteSpace(parts[0]) || + string.IsNullOrWhiteSpace(parts[1])) + { + // Если условие битое, считаем весь запрос синтаксически неверным + throw new InvalidOperationException( + $"Неверный формат условия фильтрации: '{pair}'. Ожидается 'АТРИБУТ:МАСКА'."); + } + + list.Add((parts[0].Trim(), parts[1].Trim())); + } + + return list; + } + private static bool MatchesMask(string value, string mask) { if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(mask)) diff --git a/PARR.DAL/Repositories/Base/BaseRepository.cs b/PARR.DAL/Repositories/Base/BaseRepository.cs index 229b1c23..d5737e36 100644 --- a/PARR.DAL/Repositories/Base/BaseRepository.cs +++ b/PARR.DAL/Repositories/Base/BaseRepository.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Base; using PARR.DAL.Context;