mirror of
https://github.com/Biarity/Sieve.git
synced 2024-11-25 14:53:27 +01:00
Fixed null-ref for case-(in)sensitive null-search (#165)
* Fixed null-ref for case-insensitive null-search Added null-escaping sequence (to distinguish between prop==null (as null) and prop==\null (as string)) * Added null-search case-insensitive test * Code style * Added escape-sequences description to README.md Co-authored-by: Nikita Prokhorov <nikita.prokhorov@grse.de>
This commit is contained in:
parent
7b6f3c7d85
commit
820358e8ff
@ -159,7 +159,10 @@ More formally:
|
|||||||
* `pageSize` is the number of items returned per page
|
* `pageSize` is the number of items returned per page
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
* You can use backslashes to escape commas and pipes within value fields
|
* You can use backslashes to escape special characters and sequences:
|
||||||
|
* commas: `Title@=some\,title` makes a match with "some,title"
|
||||||
|
* pipes: `Title@=some\|title` makes a match with "some|title"
|
||||||
|
* null values: `Title@=\null` will search for items with title equal to "null" (not a missing value, but "null"-string literally)
|
||||||
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
|
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
|
||||||
* If you need to look at the data before applying pagination (eg. get total count), use the optional paramters on `Apply` to defer pagination (an [example](https://github.com/Biarity/Sieve/issues/34))
|
* If you need to look at the data before applying pagination (eg. get total count), use the optional paramters on `Apply` to defer pagination (an [example](https://github.com/Biarity/Sieve/issues/34))
|
||||||
* 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)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@ -7,12 +8,16 @@ namespace Sieve.Models
|
|||||||
public class FilterTerm : IFilterTerm, IEquatable<FilterTerm>
|
public class FilterTerm : IFilterTerm, IEquatable<FilterTerm>
|
||||||
{
|
{
|
||||||
private const string EscapedPipePattern = @"(?<!($|[^\\]|^)(\\\\)*?\\)\|";
|
private const string EscapedPipePattern = @"(?<!($|[^\\]|^)(\\\\)*?\\)\|";
|
||||||
private const string PipeToEscape = @"\|";
|
|
||||||
private const string BackslashToEscape = @"\\";
|
|
||||||
private const string OperatorsRegEx = @"(!@=\*|!_=\*|!=\*|!@=|!_=|==\*|@=\*|_=\*|==|!=|>=|<=|>|<|@=|_=)";
|
private const string OperatorsRegEx = @"(!@=\*|!_=\*|!=\*|!@=|!_=|==\*|@=\*|_=\*|==|!=|>=|<=|>|<|@=|_=)";
|
||||||
private const string EscapeNegPatternForOper = @"(?<!\\)" + OperatorsRegEx;
|
private const string EscapeNegPatternForOper = @"(?<!\\)" + OperatorsRegEx;
|
||||||
private const string EscapePosPatternForOper = @"(?<=\\)" + OperatorsRegEx;
|
private const string EscapePosPatternForOper = @"(?<=\\)" + OperatorsRegEx;
|
||||||
|
|
||||||
|
private static readonly HashSet<string> _escapedSequences = new HashSet<string>
|
||||||
|
{
|
||||||
|
@"\|",
|
||||||
|
@"\\"
|
||||||
|
};
|
||||||
|
|
||||||
public string Filter
|
public string Filter
|
||||||
{
|
{
|
||||||
set
|
set
|
||||||
@ -30,8 +35,7 @@ namespace Sieve.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
Values = Regex.Split(filterSplits[2], EscapedPipePattern)
|
Values = Regex.Split(filterSplits[2], EscapedPipePattern)
|
||||||
.Select(t => t.Replace(PipeToEscape, "|").Trim())
|
.Select(UnEscape)
|
||||||
.Select(t => t.Replace(BackslashToEscape, "\\").Trim())
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +44,12 @@ namespace Sieve.Models
|
|||||||
OperatorIsCaseInsensitive = Operator.EndsWith("*");
|
OperatorIsCaseInsensitive = Operator.EndsWith("*");
|
||||||
OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!");
|
OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string UnEscape(string escapedTerm)
|
||||||
|
=> _escapedSequences.Aggregate(escapedTerm,
|
||||||
|
(current, sequence) => Regex.Replace(current, $@"(\\)({sequence})", "$2"));
|
||||||
|
|
||||||
public string[] Names { get; private set; }
|
public string[] Names { get; private set; }
|
||||||
|
|
||||||
public FilterOperator OperatorParsed { get; private set; }
|
public FilterOperator OperatorParsed { get; private set; }
|
||||||
|
@ -68,6 +68,7 @@ namespace Sieve.Services
|
|||||||
where TSortTerm : ISortTerm, new()
|
where TSortTerm : ISortTerm, new()
|
||||||
{
|
{
|
||||||
private const string NullFilterValue = "null";
|
private const string NullFilterValue = "null";
|
||||||
|
private const char EscapeChar = '\\';
|
||||||
private readonly ISieveCustomSortMethods _customSortMethods;
|
private readonly ISieveCustomSortMethods _customSortMethods;
|
||||||
private readonly ISieveCustomFilterMethods _customFilterMethods;
|
private readonly ISieveCustomFilterMethods _customFilterMethods;
|
||||||
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
|
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
|
||||||
@ -199,7 +200,7 @@ namespace Sieve.Services
|
|||||||
? Expression.Constant(null, property.PropertyType)
|
? Expression.Constant(null, property.PropertyType)
|
||||||
: ConvertStringValueToConstantExpression(filterTermValue, property, converter);
|
: ConvertStringValueToConstantExpression(filterTermValue, property, converter);
|
||||||
|
|
||||||
if (filterTerm.OperatorIsCaseInsensitive)
|
if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull)
|
||||||
{
|
{
|
||||||
propertyValue = Expression.Call(propertyValue,
|
propertyValue = Expression.Call(propertyValue,
|
||||||
typeof(string).GetMethods()
|
typeof(string).GetMethods()
|
||||||
@ -311,6 +312,10 @@ namespace Sieve.Services
|
|||||||
private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property,
|
private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property,
|
||||||
TypeConverter converter)
|
TypeConverter converter)
|
||||||
{
|
{
|
||||||
|
// to allow user to distinguish between prop==null (as null) and prop==\null (as "null"-string)
|
||||||
|
value = value.Equals(EscapeChar + NullFilterValue, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
? value.TrimStart(EscapeChar)
|
||||||
|
: value;
|
||||||
dynamic constantVal = converter.CanConvertFrom(typeof(string))
|
dynamic constantVal = converter.CanConvertFrom(typeof(string))
|
||||||
? converter.ConvertFrom(value)
|
? converter.ConvertFrom(value)
|
||||||
: Convert.ChangeType(value, property.PropertyType);
|
: Convert.ChangeType(value, property.PropertyType);
|
||||||
|
@ -38,7 +38,7 @@ namespace SieveUnitTests
|
|||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
DateCreated = DateTimeOffset.UtcNow,
|
DateCreated = DateTimeOffset.UtcNow,
|
||||||
Text = "Regular comment without n*ll.",
|
Text = "Regular comment without n*ll",
|
||||||
Author = "Mouse",
|
Author = "Mouse",
|
||||||
},
|
},
|
||||||
new Comment
|
new Comment
|
||||||
@ -47,24 +47,28 @@ namespace SieveUnitTests
|
|||||||
DateCreated = DateTimeOffset.UtcNow,
|
DateCreated = DateTimeOffset.UtcNow,
|
||||||
Text = null,
|
Text = null,
|
||||||
Author = "null",
|
Author = "null",
|
||||||
},
|
}
|
||||||
}.AsQueryable();
|
}.AsQueryable();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void Filter_Equals_Null()
|
[InlineData("Text==null")]
|
||||||
|
[InlineData("Text==*null")]
|
||||||
|
public void Filter_Equals_Null(string filter)
|
||||||
{
|
{
|
||||||
var model = new SieveModel {Filters = "Text==null"};
|
var model = new SieveModel {Filters = filter};
|
||||||
|
|
||||||
var result = _processor.Apply(model, _comments);
|
var result = _processor.Apply(model, _comments);
|
||||||
|
|
||||||
Assert.Equal(100, result.Single().Id);
|
Assert.Equal(100, result.Single().Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void Filter_NotEquals_Null()
|
[InlineData("Text!=null")]
|
||||||
|
[InlineData("Text!=*null")]
|
||||||
|
public void Filter_NotEquals_Null(string filter)
|
||||||
{
|
{
|
||||||
var model = new SieveModel {Filters = "Text!=null"};
|
var model = new SieveModel {Filters = filter};
|
||||||
|
|
||||||
var result = _processor.Apply(model, _comments);
|
var result = _processor.Apply(model, _comments);
|
||||||
|
|
||||||
@ -101,6 +105,22 @@ namespace SieveUnitTests
|
|||||||
Assert.Equal(expectedIds, result.Select(p => p.Id));
|
Assert.Equal(expectedIds, result.Select(p => p.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"Author==\null", 100)]
|
||||||
|
[InlineData(@"Author==*\null", 100)]
|
||||||
|
[InlineData(@"Author==*\NuLl", 100)]
|
||||||
|
[InlineData(@"Author!=*\null", 0, 1, 2)]
|
||||||
|
[InlineData(@"Author!=*\NulL", 0, 1, 2)]
|
||||||
|
[InlineData(@"Author!=\null", 0, 1, 2)]
|
||||||
|
public void SingleFilter_Equals_NullStringEscaped(string filter, params int[] expectedIds)
|
||||||
|
{
|
||||||
|
var model = new SieveModel {Filters = filter};
|
||||||
|
|
||||||
|
var result = _processor.Apply(model, _comments);
|
||||||
|
|
||||||
|
Assert.Equal(expectedIds, result.Select(p => p.Id));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Text_=null")]
|
[InlineData("Text_=null")]
|
||||||
[InlineData("Text_=*null")]
|
[InlineData("Text_=*null")]
|
||||||
|
Loading…
Reference in New Issue
Block a user