From faa363edbb01c778162a61f3929461aba2cb4510 Mon Sep 17 00:00:00 2001 From: Biarity Date: Fri, 16 Nov 2018 18:08:25 +1000 Subject: [PATCH] Added OR flitering for values and ability to escape delimiters. Fixes #8, #21, and #41 --- README.md | 3 +- Sieve.sln | 1 + Sieve/Models/FilterTerm.cs | 12 ++++--- Sieve/Models/IFilterTerm.cs | 2 +- Sieve/Models/SieveModel.cs | 7 +++-- Sieve/Services/SieveProcessor.cs | 50 ++++++++++++++++-------------- SieveUnitTests/Entities/Comment.cs | 1 + SieveUnitTests/General.cs | 33 ++++++++++++++++++-- 8 files changed, 75 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 76ec00e..d18173e 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,12 @@ More formally: * You can also have multiple names (for OR logic) by enclosing them in brackets and using a pipe delimiter, eg. `(LikeCount|CommentCount)>10` asks if `LikeCount` or `CommentCount` is `>10` * `{Operator}` is one of the [Operators](#operators) * `{Value}` is the value to use for filtering + * You can also have multiple values (for OR logic) by using a pipe delimiter, eg. `Title@=new|hot` will return posts with titles that contain the text "`new`" or "`hot`" * `page` is the number of page to return * `pageSize` is the number of items returned per page Notes: -* Don't forget to **remove commas (`,`), brackets (`(`, `)`), and pipes (`|`) from any `{Value}` fields** +* You can use backslashes to escape commas and pipes within value fields * You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields * Here's a [good example on how to work with enumerables](https://github.com/Biarity/Sieve/issues/2) * Another example on [how to do OR logic](https://github.com/Biarity/Sieve/issues/8) diff --git a/Sieve.sln b/Sieve.sln index c5d520e..acf6862 100644 --- a/Sieve.sln +++ b/Sieve.sln @@ -12,6 +12,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{904F25A9-5CBD-42AE-8422-ADAB98F4B4B7}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + README.md = README.md EndProjectSection EndProject Global diff --git a/Sieve/Models/FilterTerm.cs b/Sieve/Models/FilterTerm.cs index b69f33f..17fdf9c 100644 --- a/Sieve/Models/FilterTerm.cs +++ b/Sieve/Models/FilterTerm.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Text.RegularExpressions; namespace Sieve.Models { @@ -7,6 +8,8 @@ namespace Sieve.Models { public FilterTerm() { } + private const string EscapedPipePattern = @"(? t.Trim()).ToArray(); - Names = filterSplits[0].Split('|').Select(t => t.Trim()).ToArray(); - Value = filterSplits.Length > 1 ? filterSplits[1] : null; + var filterSplits = value.Split(Operators, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()).ToArray(); + Names = Regex.Split(filterSplits[0], EscapedPipePattern).Select(t => t.Trim()).ToArray(); + Values = filterSplits.Length > 1 ? Regex.Split(filterSplits[1], EscapedPipePattern).Select(t => t.Trim()).ToArray() : null; Operator = Array.Find(Operators, o => value.Contains(o)) ?? "=="; OperatorParsed = GetOperatorParsed(Operator); OperatorIsCaseInsensitive = Operator.Contains("*"); @@ -39,7 +43,7 @@ namespace Sieve.Models public FilterOperator OperatorParsed { get; private set; } - public string Value { get; private set; } + public string[] Values { get; private set; } public string Operator { get; private set; } diff --git a/Sieve/Models/IFilterTerm.cs b/Sieve/Models/IFilterTerm.cs index a844f35..fd773e9 100644 --- a/Sieve/Models/IFilterTerm.cs +++ b/Sieve/Models/IFilterTerm.cs @@ -7,6 +7,6 @@ string Operator { get; } bool OperatorIsCaseInsensitive { get; } FilterOperator OperatorParsed { get; } - string Value { get; } + string[] Values { get; } } } diff --git a/Sieve/Models/SieveModel.cs b/Sieve/Models/SieveModel.cs index cde1627..483a569 100644 --- a/Sieve/Models/SieveModel.cs +++ b/Sieve/Models/SieveModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; +using System.Text.RegularExpressions; namespace Sieve.Models { @@ -13,6 +14,8 @@ namespace Sieve.Models where TFilterTerm : IFilterTerm, new() where TSortTerm : ISortTerm, new() { + private const string EscapedCommaPattern = @"(?(); - foreach (var filter in Filters.Split(',')) + foreach (var filter in Regex.Split(Filters, EscapedCommaPattern)) { if (string.IsNullOrWhiteSpace(filter)) continue; @@ -69,7 +72,7 @@ namespace Sieve.Models if (Sorts != null) { var value = new List(); - foreach (var sort in Sorts.Split(',')) + foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern)) { if (string.IsNullOrWhiteSpace(sort)) continue; diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index e0fd90a..3e739db 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -180,33 +180,37 @@ namespace Sieve.Services if (property != null) { var converter = TypeDescriptor.GetConverter(property.PropertyType); - - dynamic constantVal = converter.CanConvertFrom(typeof(string)) - ? converter.ConvertFrom(filterTerm.Value) - : Convert.ChangeType(filterTerm.Value, property.PropertyType); - - Expression filterValue = GetClosureOverConstant(constantVal, property.PropertyType); - dynamic propertyValue = Expression.PropertyOrField(parameterExpression, property.Name); - if (filterTerm.OperatorIsCaseInsensitive) + foreach (var filterTermValue in filterTerm.Values) { - propertyValue = Expression.Call(propertyValue, - typeof(string).GetMethods() - .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); - filterValue = Expression.Call(filterValue, - typeof(string).GetMethods() - .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); - } + dynamic constantVal = converter.CanConvertFrom(typeof(string)) + ? converter.ConvertFrom(filterTermValue) + : Convert.ChangeType(filterTermValue, property.PropertyType); - if (innerExpression == null) - { - innerExpression = GetExpression(filterTerm, filterValue, propertyValue); - } - else - { - innerExpression = Expression.Or(innerExpression, GetExpression(filterTerm, filterValue, propertyValue)); + Expression filterValue = GetClosureOverConstant(constantVal, property.PropertyType); + + + if (filterTerm.OperatorIsCaseInsensitive) + { + propertyValue = Expression.Call(propertyValue, + typeof(string).GetMethods() + .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); + + filterValue = Expression.Call(filterValue, + typeof(string).GetMethods() + .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); + } + + if (innerExpression == null) + { + innerExpression = GetExpression(filterTerm, filterValue, propertyValue); + } + else + { + innerExpression = Expression.Or(innerExpression, GetExpression(filterTerm, filterValue, propertyValue)); + } } } else @@ -215,7 +219,7 @@ namespace Sieve.Services new object[] { result, filterTerm.Operator, - filterTerm.Value + filterTerm.Values }, dataForCustomMethods); } diff --git a/SieveUnitTests/Entities/Comment.cs b/SieveUnitTests/Entities/Comment.cs index d00b34b..e3418be 100644 --- a/SieveUnitTests/Entities/Comment.cs +++ b/SieveUnitTests/Entities/Comment.cs @@ -10,6 +10,7 @@ namespace SieveUnitTests.Entities [Sieve(CanFilter = true, CanSort = true)] public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; + [Sieve(CanFilter = true)] public string Text { get; set; } } } diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index f5b3353..be98853 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -69,7 +69,7 @@ namespace SieveUnitTests new Comment() { Id = 2, DateCreated = DateTimeOffset.UtcNow, - Text = "This is a brand new comment." + Text = "This is a brand new comment. ()" }, }.AsQueryable(); } @@ -148,7 +148,7 @@ namespace SieveUnitTests Filters = "LikeCount==50", }; - Console.WriteLine(model.GetFiltersParsed()[0].Value); + Console.WriteLine(model.GetFiltersParsed()[0].Values); Console.WriteLine(model.GetFiltersParsed()[0].Operator); Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed); @@ -260,7 +260,7 @@ namespace SieveUnitTests } [TestMethod] - public void OrFilteringWorks() + public void OrNameFilteringWorks() { var model = new SieveModel() { @@ -275,5 +275,32 @@ namespace SieveUnitTests Assert.AreEqual(1, resultCount); Assert.AreEqual(3, entry.Id); } + + [TestMethod] + public void OrValueFilteringWorks() + { + var model = new SieveModel() + { + Filters = "Title==C|D", + }; + + var result = _processor.Apply(model, _posts); + Assert.AreEqual(2, result.Count()); + Assert.IsTrue(result.Any(p => p.Id == 2)); + Assert.IsTrue(result.Any(p => p.Id == 3)); + } + + [TestMethod] + public void OrValueFilteringWorks2() + { + var model = new SieveModel() + { + Filters = "Text@=(|)", + }; + + var result = _processor.Apply(model, _comments); + Assert.AreEqual(1, result.Count()); + Assert.AreEqual(2, result.FirstOrDefault().Id); + } } }