From b73f748dba7606da067f644dde4f543c360dfbfe Mon Sep 17 00:00:00 2001 From: Geonosis Date: Wed, 30 Mar 2022 10:43:35 +0200 Subject: [PATCH] feat(operator): implement EndsWith #85 (#173) add a new operator adapt sieve processor update regex operator to split received filter value add unit tests & adjust existing ones Co-authored-by: Erwan RAULO --- README.md | 3 + Sieve/Models/FilterOperator.cs | 1 + Sieve/Models/FilterTerm.cs | 15 +++-- Sieve/Services/SieveProcessor.cs | 3 + SieveUnitTests/General.cs | 79 +++++++++++++++++++++---- SieveUnitTests/StringFilterNullTests.cs | 31 +++++++++- 6 files changed, 112 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 30fcc20..7511e1d 100644 --- a/README.md +++ b/README.md @@ -208,10 +208,13 @@ You can replace this DSL with your own (eg. use JSON instead) by implementing an | `<=` | Less than or equal to | | `@=` | Contains | | `_=` | Starts with | +| `_-=` | Ends with | | `!@=` | Does not Contains | | `!_=` | Does not Starts with | +| `!_-=` | Does not Ends with | | `@=*` | Case-insensitive string Contains | | `_=*` | Case-insensitive string Starts with | +| `_-=*` | Case-insensitive string Ends with | | `==*` | Case-insensitive string Equals | | `!=*` | Case-insensitive string Not equals | | `!@=*` | Case-insensitive string does not Contains | diff --git a/Sieve/Models/FilterOperator.cs b/Sieve/Models/FilterOperator.cs index c691527..9811745 100644 --- a/Sieve/Models/FilterOperator.cs +++ b/Sieve/Models/FilterOperator.cs @@ -10,5 +10,6 @@ public enum FilterOperator LessThanOrEqualTo, Contains, StartsWith, + EndsWith, } } diff --git a/Sieve/Models/FilterTerm.cs b/Sieve/Models/FilterTerm.cs index f47b81a..8ed0da0 100644 --- a/Sieve/Models/FilterTerm.cs +++ b/Sieve/Models/FilterTerm.cs @@ -8,7 +8,7 @@ namespace Sieve.Models public class FilterTerm : IFilterTerm, IEquatable { private const string EscapedPipePattern = @"(?=|<=|>|<|@=|_=)"; + private const string OperatorsRegEx = @"(!@=\*|!_=\*|!_-=\*|!=\*|!@=|!_=|!_-=|==\*|@=\*|_=\*|_-=\*|==|!=|>=|<=|>|<|@=|_=|_-=)"; private const string EscapeNegPatternForOper = @"(? t.Trim()).ToArray(); - + var filterSplits = Regex.Split(value, EscapeNegPatternForOper).Select(t => t.Trim()).ToArray(); + Names = Regex.Split(filterSplits[0], EscapedPipePattern).Select(t => t.Trim()).ToArray(); if (filterSplits.Length > 2) { - foreach (var match in Regex.Matches(filterSplits[2],EscapePosPatternForOper)) + foreach (var match in Regex.Matches(filterSplits[2], EscapePosPatternForOper)) { var matchStr = match.ToString(); filterSplits[2] = filterSplits[2].Replace('\\' + matchStr, matchStr); @@ -38,8 +38,8 @@ public string Filter .Select(UnEscape) .ToArray(); } - - Operator = Regex.Match(value,EscapeNegPatternForOper).Value; + + Operator = Regex.Match(value, EscapeNegPatternForOper).Value; OperatorParsed = GetOperatorParsed(Operator); OperatorIsCaseInsensitive = Operator.EndsWith("*"); OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!"); @@ -80,6 +80,9 @@ private FilterOperator GetOperatorParsed(string @operator) case "_=": case "!_=": return FilterOperator.StartsWith; + case "_-=": + case "!_-=": + return FilterOperator.EndsWith; default: return FilterOperator.Equals; } diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index 4c18644..5b36ea1 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -339,6 +339,9 @@ private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterVa FilterOperator.StartsWith => Expression.Call(propertyValue, typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1), filterValue), + FilterOperator.EndsWith => Expression.Call(propertyValue, + typeof(string).GetMethods().First(m => m.Name == "EndsWith" && m.GetParameters().Length == 1), + filterValue), _ => Expression.Equal(propertyValue, filterValue) }; } diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index 80b28ce..886ce50 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -73,6 +73,16 @@ public General(ITestOutputHelper testOutputHelper) CategoryId = 2, TopComment = new Comment { Id = 1, Text = "D1" }, FeaturedComment = new Comment { Id = 7, Text = "D2" } + }, + new Post + { + Id = 4, + Title = "Yen", + LikeCount = 5, + IsDraft = true, + CategoryId = 5, + TopComment = new Comment { Id = 4, Text = "Yen3" }, + FeaturedComment = new Comment { Id = 8, Text = "Yen4" } } }.AsQueryable(); @@ -124,7 +134,43 @@ public void NotEqualsCanBeCaseInsensitive() var result = _processor.Apply(model, _posts); Assert.Equal(1, result.First().Id); - Assert.True(result.Count() == 3); + Assert.True(result.Count() == 4); + } + + [Fact] + public void EndsWithWorks() + { + var model = new SieveModel + { + Filters = "Title_-=n" + }; + + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString()); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString()); + + var result = _processor.Apply(model, _posts); + + Assert.Equal(4, result.First().Id); + Assert.True(result.Count() == 1); + } + + [Fact] + public void EndsWithCanBeCaseInsensitive() + { + var model = new SieveModel + { + Filters = "Title_-=*N" + }; + + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString()); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator); + _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString()); + + var result = _processor.Apply(model, _posts); + + Assert.Equal(4, result.First().Id); + Assert.True(result.Count() == 1); } [Fact] @@ -150,7 +196,7 @@ public void NotContainsWorks() var result = _processor.Apply(model, _posts); - Assert.True(result.Count() == 3); + Assert.True(result.Count() == 4); } [Fact] @@ -205,8 +251,8 @@ public void CanFilterNullableIntsWithNotEqual() var result = _processor.Apply(model, _posts); var nullableResult = _nullableProcessor.Apply(model, _posts); - Assert.True(result.Count() == 1); - Assert.True(nullableResult.Count() == 2); + Assert.True(result.Count() == 2); + Assert.True(nullableResult.Count() == 3); } [Theory] @@ -255,7 +301,7 @@ public void CustomFiltersWork() var result = _processor.Apply(model, _posts); Assert.False(result.Any(p => p.Id == 0)); - Assert.True(result.Count() == 3); + Assert.True(result.Count() == 4); } [Fact] @@ -474,11 +520,12 @@ public void NestedFilteringWorks() }; var result = _processor.Apply(model, _posts); - Assert.Equal(3, result.Count()); + Assert.Equal(4, result.Count()); var posts = result.ToList(); Assert.Contains("B", posts[0].TopComment.Text); Assert.Contains("C", posts[1].TopComment.Text); Assert.Contains("D", posts[2].TopComment.Text); + Assert.Contains("Yen", posts[3].TopComment.Text); } [Fact] @@ -490,12 +537,13 @@ public void NestedSortingWorks() }; var result = _processor.Apply(model, _posts); - Assert.Equal(4, result.Count()); + Assert.Equal(5, result.Count()); var posts = result.ToList(); Assert.Equal(0, posts[0].Id); Assert.Equal(3, posts[1].Id); Assert.Equal(2, posts[2].Id); Assert.Equal(1, posts[3].Id); + Assert.Equal(4, posts[4].Id); } [Fact] @@ -630,13 +678,15 @@ public void BaseDefinedPropertyMappingSortingWorks_WithCustomName() }; var result = _processor.Apply(model, _posts); - Assert.Equal(4, result.Count()); + Assert.Equal(5, result.Count()); var posts = result.ToList(); - Assert.Equal(3,posts[0].Id); - Assert.Equal(2,posts[1].Id); - Assert.Equal(1,posts[2].Id); - Assert.Equal(0,posts[3].Id); + Assert.Equal(4, posts[0].Id); + Assert.Equal(3,posts[1].Id); + Assert.Equal(2,posts[2].Id); + Assert.Equal(1,posts[3].Id); + Assert.Equal(0,posts[4].Id); + } [Fact] @@ -759,10 +809,13 @@ public void CanFilterWithEscapedBackSlash(string filter) [InlineData(@"Title@=\>= ")] [InlineData(@"Title@=\@= ")] [InlineData(@"Title@=\_= ")] + [InlineData(@"Title@=\_-= ")] [InlineData(@"Title@=!\@= ")] [InlineData(@"Title@=!\_= ")] + [InlineData(@"Title@=!\_-= ")] [InlineData(@"Title@=\@=* ")] [InlineData(@"Title@=\_=* ")] + [InlineData(@"Title@=\_-=* ")] [InlineData(@"Title@=\==* ")] [InlineData(@"Title@=\!=* ")] [InlineData(@"Title@=!\@=* ")] @@ -773,7 +826,7 @@ public void CanFilterWithEscapedOperators(string filter) new Post { Id = 1, - Title = @"Operators: == != > < >= <= @= _= !@= !_= @=* _=* ==* !=* !@=* !_=* ", + Title = @"Operators: == != > < >= <= @= _= _-= !@= !_= !_-= @=* _=* ==* !=* !@=* !_=* !_-=* ", LikeCount = 1, IsDraft = true, CategoryId = 1, diff --git a/SieveUnitTests/StringFilterNullTests.cs b/SieveUnitTests/StringFilterNullTests.cs index 577d64a..42be74b 100644 --- a/SieveUnitTests/StringFilterNullTests.cs +++ b/SieveUnitTests/StringFilterNullTests.cs @@ -31,7 +31,7 @@ public StringFilterNullTests() { Id = 1, DateCreated = DateTimeOffset.UtcNow, - Text = "null is here in the text", + Text = "null is here twice in the text ending by null", Author = "Cat", }, new Comment @@ -136,6 +136,21 @@ public void Filter_StartsWith_NullString(string filter) Assert.Equal(new[] {1}, result.Select(p => p.Id)); } + [Theory] + [InlineData("Text_-=null")] + [InlineData("Text_-=*null")] + [InlineData("Text_-=*NULL")] + [InlineData("Text_-=*NulL")] + [InlineData("Text_-=*null|text")] + public void Filter_EndsWith_NullString(string filter) + { + var model = new SieveModel { Filters = filter }; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] { 1 }, result.Select(p => p.Id)); + } + [Theory] [InlineData("Text!@=null")] [InlineData("Text!@=*null")] @@ -164,5 +179,19 @@ public void Filter_DoesNotStartsWith_NullString(string filter) Assert.Equal(new[] {0, 2}, result.Select(p => p.Id)); } + + [Theory] + [InlineData("Text!_-=null")] + [InlineData("Text!_-=*null")] + [InlineData("Text!_-=*NULL")] + [InlineData("Text!_-=*NulL")] + public void Filter_DoesNotEndsWith_NullString(string filter) + { + var model = new SieveModel { Filters = filter }; + + var result = _processor.Apply(model, _comments); + + Assert.Equal(new[] { 0, 2 }, result.Select(p => p.Id)); + } } }