Added OR flitering for values and ability to escape delimiters. Fixes #8, #21, and #41

This commit is contained in:
Biarity 2018-11-16 18:08:25 +10:00
parent f9c7fb4cb0
commit faa363edbb
8 changed files with 75 additions and 34 deletions

View File

@ -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` * 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) * `{Operator}` is one of the [Operators](#operators)
* `{Value}` is the value to use for filtering * `{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 * `page` is the number of page to return
* `pageSize` is the number of items returned per page * `pageSize` is the number of items returned per page
Notes: 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 * 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) * 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) * Another example on [how to do OR logic](https://github.com/Biarity/Sieve/issues/8)

View File

@ -12,6 +12,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{904F25A9-5CBD-42AE-8422-ADAB98F4B4B7}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{904F25A9-5CBD-42AE-8422-ADAB98F4B4B7}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Global Global

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
namespace Sieve.Models namespace Sieve.Models
{ {
@ -7,6 +8,8 @@ namespace Sieve.Models
{ {
public FilterTerm() { } public FilterTerm() { }
private const string EscapedPipePattern = @"(?<!($|[^\\])(\\\\)*?\\)\|";
private static readonly string[] Operators = new string[] { private static readonly string[] Operators = new string[] {
"==*", "==*",
"@=*", "@=*",
@ -25,9 +28,10 @@ namespace Sieve.Models
{ {
set set
{ {
var filterSplits = value.Split(Operators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToArray(); var filterSplits = value.Split(Operators, StringSplitOptions.RemoveEmptyEntries)
Names = filterSplits[0].Split('|').Select(t => t.Trim()).ToArray(); .Select(t => t.Trim()).ToArray();
Value = filterSplits.Length > 1 ? filterSplits[1] : null; 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)) ?? "=="; Operator = Array.Find(Operators, o => value.Contains(o)) ?? "==";
OperatorParsed = GetOperatorParsed(Operator); OperatorParsed = GetOperatorParsed(Operator);
OperatorIsCaseInsensitive = Operator.Contains("*"); OperatorIsCaseInsensitive = Operator.Contains("*");
@ -39,7 +43,7 @@ namespace Sieve.Models
public FilterOperator OperatorParsed { get; private set; } public FilterOperator OperatorParsed { get; private set; }
public string Value { get; private set; } public string[] Values { get; private set; }
public string Operator { get; private set; } public string Operator { get; private set; }

View File

@ -7,6 +7,6 @@
string Operator { get; } string Operator { get; }
bool OperatorIsCaseInsensitive { get; } bool OperatorIsCaseInsensitive { get; }
FilterOperator OperatorParsed { get; } FilterOperator OperatorParsed { get; }
string Value { get; } string[] Values { get; }
} }
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text.RegularExpressions;
namespace Sieve.Models namespace Sieve.Models
{ {
@ -13,6 +14,8 @@ namespace Sieve.Models
where TFilterTerm : IFilterTerm, new() where TFilterTerm : IFilterTerm, new()
where TSortTerm : ISortTerm, new() where TSortTerm : ISortTerm, new()
{ {
private const string EscapedCommaPattern = @"(?<!($|[^\\])(\\\\)*?\\),";
[DataMember] [DataMember]
public string Filters { get; set; } public string Filters { get; set; }
@ -30,7 +33,7 @@ namespace Sieve.Models
if (Filters != null) if (Filters != null)
{ {
var value = new List<TFilterTerm>(); var value = new List<TFilterTerm>();
foreach (var filter in Filters.Split(',')) foreach (var filter in Regex.Split(Filters, EscapedCommaPattern))
{ {
if (string.IsNullOrWhiteSpace(filter)) continue; if (string.IsNullOrWhiteSpace(filter)) continue;
@ -69,7 +72,7 @@ namespace Sieve.Models
if (Sorts != null) if (Sorts != null)
{ {
var value = new List<TSortTerm>(); var value = new List<TSortTerm>();
foreach (var sort in Sorts.Split(',')) foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern))
{ {
if (string.IsNullOrWhiteSpace(sort)) continue; if (string.IsNullOrWhiteSpace(sort)) continue;

View File

@ -180,33 +180,37 @@ namespace Sieve.Services
if (property != null) if (property != null)
{ {
var converter = TypeDescriptor.GetConverter(property.PropertyType); 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); 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, dynamic constantVal = converter.CanConvertFrom(typeof(string))
typeof(string).GetMethods() ? converter.ConvertFrom(filterTermValue)
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); : Convert.ChangeType(filterTermValue, property.PropertyType);
}
if (innerExpression == null) Expression filterValue = GetClosureOverConstant(constantVal, property.PropertyType);
{
innerExpression = GetExpression(filterTerm, filterValue, propertyValue);
} if (filterTerm.OperatorIsCaseInsensitive)
else {
{ propertyValue = Expression.Call(propertyValue,
innerExpression = Expression.Or(innerExpression, GetExpression(filterTerm, filterValue, 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 else
@ -215,7 +219,7 @@ namespace Sieve.Services
new object[] { new object[] {
result, result,
filterTerm.Operator, filterTerm.Operator,
filterTerm.Value filterTerm.Values
}, dataForCustomMethods); }, dataForCustomMethods);
} }

View File

@ -10,6 +10,7 @@ namespace SieveUnitTests.Entities
[Sieve(CanFilter = true, CanSort = true)] [Sieve(CanFilter = true, CanSort = true)]
public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;
[Sieve(CanFilter = true)]
public string Text { get; set; } public string Text { get; set; }
} }
} }

View File

@ -69,7 +69,7 @@ namespace SieveUnitTests
new Comment() { new Comment() {
Id = 2, Id = 2,
DateCreated = DateTimeOffset.UtcNow, DateCreated = DateTimeOffset.UtcNow,
Text = "This is a brand new comment." Text = "This is a brand new comment. ()"
}, },
}.AsQueryable(); }.AsQueryable();
} }
@ -148,7 +148,7 @@ namespace SieveUnitTests
Filters = "LikeCount==50", 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].Operator);
Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed); Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed);
@ -260,7 +260,7 @@ namespace SieveUnitTests
} }
[TestMethod] [TestMethod]
public void OrFilteringWorks() public void OrNameFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel()
{ {
@ -275,5 +275,32 @@ namespace SieveUnitTests
Assert.AreEqual(1, resultCount); Assert.AreEqual(1, resultCount);
Assert.AreEqual(3, entry.Id); 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);
}
} }
} }