Merge branch 'feature/json-path-filter-using-regex' of https://github.com/andre-n/Newtonsoft.Json into andre-n-feature/json-path-filter-using-regex

This commit is contained in:
James Newton-King
2018-02-11 12:28:05 +13:00
6 changed files with 217 additions and 20 deletions
@@ -314,6 +314,17 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath
Assert.AreEqual("h\\i", (string)(JToken)expressions.Right);
}
[Test]
public void SinglePropertyAndFilterWithRegex()
{
JPath path = new JPath("Blah[ ?( @.name=~/hi/i ) ]");
Assert.AreEqual(2, path.Filters.Count);
Assert.AreEqual("Blah", ((FieldFilter)path.Filters[0]).Name);
BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression;
Assert.AreEqual(QueryOperator.RegExEquals, expressions.Operator);
Assert.AreEqual("/hi/i", (string)(JToken)expressions.Right);
}
[Test]
public void SinglePropertyAndFilterWithUnknownEscape()
{
@@ -157,6 +157,70 @@ namespace Newtonsoft.Json.Tests.Linq.JsonPath
Assert.IsFalse(compositeExpression.IsMatch(o3, o3));
}
[Test]
public void BooleanExpressionTest_RegExEqualsOperator()
{
BooleanQueryExpression e1 = new BooleanQueryExpression
{
Operator = QueryOperator.RegExEquals,
Right = new JValue("/foo.*d/"),
Left = new List<PathFilter>
{
new ArrayIndexFilter()
}
};
Assert.IsTrue(e1.IsMatch(null, new JArray("food")));
Assert.IsTrue(e1.IsMatch(null, new JArray("fooood and drink")));
Assert.IsFalse(e1.IsMatch(null, new JArray("FOOD")));
Assert.IsFalse(e1.IsMatch(null, new JArray("foo", "foog", "good")));
BooleanQueryExpression e2 = new BooleanQueryExpression
{
Operator = QueryOperator.RegExEquals,
Right = new JValue("/Foo.*d/i"),
Left = new List<PathFilter>
{
new ArrayIndexFilter()
}
};
Assert.IsTrue(e2.IsMatch(null, new JArray("food")));
Assert.IsTrue(e2.IsMatch(null, new JArray("fooood and drink")));
Assert.IsTrue(e2.IsMatch(null, new JArray("FOOD")));
Assert.IsFalse(e2.IsMatch(null, new JArray("foo", "foog", "good")));
}
[Test]
public void BooleanExpressionTest_RegExEqualsOperator_CornerCase()
{
BooleanQueryExpression e1 = new BooleanQueryExpression
{
Operator = QueryOperator.RegExEquals,
Right = new JValue("/// comment/"),
Left = new List<PathFilter>
{
new ArrayIndexFilter()
}
};
Assert.IsTrue(e1.IsMatch(null, new JArray("// comment")));
Assert.IsFalse(e1.IsMatch(null, new JArray("//comment", "/ comment")));
BooleanQueryExpression e2 = new BooleanQueryExpression
{
Operator = QueryOperator.RegExEquals,
Right = new JValue("/<tag>.*</tag>/i"),
Left = new List<PathFilter>
{
new ArrayIndexFilter()
}
};
Assert.IsTrue(e2.IsMatch(null, new JArray("<Tag>Test</Tag>", "")));
Assert.IsFalse(e2.IsMatch(null, new JArray("<tag>Test<tag>")));
}
[Test]
public void BooleanExpressionTest()
@@ -29,6 +29,7 @@ using Newtonsoft.Json.Bson;
using System.Globalization;
using System.Runtime.CompilerServices;
using Newtonsoft.Json.Serialization;
using Newtonsoft.Json.Utilities;
namespace Newtonsoft.Json.Converters
{
@@ -154,25 +155,7 @@ namespace Newtonsoft.Json.Converters
string patternText = regexText.Substring(1, patternOptionDelimiterIndex - 1);
string optionsText = regexText.Substring(patternOptionDelimiterIndex + 1);
RegexOptions options = RegexOptions.None;
foreach (char c in optionsText)
{
switch (c)
{
case 'i':
options |= RegexOptions.IgnoreCase;
break;
case 'm':
options |= RegexOptions.Multiline;
break;
case 's':
options |= RegexOptions.Singleline;
break;
case 'x':
options |= RegexOptions.ExplicitCapture;
break;
}
}
RegexOptions options = MiscellaneousUtils.GetRegexOptions(optionsText);
return new Regex(patternText, options);
}
@@ -647,6 +647,11 @@ namespace Newtonsoft.Json.Linq.JsonPath
return true;
}
}
else if (currentChar == '/')
{
value = ReadRegexString();
return true;
}
value = null;
return false;
@@ -712,6 +717,83 @@ namespace Newtonsoft.Json.Linq.JsonPath
throw new JsonException("Path ended with an open string.");
}
private string ReadRegexString()
{
var sb = new StringBuilder("/");
_currentIndex++;
while (_currentIndex < _expression.Length)
{
char currentChar = _expression[_currentIndex];
if (currentChar == '\\' && _currentIndex + 1 < _expression.Length)
{
_currentIndex++;
if (_expression[_currentIndex] == '\'')
{
sb.Append('\'');
}
else if (_expression[_currentIndex] == '\\')
{
sb.Append('\\');
}
else if (_expression[_currentIndex] == '/')
{
sb.Append('/');
}
else
{
throw new JsonException(@"Unknown escape character: \" + _expression[_currentIndex]);
}
_currentIndex++;
}
else if (currentChar == '/')
{
sb.Append(currentChar);
_currentIndex++;
string options = ParseRegexOptions();
sb.Append(options);
return sb.ToString();
}
else
{
_currentIndex++;
sb.Append(currentChar);
}
}
throw new JsonException("Path ended with an open string.");
}
private string ParseRegexOptions()
{
var optionsSb = new StringBuilder();
while (_currentIndex < _expression.Length)
{
char currentChar = _expression[_currentIndex];
if (currentChar == 'i'
|| currentChar == 'm'
|| currentChar == 's'
|| currentChar == 'u'
|| currentChar == 'x')
{
_currentIndex++;
optionsSb.Append(currentChar);
}
else
{
break;
}
}
return optionsSb.ToString();
}
private bool Match(string s)
{
int currentPosition = _currentIndex;
@@ -742,6 +824,12 @@ namespace Newtonsoft.Json.Linq.JsonPath
{
return QueryOperator.Equals;
}
if (Match("=~"))
{
return QueryOperator.RegExEquals;
}
if (Match("!=") || Match("<>"))
{
return QueryOperator.NotEquals;
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
#if !HAVE_LINQ
using Newtonsoft.Json.Utilities.LinqBridge;
#else
@@ -22,7 +23,8 @@ namespace Newtonsoft.Json.Linq.JsonPath
GreaterThan = 6,
GreaterThanOrEquals = 7,
And = 8,
Or = 9
Or = 9,
RegExEquals = 10
}
internal abstract class QueryExpression
@@ -126,6 +128,12 @@ namespace Newtonsoft.Json.Linq.JsonPath
{
switch (Operator)
{
case QueryOperator.RegExEquals:
if (RegexEquals(leftValue, rightValue))
{
return true;
}
break;
case QueryOperator.Equals:
if (EqualsWithStringCoercion(leftValue, rightValue))
{
@@ -181,6 +189,22 @@ namespace Newtonsoft.Json.Linq.JsonPath
return false;
}
private static bool RegexEquals(JValue input, JValue pattern)
{
if (input.Type != JTokenType.String || pattern.Type != JTokenType.String)
{
return false;
}
var regexText = (string)pattern.Value;
int patternOptionDelimiterIndex = regexText.LastIndexOf('/');
string patternText = regexText.Substring(1, patternOptionDelimiterIndex - 1);
string optionsText = regexText.Substring(patternOptionDelimiterIndex + 1);
return Regex.IsMatch((string)input.Value, patternText, MiscellaneousUtils.GetRegexOptions(optionsText));
}
private bool EqualsWithStringCoercion(JValue value, JValue queryValue)
{
if (value.Equals(queryValue))
@@ -30,6 +30,7 @@ using System.ComponentModel;
using System.Reflection;
using System.Text;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Newtonsoft.Json.Utilities
{
@@ -150,5 +151,31 @@ namespace Newtonsoft.Json.Utilities
return value.ToString();
}
internal static RegexOptions GetRegexOptions(string optionsText)
{
var options = RegexOptions.None;
foreach (char c in optionsText)
{
switch (c)
{
case 'i':
options |= RegexOptions.IgnoreCase;
break;
case 'm':
options |= RegexOptions.Multiline;
break;
case 's':
options |= RegexOptions.Singleline;
break;
case 'x':
options |= RegexOptions.ExplicitCapture;
break;
}
}
return options;
}
}
}