diff --git a/README.md b/README.md index fa6e313..42c3ae1 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,8 @@ Because JsonDocumentPath is using the same Json.net strategic for parsing and ev - [x] JPathParseTests - [x] QueryExpressionTests - [x] JPathExecuteTests +- [x] JsonNodeParseTests +- [x] JsonNodePathExecuteTests +- [x] JsonNodeQueryExpressionTests +- [x] JsonNodeRefTests diff --git a/src/JsonDocumentPath.Test/JPathExecuteTests.cs b/src/JsonDocumentPath.Test/JPathExecuteTests.cs index 484fe8e..e9c4a89 100644 --- a/src/JsonDocumentPath.Test/JPathExecuteTests.cs +++ b/src/JsonDocumentPath.Test/JPathExecuteTests.cs @@ -19,6 +19,7 @@ public void GreaterThanIssue1518() Assert.Equal(jObj, aa); var bb = jObj.SelectElement("$..[?(@.usingmem>27000)]");//null, 27,000 + Assert.Equal(jObj, bb); var cc = jObj.SelectElement("$..[?(@.usingmem>21437)]");//found, 21,437 @@ -1201,13 +1202,12 @@ public void QueryAgainstNonStringValues() /* Dotnet 6.0 JsonDocument Parse the TimeSpan as string '365.23:59:59' */ -#if NET6_0 - +#if NET6_0_OR_GREATER + Assert.Equal(2, t.Count); #else Assert.Equal(1, t.Count); #endif - } [Fact] @@ -1431,6 +1431,7 @@ public void RootInFilterWithRootObject() } public const string IsoDateFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK"; + [Fact] public void RootInFilterWithInitializers() { diff --git a/src/JsonDocumentPath.Test/JsonDocumentExtensions.cs b/src/JsonDocumentPath.Test/JsonDocumentExtensions.cs index 6fe2889..3a214a8 100644 --- a/src/JsonDocumentPath.Test/JsonDocumentExtensions.cs +++ b/src/JsonDocumentPath.Test/JsonDocumentExtensions.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; namespace JDocument.Test { @@ -25,4 +26,18 @@ public static bool DeepEquals(this JsonDocument left, JsonDocument? right) return DeepEquals(left.RootElement, right?.RootElement); } } -} + + public static class JsonNodeExtensions + { + public static bool DeepEquals(this JsonNode left, JsonNode? right) + { + if (right == null) + { + return false; + } + var jsonString = left.ToJsonString(); + var jsonStringR = right.ToJsonString(); + return jsonString == jsonStringR; + } + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj b/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj index 6b1a182..01dc065 100644 --- a/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj +++ b/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj @@ -1,9 +1,10 @@ - + - net6.0 + net6.0; false + latest diff --git a/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeParseTests.cs b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeParseTests.cs new file mode 100644 index 0000000..ef1dec6 --- /dev/null +++ b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeParseTests.cs @@ -0,0 +1,736 @@ +using JDocument.Test; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Xunit; + +namespace JNodePath.Test; + +public class JsonNodeParseTests +{ + [Fact] + public void BooleanQuery_TwoValues() + { + JsonNodePath path = new JsonNodePath("[?(1 > 2)]"); + Assert.Equal(1, path.Filters.Count); + BooleanQueryExpression booleanExpression = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(1, ((JsonNode)booleanExpression.Left).GetInt32()); + Assert.Equal(2, ((JsonNode)booleanExpression.Right).GetInt32()); + Assert.Equal(QueryOperator.GreaterThan, booleanExpression.Operator); + } + + [Fact] + public void BooleanQuery_TwoPaths() + { + JsonNodePath path = new JsonNodePath("[?(@.price > @.max_price)]"); + Assert.Equal(1, path.Filters.Count); + BooleanQueryExpression booleanExpression = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + List leftPaths = (List)booleanExpression.Left; + List rightPaths = (List)booleanExpression.Right; + + Assert.Equal("price", ((FieldFilter)leftPaths[0]).Name); + Assert.Equal("max_price", ((FieldFilter)rightPaths[0]).Name); + Assert.Equal(QueryOperator.GreaterThan, booleanExpression.Operator); + } + + [Fact] + public void SingleProperty() + { + JsonNodePath path = new JsonNodePath("Blah"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SingleQuotedProperty() + { + JsonNodePath path = new JsonNodePath("['Blah']"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SingleQuotedPropertyWithWhitespace() + { + JsonNodePath path = new JsonNodePath("[ 'Blah' ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SingleQuotedPropertyWithDots() + { + JsonNodePath path = new JsonNodePath("['Blah.Ha']"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah.Ha", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SingleQuotedPropertyWithBrackets() + { + JsonNodePath path = new JsonNodePath("['[*]']"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("[*]", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SinglePropertyWithRoot() + { + JsonNodePath path = new JsonNodePath("$.Blah"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SinglePropertyWithRootWithStartAndEndWhitespace() + { + JsonNodePath path = new JsonNodePath(" $.Blah "); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void RootWithBadWhitespace() + { + ExceptionAssert.Throws(() => { new JsonNodePath("$ .Blah"); }, @"Unexpected character while parsing path: "); + } + + [Fact] + public void NoFieldNameAfterDot() + { + ExceptionAssert.Throws(() => { new JsonNodePath("$.Blah."); }, @"Unexpected end while parsing path."); + } + + [Fact] + public void RootWithBadWhitespace2() + { + ExceptionAssert.Throws(() => { new JsonNodePath("$. Blah"); }, @"Unexpected character while parsing path: "); + } + + [Fact] + public void WildcardPropertyWithRoot() + { + JsonNodePath path = new JsonNodePath("$.*"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(null, ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void WildcardArrayWithRoot() + { + JsonNodePath path = new JsonNodePath("$.[*]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(null, ((ArrayIndexFilter)path.Filters[0]).Index); + } + + [Fact] + public void RootArrayNoDot() + { + JsonNodePath path = new JsonNodePath("$[1]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(1, ((ArrayIndexFilter)path.Filters[0]).Index); + } + + [Fact] + public void WildcardArray() + { + JsonNodePath path = new JsonNodePath("[*]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(null, ((ArrayIndexFilter)path.Filters[0]).Index); + } + + [Fact] + public void WildcardArrayWithProperty() + { + JsonNodePath path = new JsonNodePath("[ * ].derp"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal(null, ((ArrayIndexFilter)path.Filters[0]).Index); + Assert.Equal("derp", ((FieldFilter)path.Filters[1]).Name); + } + + [Fact] + public void QuotedWildcardPropertyWithRoot() + { + JsonNodePath path = new JsonNodePath("$.['*']"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("*", ((FieldFilter)path.Filters[0]).Name); + } + + [Fact] + public void SingleScanWithRoot() + { + JsonNodePath path = new JsonNodePath("$..Blah"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal("Blah", ((ScanFilter)path.Filters[0]).Name); + } + + [Fact] + public void QueryTrue() + { + JsonNodePath path = new JsonNodePath("$.elements[?(true)]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("elements", ((FieldFilter)path.Filters[0]).Name); + Assert.Equal(QueryOperator.Exists, ((QueryFilter)path.Filters[1]).Expression.Operator); + } + + [Fact] + public void ScanQuery() + { + JsonNodePath path = new JsonNodePath("$.elements..[?(@.id=='AAA')]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("elements", ((FieldFilter)path.Filters[0]).Name); + + BooleanQueryExpression expression = (BooleanQueryExpression)((QueryScanFilter)path.Filters[1]).Expression; + + List paths = (List)expression.Left; + + Assert.IsType(typeof(FieldFilter), paths[0]); + } + + [Fact] + public void WildcardScanWithRoot() + { + JsonNodePath path = new JsonNodePath("$..*"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(null, ((ScanFilter)path.Filters[0]).Name); + } + + [Fact] + public void WildcardScanWithRootWithWhitespace() + { + JsonNodePath path = new JsonNodePath("$..* "); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(null, ((ScanFilter)path.Filters[0]).Name); + } + + [Fact] + public void TwoProperties() + { + JsonNodePath path = new JsonNodePath("Blah.Two"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + Assert.Equal("Two", ((FieldFilter)path.Filters[1]).Name); + } + + [Fact] + public void OnePropertyOneScan() + { + JsonNodePath path = new JsonNodePath("Blah..Two"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + Assert.Equal("Two", ((ScanFilter)path.Filters[1]).Name); + } + + [Fact] + public void SinglePropertyAndIndexer() + { + JsonNodePath path = new JsonNodePath("Blah[0]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + Assert.Equal(0, ((ArrayIndexFilter)path.Filters[1]).Index); + } + + [Fact] + public void SinglePropertyAndExistsQuery() + { + JsonNodePath path = new JsonNodePath("Blah[ ?( @..name ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Exists, expressions.Operator); + List paths = (List)expressions.Left; + Assert.Equal("name", ((ScanFilter)paths[0]).Name); + Assert.Equal(1, paths.Count); + } + + [Fact] + public void SinglePropertyAndFilterWithWhitespace() + { + JsonNodePath path = new JsonNodePath("Blah[ ?( @.name=='hi' ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Equals, expressions.Operator); + Assert.Equal("hi", ((JsonNode)expressions.Right).GetString()); + } + + [Fact] + public void SinglePropertyAndFilterWithEscapeQuote() + { + JsonNodePath path = new JsonNodePath(@"Blah[ ?( @.name=='h\'i' ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Equals, expressions.Operator); + Assert.Equal("h'i", ((JsonNode)expressions.Right).GetString()); + } + + [Fact] + public void SinglePropertyAndFilterWithDoubleEscape() + { + JsonNodePath path = new JsonNodePath(@"Blah[ ?( @.name=='h\\i' ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Equals, expressions.Operator); + var r = ((JsonNode)expressions.Right); + var rStr = r.GetString(); + Assert.Equal("h\\i", ((JsonNode)expressions.Right).GetString()); + } + + [Fact] + public void SinglePropertyAndFilterWithRegexAndOptions() + { + JsonNodePath path = new JsonNodePath("Blah[ ?( @.name=~/hi/i ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.RegexEquals, expressions.Operator); + Assert.Equal("/hi/i", ((JsonNode)expressions.Right).GetString()); + } + + [Fact] + public void SinglePropertyAndFilterWithRegex() + { + JsonNodePath path = new JsonNodePath("Blah[?(@.title =~ /^.*Sword.*$/)]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.RegexEquals, expressions.Operator); + Assert.Equal("/^.*Sword.*$/", ((JsonNode)expressions.Right).GetString()); + } + + [Fact] + public void SinglePropertyAndFilterWithEscapedRegex() + { + JsonNodePath path = new JsonNodePath(@"Blah[?(@.title =~ /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g)]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.RegexEquals, expressions.Operator); + Assert.Equal(@"/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g", ((JsonNode)expressions.Right).GetString()); + } + + [Fact] + public void SinglePropertyAndFilterWithOpenRegex() + { + ExceptionAssert.Throws(() => { new JsonNodePath(@"Blah[?(@.title =~ /[\"); }, "Path ended with an open regex."); + } + + [Fact] + public void SinglePropertyAndFilterWithUnknownEscape() + { + ExceptionAssert.Throws(() => { new JsonNodePath(@"Blah[ ?( @.name=='h\i' ) ]"); }, @"Unknown escape character: \i"); + } + + [Fact] + public void SinglePropertyAndFilterWithFalse() + { + JsonNodePath path = new JsonNodePath("Blah[ ?( @.name==false ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Equals, expressions.Operator); + Assert.Equal(false, ((JsonNode)expressions.Right).GetBoolean()); + } + + [Fact] + public void SinglePropertyAndFilterWithTrue() + { + JsonNodePath path = new JsonNodePath("Blah[ ?( @.name==true ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Equals, expressions.Operator); + Assert.Equal(true, ((JsonNode)expressions.Right).GetBoolean()); + } + + [Fact] + public void SinglePropertyAndFilterWithNull() + { + JsonNodePath path = new JsonNodePath("Blah[ ?( @.name==null ) ]"); + Assert.Equal(2, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[1]).Expression; + Assert.Equal(QueryOperator.Equals, expressions.Operator); + Assert.Equal(null, ((JsonNode)expressions.Right).GetObjectValue()); + } + + [Fact] + public void FilterWithScan() + { + JsonNodePath path = new JsonNodePath("[?(@..name<>null)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + List paths = (List)expressions.Left; + Assert.Equal("name", ((ScanFilter)paths[0]).Name); + } + + [Fact] + public void FilterWithNotEquals() + { + JsonNodePath path = new JsonNodePath("[?(@.name<>null)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(QueryOperator.NotEquals, expressions.Operator); + } + + [Fact] + public void FilterWithNotEquals2() + { + JsonNodePath path = new JsonNodePath("[?(@.name!=null)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(QueryOperator.NotEquals, expressions.Operator); + } + + [Fact] + public void FilterWithLessThan() + { + JsonNodePath path = new JsonNodePath("[?(@.namenull)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(QueryOperator.GreaterThan, expressions.Operator); + } + + [Fact] + public void FilterWithGreaterThanOrEquals() + { + JsonNodePath path = new JsonNodePath("[?(@.name>=null)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(QueryOperator.GreaterThanOrEquals, expressions.Operator); + } + + [Fact] + public void FilterWithInteger() + { + JsonNodePath path = new JsonNodePath("[?(@.name>=12)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(12, ((JsonNode)expressions.Right).GetInt32()); + } + + [Fact] + public void FilterWithNegativeInteger() + { + JsonNodePath path = new JsonNodePath("[?(@.name>=-12)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(-12, ((JsonNode)expressions.Right).GetInt32()); + } + + [Fact] + public void FilterWithFloat() + { + JsonNodePath path = new JsonNodePath("[?(@.name>=12.1)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(12.1d, ((JsonNode)expressions.Right).GetDouble()); + } + + [Fact] + public void FilterExistWithAnd() + { + JsonNodePath path = new JsonNodePath("[?(@.name&&@.title)]"); + CompositeExpression expressions = (CompositeExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(QueryOperator.And, expressions.Operator); + Assert.Equal(2, expressions.Expressions.Count); + + var first = (BooleanQueryExpression)expressions.Expressions[0]; + var firstPaths = (List)first.Left; + Assert.Equal("name", ((FieldFilter)firstPaths[0]).Name); + Assert.Equal(QueryOperator.Exists, first.Operator); + + var second = (BooleanQueryExpression)expressions.Expressions[1]; + var secondPaths = (List)second.Left; + Assert.Equal("title", ((FieldFilter)secondPaths[0]).Name); + Assert.Equal(QueryOperator.Exists, second.Operator); + } + + [Fact] + public void FilterExistWithAndOr() + { + JsonNodePath path = new JsonNodePath("[?(@.name&&@.title||@.pie)]"); + CompositeExpression andExpression = (CompositeExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(QueryOperator.And, andExpression.Operator); + Assert.Equal(2, andExpression.Expressions.Count); + + var first = (BooleanQueryExpression)andExpression.Expressions[0]; + var firstPaths = (List)first.Left; + Assert.Equal("name", ((FieldFilter)firstPaths[0]).Name); + Assert.Equal(QueryOperator.Exists, first.Operator); + + CompositeExpression orExpression = (CompositeExpression)andExpression.Expressions[1]; + Assert.Equal(2, orExpression.Expressions.Count); + + var orFirst = (BooleanQueryExpression)orExpression.Expressions[0]; + var orFirstPaths = (List)orFirst.Left; + Assert.Equal("title", ((FieldFilter)orFirstPaths[0]).Name); + Assert.Equal(QueryOperator.Exists, orFirst.Operator); + + var orSecond = (BooleanQueryExpression)orExpression.Expressions[1]; + var orSecondPaths = (List)orSecond.Left; + Assert.Equal("pie", ((FieldFilter)orSecondPaths[0]).Name); + Assert.Equal(QueryOperator.Exists, orSecond.Operator); + } + + [Fact] + public void FilterWithRoot() + { + JsonNodePath path = new JsonNodePath("[?($.name>=12.1)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + List paths = (List)expressions.Left; + Assert.Equal(2, paths.Count); + Assert.IsType(typeof(RootFilter), paths[0]); + Assert.IsType(typeof(FieldFilter), paths[1]); + } + + [Fact] + public void BadOr1() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name||)]"), "Unexpected character while parsing path query: )"); + } + + [Fact] + public void BaddOr2() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name|)]"), "Unexpected character while parsing path query: |"); + } + + [Fact] + public void BaddOr3() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name|"), "Unexpected character while parsing path query: |"); + } + + [Fact] + public void BaddOr4() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name||"), "Path ended with open query."); + } + + [Fact] + public void NoAtAfterOr() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name||s"), "Unexpected character while parsing path query: s"); + } + + [Fact] + public void NoPathAfterAt() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name||@"), @"Path ended with open query."); + } + + [Fact] + public void NoPathAfterDot() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name||@."), @"Unexpected end while parsing path."); + } + + [Fact] + public void NoPathAfterDot2() + { + ExceptionAssert.Throws(() => new JsonNodePath("[?(@.name||@.)]"), @"Unexpected end while parsing path."); + } + + [Fact] + public void FilterWithFloatExp() + { + JsonNodePath path = new JsonNodePath("[?(@.name>=5.56789e+0)]"); + BooleanQueryExpression expressions = (BooleanQueryExpression)((QueryFilter)path.Filters[0]).Expression; + Assert.Equal(5.56789e+0, ((JsonNode)expressions.Right).GetDouble()); + } + + [Fact] + public void MultiplePropertiesAndIndexers() + { + JsonNodePath path = new JsonNodePath("Blah[0]..Two.Three[1].Four"); + Assert.Equal(6, path.Filters.Count); + Assert.Equal("Blah", ((FieldFilter)path.Filters[0]).Name); + Assert.Equal(0, ((ArrayIndexFilter)path.Filters[1]).Index); + Assert.Equal("Two", ((ScanFilter)path.Filters[2]).Name); + Assert.Equal("Three", ((FieldFilter)path.Filters[3]).Name); + Assert.Equal(1, ((ArrayIndexFilter)path.Filters[4]).Index); + Assert.Equal("Four", ((FieldFilter)path.Filters[5]).Name); + } + + [Fact] + public void BadCharactersInIndexer() + { + ExceptionAssert.Throws(() => { new JsonNodePath("Blah[[0]].Two.Three[1].Four"); }, @"Unexpected character while parsing path indexer: ["); + } + + [Fact] + public void UnclosedIndexer() + { + ExceptionAssert.Throws(() => { new JsonNodePath("Blah[0"); }, @"Path ended with open indexer."); + } + + [Fact] + public void IndexerOnly() + { + JsonNodePath path = new JsonNodePath("[111119990]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(111119990, ((ArrayIndexFilter)path.Filters[0]).Index); + } + + [Fact] + public void IndexerOnlyWithWhitespace() + { + JsonNodePath path = new JsonNodePath("[ 10 ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(10, ((ArrayIndexFilter)path.Filters[0]).Index); + } + + [Fact] + public void MultipleIndexes() + { + JsonNodePath path = new JsonNodePath("[111119990,3]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(2, ((ArrayMultipleIndexFilter)path.Filters[0]).Indexes.Count); + Assert.Equal(111119990, ((ArrayMultipleIndexFilter)path.Filters[0]).Indexes[0]); + Assert.Equal(3, ((ArrayMultipleIndexFilter)path.Filters[0]).Indexes[1]); + } + + [Fact] + public void MultipleIndexesWithWhitespace() + { + JsonNodePath path = new JsonNodePath("[ 111119990 , 3 ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(2, ((ArrayMultipleIndexFilter)path.Filters[0]).Indexes.Count); + Assert.Equal(111119990, ((ArrayMultipleIndexFilter)path.Filters[0]).Indexes[0]); + Assert.Equal(3, ((ArrayMultipleIndexFilter)path.Filters[0]).Indexes[1]); + } + + [Fact] + public void MultipleQuotedIndexes() + { + JsonNodePath path = new JsonNodePath("['111119990','3']"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(2, ((FieldMultipleFilter)path.Filters[0]).Names.Count); + Assert.Equal("111119990", ((FieldMultipleFilter)path.Filters[0]).Names[0]); + Assert.Equal("3", ((FieldMultipleFilter)path.Filters[0]).Names[1]); + } + + [Fact] + public void MultipleQuotedIndexesWithWhitespace() + { + JsonNodePath path = new JsonNodePath("[ '111119990' , '3' ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(2, ((FieldMultipleFilter)path.Filters[0]).Names.Count); + Assert.Equal("111119990", ((FieldMultipleFilter)path.Filters[0]).Names[0]); + Assert.Equal("3", ((FieldMultipleFilter)path.Filters[0]).Names[1]); + } + + [Fact] + public void SlicingIndexAll() + { + JsonNodePath path = new JsonNodePath("[111119990:3:2]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(111119990, ((ArraySliceFilter)path.Filters[0]).Start); + Assert.Equal(3, ((ArraySliceFilter)path.Filters[0]).End); + Assert.Equal(2, ((ArraySliceFilter)path.Filters[0]).Step); + } + + [Fact] + public void SlicingIndex() + { + JsonNodePath path = new JsonNodePath("[111119990:3]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(111119990, ((ArraySliceFilter)path.Filters[0]).Start); + Assert.Equal(3, ((ArraySliceFilter)path.Filters[0]).End); + Assert.Equal(null, ((ArraySliceFilter)path.Filters[0]).Step); + } + + [Fact] + public void SlicingIndexNegative() + { + JsonNodePath path = new JsonNodePath("[-111119990:-3:-2]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(-111119990, ((ArraySliceFilter)path.Filters[0]).Start); + Assert.Equal(-3, ((ArraySliceFilter)path.Filters[0]).End); + Assert.Equal(-2, ((ArraySliceFilter)path.Filters[0]).Step); + } + + [Fact] + public void SlicingIndexEmptyStop() + { + JsonNodePath path = new JsonNodePath("[ -3 : ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(-3, ((ArraySliceFilter)path.Filters[0]).Start); + Assert.Equal(null, ((ArraySliceFilter)path.Filters[0]).End); + Assert.Equal(null, ((ArraySliceFilter)path.Filters[0]).Step); + } + + [Fact] + public void SlicingIndexEmptyStart() + { + JsonNodePath path = new JsonNodePath("[ : 1 : ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(null, ((ArraySliceFilter)path.Filters[0]).Start); + Assert.Equal(1, ((ArraySliceFilter)path.Filters[0]).End); + Assert.Equal(null, ((ArraySliceFilter)path.Filters[0]).Step); + } + + [Fact] + public void SlicingIndexWhitespace() + { + JsonNodePath path = new JsonNodePath("[ -111119990 : -3 : -2 ]"); + Assert.Equal(1, path.Filters.Count); + Assert.Equal(-111119990, ((ArraySliceFilter)path.Filters[0]).Start); + Assert.Equal(-3, ((ArraySliceFilter)path.Filters[0]).End); + Assert.Equal(-2, ((ArraySliceFilter)path.Filters[0]).Step); + } + + [Fact] + public void EmptyIndexer() + { + ExceptionAssert.Throws(() => { new JsonNodePath("[]"); }, "Array index expected."); + } + + [Fact] + public void IndexerCloseInProperty() + { + ExceptionAssert.Throws(() => { new JsonNodePath("]"); }, "Unexpected character while parsing path: ]"); + } + + [Fact] + public void AdjacentIndexers() + { + JsonNodePath path = new JsonNodePath("[1][0][0][" + int.MaxValue + "]"); + Assert.Equal(4, path.Filters.Count); + Assert.Equal(1, ((ArrayIndexFilter)path.Filters[0]).Index); + Assert.Equal(0, ((ArrayIndexFilter)path.Filters[1]).Index); + Assert.Equal(0, ((ArrayIndexFilter)path.Filters[2]).Index); + Assert.Equal(int.MaxValue, ((ArrayIndexFilter)path.Filters[3]).Index); + } + + [Fact] + public void MissingDotAfterIndexer() + { + ExceptionAssert.Throws(() => { new JsonNodePath("[1]Blah"); }, "Unexpected character following indexer: B"); + } + + [Fact] + public void PropertyFollowingEscapedPropertyName() + { + JsonNodePath path = new JsonNodePath("frameworks.dnxcore50.dependencies.['System.Xml.ReaderWriter'].source"); + Assert.Equal(5, path.Filters.Count); + + Assert.Equal("frameworks", ((FieldFilter)path.Filters[0]).Name); + Assert.Equal("dnxcore50", ((FieldFilter)path.Filters[1]).Name); + Assert.Equal("dependencies", ((FieldFilter)path.Filters[2]).Name); + Assert.Equal("System.Xml.ReaderWriter", ((FieldFilter)path.Filters[3]).Name); + Assert.Equal("source", ((FieldFilter)path.Filters[4]).Name); + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath.Test/JsonNodePath/JsonNodePathExecuteTests.cs b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodePathExecuteTests.cs new file mode 100644 index 0000000..f9be30b --- /dev/null +++ b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodePathExecuteTests.cs @@ -0,0 +1,1485 @@ +using JDocument.Test; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Xunit; + +namespace JNodePath.Test; + +public class JsonNodePathExecuteTests +{ + [Fact] + public void GreaterThanIssue1518() + { + string statusJson = @"{""usingmem"": ""214376""}";//214,376 + var jObj = JsonNode.Parse(statusJson); + + var aa = jObj.SelectNode("$..[?(@.usingmem>10)]");//found,10 + Assert.Equal(jObj, aa); + + var bb = jObj.SelectNode("$..[?(@.usingmem>27000)]");//null, 27,000 + + Assert.Equal(jObj, bb); + + var cc = jObj.SelectNode("$..[?(@.usingmem>21437)]");//found, 21,437 + Assert.Equal(jObj, cc); + + var dd = jObj.SelectNode("$..[?(@.usingmem>21438)]");//null,21,438 + Assert.Equal(jObj, dd); + } + + [Fact] + public void GreaterThanWithIntegerParameterAndStringValue() + { + string json = @"{ + ""persons"": [ + { + ""name"" : ""John"", + ""age"": ""26"" + }, + { + ""name"" : ""Jane"", + ""age"": ""2"" + } + ] +}"; + + var models = JsonNode.Parse(json); + + var results = models.SelectNodes("$.persons[?(@.age > 3)]").ToList(); + + Assert.Equal(1, results.Count); + } + + [Fact] + public void GreaterThanWithStringParameterAndIntegerValue() + { + string json = @"{ + ""persons"": [ + { + ""name"" : ""John"", + ""age"": 26 + }, + { + ""name"" : ""Jane"", + ""age"": 2 + } + ] + }"; + + var models = JsonNode.Parse(json); + + var results = models.SelectNodes("$.persons[?(@.age > '3')]").ToList(); + + Assert.Equal(1, results.Count); + } + + [Fact] + public void RecursiveWildcard() + { + string json = @"{ + ""a"": [ + { + ""id"": 1 + } + ], + ""b"": [ + { + ""id"": 2 + }, + { + ""id"": 3, + ""c"": { + ""id"": 4 + } + } + ], + ""d"": [ + { + ""id"": 5 + } + ] + }"; + + var models = JsonNode.Parse(json); + var results = models.SelectNodes("$.b..*.id").ToList(); + + Assert.Equal(3, results.Count); + Assert.Equal(2, results[0].GetValue()); + Assert.Equal(3, results[1].GetValue()); + Assert.Equal(4, results[2].GetValue()); + } + + [Fact] + public void ScanFilter() + { + string json = @"{ + ""elements"": [ + { + ""id"": ""A"", + ""children"": [ + { + ""id"": ""AA"", + ""children"": [ + { + ""id"": ""AAA"" + }, + { + ""id"": ""AAB"" + } + ] + }, + { + ""id"": ""AB"" + } + ] + }, + { + ""id"": ""B"", + ""children"": [] + } + ] + }"; + + var models = JsonNode.Parse(json); + var results = models.SelectNodes("$.elements..[?(@.id=='AAA')]").ToList(); + Assert.Equal(1, results.Count); + + var oModels = (JsonObject)models; + + var tryGetResult = oModels + ["elements"][0] + ["children"][0] + ["children"][0]; + + Assert.Equal(tryGetResult, results[0]); + } + + [Fact] + public void FilterTrue() + { + string json = @"{ + ""elements"": [ + { + ""id"": ""A"", + ""children"": [ + { + ""id"": ""AA"", + ""children"": [ + { + ""id"": ""AAA"" + }, + { + ""id"": ""AAB"" + } + ] + }, + { + ""id"": ""AB"" + } + ] + }, + { + ""id"": ""B"", + ""children"": [] + } + ] + }"; + + var models = JsonNode.Parse(json); + + var results = models.SelectNodes("$.elements[?(true)]").ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal(results[0], models["elements"][0]); + Assert.Equal(results[1], models["elements"][1]); + } + + [Fact] + public void ScanFilterTrue() + { + string json = @"{ + ""elements"": [ + { + ""id"": ""A"", + ""children"": [ + { + ""id"": ""AA"", + ""children"": [ + { + ""id"": ""AAA"" + }, + { + ""id"": ""AAB"" + } + ] + }, + { + ""id"": ""AB"" + } + ] + }, + { + ""id"": ""B"", + ""children"": [] + } + ] + }"; + + var models = JsonNode.Parse(json); + + var results = models.SelectNodes("$.elements..[?(true)]").ToList(); + + Assert.Equal(25, results.Count); + } + + [Fact] + public void ScanFilterDeepTrue() + { + string json = @"{ + ""elements"": [ + { + ""id"": ""A"", + ""children"": [ + { + ""id"": ""AA"", + ""children"": [ + { + ""id"": ""AAA"" + }, + { + ""id"": ""AAB"" + } + ] + }, + { + ""id"": ""AB"" + } + ] + }, + { + ""id"": ""B"", + ""children"": [] + } + ] + }"; + + var models = JsonNode.Parse(json); + var results = models.SelectNodes("$.elements..[?(@.id=='AA')]").ToList(); + + Assert.Single(results); + } + + [Fact] + public void ScanQuoted() + { + string json = @"{ + ""Node1"": { + ""Child1"": { + ""Name"": ""IsMe"", + ""TargetNode"": { + ""Prop1"": ""Val1"", + ""Prop2"": ""Val2"" + } + }, + ""My.Child.Node"": { + ""TargetNode"": { + ""Prop1"": ""Val1"", + ""Prop2"": ""Val2"" + } + } + }, + ""Node2"": { + ""TargetNode"": { + ""Prop1"": ""Val1"", + ""Prop2"": ""Val2"" + } + } + }"; + + var models = JsonNode.Parse(json); + + int result = models.SelectNodes("$..['My.Child.Node']").Count(); + Assert.Equal(1, result); + + result = models.SelectNodes("..['My.Child.Node']").Count(); + Assert.Equal(1, result); + } + + [Fact] + public void ScanMultipleQuoted() + { + string json = @"{ + ""Node1"": { + ""Child1"": { + ""Name"": ""IsMe"", + ""TargetNode"": { + ""Prop1"": ""Val1"", + ""Prop2"": ""Val2"" + } + }, + ""My.Child.Node"": { + ""TargetNode"": { + ""Prop1"": ""Val3"", + ""Prop2"": ""Val4"" + } + } + }, + ""Node2"": { + ""TargetNode"": { + ""Prop1"": ""Val5"", + ""Prop2"": ""Val6"" + } + } + }"; + + var models = JsonNode.Parse(json); + + var results = models.SelectNodes("$..['My.Child.Node','Prop1','Prop2']").ToList(); + Assert.Equal("Val1", results[0].GetValue()); + Assert.Equal("Val2", results[1].GetValue()); + Assert.Equal(JsonValueKind.Object, results[2].GetValueKind()); + Assert.Equal("Val3", results[3].GetValue()); + Assert.Equal("Val4", results[4].GetValue()); + Assert.Equal("Val5", results[5].GetValue()); + Assert.Equal("Val6", results[6].GetValue()); + } + + [Fact] + public void ParseWithEmptyArrayContent() + { + var json = @"{ + ""controls"": [ + { + ""messages"": { + ""addSuggestion"": { + ""en-US"": ""Add"" + } + } + }, + { + ""header"": { + ""controls"": [] + }, + ""controls"": [ + { + ""controls"": [ + { + ""defaultCaption"": { + ""en-US"": ""Sort by"" + }, + ""sortOptions"": [ + { + ""label"": { + ""en-US"": ""Name"" + } + } + ] + } + ] + } + ] + } + ] + }"; + var node = JsonNode.Parse(json); + var elements = node.SelectNodes("$..en-US").ToList(); + + Assert.Equal(3, elements.Count); + Assert.Equal("Add", elements[0].GetValue()); + Assert.Equal("Sort by", elements[1].GetValue()); + Assert.Equal("Name", elements[2].GetValue()); + } + + [Fact] + public void SelectElementAfterEmptyContainer() + { + string json = @"{ + ""cont"": [], + ""test"": ""no one will find me"" + }"; + + var node = JsonNode.Parse(json); + + var results = node.SelectNodes("$..test").ToList(); + + Assert.Equal(1, results.Count); + Assert.Equal("no one will find me", results[0].GetValue()); + } + + [Fact] + public void EvaluatePropertyWithRequired() + { + string json = "{\"bookId\":\"1000\"}"; + var node = JsonNode.Parse(json); + + string bookId = (string)node.SelectNode("bookId", true).GetValue(); + + Assert.Equal("1000", bookId); + } + + [Fact] + public void EvaluateEmptyPropertyIndexer() + { + string json = @"{ + """": 1 + }"; + + var node = JsonNode.Parse(json); + + var t = node.SelectNode("['']"); + Assert.Equal(1, t.GetValue()); + } + + [Fact] + public void EvaluateEmptyString() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + var t = node.SelectNode(""); + Assert.Equal(node, t); + + t = node.SelectNode("['']"); + Assert.Equal(null, t); + } + + [Fact] + public void EvaluateEmptyStringWithMatchingEmptyProperty() + { + string json = @"{ + "" "": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("[' ']"); + Assert.Equal(1, t.GetValue()); + } + + [Fact] + public void EvaluateWhitespaceString() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode(" "); + Assert.Equal(node, t); + } + + [Fact] + public void EvaluateDollarString() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("$"); + Assert.Equal(node, t); + } + + [Fact] + public void EvaluateDollarTypeString() + { + string json = @"{ + ""$values"": [1,2,3] + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("$values[1]"); + Assert.Equal(2, t.GetValue()); + } + + [Fact] + public void EvaluateSingleProperty() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("Blah"); + Assert.NotNull(t); + Assert.Equal(JsonValueKind.Number, t.GetValueKind()); + Assert.Equal(1, t.GetValue()); + } + + [Fact] + public void EvaluateWildcardProperty() + { + string json = @"{ + ""Blah"": 1, + ""Blah2"": 2 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNodes("$.*").ToList(); + Assert.NotNull(t); + Assert.Equal(2, t.Count); + Assert.Equal(1, t[0].GetValue()); + Assert.Equal(2, t[1].GetValue()); + } + + [Fact] + public void QuoteName() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("['Blah']"); + Assert.NotNull(t); + Assert.Equal(JsonValueKind.Number, t.GetValueKind()); + Assert.Equal(1, t.GetValue()); + } + + [Fact] + public void EvaluateMissingProperty() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("Missing[1]"); + Assert.Null(t); + } + + [Fact] + public void EvaluateIndexerOnObject() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("[1]"); + Assert.Null(t); + } + + [Fact] + public void EvaluateIndexerOnObjectWithError() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => + { + node.SelectNode("[1]", true); + }, @"Index 1 not valid on JsonObject."); + } + + [Fact] + public void EvaluateWildcardIndexOnObjectWithError() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => + { + node.SelectNode("[*]", true); + }, @"Index * not valid on JsonObject."); + } + + [Fact] + public void EvaluateSliceOnObjectWithError() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => + { + node.SelectNode("[:]", true); + }, @"Array slice is not valid on JsonObject."); + } + + [Fact] + public void EvaluatePropertyOnArray() + { + string json = @"[1,2,3,4,5]"; + var node = JsonNode.Parse(json); + + var t = node.SelectNode("BlahBlah"); + Assert.Null(t); + } + + [Fact] + public void EvaluateMultipleResultsError() + { + string json = @"[1,2,3,4,5]"; + var node = JsonNode.Parse(json); + ExceptionAssert.Throws(() => { node.SelectNode("[0, 1]"); }, @"Path returned multiple tokens."); + } + + [Fact] + public void EvaluatePropertyOnArrayWithError() + { + string json = @"[1,2,3,4,5]"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => + { + node.SelectNode("BlahBlah", true); + }, @"Property 'BlahBlah' not valid on JsonArray."); + } + + [Fact] + public void EvaluateNoResultsWithMultipleArrayIndexes() + { + string json = @"[1,2,3,4,5]"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => { node.SelectNode("[9,10]", true); }, @"Index 9 outside the bounds of JArray."); + } + + [Fact] + public void EvaluateMissingPropertyWithError() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => { node.SelectNode("Missing", true); }, "Property 'Missing' does not exist on JsonElement."); + } + + [Fact] + public void EvaluatePropertyWithoutError() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + var v = node.SelectNode("Blah", true).GetValue(); + Assert.Equal(1, v); + } + + [Fact] + public void EvaluateMissingPropertyIndexWithError() + { + string json = @"{ + ""Blah"": 1 + }"; + var node = JsonNode.Parse(json); + + ExceptionAssert.Throws(() => + { + node.SelectNode("['Missing','Missing2']", true); + }, "Property 'Missing' does not exist on JsonObject."); + } + + [Fact] + public void EvaluateMultiPropertyIndexOnArrayWithError() + { + var a = JsonNode.Parse("[1,2,3,4,5]"); + + ExceptionAssert.Throws(() => + { + a.SelectNode("['Missing','Missing2']", true); + }, "Properties 'Missing', 'Missing2' not valid on JsonArray."); + } + + [Fact] + public void EvaluateArraySliceWithError() + { + var a = JsonNode.Parse("[1,2,3,4,5]"); + + ExceptionAssert.Throws(() => { a.SelectNode("[99:]", true); }, "Array slice of 99 to * returned no results."); + + ExceptionAssert.Throws(() => { a.SelectNode("[1:-19]", true); }, "Array slice of 1 to -19 returned no results."); + + ExceptionAssert.Throws(() => { a.SelectNode("[:-19]", true); }, "Array slice of * to -19 returned no results."); + + a = JsonNode.Parse("[]"); + + ExceptionAssert.Throws(() => { a.SelectNode("[:]", true); }, "Array slice of * to * returned no results."); + } + + [Fact] + public void EvaluateOutOfBoundsIndxer() + { + var a = JsonNode.Parse("[1,2,3,4,5]"); + + var t = a.SelectNode("[1000].Ha"); + Assert.Null(t); + } + + [Fact] + public void EvaluateArrayOutOfBoundsIndxerWithError() + { + var a = JsonNode.Parse("[1,2,3,4,5]"); + + ExceptionAssert.Throws(() => { a.SelectNode("[1000].Ha", true); }, "Index 1000 outside the bounds of JArray."); + } + + [Fact] + public void EvaluateArray() + { + var a = JsonNode.Parse("[1,2,3,4]"); + + var t = a.SelectNode("[1]"); + Assert.NotNull(t); + Assert.Equal(JsonValueKind.Number, t.GetValueKind()); + Assert.Equal(2, t.GetValue()); + } + + [Fact] + public void EvaluateArraySlice() + { + var a = JsonNode.Parse(@"[1, 2, 3, 4, 5, 6, 7, 8, 9]"); + List t = null; + + t = a.SelectNodes("[-3:]").ToList(); + Assert.Equal(3, t.Count); + Assert.Equal(7, t[0].GetValue()); + Assert.Equal(8, t[1].GetValue()); + Assert.Equal(9, t[2].GetValue()); + + t = a.SelectNodes("[-1:-2:-1]").ToList(); + Assert.Equal(1, t.Count); + Assert.Equal(9, t[0].GetValue()); + + t = a.SelectNodes("[-2:-1]").ToList(); + Assert.Equal(1, t.Count); + Assert.Equal(8, t[0].GetValue()); + + t = a.SelectNodes("[1:1]").ToList(); + Assert.Equal(0, t.Count); + + t = a.SelectNodes("[1:2]").ToList(); + Assert.Equal(1, t.Count); + Assert.Equal(2, t[0].GetValue()); + + t = a.SelectNodes("[::-1]").ToList(); + Assert.Equal(9, t.Count); + Assert.Equal(9, t[0].GetValue()); + Assert.Equal(8, t[1].GetValue()); + Assert.Equal(7, t[2].GetValue()); + Assert.Equal(6, t[3].GetValue()); + Assert.Equal(5, t[4].GetValue()); + Assert.Equal(4, t[5].GetValue()); + Assert.Equal(3, t[6].GetValue()); + Assert.Equal(2, t[7].GetValue()); + Assert.Equal(1, t[8].GetValue()); + + t = a.SelectNodes("[::-2]").ToList(); + Assert.Equal(5, t.Count); + Assert.Equal(9, t[0].GetValue()); + Assert.Equal(7, t[1].GetValue()); + Assert.Equal(5, t[2].GetValue()); + Assert.Equal(3, t[3].GetValue()); + Assert.Equal(1, t[4].GetValue()); + } + + [Fact] + public void EvaluateWildcardArray() + { + var a = JsonNode.Parse(@"[1, 2, 3, 4]"); + + List t = a.SelectNodes("[*]").ToList(); + Assert.NotNull(t); + Assert.Equal(4, t.Count); + Assert.Equal(1, t[0].GetValue()); + Assert.Equal(2, t[1].GetValue()); + Assert.Equal(3, t[2].GetValue()); + Assert.Equal(4, t[3].GetValue()); + } + + [Fact] + public void EvaluateArrayMultipleIndexes() + { + var a = JsonNode.Parse(@"[1, 2, 3, 4]"); + + IEnumerable t = a.SelectNodes("[1,2,0]").ToList(); + Assert.NotNull(t); + Assert.Equal(3, t.Count()); + Assert.Equal(2, t.ElementAt(0).GetValue()); + Assert.Equal(3, t.ElementAt(1).GetValue()); + Assert.Equal(1, t.ElementAt(2).GetValue()); + } + + [Fact] + public void EvaluateScan() + { + JsonNode o1 = JsonNode.Parse(@"{ ""Name"": 1 }"); + JsonNode o2 = JsonNode.Parse(@"{ ""Name"": 2 }"); + var a = JsonNode.Parse(@"[{ ""Name"": 1 }, { ""Name"": 2 }]"); + + var t = a.SelectNodes("$..Name").ToList(); + Assert.NotNull(t); + Assert.Equal(2, t.Count); + Assert.Equal(1, t[0].GetValue()); + Assert.Equal(2, t[1].GetValue()); + } + + [Fact] + public void EvaluateWildcardScan() + { + JsonNode o1 = JsonNode.Parse(@"{ ""Name"": 1 }"); + JsonNode o2 = JsonNode.Parse(@"{ ""Name"": 2 }"); + var a = JsonNode.Parse(@"[{ ""Name"": 1 }, { ""Name"": 2 }]"); + + var t = a.SelectNodes("$..*").ToList(); + Assert.NotNull(t); + Assert.Equal(5, t.Count); + + Assert.True(a.DeepEquals(t[0])); + + Assert.True(o1.DeepEquals(t[1]!)); + + Assert.Equal(1, t[2].GetValue()); + Assert.True(o2.DeepEquals(t[3])); + Assert.Equal(2, t[4].GetValue()); + } + + [Fact] + public void EvaluateScanNestResults() + { + JsonNode o1 = JsonNode.Parse(@"{ ""Name"": 1 }"); + JsonNode o2 = JsonNode.Parse(@"{ ""Name"": 2 }"); + JsonNode o3 = JsonNode.Parse(@"{ ""Name"": { ""Name"": [ 3 ] } }"); + var a = JsonNode.Parse(@"[ + { ""Name"": 1 }, + { ""Name"": 2 }, + { ""Name"": { ""Name"": [3] } } + ]"); + + var t = a.SelectNodes("$..Name").ToList(); + Assert.NotNull(t); + Assert.Equal(4, t.Count); + Assert.Equal(1, t[0].GetValue()); + Assert.Equal(2, t[1].GetValue()); + Assert.True(JsonNode.Parse(@"{ ""Name"": [3] }").DeepEquals(t[2])); + Assert.True(JsonNode.Parse("[3]").DeepEquals(t[3])); + } + + [Fact] + public void EvaluateWildcardScanNestResults() + { + JsonNode o1 = JsonNode.Parse(@"{ ""Name"": 1 }"); + JsonNode o2 = JsonNode.Parse(@"{ ""Name"": 2 }"); + JsonNode o3 = JsonNode.Parse(@"{ ""Name"": { ""Name"": [3] } }"); + var a = JsonNode.Parse(@"[ + { ""Name"": 1 }, + { ""Name"": 2 }, + { ""Name"": { ""Name"": [3] } } + ]"); + + var t = a.SelectNodes("$..*").ToList(); + Assert.NotNull(t); + Assert.Equal(9, t.Count); + + Assert.True(a.DeepEquals(t[0])); + Assert.True(o1.DeepEquals(t[1])); + Assert.Equal(1, t[2].GetValue()); + Assert.True(o2.DeepEquals(t[3])); + Assert.Equal(2, t[4].GetValue()); + Assert.True(o3.DeepEquals(t[5])); + Assert.True(JsonNode.Parse(@"{ ""Name"": [3] }").DeepEquals(t[6])); + Assert.True(JsonNode.Parse("[3]").DeepEquals(t[7])); + Assert.Equal(3, t[8].GetValue()); + Assert.True(JsonNode.Parse("[3]").DeepEquals(t[7])); + } + + [Fact] + public void EvaluateSinglePropertyReturningArray() + { + var o = JsonNode.Parse(@"{ ""Blah"": [ 1, 2, 3 ] }"); + + var t = o.SelectNode("Blah"); + Assert.NotNull(t); + Assert.Equal(JsonValueKind.Array, t?.GetValueKind()); + + t = o.SelectNode("Blah[2]"); + Assert.Equal(JsonValueKind.Number, t?.GetValueKind()); + Assert.Equal(3, t?.GetValue()); + } + + [Fact] + public void EvaluateLastSingleCharacterProperty() + { + JsonNode o2 = JsonNode.Parse(@"{""People"":[{""N"":""Jeff""}]}"); + var a2 = o2.SelectNode("People[0].N").GetValue(); + + Assert.Equal("Jeff", a2); + } + + [Fact] + public void ExistsQuery() + { + var a = JsonNode.Parse(@"[ + { ""hi"": ""ho"" }, + { ""hi2"": ""ha"" } + ]"); + + var t = a.SelectNodes("[ ?( @.hi ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(1, t.Count); + Assert.True(JsonNode.Parse(@"{ ""hi"": ""ho"" }").DeepEquals(t[0])); + } + + [Fact] + public void EqualsQuery() + { + var a = JsonNode.Parse(@"[ + { ""hi"": ""ho"" }, + { ""hi"": ""ha"" } + ]"); + + var t = a.SelectNodes("[ ?( @.['hi'] == 'ha' ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(1, t.Count); + Assert.True(JsonNode.Parse(@"{ ""hi"": ""ha"" }").DeepEquals(t[0])); + } + + [Fact] + public void NotEqualsQuery() + { + var a = JsonNode.Parse(@"[ + { ""hi"": ""ho"" }, + { ""hi"": ""ha"" } + ]"); + + var t = a.SelectNodes("[ ?( @..hi <> 'ha' ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(1, t.Count); + Assert.True(JsonNode.Parse(@"{ ""hi"": ""ho"" }").DeepEquals(t[0])); + } + + [Fact] + public void NoPathQuery() + { + var a = JsonNode.Parse("[1, 2, 3]"); + + var t = a.SelectNodes("[ ?( @ > 1 ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(2, t.Count); + Assert.Equal(2, t[0].GetValue()); + Assert.Equal(3, t[1].GetValue()); + } + + [Fact] + public void MultipleQueries() + { + var a = JsonNode.Parse("[1, 2, 3, 4, 5, 6, 7, 8, 9]"); + + // json path does item based evaluation - http://www.sitepen.com/blog/2008/03/17/jsonpath-support/ + // first query resolves array to ints + // int has no children to query + var t = a.SelectNodes("[?(@ <> 1)][?(@ <> 4)][?(@ < 7)]").ToList(); + Assert.NotNull(t); + Assert.Equal(0, t.Count); + } + + [Fact] + public void GreaterQuery() + { + var a = JsonNode.Parse(@" + [ + { ""hi"": 1 }, + { ""hi"": 2 }, + { ""hi"": 3 } + ]"); + + var t = a.SelectNodes("[ ?( @.hi > 1 ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(2, t.Count); + Assert.True(JsonNode.Parse(@"{ ""hi"": 2 }").DeepEquals(t[0])); + Assert.True(JsonNode.Parse(@"{ ""hi"": 3 }").DeepEquals(t[1])); + } + + [Fact] + public void LesserQuery_ValueFirst() + { + var a = JsonNode.Parse(@" + [ + { ""hi"": 1 }, + { ""hi"": 2 }, + { ""hi"": 3 } + ]"); + + var t = a.SelectNodes("[ ?( 1 < @.hi ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(2, t.Count); + Assert.True(JsonNode.Parse(@"{ ""hi"": 2 }").DeepEquals(t[0])); + Assert.True(JsonNode.Parse(@"{ ""hi"": 3 }").DeepEquals(t[1])); + } + + [Fact] + public void GreaterOrEqualQuery() + { + var a = JsonNode.Parse(@" + [ + { ""hi"": 1 }, + { ""hi"": 2 }, + { ""hi"": 2.0 }, + { ""hi"": 3 } + ]"); + + var t = a.SelectNodes("[ ?( @.hi >= 1 ) ]").ToList(); + Assert.NotNull(t); + Assert.Equal(4, t.Count); + Assert.True(JsonNode.Parse(@"{ ""hi"": 1 }").DeepEquals(t[0])); + Assert.True(JsonNode.Parse(@"{ ""hi"": 2 }").DeepEquals(t[1])); + Assert.True(JsonNode.Parse(@"{ ""hi"": 2.0 }").DeepEquals(t[2])); + Assert.True(JsonNode.Parse(@"{ ""hi"": 3 }").DeepEquals(t[3])); + } + + [Fact] + public void NestedQuery() + { + var a = JsonNode.Parse(@" + [ + { + ""name"": ""Bad Boys"", + ""cast"": [ { ""name"": ""Will Smith"" } ] + }, + { + ""name"": ""Independence Day"", + ""cast"": [ { ""name"": ""Will Smith"" } ] + }, + { + ""name"": ""The Rock"", + ""cast"": [ { ""name"": ""Nick Cage"" } ] + } + ]"); + + var t = a.SelectNodes("[?(@.cast[?(@.name=='Will Smith')])].name").ToList(); + Assert.NotNull(t); + Assert.Equal(2, t.Count); + Assert.Equal("Bad Boys", t[0].GetValue()); + Assert.Equal("Independence Day", t[1].GetValue()); + } + + [Fact] + public void MultiplePaths() + { + var a = JsonNode.Parse(@"[ + { + ""price"": 199, + ""max_price"": 200 + }, + { + ""price"": 200, + ""max_price"": 200 + }, + { + ""price"": 201, + ""max_price"": 200 + } + ]"); + + var results = a.SelectNodes("[?(@.price > @.max_price)]").ToList(); + Assert.Equal(1, results.Count); + Assert.True(a[2].DeepEquals(results[0])); + } + + [Fact] + public void Exists_True() + { + var a = JsonNode.Parse(@"[ + { + ""price"": 199, + ""max_price"": 200 + }, + { + ""price"": 200, + ""max_price"": 200 + }, + { + ""price"": 201, + ""max_price"": 200 + } + ]"); + + var results = a.SelectNodes("[?(true)]").ToList(); + Assert.Equal(3, results.Count); + Assert.True(a[0].DeepEquals(results[0])); + Assert.True(a[1].DeepEquals(results[1])); + Assert.True(a[2].DeepEquals(results[2])); + } + + [Fact] + public void Exists_Null() + { + var a = JsonNode.Parse(@"[ + { + ""price"": 199, + ""max_price"": 200 + }, + { + ""price"": 200, + ""max_price"": 200 + }, + { + ""price"": 201, + ""max_price"": 200 + } + ]"); + + var results = a.SelectNodes("[?(true)]").ToList(); + Assert.Equal(3, results.Count); + Assert.True(a[0].DeepEquals(results[0])); + Assert.True(a[1].DeepEquals(results[1])); + Assert.True(a[2].DeepEquals(results[2])); + } + + [Fact] + public void WildcardWithProperty() + { + var o = JsonNode.Parse(@"{ + ""station"": 92000041000001, + ""containers"": [ + { + ""id"": 1, + ""text"": ""Sort system"", + ""containers"": [ + { + ""id"": ""2"", + ""text"": ""Yard 11"" + }, + { + ""id"": ""92000020100006"", + ""text"": ""Sort yard 12"" + }, + { + ""id"": ""92000020100005"", + ""text"": ""Yard 13"" + } + ] + }, + { + ""id"": ""92000020100011"", + ""text"": ""TSP-1"" + }, + { + ""id"":""92000020100007"", + ""text"": ""Passenger 15"" + } + ] + }"); + + var tokens = o.SelectNodes("$..*[?(@.text)]").ToList(); + int i = 0; + Assert.Equal("Sort system", tokens[i++]["text"].GetString()); + Assert.Equal("TSP-1", tokens[i++]["text"].GetString()); + Assert.Equal("Passenger 15", tokens[i++]["text"].GetString()); + Assert.Equal("Yard 11", tokens[i++]["text"].GetString()); + Assert.Equal("Sort yard 12", tokens[i++]["text"].GetString()); + Assert.Equal("Yard 13", tokens[i++]["text"].GetString()); + Assert.Equal(6, tokens.Count); + } + + [Fact] + public void QueryAgainstNonStringValues() + { + IList values = new List + { + "ff2dc672-6e15-4aa2-afb0-18f4f69596ad", + new Guid("ff2dc672-6e15-4aa2-afb0-18f4f69596ad"), + "http://localhost", + new Uri("http://localhost"), + "2000-12-05T05:07:59Z", + new DateTime(2000, 12, 5, 5, 7, 59, DateTimeKind.Utc), + #if !NET20 + "2000-12-05T05:07:59-10:00", + new DateTimeOffset(2000, 12, 5, 5, 7, 59, -TimeSpan.FromHours(10)), + #endif + "SGVsbG8gd29ybGQ=", + Encoding.UTF8.GetBytes("Hello world"), + "365.23:59:59", + new TimeSpan(365, 23, 59, 59) + }; + var json = @"{ + ""prop"": [ " + + String.Join(", ", values.Select(v => $"{{\"childProp\": {JsonSerializer.Serialize(v)}}}")) + + @"] + }"; + var o = JsonNode.Parse(json); + + var t = o.SelectNodes("$.prop[?(@.childProp =='ff2dc672-6e15-4aa2-afb0-18f4f69596ad')]").ToList(); + Assert.Equal(2, t.Count); + + t = o.SelectNodes("$.prop[?(@.childProp =='http://localhost')]").ToList(); + Assert.Equal(2, t.Count); + + t = o.SelectNodes("$.prop[?(@.childProp =='2000-12-05T05:07:59Z')]").ToList(); + Assert.Equal(2, t.Count); + +#if !NET20 + t = o.SelectNodes("$.prop[?(@.childProp =='2000-12-05T05:07:59-10:00')]").ToList(); + Assert.Equal(2, t.Count); +#endif + + t = o.SelectNodes("$.prop[?(@.childProp =='SGVsbG8gd29ybGQ=')]").ToList(); + Assert.Equal(2, t.Count); + + t = o.SelectNodes("$.prop[?(@.childProp =='365.23:59:59')]").ToList(); + + /* + Dotnet 6.0 JsonNode Parse the TimeSpan as string '365.23:59:59' + */ +#if NET6_0_OR_GREATER + + Assert.Equal(2, t.Count); +#else + Assert.Equal(1, t.Count); +#endif + } + + [Fact] + public void Example() + { + var o = JsonNode.Parse(@"{ + ""Stores"": [ + ""Lambton Quay"", + ""Willis Street"" + ], + ""Manufacturers"": [ + { + ""Name"": ""Acme Co"", + ""Products"": [ + { + ""Name"": ""Anvil"", + ""Price"": 50 + } + ] + }, + { + ""Name"": ""Contoso"", + ""Products"": [ + { + ""Name"": ""Elbow Grease"", + ""Price"": 99.95 + }, + { + ""Name"": ""Headlight Fluid"", + ""Price"": 4 + } + ] + } + ] + }"); + + string? name = o.SelectNode("Manufacturers[0].Name").GetValue(); + // Acme Co + + decimal? productPrice = o.SelectNode("Manufacturers[0].Products[0].Price").GetValue(); + // 50 + + string? productName = o.SelectNode("Manufacturers[1].Products[0].Name").GetValue(); + // Elbow Grease + + Assert.Equal("Acme Co", name); + Assert.Equal(50m, productPrice); + Assert.Equal("Elbow Grease", productName); + + IList storeNames = ((JsonArray)o.SelectNode("Stores"))!.Select(s => s.GetString()).ToList(); + // Lambton Quay + // Willis Street + + IList firstProductNames = ((JsonArray)o["Manufacturers"])!.Select( + m => m.SelectNode("Products[1].Name")?.GetString()).ToList(); + // null + // Headlight Fluid + + decimal totalPrice = ((JsonArray)o["Manufacturers"])!.Aggregate( + 0M, (sum, m) => sum + m.SelectNode("Products[0].Price").GetValue()); + // 149.95 + + Assert.Equal(2, storeNames.Count); + Assert.Equal("Lambton Quay", storeNames[0]); + Assert.Equal("Willis Street", storeNames[1]); + Assert.Equal(2, firstProductNames.Count); + Assert.Equal(null, firstProductNames[0]); + Assert.Equal("Headlight Fluid", firstProductNames[1]); + Assert.Equal(149.95m, totalPrice); + } + + [Fact] + public void NotEqualsAndNonPrimativeValues() + { + string json = @"[ + { + ""name"": ""string"", + ""value"": ""aString"" + }, + { + ""name"": ""number"", + ""value"": 123 + }, + { + ""name"": ""array"", + ""value"": [ + 1, + 2, + 3, + 4 + ] + }, + { + ""name"": ""object"", + ""value"": { + ""1"": 1 + } + } + ]"; + + var a = JsonNode.Parse(json); + + var result = a.SelectNodes("$.[?(@.value!=1)]").ToList(); + Assert.Equal(4, result.Count); + + result = a.SelectNodes("$.[?(@.value!='2000-12-05T05:07:59-10:00')]").ToList(); + Assert.Equal(4, result.Count); + + result = a.SelectNodes("$.[?(@.value!=null)]").ToList(); + Assert.Equal(4, result.Count); + + result = a.SelectNodes("$.[?(@.value!=123)]").ToList(); + Assert.Equal(3, result.Count); + + result = a.SelectNodes("$.[?(@.value)]").ToList(); + Assert.Equal(4, result.Count); + } + + [Fact] + public void RootInFilter() + { + string json = @"[ + { + ""store"" : { + ""book"" : [ + { + ""category"" : ""reference"", + ""author"" : ""Nigel Rees"", + ""title"" : ""Sayings of the Century"", + ""price"" : 8.95 + }, + { + ""category"" : ""fiction"", + ""author"" : ""Evelyn Waugh"", + ""title"" : ""Sword of Honour"", + ""price"" : 12.99 + }, + { + ""category"" : ""fiction"", + ""author"" : ""Herman Melville"", + ""title"" : ""Moby Dick"", + ""isbn"" : ""0-553-21311-3"", + ""price"" : 8.99 + }, + { + ""category"" : ""fiction"", + ""author"" : ""J. R. R. Tolkien"", + ""title"" : ""The Lord of the Rings"", + ""isbn"" : ""0-395-19395-8"", + ""price"" : 22.99 + } + ], + ""bicycle"" : { + ""color"" : ""red"", + ""price"" : 19.95 + } + }, + ""expensive"" : 10 + } + ]"; + + var a = JsonNode.Parse(json); + + var result = a.SelectNodes("$.[?($.[0].store.bicycle.price < 20)]").ToList(); + Assert.Equal(1, result.Count); + + result = a.SelectNodes("$.[?($.[0].store.bicycle.price < 10)]").ToList(); + Assert.Equal(0, result.Count); + } + + [Fact] + public void RootInFilterWithRootObject() + { + string json = @"{ + ""store"" : { + ""book"" : [ + { + ""category"" : ""reference"", + ""author"" : ""Nigel Rees"", + ""title"" : ""Sayings of the Century"", + ""price"" : 8.95 + }, + { + ""category"" : ""fiction"", + ""author"" : ""Evelyn Waugh"", + ""title"" : ""Sword of Honour"", + ""price"" : 12.99 + }, + { + ""category"" : ""fiction"", + ""author"" : ""Herman Melville"", + ""title"" : ""Moby Dick"", + ""isbn"" : ""0-553-21311-3"", + ""price"" : 8.99 + }, + { + ""category"" : ""fiction"", + ""author"" : ""J. R. R. Tolkien"", + ""title"" : ""The Lord of the Rings"", + ""isbn"" : ""0-395-19395-8"", + ""price"" : 22.99 + } + ], + ""bicycle"" : [ + { + ""color"" : ""red"", + ""price"" : 19.95 + } + ] + }, + ""expensive"" : 10 + }"; + + JsonNode a = JsonNode.Parse(json); + + var result = a.SelectNodes("$..book[?(@.price <= $['expensive'])]").ToList(); + Assert.Equal(2, result.Count); + + result = a.SelectNodes("$.store..[?(@.price > $.expensive)]").ToList(); + Assert.Equal(3, result.Count); + } + + public const string IsoDateFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK"; + + [Fact] + public void RootInFilterWithInitializers() + { + var minDate = DateTime.MinValue.ToString(IsoDateFormat); + + JsonNode rootObject = JsonNode.Parse(@" + { + ""referenceDate"": """ + minDate + @""", + ""dateObjectsArray"": [ + { ""date"": """ + minDate + @""" }, + { ""date"": """ + DateTime.MaxValue.ToString(IsoDateFormat) + @""" }, + { ""date"": """ + DateTime.Now.ToString(IsoDateFormat) + @""" }, + { ""date"": """ + minDate + @""" } + ] + }"); + + var result = rootObject.SelectNodes("$.dateObjectsArray[?(@.date == $.referenceDate)]").ToList(); + Assert.Equal(2, result.Count); + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeQueryExpressionTests.cs b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeQueryExpressionTests.cs new file mode 100644 index 0000000..bb47732 --- /dev/null +++ b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeQueryExpressionTests.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Xunit; + +namespace JNodePath.Test; + +public class JsonNodeQueryExpressionTests +{ + [Fact] + public void AndExpressionTest() + { + CompositeExpression compositeExpression = new CompositeExpression(QueryOperator.And) + { + Expressions = new List + { + new BooleanQueryExpression(QueryOperator.Exists,new List + { + new FieldFilter("FirstName") + },null), + new BooleanQueryExpression(QueryOperator.Exists,new List + { + new FieldFilter("LastName") + },null) + } + }; + var o1 = JsonNode.Parse("{\"Title\":\"Title!\",\"FirstName\":\"FirstName!\",\"LastName\":\"LastName!\"}"); + + Assert.True(compositeExpression.IsMatch(o1, o1)); + + var o2 = JsonNode.Parse("{\"Title\":\"Title!\",\"FirstName\":\"FirstName!\"}"); + + Assert.False(compositeExpression.IsMatch(o2, o2)); + + var o3 = JsonNode.Parse("{\"Title\":\"Title!\"}"); + + Assert.False(compositeExpression.IsMatch(o3, o3)); + } + + [Fact] + public void OrExpressionTest() + { + CompositeExpression compositeExpression = new CompositeExpression(QueryOperator.Or) + { + Expressions = new List + { + new BooleanQueryExpression(QueryOperator.Exists,new List + { + new FieldFilter("FirstName") + },null), + new BooleanQueryExpression(QueryOperator.Exists,new List + { + new FieldFilter("LastName") + },null) + } + }; + + var o1 = JsonNode.Parse("{\"Title\":\"Title!\",\"FirstName\":\"FirstName!\",\"LastName\":\"LastName!\"}"); + + Assert.True(compositeExpression.IsMatch(o1, o1)); + + var o2 = JsonNode.Parse("{\"Title\":\"Title!\",\"FirstName\":\"FirstName!\"}"); + + Assert.True(compositeExpression.IsMatch(o2, o2)); + + var o3 = JsonNode.Parse("{\"Title\":\"Title!\"}"); + + Assert.False(compositeExpression.IsMatch(o3, o3)); + } + + [Fact] + public void BooleanExpressionTest_RegexEqualsOperator() + { + BooleanQueryExpression e1 = new BooleanQueryExpression(QueryOperator.RegexEquals, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("\"/foo.*d/\"")); + + var oNull = JsonNode.Parse("null"); + + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[\"food\"]"))); + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[\"fooood and drink\"]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"FOOD\"]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"foo\",\"foog\",\"good\"]"))); + + BooleanQueryExpression e2 = new BooleanQueryExpression(QueryOperator.RegexEquals, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("\"/Foo.*d/i\"")); + + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[\"food\"]"))); + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[\"fooood and drink\"]"))); + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[\"FOOD\"]"))); + Assert.False(e2.IsMatch(oNull, JsonNode.Parse("[\"foo\",\"foog\",\"good\"]"))); + } + + [Fact] + public void BooleanExpressionTest_RegexEqualsOperator_CornerCase() + { + BooleanQueryExpression e1 = new BooleanQueryExpression(QueryOperator.RegexEquals, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("\"/// comment/\"")); + + var oNull = JsonNode.Parse("null"); + + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[\"// comment\"]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"//comment\",\"/ comment\"]"))); + + BooleanQueryExpression e2 = new BooleanQueryExpression(QueryOperator.RegexEquals, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("\"/.*/i\"")); + + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[\"Test\",\"\"]"))); + Assert.False(e2.IsMatch(oNull, JsonNode.Parse("[\"Test\"]"))); + } + + [Fact] + public void BooleanExpressionTest() + { + BooleanQueryExpression e1 = new BooleanQueryExpression(QueryOperator.LessThan, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("3")); + + var oNull = JsonNode.Parse("null"); + + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[1,2,3,4,5]"))); + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[2,3,4,5]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[3,4,5]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[4,5]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"11\",5]"))); + + BooleanQueryExpression e2 = new BooleanQueryExpression(QueryOperator.LessThanOrEquals, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("3")); + + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[1,2,3,4,5]"))); + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[2,3,4,5]"))); + Assert.True(e2.IsMatch(oNull, JsonNode.Parse("[3,4,5]"))); + Assert.False(e2.IsMatch(oNull, JsonNode.Parse("[4,5]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"11\",5]"))); + } + + [Fact] + public void BooleanExpressionTest_GreaterThanOperator() + { + BooleanQueryExpression e1 = new BooleanQueryExpression(QueryOperator.GreaterThan, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("3")); + + var oNull = JsonNode.Parse("null"); + + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[\"2\",\"26\"]"))); + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[2,26]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[2,3]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"2\",\"3\"]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[null,false,true,[],\"3\"]"))); + } + + [Fact] + public void BooleanExpressionTest_GreaterThanOrEqualsOperator() + { + BooleanQueryExpression e1 = new BooleanQueryExpression(QueryOperator.GreaterThanOrEquals, new List + { + new ArrayIndexFilter() + }, JsonNode.Parse("3")); + + var oNull = JsonNode.Parse("null"); + + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[\"2\",\"26\"]"))); + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[2,26]"))); + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[2,3]"))); + Assert.True(e1.IsMatch(oNull, JsonNode.Parse("[\"2\",\"3\"]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[2,1]"))); + Assert.False(e1.IsMatch(oNull, JsonNode.Parse("[\"2\",\"1\"]"))); + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeRefTests.cs b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeRefTests.cs new file mode 100644 index 0000000..40d8e8a --- /dev/null +++ b/src/JsonDocumentPath.Test/JsonNodePath/JsonNodeRefTests.cs @@ -0,0 +1,40 @@ +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Xunit; + +namespace JNodePath.Test; + +public class JsonNodeRefTests +{ + [Fact] + public void FindSelectNodeParent() + { + var jNode = JsonNode.Parse(""" + { + "a":"", + "b": + { + "c":1, + "d": + { + "e":2, + "f": + [ + { + "fa":"result" + } + ] + } + } + } + """); + + var result = jNode.SelectNodes("$.b.d.f.[*].fa"); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(result.First().Parent.Parent.Parent.Parent.Parent, jNode); + Assert.Equal(jNode.ChildrenNodes(), jNode.SelectNodes("$.*")); + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Extensions.cs b/src/JsonDocumentPath/Extensions.cs index 82645e1..d7e799c 100644 --- a/src/JsonDocumentPath/Extensions.cs +++ b/src/JsonDocumentPath/Extensions.cs @@ -1,9 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +#if NET6_0_OR_GREATER + +using System.Text.Json.Nodes; + +#endif namespace System.Text.Json { - internal static class Extensions + internal static partial class Extensions { public static bool IsValue(this JsonElement src) { @@ -14,10 +19,12 @@ public static bool IsValue(this JsonElement src) src.ValueKind == JsonValueKind.Null || src.ValueKind == JsonValueKind.Undefined; } + public static bool IsContainer(this JsonElement src) { return src.ValueKind == JsonValueKind.Array || src.ValueKind == JsonValueKind.Object; } + public static bool IsContainer(this JsonElement? src) { if (src.HasValue) @@ -249,4 +256,177 @@ private static int Compare(JsonValueKind valueType, JsonElement objA, JsonElemen return -1; } } -} + +#if NET6_0 || NET7_0 + internal partial class Extensions + { + public static JsonValueKind GetValueKind(this JsonNode jsonNode) + { + var el = JsonSerializer.SerializeToElement(jsonNode); + + return el.ValueKind; + } + } +#endif + +#if NET6_0_OR_GREATER + + internal static partial class Extensions + { + public static bool IsValue(this JsonNode? src) + { + // JsonNode 无法表示 JsonValueKind.Null 但 null 是正常 JsonValue + if (src == null) + return true; + + var nodeValueKind = src.GetSafeJsonValueKind(); + + return nodeValueKind == JsonValueKind.False || + nodeValueKind == JsonValueKind.True || + nodeValueKind == JsonValueKind.String || + nodeValueKind == JsonValueKind.Number || + nodeValueKind == JsonValueKind.Null || + nodeValueKind == JsonValueKind.Undefined; + } + + public static bool IsContainer(this JsonNode src) + { + var nodeValueKind = src.GetSafeJsonValueKind(); + + return nodeValueKind == JsonValueKind.Array || nodeValueKind == JsonValueKind.Object; + } + + public static bool TryGetFirstFromObject(this JsonNode? src, out JsonNode? element) + { + element = null; + + if (src == null) + return false; + + var nodeValueKind = src.GetSafeJsonValueKind(); + if (nodeValueKind == JsonValueKind.Object) + { + var currentObject = src.AsObject(); + var enumerator = currentObject.GetEnumerator(); + if (enumerator.MoveNext()) + { + element = enumerator.Current.Value; + return true; + } + } + return false; + } + + public static bool TryMoveNextFromObject(this JsonNode? src, int cycle, out JsonNode? element) + { + element = null; + + if (src == null) + return false; + + var nodeValueKind = src.GetSafeJsonValueKind(); + + if (nodeValueKind == JsonValueKind.Object) + { + var currentObject = src.AsObject().GetEnumerator(); + for (int i = 0; i < cycle; i++) + { + currentObject.MoveNext(); + } + element = currentObject.Current.Value; + return true; + } + return false; + } + + public static IEnumerable ChildrenNodes(this JsonNode src) + { + var srcValueKind = src.GetSafeJsonValueKind(); + + if (srcValueKind == JsonValueKind.Object) + { + var srcObject = src.AsObject(); + foreach (var item in srcObject) + { + yield return item.Value; + } + } + + if (srcValueKind == JsonValueKind.Array) + { + var srcArray = src.AsArray(); + foreach (var item in srcArray) + { + yield return item; + } + } + } + + public static int CompareTo(this JsonNode value, JsonNode queryValue) + { + JsonValueKind comparisonType = (value.GetSafeJsonValueKind() == JsonValueKind.String && value.GetSafeJsonValueKind() != queryValue.GetSafeJsonValueKind()) + ? queryValue.GetSafeJsonValueKind() + : value.GetSafeJsonValueKind(); + + return Compare(comparisonType, value, queryValue); + } + + private static int Compare(JsonValueKind valueType, JsonNode objA, JsonNode objB) + { + JsonValueKind aValueKind = objA.GetSafeJsonValueKind(); + JsonValueKind bValueKind = objB.GetSafeJsonValueKind(); + + /*Same types*/ + if (aValueKind == JsonValueKind.Null && bValueKind == JsonValueKind.Null) + { + return 0; + } + if (aValueKind == JsonValueKind.Undefined && bValueKind == JsonValueKind.Undefined) + { + return 0; + } + if (aValueKind == JsonValueKind.True && bValueKind == JsonValueKind.True) + { + return 0; + } + if (aValueKind == JsonValueKind.False && bValueKind == JsonValueKind.False) + { + return 0; + } + if (aValueKind == JsonValueKind.Number && bValueKind == JsonValueKind.Number) + { + return objA.GetDouble().CompareTo(objB.GetDouble()); + } + if (aValueKind == JsonValueKind.String && bValueKind == JsonValueKind.String) + { + return objA.GetString().CompareTo(objB.GetString()); + } + //When objA is a number and objB is not. + if (aValueKind == JsonValueKind.Number) + { + var valueObjA = objA.GetDouble(); + if (bValueKind == JsonValueKind.String) + { + if (double.TryParse(objB.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) + { + return valueObjA.CompareTo(queryValueTyped); + } + } + } + //When objA is a string and objB is not. + if (aValueKind == JsonValueKind.String) + { + if (bValueKind == JsonValueKind.Number) + { + if (double.TryParse(objA.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) + { + return valueTyped.CompareTo(objB.GetDouble()); + } + } + } + return -1; + } + } + +#endif +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs new file mode 100644 index 0000000..881153d --- /dev/null +++ b/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs @@ -0,0 +1,46 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class ArrayIndexFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode t in current) + { + if (Index != null) + { + JsonNode? v = GetTokenIndex(t, errorWhenNoMatch, Index.GetValueOrDefault()); + + if (v != null) + { + yield return v; + } + } + else + { + if (t.GetSafeJsonValueKind() == JsonValueKind.Array) + { + var tArrayEnumerator = t.AsArray().GetEnumerator(); + while (tArrayEnumerator.MoveNext()) + { + yield return tArrayEnumerator.Current; + } + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Index * not valid on {t.GetType().Name}."); + } + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs b/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs index c48a64a..739bc2e 100644 --- a/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs +++ b/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class ArrayIndexFilter : PathFilter + internal partial class ArrayIndexFilter : PathFilter { public int? Index { get; set; } @@ -39,4 +39,4 @@ internal class ArrayIndexFilter : PathFilter } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs new file mode 100644 index 0000000..93710e2 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs @@ -0,0 +1,28 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class ArrayMultipleIndexFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode t in current) + { + foreach (int i in Indexes) + { + JsonNode? v = GetTokenIndex(t, errorWhenNoMatch, i); + + if (v != null) + { + yield return v; + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs index bbcf52c..b523c21 100644 --- a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs +++ b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class ArrayMultipleIndexFilter : PathFilter + internal partial class ArrayMultipleIndexFilter : PathFilter { internal List Indexes; @@ -27,4 +27,4 @@ public ArrayMultipleIndexFilter(List indexes) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs new file mode 100644 index 0000000..fea1df2 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs @@ -0,0 +1,74 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class ArraySliceFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + if (Step == 0) + { + throw new JsonException("Step cannot be zero."); + } + + foreach (JsonNode t in current) + { + if (t.GetSafeJsonValueKind() == JsonValueKind.Array) + { + var aCount = t.AsArray().Count; + // set defaults for null arguments + int stepCount = Step ?? 1; + int startIndex = Start ?? ((stepCount > 0) ? 0 : aCount - 1); + int stopIndex = End ?? ((stepCount > 0) ? aCount : -1); + + // start from the end of the list if start is negative + if (Start < 0) + { + startIndex = aCount + startIndex; + } + + // end from the start of the list if stop is negative + if (End < 0) + { + stopIndex = aCount + stopIndex; + } + + // ensure indexes keep within collection bounds + startIndex = Math.Max(startIndex, (stepCount > 0) ? 0 : int.MinValue); + startIndex = Math.Min(startIndex, (stepCount > 0) ? aCount : aCount - 1); + stopIndex = Math.Max(stopIndex, -1); + stopIndex = Math.Min(stopIndex, aCount); + + bool positiveStep = (stepCount > 0); + + if (IsValid(startIndex, stopIndex, positiveStep)) + { + for (int i = startIndex; IsValid(i, stopIndex, positiveStep); i += stepCount) + { + yield return t[i]; + } + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Array slice of {(Start != null ? Start.GetValueOrDefault().ToString() : "*")} to {(End != null ? End.GetValueOrDefault().ToString() : "*")} returned no results."); + } + } + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Array slice is not valid on {t.GetType().Name}."); + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArraySliceFilter.cs b/src/JsonDocumentPath/Filters/ArraySliceFilter.cs index 3012c59..6516c14 100644 --- a/src/JsonDocumentPath/Filters/ArraySliceFilter.cs +++ b/src/JsonDocumentPath/Filters/ArraySliceFilter.cs @@ -2,10 +2,12 @@ namespace System.Text.Json { - internal class ArraySliceFilter : PathFilter + internal partial class ArraySliceFilter : PathFilter { public int? Start { get; set; } + public int? End { get; set; } + public int? Step { get; set; } public override IEnumerable ExecuteFilter(JsonElement root, IEnumerable current, bool errorWhenNoMatch) @@ -80,4 +82,4 @@ private bool IsValid(int index, int stopIndex, bool positiveStep) return (index > stopIndex); } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs new file mode 100644 index 0000000..7f4dd6c --- /dev/null +++ b/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs @@ -0,0 +1,57 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class FieldFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode t in current) + { + if (t.GetSafeJsonValueKind() == JsonValueKind.Object) + { + if (Name != null) + { + var tObject = t.AsObject(); + if (tObject.TryGetPropertyValue(Name, out JsonNode? v)) + { + if (v?.GetSafeJsonValueKind() != JsonValueKind.Null) + { + yield return v; + } + else if (errorWhenNoMatch) + { + throw new JsonException($"Property '{Name}' does not exist on JObject."); + } + } + } + else + { + foreach (var p in +#if NET6_0 || NET7_0 + Extensions.ChildrenNodes(t) +#else + t.ChildrenNodes() +#endif + ) + { + yield return p; + } + } + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Property '{Name ?? "*"}' not valid on {t.GetType().Name}."); + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/FieldFilter.cs b/src/JsonDocumentPath/Filters/FieldFilter.cs index 1087b98..a376ac3 100644 --- a/src/JsonDocumentPath/Filters/FieldFilter.cs +++ b/src/JsonDocumentPath/Filters/FieldFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class FieldFilter : PathFilter + internal partial class FieldFilter : PathFilter { internal string? Name; @@ -49,4 +49,4 @@ public FieldFilter(string? name) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs new file mode 100644 index 0000000..3cfaf16 --- /dev/null +++ b/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs @@ -0,0 +1,45 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class FieldMultipleFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode? t in current) + { + if (t?.GetSafeJsonValueKind() == JsonValueKind.Object) + { + var tObject = t.AsObject(); + foreach (string name in Names) + { + if (tObject.TryGetPropertyValue(name, out JsonNode? v)) + { + if (v?.GetSafeJsonValueKind() != JsonValueKind.Null) + { + yield return v; + } + else if (errorWhenNoMatch) + { + throw new JsonException($"Property '{name}' does not exist on JObject."); + } + } + } + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Properties {string.Join(", ", Names.Select(n => "'" + n + "'").ToArray())} not valid on {t.GetType().Name}."); + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs b/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs index 538b060..c0607d1 100644 --- a/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs +++ b/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs @@ -3,7 +3,7 @@ namespace System.Text.Json { - internal class FieldMultipleFilter : PathFilter + internal partial class FieldMultipleFilter : PathFilter { internal List Names; @@ -43,4 +43,4 @@ public FieldMultipleFilter(List names) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs new file mode 100644 index 0000000..47d98bf --- /dev/null +++ b/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs @@ -0,0 +1,41 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class QueryFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode? el in current) + { + if (el?.GetSafeJsonValueKind() == JsonValueKind.Array) + { + var elArray = el.AsArray(); + foreach (JsonNode? v in elArray) + { + if (Expression.IsMatch(root, v)) + { + yield return v; + } + } + } + else if (el?.GetSafeJsonValueKind() == JsonValueKind.Object) + { + var elObject = el.AsObject(); + foreach (KeyValuePair v in elObject) + { + if (Expression.IsMatch(root, v.Value)) + { + yield return v.Value; + } + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/QueryFilter.cs b/src/JsonDocumentPath/Filters/QueryFilter.cs index 2b05f4f..ed144f5 100644 --- a/src/JsonDocumentPath/Filters/QueryFilter.cs +++ b/src/JsonDocumentPath/Filters/QueryFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class QueryFilter : PathFilter + internal partial class QueryFilter : PathFilter { internal QueryExpression Expression; @@ -38,4 +38,4 @@ public QueryFilter(QueryExpression expression) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs new file mode 100644 index 0000000..e8de62c --- /dev/null +++ b/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs @@ -0,0 +1,26 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class QueryScanFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode? t in current) + { + foreach (var (_, Value) in GetNextScanValue(t)) + { + if (Expression.IsMatch(root, Value)) + { + yield return Value; + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/QueryScanFilter.cs b/src/JsonDocumentPath/Filters/QueryScanFilter.cs index 810f5db..2d4b4e8 100644 --- a/src/JsonDocumentPath/Filters/QueryScanFilter.cs +++ b/src/JsonDocumentPath/Filters/QueryScanFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class QueryScanFilter : PathFilter + internal partial class QueryScanFilter : PathFilter { internal QueryExpression Expression; @@ -25,4 +25,4 @@ public QueryScanFilter(QueryExpression expression) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs new file mode 100644 index 0000000..a5598c7 --- /dev/null +++ b/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs @@ -0,0 +1,17 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class RootFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + return [root]; + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/RootFilter.cs b/src/JsonDocumentPath/Filters/RootFilter.cs index f2a37ae..d2ac134 100644 --- a/src/JsonDocumentPath/Filters/RootFilter.cs +++ b/src/JsonDocumentPath/Filters/RootFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class RootFilter : PathFilter + internal partial class RootFilter : PathFilter { public static readonly RootFilter Instance = new RootFilter(); @@ -15,4 +15,4 @@ private RootFilter() return new JsonElement?[1] { root }; } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs new file mode 100644 index 0000000..b81d993 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs @@ -0,0 +1,26 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +namespace System.Text.Json +{ + internal partial class ScanFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode? c in current) + { + foreach (var e in GetNextScanValue(c)) + { + if (e.Name == Name) + { + yield return e.Value; + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ScanFilter.cs b/src/JsonDocumentPath/Filters/ScanFilter.cs index 3b75ef7..7ae213c 100644 --- a/src/JsonDocumentPath/Filters/ScanFilter.cs +++ b/src/JsonDocumentPath/Filters/ScanFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Linq; namespace System.Text.Json { - internal class ScanFilter : PathFilter + internal partial class ScanFilter : PathFilter { internal string? Name; @@ -26,4 +25,4 @@ public ScanFilter(string? name) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs new file mode 100644 index 0000000..4762465 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs @@ -0,0 +1,35 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Xml.Linq; + +namespace System.Text.Json +{ + internal partial class ScanMultipleFilter + { + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + foreach (JsonNode c in current) + { + JsonNode? value = c; + + foreach (var e in GetNextScanValue(c)) + { + if (e.Name != null) + { + foreach (string name in _names) + { + if (e.Name == name) + { + yield return e.Value; + } + } + } + } + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs b/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs index dc7d35a..fd57a5b 100644 --- a/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs +++ b/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs @@ -2,7 +2,7 @@ namespace System.Text.Json { - internal class ScanMultipleFilter : PathFilter + internal partial class ScanMultipleFilter : PathFilter { private List _names; @@ -33,4 +33,4 @@ public ScanMultipleFilter(List names) } } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonDocumentPath.cs b/src/JsonDocumentPath/JsonDocumentPath.cs index 27f8b7c..23f2d20 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.cs +++ b/src/JsonDocumentPath/JsonDocumentPath.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Globalization; -using System.Text.RegularExpressions; +#pragma warning disable IDE0130 // 命名空间与文件夹结构不匹配 namespace System.Text.Json +#pragma warning restore IDE0130 // 命名空间与文件夹结构不匹配 { internal class JsonDocumentPath { @@ -97,16 +98,19 @@ private bool ParsePath(List filters, int currentPartStartIndex, bool followingIndexer = true; followingDot = false; break; + case ']': case ')': ended = true; break; + case ' ': if (_currentIndex < _expression.Length) { ended = true; } break; + case '.': if (_currentIndex > currentPartStartIndex) { @@ -129,6 +133,7 @@ private bool ParsePath(List filters, int currentPartStartIndex, bool followingIndexer = false; followingDot = true; break; + default: if (query && (currentChar == '=' || currentChar == '<' || currentChar == '!' || currentChar == '>' || currentChar == '|' || currentChar == '&')) { @@ -659,27 +664,34 @@ private string ReadQuotedString() case 'b': resolvedChar = '\b'; break; + case 't': resolvedChar = '\t'; break; + case 'n': resolvedChar = '\n'; break; + case 'f': resolvedChar = '\f'; break; + case 'r': resolvedChar = '\r'; break; + case '\\': resolvedChar = '\\'; sb.Append('\\');//duplicate break; + case '"': case '\'': case '/': resolvedChar = currentChar; break; + default: throw new JsonException(@"Unknown escape character: \" + currentChar); } @@ -748,7 +760,7 @@ private string ReadRegexString() } _currentIndex++; - sb.Append(currentChar); + sb.Append(currentChar); } else { @@ -912,5 +924,21 @@ private void EnsureLength(string message) return current; } + + //internal IEnumerable Evaluate(JsonNode root, JsonNode t, bool errorWhenNoMatch) + //{ + // return Evaluate(Filters, root, t, errorWhenNoMatch); + //} + + //internal static IEnumerable Evaluate(List filters, JsonNode root, JsonNode t, bool errorWhenNoMatch) + //{ + // IEnumerable current = new JsonNode?[] { t }; + // foreach (PathFilter filter in filters) + // { + // current = filter.ExecuteFilter(root, current, errorWhenNoMatch); + // } + + // return current; + //} } } \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonDocumentPath.csproj b/src/JsonDocumentPath/JsonDocumentPath.csproj index ad2dfd7..c43a9dd 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.csproj +++ b/src/JsonDocumentPath/JsonDocumentPath.csproj @@ -1,7 +1,7 @@ - netstandard2.1;net6.0 + netstandard2.1;net6.0;net7.0;net8.0;net9.0 1.0.3 1.0.0.3 1.0.0.3 @@ -15,6 +15,8 @@ JsonDocumentPath, Json, JsonDocument, JsonPath MIT https://github.com/azambrano/JsonDocumentPath + enable + latest diff --git a/src/JsonDocumentPath/JsonDocumentPathExtensions.cs b/src/JsonDocumentPath/JsonDocumentPathExtensions.cs index 9d93404..ad405c5 100644 --- a/src/JsonDocumentPath/JsonDocumentPathExtensions.cs +++ b/src/JsonDocumentPath/JsonDocumentPathExtensions.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +#pragma warning disable IDE0130 // 命名空间与文件夹结构不匹配 namespace System.Text.Json +#pragma warning restore IDE0130 // 命名空间与文件夹结构不匹配 { public static class JsonDocumentPathExtensions { @@ -152,4 +154,4 @@ public static object GetObjectValue(this JsonElement src) return src.GetRawText(); } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonNodeExtensions.cs b/src/JsonDocumentPath/JsonNodeExtensions.cs new file mode 100644 index 0000000..e790986 --- /dev/null +++ b/src/JsonDocumentPath/JsonNodeExtensions.cs @@ -0,0 +1,58 @@ +#if NET6_0_OR_GREATER + +using System.Text.Json.Nodes; + +namespace System.Text.Json; + +internal static class JsonNodeExtensions +{ + public static string GetString(this JsonNode node) + { + return node.GetValue(); + } + + public static bool GetBoolean(this JsonNode node) + { + return node.GetValue(); + } + + public static double GetDouble(this JsonNode node) + { + return node.GetValue(); + } + + public static int GetInt32(this JsonNode node) + { + return node.GetValue(); + } + + public static object? GetObjectValue(this JsonNode node) + { + var nodeValueKind = node.GetSafeJsonValueKind(); + + return nodeValueKind switch + { + JsonValueKind.Null => null, + JsonValueKind.String => node.GetString(), + JsonValueKind.Number => node.GetDouble(), + JsonValueKind.True or JsonValueKind.False => node.GetBoolean(), + _ => node.ToJsonString(), + }; + } + + /// + /// 获取安全的 JsonValueKind + /// + /// JsonNode 无法表示 JsonValueKind.Null + /// + /// + public static JsonValueKind GetSafeJsonValueKind(this JsonNode? node) + { + if (node == null) + return JsonValueKind.Null; + + return node.GetValueKind(); + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonNodePath.cs b/src/JsonDocumentPath/JsonNodePath.cs new file mode 100644 index 0000000..ef7c5ef --- /dev/null +++ b/src/JsonDocumentPath/JsonNodePath.cs @@ -0,0 +1,930 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json.Nodes; + +namespace System.Text.Json; + +public class JsonNodePath +{ + private static readonly char[] FloatCharacters = ['.', 'E', 'e']; + + private readonly string _expression; + public List Filters { get; } + + private int _currentIndex; + + public JsonNodePath(string expression) + { + _expression = expression; + Filters = []; + + ParseMain(); + } + + private void ParseMain() + { + int currentPartStartIndex = _currentIndex; + + EatWhitespace(); + + if (_expression.Length == _currentIndex) + { + return; + } + + if (_expression[_currentIndex] == '$') + { + if (_expression.Length == 1) + { + return; + } + + // only increment position for "$." or "$[" + // otherwise assume property that starts with $ + char c = _expression[_currentIndex + 1]; + if (c == '.' || c == '[') + { + _currentIndex++; + currentPartStartIndex = _currentIndex; + } + } + + if (!ParsePath(Filters, currentPartStartIndex, false)) + { + int lastCharacterIndex = _currentIndex; + + EatWhitespace(); + + if (_currentIndex < _expression.Length) + { + throw new JsonException("Unexpected character while parsing path: " + _expression[lastCharacterIndex]); + } + } + } + + private bool ParsePath(List filters, int currentPartStartIndex, bool query) + { + bool scan = false; + bool followingIndexer = false; + bool followingDot = false; + + bool ended = false; + while (_currentIndex < _expression.Length && !ended) + { + char currentChar = _expression[_currentIndex]; + + switch (currentChar) + { + case '[': + case '(': + if (_currentIndex > currentPartStartIndex) + { + string? member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex); + if (member == "*") + { + member = null; + } + + filters.Add(CreatePathFilter(member, scan)); + scan = false; + } + + filters.Add(ParseIndexer(currentChar, scan)); + scan = false; + + _currentIndex++; + currentPartStartIndex = _currentIndex; + followingIndexer = true; + followingDot = false; + break; + + case ']': + case ')': + ended = true; + break; + + case ' ': + if (_currentIndex < _expression.Length) + { + ended = true; + } + break; + + case '.': + if (_currentIndex > currentPartStartIndex) + { + string? member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex); + if (member == "*") + { + member = null; + } + + filters.Add(CreatePathFilter(member, scan)); + scan = false; + } + if (_currentIndex + 1 < _expression.Length && _expression[_currentIndex + 1] == '.') + { + scan = true; + _currentIndex++; + } + _currentIndex++; + currentPartStartIndex = _currentIndex; + followingIndexer = false; + followingDot = true; + break; + + default: + if (query && (currentChar == '=' || currentChar == '<' || currentChar == '!' || currentChar == '>' || currentChar == '|' || currentChar == '&')) + { + ended = true; + } + else + { + if (followingIndexer) + { + throw new JsonException("Unexpected character following indexer: " + currentChar); + } + + _currentIndex++; + } + break; + } + } + + bool atPathEnd = (_currentIndex == _expression.Length); + + if (_currentIndex > currentPartStartIndex) + { + string? member = _expression.Substring(currentPartStartIndex, _currentIndex - currentPartStartIndex).TrimEnd(); + if (member == "*") + { + member = null; + } + filters.Add(CreatePathFilter(member, scan)); + } + else + { + // no field name following dot in path and at end of base path/query + if (followingDot && (atPathEnd || query)) + { + throw new JsonException("Unexpected end while parsing path."); + } + } + + return atPathEnd; + } + + private static PathFilter CreatePathFilter(string? member, bool scan) + { + PathFilter filter = (scan) ? (PathFilter)new ScanFilter(member) : new FieldFilter(member); + return filter; + } + + private PathFilter ParseIndexer(char indexerOpenChar, bool scan) + { + _currentIndex++; + + char indexerCloseChar = (indexerOpenChar == '[') ? ']' : ')'; + + EnsureLength("Path ended with open indexer."); + + EatWhitespace(); + + if (_expression[_currentIndex] == '\'') + { + return ParseQuotedField(indexerCloseChar, scan); + } + else if (_expression[_currentIndex] == '?') + { + return ParseQuery(indexerCloseChar, scan); + } + else + { + return ParseArrayIndexer(indexerCloseChar); + } + } + + private PathFilter ParseArrayIndexer(char indexerCloseChar) + { + int start = _currentIndex; + int? end = null; + List? indexes = null; + int colonCount = 0; + int? startIndex = null; + int? endIndex = null; + int? step = null; + + while (_currentIndex < _expression.Length) + { + char currentCharacter = _expression[_currentIndex]; + + if (currentCharacter == ' ') + { + end = _currentIndex; + EatWhitespace(); + continue; + } + + if (currentCharacter == indexerCloseChar) + { + int length = (end ?? _currentIndex) - start; + + if (indexes != null) + { + if (length == 0) + { + throw new JsonException("Array index expected."); + } + + string indexer = _expression.Substring(start, length); + int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture); + + indexes.Add(index); + return new ArrayMultipleIndexFilter(indexes); + } + else if (colonCount > 0) + { + if (length > 0) + { + string indexer = _expression.Substring(start, length); + int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture); + + if (colonCount == 1) + { + endIndex = index; + } + else + { + step = index; + } + } + + return new ArraySliceFilter { Start = startIndex, End = endIndex, Step = step }; + } + else + { + if (length == 0) + { + throw new JsonException("Array index expected."); + } + + string indexer = _expression.Substring(start, length); + int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture); + + return new ArrayIndexFilter { Index = index }; + } + } + else if (currentCharacter == ',') + { + int length = (end ?? _currentIndex) - start; + + if (length == 0) + { + throw new JsonException("Array index expected."); + } + + if (indexes == null) + { + indexes = new List(); + } + + string indexer = _expression.Substring(start, length); + indexes.Add(Convert.ToInt32(indexer, CultureInfo.InvariantCulture)); + + _currentIndex++; + + EatWhitespace(); + + start = _currentIndex; + end = null; + } + else if (currentCharacter == '*') + { + _currentIndex++; + EnsureLength("Path ended with open indexer."); + EatWhitespace(); + + if (_expression[_currentIndex] != indexerCloseChar) + { + throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter); + } + + return new ArrayIndexFilter(); + } + else if (currentCharacter == ':') + { + int length = (end ?? _currentIndex) - start; + + if (length > 0) + { + string indexer = _expression.Substring(start, length); + int index = Convert.ToInt32(indexer, CultureInfo.InvariantCulture); + + if (colonCount == 0) + { + startIndex = index; + } + else if (colonCount == 1) + { + endIndex = index; + } + else + { + step = index; + } + } + + colonCount++; + + _currentIndex++; + + EatWhitespace(); + + start = _currentIndex; + end = null; + } + else if (!char.IsDigit(currentCharacter) && currentCharacter != '-') + { + throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter); + } + else + { + if (end != null) + { + throw new JsonException("Unexpected character while parsing path indexer: " + currentCharacter); + } + + _currentIndex++; + } + } + + throw new JsonException("Path ended with open indexer."); + } + + private void EatWhitespace() + { + while (_currentIndex < _expression.Length) + { + if (_expression[_currentIndex] != ' ') + { + break; + } + + _currentIndex++; + } + } + + private PathFilter ParseQuery(char indexerCloseChar, bool scan) + { + _currentIndex++; + EnsureLength("Path ended with open indexer."); + + if (_expression[_currentIndex] != '(') + { + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + } + + _currentIndex++; + + QueryExpression expression = ParseExpression(); + + _currentIndex++; + EnsureLength("Path ended with open indexer."); + EatWhitespace(); + + if (_expression[_currentIndex] != indexerCloseChar) + { + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + } + + if (!scan) + { + return new QueryFilter(expression); + } + else + { + return new QueryScanFilter(expression); + } + } + + private bool TryParseExpression(out List? expressionPath) + { + if (_expression[_currentIndex] == '$') + { + expressionPath = new List { RootFilter.Instance }; + } + else if (_expression[_currentIndex] == '@') + { + expressionPath = new List(); + } + else + { + expressionPath = null; + return false; + } + + _currentIndex++; + + if (ParsePath(expressionPath!, _currentIndex, true)) + { + throw new JsonException("Path ended with open query."); + } + + return true; + } + + private JsonException CreateUnexpectedCharacterException() + { + return new JsonException("Unexpected character while parsing path query: " + _expression[_currentIndex]); + } + + private object? ParseSide() + { + EatWhitespace(); + + if (TryParseExpression(out List? expressionPath)) + { + EatWhitespace(); + EnsureLength("Path ended with open query."); + + return expressionPath!; + } + + if (TryParseValue(out var value)) + { + EatWhitespace(); + EnsureLength("Path ended with open query."); + + return SafeValue(value); + } + + throw CreateUnexpectedCharacterException(); + } + + private JsonNode? SafeValue(object? value) + { + if (value == null) + { + return JsonValue.Create(null); + } + if (value is string) + { + return JsonNode.Parse(string.Concat("\"", value, "\""))?.GetValue(); + } + if (value is bool) + { + if ((bool)(value)) + { + return JsonValue.Create(true); + } + return JsonValue.Create(false); + } + return JsonNode.Parse(string.Concat(value))!; + } + + private QueryExpression ParseExpression() + { + QueryExpression? rootExpression = null; + CompositeExpression? parentExpression = null; + + while (_currentIndex < _expression.Length) + { + object left = ParseSide()!; + object? right = null; + + QueryOperator op; + if (_expression[_currentIndex] == ')' + || _expression[_currentIndex] == '|' + || _expression[_currentIndex] == '&') + { + op = QueryOperator.Exists; + } + else + { + op = ParseOperator(); + + right = ParseSide(); + } + + BooleanQueryExpression booleanExpression = new BooleanQueryExpression(op, left, right); + + if (_expression[_currentIndex] == ')') + { + if (parentExpression != null) + { + parentExpression.Expressions.Add(booleanExpression); + return rootExpression!; + } + + return booleanExpression; + } + if (_expression[_currentIndex] == '&') + { + if (!Match("&&")) + { + throw CreateUnexpectedCharacterException(); + } + + if (parentExpression == null || parentExpression.Operator != QueryOperator.And) + { + CompositeExpression andExpression = new CompositeExpression(QueryOperator.And); + + parentExpression?.Expressions.Add(andExpression); + + parentExpression = andExpression; + + if (rootExpression == null) + { + rootExpression = parentExpression; + } + } + + parentExpression.Expressions.Add(booleanExpression); + } + if (_expression[_currentIndex] == '|') + { + if (!Match("||")) + { + throw CreateUnexpectedCharacterException(); + } + + if (parentExpression == null || parentExpression.Operator != QueryOperator.Or) + { + CompositeExpression orExpression = new CompositeExpression(QueryOperator.Or); + + parentExpression?.Expressions.Add(orExpression); + + parentExpression = orExpression; + + if (rootExpression == null) + { + rootExpression = parentExpression; + } + } + + parentExpression.Expressions.Add(booleanExpression); + } + } + + throw new JsonException("Path ended with open query."); + } + + private bool TryParseValue(out object? value) + { + char currentChar = _expression[_currentIndex]; + if (currentChar == '\'') + { + value = ReadQuotedString(); + return true; + } + else if (char.IsDigit(currentChar) || currentChar == '-') + { + StringBuilder sb = new StringBuilder(); + sb.Append(currentChar); + + _currentIndex++; + while (_currentIndex < _expression.Length) + { + currentChar = _expression[_currentIndex]; + if (currentChar == ' ' || currentChar == ')') + { + string numberText = sb.ToString(); + + if (numberText.IndexOfAny(FloatCharacters) != -1) + { + bool result = double.TryParse(numberText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var d); + value = d; + return result; + } + else + { + bool result = long.TryParse(numberText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l); + value = l; + return result; + } + } + else + { + sb.Append(currentChar); + _currentIndex++; + } + } + } + else if (currentChar == 't') + { + if (Match("true")) + { + value = true; + return true; + } + } + else if (currentChar == 'f') + { + if (Match("false")) + { + value = false; + return true; + } + } + else if (currentChar == 'n') + { + if (Match("null")) + { + value = null; + return true; + } + } + else if (currentChar == '/') + { + value = ReadRegexString(); + return true; + } + + value = null; + return false; + } + + private string ReadQuotedString() + { + StringBuilder sb = new StringBuilder(); + + _currentIndex++; + while (_currentIndex < _expression.Length) + { + char currentChar = _expression[_currentIndex]; + if (currentChar == '\\' && _currentIndex + 1 < _expression.Length) + { + _currentIndex++; + currentChar = _expression[_currentIndex]; + + char resolvedChar; + switch (currentChar) + { + case 'b': + resolvedChar = '\b'; + break; + + case 't': + resolvedChar = '\t'; + break; + + case 'n': + resolvedChar = '\n'; + break; + + case 'f': + resolvedChar = '\f'; + break; + + case 'r': + resolvedChar = '\r'; + break; + + case '\\': + resolvedChar = '\\'; + sb.Append('\\');//duplicate + break; + + case '"': + case '\'': + case '/': + resolvedChar = currentChar; + break; + + default: + throw new JsonException(@"Unknown escape character: \" + currentChar); + } + + sb.Append(resolvedChar); + + _currentIndex++; + } + else + if (currentChar == '\'') + { + _currentIndex++; + return sb.ToString(); + } + else + { + _currentIndex++; + sb.Append(currentChar); + } + } + + throw new JsonException("Path ended with an open string."); + } + + private string ReadRegexString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("\\/"); + + _currentIndex++; + while (_currentIndex < _expression.Length) + { + char currentChar = _expression[_currentIndex]; + + // handle escaped / character + if (currentChar == '\\' && _currentIndex + 1 < _expression.Length) + { + _currentIndex++; + sb.Append(currentChar); + sb.Append('\\');//duplicate + + //if the next char is '/' skip it. + if (_currentIndex < _expression.Length) + { + currentChar = _expression[_currentIndex]; + if (currentChar == '/') + { + _currentIndex++; + sb.Append(currentChar); + } + } + } + else if (currentChar == '/') + { + _currentIndex++; + + while (_currentIndex < _expression.Length) + { + currentChar = _expression[_currentIndex]; + + if (char.IsLetter(currentChar)) + { + if (_expression[_currentIndex - 1] == '/') + { + sb.Append("\\/"); + } + + _currentIndex++; + sb.Append(currentChar); + } + else + { + if (_expression[_currentIndex - 1] == '/') + { + sb.Append("\\/"); + } + + break; + } + } + + return sb.ToString(); + } + else + { + _currentIndex++; + sb.Append(currentChar); + } + } + + throw new JsonException("Path ended with an open regex."); + } + + private bool Match(string s) + { + int currentPosition = _currentIndex; + for (int i = 0; i < s.Length; i++) + { + if (currentPosition < _expression.Length && _expression[currentPosition] == s[i]) + { + currentPosition++; + } + else + { + return false; + } + } + + _currentIndex = currentPosition; + return true; + } + + private QueryOperator ParseOperator() + { + if (_currentIndex + 1 >= _expression.Length) + { + throw new JsonException("Path ended with open query."); + } + + if (Match("===")) + { + return QueryOperator.StrictEquals; + } + + if (Match("==")) + { + return QueryOperator.Equals; + } + + if (Match("=~")) + { + return QueryOperator.RegexEquals; + } + + if (Match("!==")) + { + return QueryOperator.StrictNotEquals; + } + + if (Match("!=") || Match("<>")) + { + return QueryOperator.NotEquals; + } + if (Match("<=")) + { + return QueryOperator.LessThanOrEquals; + } + if (Match("<")) + { + return QueryOperator.LessThan; + } + if (Match(">=")) + { + return QueryOperator.GreaterThanOrEquals; + } + if (Match(">")) + { + return QueryOperator.GreaterThan; + } + + throw new JsonException("Could not read query operator."); + } + + private PathFilter ParseQuotedField(char indexerCloseChar, bool scan) + { + List? fields = null; + + while (_currentIndex < _expression.Length) + { + string field = ReadQuotedString(); + + EatWhitespace(); + EnsureLength("Path ended with open indexer."); + + if (_expression[_currentIndex] == indexerCloseChar) + { + if (fields != null) + { + fields.Add(field); + return (scan) + ? (PathFilter)new ScanMultipleFilter(fields) + : (PathFilter)new FieldMultipleFilter(fields); + } + else + { + return CreatePathFilter(field, scan); + } + } + else if (_expression[_currentIndex] == ',') + { + _currentIndex++; + EatWhitespace(); + + if (fields == null) + { + fields = new List(); + } + + fields.Add(field); + } + else + { + throw new JsonException("Unexpected character while parsing path indexer: " + _expression[_currentIndex]); + } + } + + throw new JsonException("Path ended with open indexer."); + } + + private void EnsureLength(string message) + { + if (_currentIndex >= _expression.Length) + { + throw new JsonException(message); + } + } + + internal IEnumerable Evaluate(JsonNode root, JsonNode t, bool errorWhenNoMatch) + { + return Evaluate(Filters, root, t, errorWhenNoMatch); + } + + internal static IEnumerable Evaluate(List filters, JsonNode root, JsonNode t, bool errorWhenNoMatch) + { + IEnumerable current = new JsonNode?[] { t }; + foreach (PathFilter filter in filters) + { + current = filter.ExecuteFilter(root, current, errorWhenNoMatch); + } + + return current; + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonNodePathExtensions.cs b/src/JsonDocumentPath/JsonNodePathExtensions.cs new file mode 100644 index 0000000..12c1706 --- /dev/null +++ b/src/JsonDocumentPath/JsonNodePathExtensions.cs @@ -0,0 +1,51 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Text.Json.Nodes; + +#pragma warning disable IDE0130 // 命名空间与文件夹结构不匹配 +namespace System.Text.Json +#pragma warning restore IDE0130 // 命名空间与文件夹结构不匹配 +{ + public static class JsonNodePathExtensions + { + /// + /// Selects a collection of elements using a JSONPath expression. + /// + /// + /// A that contains a JSONPath expression. + /// + /// A flag to indicate whether an error should be thrown if no tokens are found when evaluating part of the expression. + /// An of that contains the selected elements. + public static IEnumerable SelectNodes(this System.Text.Json.Nodes.JsonNode src, string path, bool errorWhenNoMatch = false) + { + var parser = new JsonNodePath(path); + return parser.Evaluate(src, src, errorWhenNoMatch); + } + + /// + /// Selects a using a JSONPath expression. Selects the token that matches the object path. + /// + /// + /// A that contains a JSONPath expression. + /// + /// A flag to indicate whether an error should be thrown if no tokens are found when evaluating part of the expression. + /// A . + public static JsonNode? SelectNode(this JsonNode src, string path, bool errorWhenNoMatch = false) + { + var p = new JsonNodePath(path); + JsonNode? el = null; + foreach (JsonNode? t in p.Evaluate(src, src, errorWhenNoMatch)) + { + if (el != null) + { + throw new JsonException("Path returned multiple tokens."); + } + el = t; + } + return el; + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/PathFilter.cs b/src/JsonDocumentPath/PathFilter.cs index bc804a5..ac1e049 100644 --- a/src/JsonDocumentPath/PathFilter.cs +++ b/src/JsonDocumentPath/PathFilter.cs @@ -1,5 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; +#if NET6_0_OR_GREATER + +using System.Text.Json.Nodes; + +#endif namespace System.Text.Json { public abstract class PathFilter @@ -60,5 +65,69 @@ public abstract class PathFilter } } } + +#if NET6_0_OR_GREATER + + public abstract IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch); + + protected static JsonNode? GetTokenIndex(JsonNode t, bool errorWhenNoMatch, int index) + { + if (t.GetSafeJsonValueKind() == JsonValueKind.Array) + { + var tArray = t.AsArray(); + if (tArray.Count <= index) + { + if (errorWhenNoMatch) + { + throw new JsonException($"Index {index} outside the bounds of JArray."); + } + + return null; + } + + return tArray[index]; + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Index {index} not valid on {t.GetType().Name}."); + } + + return null; + } + } + + protected static IEnumerable<(string? Name, JsonNode? Value)> GetNextScanValue(JsonNode? value) + { + yield return (null, value); + + if (value.GetSafeJsonValueKind() == JsonValueKind.Array) + { + foreach (var e in value?.AsArray()) + { + foreach (var c in GetNextScanValue(e)) + { + yield return c; + } + } + } + else if (value.GetSafeJsonValueKind() == JsonValueKind.Object) + { + var propertyEnumerator = value?.AsObject().GetEnumerator(); + + while (propertyEnumerator.MoveNext()) + { + yield return (propertyEnumerator.Current.Key, propertyEnumerator.Current.Value); + + foreach (var c in GetNextScanValue(propertyEnumerator.Current.Value)) + { + yield return c; + } + } + } + } + +#endif } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/QueryExpression.JsonNode.cs b/src/JsonDocumentPath/QueryExpression.JsonNode.cs new file mode 100644 index 0000000..61934ab --- /dev/null +++ b/src/JsonDocumentPath/QueryExpression.JsonNode.cs @@ -0,0 +1,281 @@ +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace System.Text.Json +{ + internal partial class QueryExpression + { + public abstract bool IsMatch(JsonNode root, JsonNode t); + } + + internal partial class CompositeExpression + { + public override bool IsMatch(JsonNode root, JsonNode t) + { + switch (Operator) + { + case QueryOperator.And: + foreach (QueryExpression e in Expressions) + { + if (!e.IsMatch(root, t)) + { + return false; + } + } + return true; + + case QueryOperator.Or: + foreach (QueryExpression e in Expressions) + { + if (e.IsMatch(root, t)) + { + return true; + } + } + return false; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + internal partial class BooleanQueryExpression + { + private IEnumerable GetResult(JsonNode root, JsonNode t, object? o) + { + if (o is null) + { + return [null]; + } + if (o is JsonNode resultToken) + { + return [resultToken]; + } + + if (o is JsonElement resultElement) + { + return [JsonNode.Parse(resultElement.GetRawText())]; + } + + if (o is List pathFilters) + { + return JsonNodePath.Evaluate(pathFilters, root, t, false); + } + + return Enumerable.Empty(); + } + + public override bool IsMatch(JsonNode root, JsonNode t) + { + if (Operator == QueryOperator.Exists) + { + return GetResult(root, t, Left).Any(); + } + + using (IEnumerator leftResults = GetResult(root, t, Left).GetEnumerator()) + { + if (leftResults.MoveNext()) + { + IEnumerable rightResultsEn = GetResult(root, t, Right); + ICollection rightResults = rightResultsEn as ICollection ?? rightResultsEn.ToList(); + + do + { + JsonNode? leftResult = leftResults.Current; + foreach (JsonNode? rightResult in rightResults) + { + if (MatchTokens(leftResult, rightResult)) + { + return true; + } + } + } while (leftResults.MoveNext()); + } + } + + return false; + } + + private bool MatchTokens(JsonNode leftResult, JsonNode rightResult) + { + if (leftResult.IsValue() && rightResult.IsValue()) + { + switch (Operator) + { + case QueryOperator.RegexEquals: + if (RegexEquals(leftResult, rightResult)) + { + return true; + } + break; + + case QueryOperator.Equals: + if (EqualsWithStringCoercion(leftResult, rightResult)) + { + return true; + } + break; + + case QueryOperator.StrictEquals: + if (EqualsWithStrictMatch(leftResult, rightResult)) + { + return true; + } + break; + + case QueryOperator.NotEquals: + if (!EqualsWithStringCoercion(leftResult, rightResult)) + { + return true; + } + break; + + case QueryOperator.StrictNotEquals: + if (!EqualsWithStrictMatch(leftResult, rightResult)) + { + return true; + } + break; + + case QueryOperator.GreaterThan: + if (leftResult.CompareTo(rightResult) > 0) + { + return true; + } + break; + + case QueryOperator.GreaterThanOrEquals: + if (leftResult.CompareTo(rightResult) >= 0) + { + return true; + } + break; + + case QueryOperator.LessThan: + if (leftResult.CompareTo(rightResult) < 0) + { + return true; + } + break; + + case QueryOperator.LessThanOrEquals: + if (leftResult.CompareTo(rightResult) <= 0) + { + return true; + } + break; + + case QueryOperator.Exists: + return true; + } + } + else + { + switch (Operator) + { + case QueryOperator.Exists: + // you can only specify primitive types in a comparison + // notequals will always be true + case QueryOperator.NotEquals: + return true; + } + } + + return false; + } + + private static bool RegexEquals(JsonNode input, JsonNode pattern) + { + var inputValueKind = input.GetSafeJsonValueKind(); + var patternValueKind = pattern.GetSafeJsonValueKind(); + if (inputValueKind != JsonValueKind.String || patternValueKind != JsonValueKind.String) + { + return false; + } + + string regexText = pattern.GetValue(); + int patternOptionDelimiterIndex = regexText.LastIndexOf('/'); + + string patternText = regexText.Substring(1, patternOptionDelimiterIndex - 1); + string optionsText = regexText.Substring(patternOptionDelimiterIndex + 1); + + return Regex.IsMatch(input.GetValue(), patternText, GetRegexOptions(optionsText)); + } + + internal static bool EqualsWithStringCoercion(JsonNode value, JsonNode queryValue) + { + if (value.Equals(queryValue)) + { + return true; + } + + // Handle comparing an integer with a float + // e.g. Comparing 1 and 1.0 + + var valueKind = value.GetSafeJsonValueKind(); + var queryValueKind = queryValue.GetSafeJsonValueKind(); + + if (valueKind == JsonValueKind.Number && queryValueKind == JsonValueKind.Number) + { + return value.GetDouble() == queryValue.GetDouble(); + } + + if (queryValueKind != JsonValueKind.String) + { + return false; + } + + return string.Equals(value.ToString(), queryValue.GetString(), StringComparison.Ordinal); + } + + internal static bool EqualsWithStrictMatch(JsonNode value, JsonNode queryValue) + { + // we handle floats and integers the exact same way, so they are pseudo equivalent + + JsonValueKind thisValueKind = value.GetSafeJsonValueKind(); + JsonValueKind queryValueKind = queryValue.GetSafeJsonValueKind(); + + if (thisValueKind != queryValueKind) + { + return false; + } + + // Handle comparing an integer with a float + // e.g. Comparing 1 and 1.0 + if (thisValueKind == JsonValueKind.Number && queryValueKind == JsonValueKind.Number) + { + return value.GetValue() == queryValue.GetValue(); + } + + if (thisValueKind == JsonValueKind.String && queryValueKind == JsonValueKind.String) + { + return value.GetValue() == queryValue.GetValue(); + } + + if (thisValueKind == JsonValueKind.Null && queryValueKind == JsonValueKind.Null) + { + return true; + } + + if (thisValueKind == JsonValueKind.Undefined && queryValueKind == JsonValueKind.Undefined) + { + return true; + } + + if ((thisValueKind == JsonValueKind.False || thisValueKind == JsonValueKind.True) && + (queryValueKind == JsonValueKind.False || queryValueKind == JsonValueKind.True)) + { + return value.GetValue() == queryValue.GetValue(); + } + + return value.Equals(queryValue); + } + } +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/QueryExpression.cs b/src/JsonDocumentPath/QueryExpression.cs index ba7a6ef..ede3769 100644 --- a/src/JsonDocumentPath/QueryExpression.cs +++ b/src/JsonDocumentPath/QueryExpression.cs @@ -21,7 +21,7 @@ internal enum QueryOperator StrictNotEquals = 12 } - internal abstract class QueryExpression + internal abstract partial class QueryExpression { internal QueryOperator Operator; @@ -33,7 +33,7 @@ public QueryExpression(QueryOperator @operator) public abstract bool IsMatch(JsonElement root, JsonElement t); } - internal class CompositeExpression : QueryExpression + internal partial class CompositeExpression : QueryExpression { public List Expressions { get; set; } @@ -55,6 +55,7 @@ public override bool IsMatch(JsonElement root, JsonElement t) } } return true; + case QueryOperator.Or: foreach (QueryExpression e in Expressions) { @@ -64,13 +65,14 @@ public override bool IsMatch(JsonElement root, JsonElement t) } } return false; + default: throw new ArgumentOutOfRangeException(); } } } - internal class BooleanQueryExpression : QueryExpression + internal partial class BooleanQueryExpression : QueryExpression { public readonly object Left; public readonly object? Right; @@ -139,54 +141,63 @@ private bool MatchTokens(JsonElement leftResult, JsonElement rightResult) return true; } break; + case QueryOperator.Equals: if (EqualsWithStringCoercion(leftResult, rightResult)) { return true; } break; + case QueryOperator.StrictEquals: if (EqualsWithStrictMatch(leftResult, rightResult)) { return true; } break; + case QueryOperator.NotEquals: if (!EqualsWithStringCoercion(leftResult, rightResult)) { return true; } break; + case QueryOperator.StrictNotEquals: if (!EqualsWithStrictMatch(leftResult, rightResult)) { return true; } break; + case QueryOperator.GreaterThan: if (leftResult.CompareTo(rightResult) > 0) { return true; } break; + case QueryOperator.GreaterThanOrEquals: if (leftResult.CompareTo(rightResult) >= 0) { return true; } break; + case QueryOperator.LessThan: if (leftResult.CompareTo(rightResult) < 0) { return true; } break; + case QueryOperator.LessThanOrEquals: if (leftResult.CompareTo(rightResult) <= 0) { return true; } break; + case QueryOperator.Exists: return true; } @@ -247,35 +258,39 @@ internal static bool EqualsWithStringCoercion(JsonElement value, JsonElement que internal static bool EqualsWithStrictMatch(JsonElement value, JsonElement queryValue) { // we handle floats and integers the exact same way, so they are pseudo equivalent - if (value.ValueKind != queryValue.ValueKind) + + JsonValueKind thisValueKind = value.ValueKind; + JsonValueKind queryValueKind = queryValue.ValueKind; + + if (thisValueKind != queryValueKind) { return false; } // Handle comparing an integer with a float // e.g. Comparing 1 and 1.0 - if (value.ValueKind == JsonValueKind.Number && queryValue.ValueKind == JsonValueKind.Number) + if (thisValueKind == JsonValueKind.Number && queryValueKind == JsonValueKind.Number) { return value.GetDouble() == queryValue.GetDouble(); } - if (value.ValueKind == JsonValueKind.String && queryValue.ValueKind == JsonValueKind.String) + if (thisValueKind == JsonValueKind.String && queryValueKind == JsonValueKind.String) { return value.GetString() == queryValue.GetString(); } - if (value.ValueKind == JsonValueKind.Null && queryValue.ValueKind == JsonValueKind.Null) + if (thisValueKind == JsonValueKind.Null && queryValueKind == JsonValueKind.Null) { return true; } - if (value.ValueKind == JsonValueKind.Undefined && queryValue.ValueKind == JsonValueKind.Undefined) + if (thisValueKind == JsonValueKind.Undefined && queryValueKind == JsonValueKind.Undefined) { return true; } - if ((value.ValueKind == JsonValueKind.False || value.ValueKind == JsonValueKind.True) && - (queryValue.ValueKind == JsonValueKind.False || queryValue.ValueKind == JsonValueKind.True)) + if ((thisValueKind == JsonValueKind.False || thisValueKind == JsonValueKind.True) && + (queryValueKind == JsonValueKind.False || queryValueKind == JsonValueKind.True)) { return value.GetBoolean() == queryValue.GetBoolean(); } @@ -294,12 +309,15 @@ internal static RegexOptions GetRegexOptions(string optionsText) case 'i': options |= RegexOptions.IgnoreCase; break; + case 'm': options |= RegexOptions.Multiline; break; + case 's': options |= RegexOptions.Singleline; break; + case 'x': options |= RegexOptions.ExplicitCapture; break; @@ -309,4 +327,4 @@ internal static RegexOptions GetRegexOptions(string optionsText) return options; } } -} +} \ No newline at end of file