Skip to content

Commit

Permalink
add StartsWith and EndsWith
Browse files Browse the repository at this point in the history
fix #7
  • Loading branch information
alirezanet committed Aug 4, 2021
1 parent c10eddb commit a91db96
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 76 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ but for example, if you need to just filter your data without paging or sorting
| LessThanOrEqual | `<=` | `"FieldName <=Value"` |
| Contains - Like | `=*` | `"FieldName =*Value"` |
| NotContains - NotLike | `!*` | `"FieldName !*Value"` |
| StartsWith | `^` | `"FieldName ^ Value"` |
| NotStartsWith | `!^` | `"FieldName !^ Value"` |
| EndsWith | `$` | `"FieldName $ Value"` |
| NotEndsWith | `!$` | `"FieldName !$ Value"` |
| AND - && | `,` | `"FirstName ==Value, LastName ==Value2"` |
| OR - &#124;&#124; | <code>&#124;</code> | <code>"FirstName==Value&#124;LastName==Value2"</code>
| Parenthesis | `()` | <code>"(FirstName=*Jo,Age<30)&#124;(FirstName!=Hn,Age>30)"</code> |
Expand Down
8 changes: 8 additions & 0 deletions src/Core/SyntaxTree/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public SyntaxToken NextToken()
return new SyntaxToken(SyntaxKind.And, _position++, ",");
case '|':
return new SyntaxToken(SyntaxKind.Or, _position++, "|");
case '^':
return new SyntaxToken(SyntaxKind.StartsWith, _position++, "^");
case '$':
return new SyntaxToken(SyntaxKind.EndsWith, _position++, "$");
case '!' when peek == '^':
return new SyntaxToken(SyntaxKind.NotStartsWith, _position += 2, "!^");
case '!' when peek == '$':
return new SyntaxToken(SyntaxKind.NotEndsWith, _position += 2, "!$");
case '=' when peek == '=':
return new SyntaxToken(SyntaxKind.Equal, _position += 2, "==");
case '=' when peek == '*':
Expand Down
6 changes: 5 additions & 1 deletion src/Core/SyntaxTree/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ private ExpressionSyntax ParseFactor()
SyntaxKind.GreaterThan,
SyntaxKind.LessThan,
SyntaxKind.GreaterOrEqualThan,
SyntaxKind.LessOrEqualThan
SyntaxKind.LessOrEqualThan,
SyntaxKind.StartsWith,
SyntaxKind.EndsWith,
SyntaxKind.NotStartsWith,
SyntaxKind.NotEndsWith
};

while (binaryKinds.Contains(Current.Kind))
Expand Down
6 changes: 5 additions & 1 deletion src/Core/SyntaxTree/SyntaxKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ public enum SyntaxKind
GreaterThan,
LessOrEqualThan,
GreaterOrEqualThan,
StartsWith,
EndsWith,

// expressions
FieldExpression,
BinaryExpression,
ValueExpression,
ValueToken,
ParenthesizedExpression
ParenthesizedExpression,
NotStartsWith,
NotEndsWith
}
}
32 changes: 26 additions & 6 deletions src/Core/SyntaxTree/SyntaxTreeToQueryConvertor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,42 @@ private static Expression<Func<T, bool>> ConvertBinaryExpressionSyntaxToQuery<T>
case SyntaxKind.NotLike:
be = Expression.Not(Expression.Call(body, GetContainsMethod(), Expression.Constant(value, body.Type)));
break;
case SyntaxKind.StartsWith:
body = Expression.Call(body, GetToStringMethod());
be = Expression.Call(body, GetStartWithMethod(), Expression.Constant(value?.ToString(), body.Type));
break;
case SyntaxKind.EndsWith:
body = Expression.Call(body, GetToStringMethod());
be = Expression.Call(body, GetEndsWithMethod(), Expression.Constant(value?.ToString(), body.Type));
break;
case SyntaxKind.NotStartsWith:
body = Expression.Call(body, GetToStringMethod());
be = Expression.Not(Expression.Call(body, GetStartWithMethod(), Expression.Constant(value?.ToString(), body.Type)));
break;
case SyntaxKind.NotEndsWith:
body = Expression.Call(body, GetToStringMethod());
be = Expression.Not(Expression.Call(body, GetEndsWithMethod(), Expression.Constant(value?.ToString(), body.Type)));
break;
default:
return null;
}

return Expression.Lambda<Func<T, bool>>(be, exp.Parameters);
}
catch (Exception)
catch (Exception ex)
{
// Unhandled exceptions ignores gridify completely,
// Not sure this is the best approach or not yet
return null;
}
}

private static MethodInfo GetContainsMethod()
{
return typeof(string).GetMethod("Contains", new[] {typeof(string)});
}
private static MethodInfo GetEndsWithMethod() => typeof(string).GetMethod("EndsWith", new[] {typeof(string)});

private static MethodInfo GetStartWithMethod() => typeof(string).GetMethod("StartsWith", new[] {typeof(string)});

private static MethodInfo GetContainsMethod() => typeof(string).GetMethod("Contains", new[] {typeof(string)});

private static MethodInfo GetToStringMethod() => typeof(object).GetMethod("ToString");

internal static Expression<Func<T, bool>> GenerateQuery<T>(ExpressionSyntax expression, IGridifyMapper<T> mapper)
{
Expand Down
234 changes: 166 additions & 68 deletions test/Core.Tests/GridifyExtensionsShould.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,74 +15,6 @@ public GridifyExtensionsShould()
_fakeRepository = new List<TestClass>(GetSampleData());
}

#region "Other"

[Theory]
[InlineData(1, 5, true)]
[InlineData(2, 5, false)]
[InlineData(1, 10, true)]
[InlineData(4, 3, false)]
[InlineData(5, 3, true)]
[InlineData(1, 15, false)]
[InlineData(20, 10, true)]
public void ApplyOrderingAndPaging_UsingCustomValues(short page, int pageSize, bool isSortAsc)
{
var gq = new GridifyQuery {Page = page, PageSize = pageSize, SortBy = "Name", IsSortAsc = isSortAsc};
// actual
var actual = _fakeRepository.AsQueryable()
.ApplyOrderingAndPaging(gq)
.ToList();

// expected
var skip = (page - 1) * pageSize;
var expectedQuery = _fakeRepository.AsQueryable();
if (isSortAsc)
expectedQuery = expectedQuery.OrderBy(q => q.Name);
else
expectedQuery = expectedQuery.OrderByDescending(q => q.Name);
var expected = expectedQuery.Skip(skip).Take(pageSize).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
}

#endregion

#region "Data"

private List<TestClass> GetSampleData()
{
var lst = new List<TestClass>();
lst.Add(new TestClass(1, "John", null, Guid.NewGuid(), DateTime.Now));
lst.Add(new TestClass(2, "Bob", null, Guid.NewGuid(), DateTime.UtcNow));
lst.Add(new TestClass(3, "Jack", (TestClass) lst[0].Clone(), Guid.Empty, DateTime.Now.AddDays(2)));
lst.Add(new TestClass(4, "Rose", null, Guid.Parse("e2cec5dd-208d-4bb5-a852-50008f8ba366")));
lst.Add(new TestClass(5, "Ali", null));
lst.Add(new TestClass(6, "Hamid", (TestClass) lst[0].Clone(), Guid.Parse("de12bae1-93fa-40e4-92d1-2e60f95b468c")));
lst.Add(new TestClass(7, "Hasan", (TestClass) lst[1].Clone()));
lst.Add(new TestClass(8, "Farhad", (TestClass) lst[2].Clone(), Guid.Empty));
lst.Add(new TestClass(9, "Sara", null));
lst.Add(new TestClass(10, "Jorge", null));
lst.Add(new TestClass(11, "joe", null));
lst.Add(new TestClass(12, "jimmy", (TestClass) lst[0].Clone()));
lst.Add(new TestClass(13, "Nazanin", null));
lst.Add(new TestClass(14, "Reza", null));
lst.Add(new TestClass(15, "Korosh", (TestClass) lst[0].Clone()));
lst.Add(new TestClass(16, "Kamran", (TestClass) lst[1].Clone()));
lst.Add(new TestClass(17, "Saeid", (TestClass) lst[2].Clone()));
lst.Add(new TestClass(18, "jessi==ca", null));
lst.Add(new TestClass(19, "Ped=ram", null));
lst.Add(new TestClass(20, "Peyman!", null));
lst.Add(new TestClass(21, "Fereshte", null));
lst.Add(new TestClass(22, "LIAM", null));
lst.Add(new TestClass(22, @"\Liam", null));
lst.Add(new TestClass(23, "LI | AM", null));
lst.Add(new TestClass(24, "(LI,AM)", null));

return lst;
}

#endregion

#region "ApplyFiltering"

Expand Down Expand Up @@ -270,6 +202,102 @@ public void ApplyFiltering_CustomConvertor()
Assert.True(actual.Any());
}

[Fact]
public void ApplyFiltering_StartsWith()
{
const string gq = "name^A";

var actual = _fakeRepository.AsQueryable()
.ApplyFiltering(gq)
.ToList();

var expected = _fakeRepository.Where(q => q.Name.StartsWith("A")).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}

[Fact]
public void ApplyEverything_EmptyGridifyQuery()
{
var gq = new GridifyQuery();

var actual = _fakeRepository.AsQueryable()
.ApplyEverything(gq)
.ToList();

var expected = _fakeRepository.Skip(0).Take(GridifyExtensions.DefaultPageSize).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}


[Fact]
public void ApplyFiltering_StartsWithOnNotStringsShouldThrowError()
{
const string gq = "Id^2";

var actual = _fakeRepository.AsQueryable()
.ApplyFiltering(gq)
.ToList();

var expected = _fakeRepository.Where(q => q.Id.ToString().StartsWith("2")).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}

[Fact]
public void ApplyFiltering_NotStartsWith()
{
const string gq = "name!^A";

var actual = _fakeRepository.AsQueryable()
.ApplyFiltering(gq)
.ToList();

var expected = _fakeRepository.Where(q => !q.Name.StartsWith("A")).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}

[Fact]
public void ApplyFiltering_EndsWith()
{
const string gq = "name $ li";

var actual = _fakeRepository.AsQueryable()
.ApplyFiltering(gq)
.ToList();

var expected = _fakeRepository.Where(q => q.Name.EndsWith("li")).ToList();
Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}

[Fact]
public void ApplyFiltering_NotEndsWith()
{
const string gq = "name !$ i";

var actual = _fakeRepository.AsQueryable()
.ApplyFiltering(gq)
.ToList();

var expected = _fakeRepository.Where(q => !q.Name.EndsWith("i")).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}

[Fact]
public void ApplyFiltering_MultipleCondition()
{
Expand Down Expand Up @@ -451,5 +479,75 @@ public void ApplyPaging_UsingCustomValues(short page, int pageSize)
}

#endregion

#region "Other"

[Theory]
[InlineData(0, 5, true)]
[InlineData(1, 5, false)]
[InlineData(0, 10, true)]
[InlineData(3, 3, false)]
[InlineData(4, 3, true)]
[InlineData(0, 15, false)]
[InlineData(19, 10, true)]
public void ApplyOrderingAndPaging_UsingCustomValues(short page, int pageSize, bool isSortAsc)
{
var gq = new GridifyQuery {Page = page, PageSize = pageSize, SortBy = "Name", IsSortAsc = isSortAsc};
// actual
var actual = _fakeRepository.AsQueryable()
.ApplyOrderingAndPaging(gq)
.ToList();

// expected
var skip = (page - 1) * pageSize;
var expectedQuery = _fakeRepository.AsQueryable();
if (isSortAsc)
expectedQuery = expectedQuery.OrderBy(q => q.Name);
else
expectedQuery = expectedQuery.OrderByDescending(q => q.Name);
var expected = expectedQuery.Skip(skip).Take(pageSize).ToList();

Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected.FirstOrDefault()?.Id, actual.FirstOrDefault()?.Id);
Assert.Equal(expected.LastOrDefault()?.Id, actual.LastOrDefault()?.Id);
}

#endregion

#region "Data"

private static IEnumerable<TestClass> GetSampleData()
{
var lst = new List<TestClass>();
lst.Add(new TestClass(1, "John", null, Guid.NewGuid(), DateTime.Now));
lst.Add(new TestClass(2, "Bob", null, Guid.NewGuid(), DateTime.UtcNow));
lst.Add(new TestClass(3, "Jack", (TestClass) lst[0].Clone(), Guid.Empty, DateTime.Now.AddDays(2)));
lst.Add(new TestClass(4, "Rose", null, Guid.Parse("e2cec5dd-208d-4bb5-a852-50008f8ba366")));
lst.Add(new TestClass(5, "Ali", null));
lst.Add(new TestClass(6, "Hamid", (TestClass) lst[0].Clone(), Guid.Parse("de12bae1-93fa-40e4-92d1-2e60f95b468c")));
lst.Add(new TestClass(7, "Hasan", (TestClass) lst[1].Clone()));
lst.Add(new TestClass(8, "Farhad", (TestClass) lst[2].Clone(), Guid.Empty));
lst.Add(new TestClass(9, "Sara", null));
lst.Add(new TestClass(10, "Jorge", null));
lst.Add(new TestClass(11, "joe", null));
lst.Add(new TestClass(12, "jimmy", (TestClass) lst[0].Clone()));
lst.Add(new TestClass(13, "Nazanin", null));
lst.Add(new TestClass(14, "Reza", null));
lst.Add(new TestClass(15, "Korosh", (TestClass) lst[0].Clone()));
lst.Add(new TestClass(16, "Kamran", (TestClass) lst[1].Clone()));
lst.Add(new TestClass(17, "Saeid", (TestClass) lst[2].Clone()));
lst.Add(new TestClass(18, "jessi==ca", null));
lst.Add(new TestClass(19, "Ped=ram", null));
lst.Add(new TestClass(20, "Peyman!", null));
lst.Add(new TestClass(21, "Fereshte", null));
lst.Add(new TestClass(22, "LIAM", null));
lst.Add(new TestClass(22, @"\Liam", null));
lst.Add(new TestClass(23, "LI | AM", null));
lst.Add(new TestClass(24, "(LI,AM)", null));

return lst;
}

#endregion
}
}

0 comments on commit a91db96

Please sign in to comment.