-Initial JsonPath

This commit is contained in:
James Newton-King
2013-11-15 00:20:20 +13:00
parent 12932404d4
commit d8ddf1b433
3 changed files with 484 additions and 101 deletions
+104 -31
View File
@@ -50,7 +50,23 @@ namespace Newtonsoft.Json.Tests.Linq
{
JPath path = new JPath("Blah");
Assert.AreEqual(1, path.Parts.Count);
Assert.AreEqual("Blah", path.Parts[0]);
Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name);
}
[Test]
public void SinglePropertyWithRoot()
{
JPath path = new JPath("$.Blah");
Assert.AreEqual(1, path.Parts.Count);
Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name);
}
[Test]
public void SingleScanWithRoot()
{
JPath path = new JPath("$..Blah");
Assert.AreEqual(1, path.Parts.Count);
Assert.AreEqual("Blah", ((ScanFilter)path.Parts[0]).Name);
}
[Test]
@@ -58,8 +74,17 @@ namespace Newtonsoft.Json.Tests.Linq
{
JPath path = new JPath("Blah.Two");
Assert.AreEqual(2, path.Parts.Count);
Assert.AreEqual("Blah", path.Parts[0]);
Assert.AreEqual("Two", path.Parts[1]);
Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name);
Assert.AreEqual("Two", ((FieldFilter)path.Parts[1]).Name);
}
[Test]
public void OnePropertyOneScan()
{
JPath path = new JPath("Blah..Two");
Assert.AreEqual(2, path.Parts.Count);
Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name);
Assert.AreEqual("Two", ((ScanFilter)path.Parts[1]).Name);
}
[Test]
@@ -67,21 +92,34 @@ namespace Newtonsoft.Json.Tests.Linq
{
JPath path = new JPath("Blah[0]");
Assert.AreEqual(2, path.Parts.Count);
Assert.AreEqual("Blah", path.Parts[0]);
Assert.AreEqual(0, path.Parts[1]);
Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name);
Assert.AreEqual(0, ((ArrayIndexFilter)path.Parts[1]).Index);
}
[Test]
public void SinglePropertyAndFilter()
{
JPath path = new JPath("Blah[?(@.name=='hi')]");
Assert.AreEqual(2, path.Parts.Count);
Assert.AreEqual("Blah", ((FieldFilter)path.Parts[0]).Name);
List<object> expressions = ((QueryFilter)path.Parts[1]).Expression;
Assert.AreEqual(1, expressions.Count);
}
[Test]
public void MultiplePropertiesAndIndexers()
{
JPath path = new JPath("Blah[0].Two.Three[1].Four");
JPath path = new JPath("Blah[0]..Two.Three[1].Four");
Assert.AreEqual(6, path.Parts.Count);
Assert.AreEqual("Blah", path.Parts[0]);
Assert.AreEqual(0, path.Parts[1]);
Assert.AreEqual("Two", path.Parts[2]);
Assert.AreEqual("Three", path.Parts[3]);
Assert.AreEqual(1, path.Parts[4]);
Assert.AreEqual("Four", path.Parts[5]);
Assert.AreEqual(FilterType.Field, path.Parts[0].Type);
Assert.AreEqual("Blah", ((FieldFilter) path.Parts[0]).Name);
Assert.AreEqual(FilterType.ArrayIndex, path.Parts[1].Type);
Assert.AreEqual(0, ((ArrayIndexFilter) path.Parts[1]).Index);
Assert.AreEqual(FilterType.Scan, path.Parts[2].Type);
Assert.AreEqual("Two", ((ScanFilter)path.Parts[2]).Name);
Assert.AreEqual("Three", ((FieldFilter)path.Parts[3]).Name);
Assert.AreEqual(1, ((ArrayIndexFilter)path.Parts[4]).Index);
Assert.AreEqual("Four", ((FieldFilter)path.Parts[5]).Name);
}
[Test]
@@ -96,36 +134,46 @@ namespace Newtonsoft.Json.Tests.Linq
public void UnclosedIndexer()
{
ExceptionAssert.Throws<JsonException>(
@"Path ended with open indexer. Expected ]",
@"Path ended with open indexer.",
() => { new JPath("Blah[0"); });
}
[Test]
public void AdditionalDots()
{
JPath path = new JPath(".Blah..[0]..Two.Three....[1].Four.");
Assert.AreEqual(6, path.Parts.Count);
Assert.AreEqual("Blah", path.Parts[0]);
Assert.AreEqual(0, path.Parts[1]);
Assert.AreEqual("Two", path.Parts[2]);
Assert.AreEqual("Three", path.Parts[3]);
Assert.AreEqual(1, path.Parts[4]);
Assert.AreEqual("Four", path.Parts[5]);
}
//[Test]
//public void AdditionalDots()
//{
// JPath path = new JPath(".Blah..[0]..Two.Three....[1].Four.");
// Assert.AreEqual(6, path.Parts.Count);
// Assert.AreEqual("Blah", path.Parts[0]);
// Assert.AreEqual(0, path.Parts[1]);
// Assert.AreEqual("Two", path.Parts[2]);
// Assert.AreEqual("Three", path.Parts[3]);
// Assert.AreEqual(1, path.Parts[4]);
// Assert.AreEqual("Four", path.Parts[5]);
//}
[Test]
public void IndexerOnly()
{
JPath path = new JPath("[111119990]");
Assert.AreEqual(1, path.Parts.Count);
Assert.AreEqual(111119990, path.Parts[0]);
Assert.AreEqual(111119990, ((ArrayIndexFilter)path.Parts[0]).Index);
}
[Test]
public void MultipleIndexes()
{
JPath path = new JPath("[111119990,3]");
Assert.AreEqual(1, path.Parts.Count);
Assert.AreEqual(2, ((ArrayMultipleIndexFilter)path.Parts[0]).Indexes.Count);
Assert.AreEqual(111119990, ((ArrayMultipleIndexFilter)path.Parts[0]).Indexes[0]);
Assert.AreEqual(3, ((ArrayMultipleIndexFilter)path.Parts[0]).Indexes[1]);
}
[Test]
public void EmptyIndexer()
{
ExceptionAssert.Throws<JsonException>(
"Empty path indexer.",
"Array index expected.",
() => { new JPath("[]"); });
}
@@ -142,10 +190,10 @@ namespace Newtonsoft.Json.Tests.Linq
{
JPath path = new JPath("[1][0][0][" + int.MaxValue + "]");
Assert.AreEqual(4, path.Parts.Count);
Assert.AreEqual(1, path.Parts[0]);
Assert.AreEqual(0, path.Parts[1]);
Assert.AreEqual(0, path.Parts[2]);
Assert.AreEqual(int.MaxValue, path.Parts[3]);
Assert.AreEqual(1, ((ArrayIndexFilter)path.Parts[0]).Index);
Assert.AreEqual(0, ((ArrayIndexFilter)path.Parts[1]).Index);
Assert.AreEqual(0, ((ArrayIndexFilter)path.Parts[2]).Index);
Assert.AreEqual(int.MaxValue, ((ArrayIndexFilter)path.Parts[3]).Index);
}
[Test]
@@ -168,6 +216,18 @@ namespace Newtonsoft.Json.Tests.Linq
Assert.AreEqual(1, (int)t);
}
[Test]
public void QuoteName()
{
JObject o = new JObject(
new JProperty("Blah", 1));
JToken t = o.SelectToken("['Blah']");
Assert.IsNotNull(t);
Assert.AreEqual(JTokenType.Integer, t.Type);
Assert.AreEqual(1, (int)t);
}
[Test]
public void EvaluateMissingProperty()
{
@@ -269,6 +329,19 @@ namespace Newtonsoft.Json.Tests.Linq
Assert.AreEqual(2, (int)t);
}
[Test]
public void EvaluateArrayMultipleIndexes()
{
JArray a = new JArray(1, 2, 3, 4);
IEnumerable<JToken> t = a.SelectTokens("[1,2,0]");
Assert.IsNotNull(t);
Assert.AreEqual(3, t.Count());
Assert.AreEqual(2, (int)t.ElementAt(0));
Assert.AreEqual(3, (int)t.ElementAt(1));
Assert.AreEqual(1, (int)t.ElementAt(2));
}
[Test]
public void EvaluateSinglePropertyReturningArray()
{
+363 -70
View File
@@ -26,14 +26,92 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json.Utilities;
namespace Newtonsoft.Json.Linq
{
internal enum FilterType
{
ArrayIndex,
ArrayMultipleIndex,
ArraySlice,
Field,
Scan,
Query
}
internal abstract class PathFilter
{
public abstract FilterType Type { get; }
}
internal class ArrayIndexFilter : PathFilter
{
public int Index { get; set; }
public override FilterType Type
{
get { return FilterType.ArrayIndex; }
}
}
internal class ArrayMultipleIndexFilter : PathFilter
{
public List<int> Indexes { get; set; }
public override FilterType Type
{
get { return FilterType.ArrayMultipleIndex; }
}
}
internal class ArraySliceFilter : PathFilter
{
public int? Start { get; set; }
public int? End { get; set; }
public int? Skip { get; set; }
public override FilterType Type
{
get { return FilterType.ArraySlice; }
}
}
internal class FieldFilter : PathFilter
{
public string Name { get; set; }
public override FilterType Type
{
get { return FilterType.Field; }
}
}
internal class ScanFilter : PathFilter
{
public string Name { get; set; }
public override FilterType Type
{
get { return FilterType.Scan; }
}
}
internal class QueryFilter : PathFilter
{
public List<object> Expression { get; set; }
public override FilterType Type
{
get { return FilterType.Query; }
}
}
internal class JPath
{
private readonly string _expression;
public List<object> Parts { get; private set; }
public List<PathFilter> Parts { get; private set; }
private int _currentIndex;
@@ -41,7 +119,7 @@ namespace Newtonsoft.Json.Linq
{
ValidationUtils.ArgumentNotNull(expression, "expression");
_expression = expression;
Parts = new List<object>();
Parts = new List<PathFilter>();
ParseMain();
}
@@ -50,6 +128,34 @@ namespace Newtonsoft.Json.Linq
{
int currentPartStartIndex = _currentIndex;
bool followingIndexer = false;
bool scan = false;
EnsureLength("Empty path.");
if (_expression[_currentIndex] == '$')
{
_currentIndex++;
EnsureLength("Unexpected end while parsing path.");
if (_expression[_currentIndex] != '.')
throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]);
_currentIndex++;
EnsureLength("Unexpected end while parsing path.");
if (_expression[_currentIndex] == '.')
{
scan = true;
_currentIndex++;
currentPartStartIndex = 3;
}
else
{
currentPartStartIndex = 2;
}
}
while (_currentIndex < _expression.Length)
{
@@ -62,7 +168,9 @@ namespace Newtonsoft.Json.Linq
if (_currentIndex > currentPartStartIndex)
{
string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
Parts.Add(member);
PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member };
Parts.Add(filter);
scan = false;
}
ParseIndexer(currentChar);
@@ -76,7 +184,14 @@ namespace Newtonsoft.Json.Linq
if (_currentIndex > currentPartStartIndex)
{
string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
Parts.Add(member);
PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member };
Parts.Add(filter);
scan = false;
}
if (_currentIndex + 1 < _expression.Length && _expression[_currentIndex + 1] == '.')
{
scan = true;
_currentIndex++;
}
currentPartStartIndex = _currentIndex + 1;
followingIndexer = false;
@@ -93,7 +208,8 @@ namespace Newtonsoft.Json.Linq
if (_currentIndex > currentPartStartIndex)
{
string member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex);
Parts.Add(member);
PathFilter filter = (scan) ? (PathFilter)new ScanFilter() { Name = member } : new FieldFilter() { Name = member };
Parts.Add(filter);
}
}
@@ -102,23 +218,74 @@ namespace Newtonsoft.Json.Linq
_currentIndex++;
char indexerCloseChar = (indexerOpenChar == '[') ? ']' : ')';
int indexerStart = _currentIndex;
int indexerLength = 0;
bool indexerClosed = false;
EnsureLength("Path ended with open indexer.");
if (_expression[_currentIndex] == '\'')
{
ParseQuotedField(indexerCloseChar);
}
else if (_expression[_currentIndex] == '?')
{
ParseQuery(indexerCloseChar);
}
else
{
ParseArrayIndexer(indexerCloseChar);
}
}
private void ParseArrayIndexer(char indexerCloseChar)
{
int start = _currentIndex;
List<int> indexes = null;
int? startIndex = null;
int? endIndex = null;
int? step = null;
while (_currentIndex < _expression.Length)
{
char currentCharacter = _expression[_currentIndex];
if (char.IsDigit(currentCharacter))
if (currentCharacter == indexerCloseChar)
{
indexerLength++;
int length = _currentIndex - start;
if (length == 0)
throw new JsonException("Array index expected.");
string indexer = _expression.Substring(start, length);
int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture);
if (indexes != null)
{
indexes.Add(index);
Parts.Add(new ArrayMultipleIndexFilter { Indexes = indexes });
}
else
{
Parts.Add(new ArrayIndexFilter { Index = index });
}
return;
}
else if (currentCharacter == indexerCloseChar)
if (currentCharacter == ',')
{
indexerClosed = true;
break;
int length = _currentIndex - start;
if (length == 0)
throw new JsonException("Array index expected.");
if (indexes == null)
indexes = new List<int>();
string indexer = _expression.Substring(start, length);
indexes.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture));
start = _currentIndex + 1;
}
else
else if (!char.IsDigit(currentCharacter))
{
throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter);
}
@@ -126,83 +293,209 @@ namespace Newtonsoft.Json.Linq
_currentIndex++;
}
if (!indexerClosed)
throw new JsonException("Path ended with open indexer. Expected " + indexerCloseChar);
if (indexerLength == 0)
throw new JsonException("Empty path indexer.");
string indexer = _expression.Substring(indexerStart, indexerLength);
Parts.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture));
throw new JsonException("Path ended with open indexer.");
}
internal JToken Evaluate(JToken root, bool errorWhenNoMatch)
private void ParseQuery(char indexerCloseChar)
{
JToken current = root;
_currentIndex++;
EnsureLength("Path ended with open indexer.");
foreach (object part in Parts)
if (_expression[_currentIndex] != '(')
throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]);
List<object> expressions = new List<object>();
while (true)
{
string propertyName = part as string;
if (propertyName != null)
ParseExpression();
expressions.Add(string.Empty);
if (_expression[_currentIndex] == ')')
{
JObject o = current as JObject;
if (o != null)
_currentIndex++;
EnsureLength("Path ended with open indexer.");
if (_expression[_currentIndex] != indexerCloseChar)
throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]);
Parts.Add(new QueryFilter
{
current = o[propertyName];
if (current == null && errorWhenNoMatch)
throw new JsonException("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, propertyName));
}
else
{
if (errorWhenNoMatch)
throw new JsonException("Property '{0}' not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, propertyName, current.GetType().Name));
return null;
}
Expression = expressions
});
return;
}
else
{
int index = (int)part;
_currentIndex++;
}
}
}
JArray a = current as JArray;
JConstructor c = current as JConstructor;
private void ParseExpression()
{
_currentIndex++;
while (_currentIndex < _expression.Length)
{
char currentCharacter = _expression[_currentIndex];
if (a != null)
{
if (a.Count <= index)
{
if (errorWhenNoMatch)
throw new JsonException("Index {0} outside the bounds of JArray.".FormatWith(CultureInfo.InvariantCulture, index));
if (currentCharacter == ')')
{
return;
}
else
{
_currentIndex++;
}
}
}
return null;
}
private void ParseQuotedField(char indexerCloseChar)
{
_currentIndex++;
int start = _currentIndex;
current = a[index];
}
else if (c != null)
{
if (c.Count <= index)
{
if (errorWhenNoMatch)
throw new JsonException("Index {0} outside the bounds of JConstructor.".FormatWith(CultureInfo.InvariantCulture, index));
while (_currentIndex < _expression.Length)
{
char currentCharacter = _expression[_currentIndex];
if (currentCharacter == '\'')
{
int length = _currentIndex - start;
return null;
}
if (length == 0)
throw new JsonException("Empty path indexer.");
current = c[index];
}
else
{
if (errorWhenNoMatch)
throw new JsonException("Index {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, index, current.GetType().Name));
// check that character after the quote is to close the index
_currentIndex++;
EnsureLength("Path ended with open indexer.");
if (_expression[_currentIndex] != indexerCloseChar)
throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter);
return null;
}
string indexer = _expression.Substring(start, length);
Parts.Add(new FieldFilter { Name = indexer });
return;
}
_currentIndex++;
}
throw new JsonException("Path ended with open indexer.");
}
private void EnsureLength(string message)
{
if (_currentIndex >= _expression.Length)
throw new JsonException(message);
}
internal IEnumerable<JToken> Evaluate(JToken root, bool errorWhenNoMatch)
{
IEnumerable<JToken> current = new [] { root };
foreach (PathFilter filter in Parts)
{
switch (filter.Type)
{
case FilterType.ArrayIndex:
current = ExecuteArrayIndexFilter(current, errorWhenNoMatch, (ArrayIndexFilter)filter);
break;
case FilterType.ArrayMultipleIndex:
current = ExecuteArrayMultipleIndexFilter(current, errorWhenNoMatch, (ArrayMultipleIndexFilter)filter);
break;
case FilterType.Field:
current = ExecuteFieldFilter(current, errorWhenNoMatch, (FieldFilter)filter);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return current;
}
private IEnumerable<JToken> ExecuteArrayMultipleIndexFilter(IEnumerable<JToken> current, bool errorWhenNoMatch, ArrayMultipleIndexFilter arrayMultipleIndexFilter)
{
foreach (JToken t in current)
{
foreach (int i in arrayMultipleIndexFilter.Indexes)
{
JToken v = GetTokenIndex(t, errorWhenNoMatch, i);
if (v != null)
yield return v;
}
}
}
private static IEnumerable<JToken> ExecuteFieldFilter(IEnumerable<JToken> current, bool errorWhenNoMatch, FieldFilter fieldFilter)
{
foreach (JToken t in current)
{
JObject o = t as JObject;
if (o != null)
{
JToken v = o[fieldFilter.Name];
if (v == null && errorWhenNoMatch)
throw new JsonException("Property '{0}' does not exist on JObject.".FormatWith(CultureInfo.InvariantCulture, fieldFilter.Name));
yield return v;
}
else
{
if (errorWhenNoMatch)
throw new JsonException("Property '{0}' not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, fieldFilter.Name, t.GetType().Name));
}
}
}
private static IEnumerable<JToken> ExecuteArrayIndexFilter(IEnumerable<JToken> current, bool errorWhenNoMatch, ArrayIndexFilter arrayIndexFilter)
{
foreach (JToken t in current)
{
JToken v = GetTokenIndex(t, errorWhenNoMatch, arrayIndexFilter.Index);
if (v != null)
yield return v;
}
}
private static JToken GetTokenIndex(JToken t, bool errorWhenNoMatch, int index)
{
JArray a = t as JArray;
JConstructor c = t as JConstructor;
if (a != null)
{
if (a.Count <= index)
{
if (errorWhenNoMatch)
throw new JsonException("Index {0} outside the bounds of JArray.".FormatWith(CultureInfo.InvariantCulture, index));
return null;
}
return a[index];
}
else if (c != null)
{
if (c.Count <= index)
{
if (errorWhenNoMatch)
throw new JsonException("Index {0} outside the bounds of JConstructor.".FormatWith(CultureInfo.InvariantCulture, index));
return null;
}
return c[index];
}
else
{
if (errorWhenNoMatch)
throw new JsonException("Index {0} not valid on {1}.".FormatWith(CultureInfo.InvariantCulture, index, t.GetType().Name));
return null;
}
}
}
}
+17
View File
@@ -1932,6 +1932,23 @@ namespace Newtonsoft.Json.Linq
/// <param name="errorWhenNoMatch">A flag to indicate whether an error should be thrown if no token is found.</param>
/// <returns>The <see cref="JToken"/> that matches the object path.</returns>
public JToken SelectToken(string path, bool errorWhenNoMatch)
{
JPath p = new JPath(path);
// todo change this to a foreach
List<JToken> result = p.Evaluate(this, errorWhenNoMatch).ToList();
if (result.Count > 1)
throw new Exception("TODO");
return result.SingleOrDefault();
}
public IEnumerable<JToken> SelectTokens(string path)
{
return SelectTokens(path, false);
}
public IEnumerable<JToken> SelectTokens(string path, bool errorWhenNoMatch)
{
JPath p = new JPath(path);
return p.Evaluate(this, errorWhenNoMatch);