| | | 1 | | using System.Text.Json; |
| | | 2 | | using System.Text.RegularExpressions; |
| | | 3 | | |
| | | 4 | | namespace OzonOrdersWeb.WebServices.Orders.Grid; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Проверка JSON настройки <c>computedColumns</c> перед сохранением: лимиты, формулы (та же грамматика, что на клиенте) |
| | | 8 | | /// ссылки только на поля, разрешённые пользователю, правила цвета — как формулы над <c>value</c>. |
| | | 9 | | /// </summary> |
| | | 10 | | public static partial class OrdersGridComputedColumns |
| | | 11 | | { |
| | | 12 | | public const int MaxColumns = 30; |
| | | 13 | | public const int MaxNameLength = 60; |
| | | 14 | | public const int MaxColorRules = 5; |
| | | 15 | | |
| | 3 | 16 | | public sealed class ValidationException(string message) : Exception(message); |
| | | 17 | | |
| | | 18 | | [GeneratedRegex("^#[0-9a-fA-F]{6}$")] |
| | | 19 | | private static partial Regex HexColor(); |
| | | 20 | | |
| | | 21 | | [GeneratedRegex("^c_[a-z0-9]{3,16}$")] |
| | | 22 | | private static partial Regex ColumnId(); |
| | | 23 | | |
| | | 24 | | /// <summary>Бросает <see cref="ValidationException"/> с человекочитаемым сообщением.</summary> |
| | | 25 | | public static void Validate(string? json, IEnumerable<string> allowedColumns) |
| | | 26 | | { |
| | 4 | 27 | | if (string.IsNullOrWhiteSpace(json)) return; |
| | | 28 | | |
| | | 29 | | JsonDocument doc; |
| | 8 | 30 | | try { doc = JsonDocument.Parse(json); } |
| | 0 | 31 | | catch (JsonException) { throw new ValidationException("Некорректный JSON вычисляемых столбцов."); } |
| | | 32 | | |
| | 4 | 33 | | using (doc) |
| | | 34 | | { |
| | 4 | 35 | | if (doc.RootElement.ValueKind != JsonValueKind.Array) throw new ValidationException("Ожидается массив столбц |
| | 4 | 36 | | var count = doc.RootElement.GetArrayLength(); |
| | 4 | 37 | | if (count > MaxColumns) throw new ValidationException($"Не больше {MaxColumns} вычисляемых столбцов."); |
| | | 38 | | |
| | | 39 | | // Поля доступны и по ключу, и по русскому названию (в формуле — [Название]). |
| | 4 | 40 | | var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
| | 68 | 41 | | foreach (var f in OrdersGridFormulaFields.ForAllowed(allowedColumns)) |
| | | 42 | | { |
| | 30 | 43 | | fields[f.Key] = f.Type; |
| | 30 | 44 | | fields[f.Label] = f.Type; |
| | | 45 | | } |
| | 4 | 46 | | var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
| | | 47 | | |
| | 13 | 48 | | foreach (var item in doc.RootElement.EnumerateArray()) |
| | | 49 | | { |
| | 4 | 50 | | if (item.ValueKind != JsonValueKind.Object) throw new ValidationException("Столбец должен быть объектом. |
| | 4 | 51 | | var id = GetString(item, "id"); |
| | 5 | 52 | | if (id is null || !ColumnId().IsMatch(id)) throw new ValidationException("Некорректный идентификатор сто |
| | 3 | 53 | | if (!ids.Add(id)) throw new ValidationException("Повторяющийся идентификатор столбца."); |
| | | 54 | | |
| | 3 | 55 | | var name = GetString(item, "name")?.Trim(); |
| | 3 | 56 | | if (string.IsNullOrEmpty(name)) throw new ValidationException("У столбца нет названия."); |
| | 3 | 57 | | if (name.Length > MaxNameLength) throw new ValidationException($"Название «{name[..20]}…» длиннее {MaxNa |
| | | 58 | | |
| | 3 | 59 | | var formula = GetString(item, "formula"); |
| | | 60 | | try |
| | | 61 | | { |
| | 3 | 62 | | OrdersGridFormula.Validate(formula ?? string.Empty, fields); |
| | 2 | 63 | | } |
| | 1 | 64 | | catch (OrdersGridFormula.FormulaException ex) |
| | | 65 | | { |
| | 1 | 66 | | throw new ValidationException($"Столбец «{name}»: {ex.Message} (позиция {ex.Position + 1})."); |
| | | 67 | | } |
| | | 68 | | |
| | 2 | 69 | | var ownColor = GetString(item, "color"); |
| | 2 | 70 | | if (ownColor != null && !HexColor().IsMatch(ownColor)) throw new ValidationException($"Столбец «{name}»: |
| | | 71 | | |
| | 2 | 72 | | if (item.TryGetProperty("group", out var group) && group.ValueKind == JsonValueKind.Object) |
| | | 73 | | { |
| | 2 | 74 | | var color = GetString(group, "color"); |
| | 3 | 75 | | if (color != null && !HexColor().IsMatch(color)) throw new ValidationException($"Столбец «{name}»: ц |
| | 1 | 76 | | var groupName = GetString(group, "name"); |
| | 1 | 77 | | if (groupName != null && groupName.Length > MaxNameLength) throw new ValidationException($"Столбец « |
| | | 78 | | } |
| | | 79 | | |
| | 1 | 80 | | if (item.TryGetProperty("colors", out var colors) && colors.ValueKind == JsonValueKind.Array) |
| | | 81 | | { |
| | 1 | 82 | | if (colors.GetArrayLength() > MaxColorRules) throw new ValidationException($"Столбец «{name}»: не бо |
| | 1 | 83 | | var ruleFields = new Dictionary<string, string>(fields, StringComparer.OrdinalIgnoreCase) { ["value" |
| | 4 | 84 | | foreach (var rule in colors.EnumerateArray()) |
| | | 85 | | { |
| | 1 | 86 | | var when = GetString(rule, "when"); |
| | 2 | 87 | | try { OrdersGridFormula.Validate(when ?? string.Empty, ruleFields); } |
| | 0 | 88 | | catch (OrdersGridFormula.FormulaException ex) { throw new ValidationException($"Столбец «{name}» |
| | 1 | 89 | | var bg = GetString(rule, "bg"); |
| | 1 | 90 | | var fg = GetString(rule, "fg"); |
| | 1 | 91 | | if ((bg != null && !HexColor().IsMatch(bg)) || (fg != null && !HexColor().IsMatch(fg))) |
| | 0 | 92 | | throw new ValidationException($"Столбец «{name}»: цвет правила должен быть в виде #rrggbb.") |
| | | 93 | | } |
| | | 94 | | } |
| | | 95 | | } |
| | | 96 | | } |
| | 1 | 97 | | } |
| | | 98 | | |
| | | 99 | | private static string? GetString(JsonElement obj, string name) |
| | 18 | 100 | | => obj.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; |
| | | 101 | | } |