using System.Collections; using System.Globalization; using System.Reflection; using System.Text.RegularExpressions; using IdokladSdk.Clients; using IdokladSdk.Requests.Core; using IdokladSdk.Requests.Core.Modifiers.Filters.Common; using IdokladSdk.Requests.Core.Modifiers.Sort.Common; using IdokladSdk.Response; namespace Idoklad.Client; /// /// Translates iDoklad-style filter/sort query strings into the SDK's strongly-typed /// / /// modifiers, so the REST /// list endpoints expose the full server-side filtering the SDK supports (operators /// eq/neq/gt/gte/lt/lte/ct/nct, AND/OR combining, and multi-column sort) instead of only paging. /// /// Filter format (identical to the public iDoklad API): one or more /// (Property~operator~value) conditions joined by , — e.g. /// (DateOfTaxing~gte~2024-01-01),(IsPaid~eq~false). Parentheses are optional for a single /// condition. The filtertype query parameter (and | or, default and) /// decides how multiple conditions combine. /// /// Operators: eq (=), neq (≠), gt (>), gte (≥), /// lt (<), lte (≤), ct (contains), nct (not contains). The available /// operators per field follow the SDK filter type (e.g. dates support the compare operators, /// text fields support contains). /// /// Sort format: Property~asc|desc, multiple joined by , — e.g. /// DateOfIssue~desc,Id~asc. Direction defaults to ascending. /// public static class ListModifiers { private static readonly Regex GroupRegex = new(@"\(([^()]*)\)", RegexOptions.Compiled); private static readonly Dictionary OperatorMethods = new(StringComparer.OrdinalIgnoreCase) { ["eq"] = "IsEqual", ["neq"] = "IsNotEqual", ["gt"] = "IsGreaterThan", ["gte"] = "IsGreaterThanOrEqual", ["lt"] = "IsLowerThan", ["lte"] = "IsLowerThanOrEqual", ["ct"] = "Contains", ["nct"] = "NotContains", }; /// /// Applies paging plus optional / to a list /// request and returns the unwrapped page. Shared by every list endpoint. /// public static async Task> GetPageAsync( this BaseList list, int page, int pageSize, string? filter, string? filterType, string? sort, CancellationToken ct) where TList : BaseList where TClient : BaseClient where TFilter : new() where TSort : new() where TGetModel : new() { BaseList built = list; if (!string.IsNullOrWhiteSpace(filter)) { var useOr = IsOr(filterType); built = built.Filter(f => BuildFilter(f!, filter, useOr)); } if (!string.IsNullOrWhiteSpace(sort)) { built = built.Sort(BuildSort(sort)); } return (await built.Page(page).PageSize(pageSize).GetAsync(ct)).Unwrap(); } /// True when the filtertype query value requests OR combining (default AND). public static bool IsOr(string? filterType) => string.Equals(filterType, "or", StringComparison.OrdinalIgnoreCase); /// /// Builds a combined from an iDoklad-style filter string /// against the SDK filter object (passed in by the SDK at call time). /// Conditions are combined with OR when is true, otherwise AND. /// public static FilterExpressionBase BuildFilter(object filter, string filterString, bool useOr) { FilterExpressionBase? combined = null; foreach (var group in ExtractConditions(filterString)) { var expression = BuildCondition(filter, group); combined = combined is null ? expression : (useOr ? combined | expression : combined & expression); } if (combined is null) { throw new ArgumentException($"Filtr '{filterString}' neobsahuje žádnou platnou podmínku."); } return combined; } /// Builds the SDK sort selectors from a Field~asc|desc,... string. public static Func[] BuildSort(string sort) { var parts = sort.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var selectors = new List>(); foreach (var part in parts) { var segments = part.Split('~', StringSplitOptions.TrimEntries); var field = segments[0]; var descending = segments.Length > 1 && segments[1].Equals("desc", StringComparison.OrdinalIgnoreCase); selectors.Add(sortObj => { var property = typeof(TSort).GetProperty(field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) ?? throw new ArgumentException($"Řazení podle pole '{field}' není u této agendy podporováno."); var item = (SortItem)property.GetValue(sortObj)!; return descending ? item.Desc() : item.Asc(); }); } return selectors.ToArray(); } /// Splits the filter string into individual Name~op~value conditions. private static IEnumerable ExtractConditions(string filterString) { filterString = filterString.Trim(); if (filterString.Contains('(')) { return GroupRegex.Matches(filterString) .Select(m => m.Groups[1].Value.Trim()) .Where(s => s.Length > 0); } return filterString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } private static FilterExpressionBase BuildCondition(object filter, string condition) { var firstTilde = condition.IndexOf('~'); var secondTilde = firstTilde < 0 ? -1 : condition.IndexOf('~', firstTilde + 1); if (firstTilde < 0 || secondTilde < 0) { throw new ArgumentException($"Neplatná filtrační podmínka '{condition}'. Očekává se tvar 'Pole~operátor~hodnota'."); } var name = condition[..firstTilde].Trim(); var op = condition[(firstTilde + 1)..secondTilde].Trim(); var rawValue = condition[(secondTilde + 1)..].Trim(); if (!OperatorMethods.TryGetValue(op, out var methodName)) { throw new ArgumentException($"Neznámý filtrační operátor '{op}'. Povolené: eq, neq, gt, gte, lt, lte, ct, nct."); } var property = filter.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) ?? throw new ArgumentException($"Filtrování podle pole '{name}' není u této agendy podporováno."); var item = property.GetValue(filter) ?? throw new ArgumentException($"Filtrační pole '{name}' není dostupné."); var method = FindOperatorMethod(item.GetType(), methodName) ?? throw new ArgumentException($"Operátor '{op}' není pro pole '{name}' podporován."); var targetType = method.GetParameters()[0].ParameterType; var value = ConvertValue(rawValue, targetType, name); try { return (FilterExpressionBase)method.Invoke(item, new[] { value })!; } catch (TargetInvocationException ex) when (ex.InnerException is not null) { throw new ArgumentException($"Filtr '{name}~{op}~{rawValue}' se nepodařilo sestavit: {ex.InnerException.Message}"); } } private static MethodInfo? FindOperatorMethod(Type itemType, string methodName) { var candidates = itemType .GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => m.Name == methodName && m.GetParameters().Length == 1) .ToList(); if (candidates.Count <= 1) { return candidates.FirstOrDefault(); } // Some filter items (e.g. the Id filter) overload Contains with a scalar and a collection — // prefer the scalar overload for a single query value. return candidates.FirstOrDefault(m => { var pt = m.GetParameters()[0].ParameterType; return pt == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(pt); }) ?? candidates[0]; } private static object ConvertValue(string raw, Type target, string fieldName) { var type = Nullable.GetUnderlyingType(target) ?? target; try { if (type == typeof(string)) { return raw; } if (type.IsEnum) { return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric) ? Enum.ToObject(type, numeric) : Enum.Parse(type, raw, ignoreCase: true); } if (type == typeof(DateTime)) { return DateTime.Parse(raw, CultureInfo.InvariantCulture, DateTimeStyles.None); } if (type == typeof(bool)) { return bool.Parse(raw); } if (type == typeof(Guid)) { return Guid.Parse(raw); } return Convert.ChangeType(raw, type, CultureInfo.InvariantCulture); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException or ArgumentException) { throw new ArgumentException($"Hodnotu '{raw}' filtru '{fieldName}' nelze převést na typ {type.Name}."); } } }