From 96899ac5b144d0ab0396c7c26017536b2db7eed2 Mon Sep 17 00:00:00 2001 From: yingpanwang <49632937+yingpanwang@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:55:25 +0800 Subject: [PATCH 1/5] Feature/jsonnodepath (#1) * support JsonNode jsonpath query * fix unit tests bugs * fix unit test bugs * add JsonNodeRef test --- .../JPathExecuteTests.cs | 7 +- .../JsonDocumentExtensions.cs | 17 +- .../JsonDocumentPath.Test.csproj | 2 +- .../JsonNodePath/JsonNodeParseTests.cs | 736 ++++++++ .../JsonNodePath/JsonNodePathExecuteTests.cs | 1485 +++++++++++++++++ .../JsonNodeQueryExpressionTests.cs | 181 ++ .../JsonNodePath/JsonNodeRefTests.cs | 40 + src/JsonDocumentPath/Extensions.cs | 458 +++-- .../Filters/ArrayIndexFilter.cs | 37 +- .../Filters/ArrayMultipleIndexFilter.cs | 19 +- .../Filters/ArraySliceFilter.cs | 67 +- src/JsonDocumentPath/Filters/FieldFilter.cs | 42 +- .../Filters/FieldMultipleFilter.cs | 35 +- src/JsonDocumentPath/Filters/QueryFilter.cs | 32 +- .../Filters/QueryScanFilter.cs | 17 +- src/JsonDocumentPath/Filters/RootFilter.cs | 8 +- src/JsonDocumentPath/Filters/ScanFilter.cs | 18 +- .../Filters/ScanMultipleFilter.cs | 25 +- src/JsonDocumentPath/JsonDocumentPath.cs | 31 +- src/JsonDocumentPath/JsonDocumentPath.csproj | 3 +- .../JsonDocumentPathExtensions.cs | 293 ++-- src/JsonDocumentPath/JsonNodeExtensions.cs | 54 + src/JsonDocumentPath/JsonNodePath.cs | 926 ++++++++++ src/JsonDocumentPath/PathFilter.cs | 63 +- src/JsonDocumentPath/QueryExpression.cs | 296 +++- 25 files changed, 4585 insertions(+), 307 deletions(-) create mode 100644 src/JsonDocumentPath.Test/JsonNodePath/JsonNodeParseTests.cs create mode 100644 src/JsonDocumentPath.Test/JsonNodePath/JsonNodePathExecuteTests.cs create mode 100644 src/JsonDocumentPath.Test/JsonNodePath/JsonNodeQueryExpressionTests.cs create mode 100644 src/JsonDocumentPath.Test/JsonNodePath/JsonNodeRefTests.cs create mode 100644 src/JsonDocumentPath/JsonNodeExtensions.cs create mode 100644 src/JsonDocumentPath/JsonNodePath.cs 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..2080b28 100644 --- a/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj +++ b/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false 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..b50803a 100644 --- a/src/JsonDocumentPath/Extensions.cs +++ b/src/JsonDocumentPath/Extensions.cs @@ -1,252 +1,408 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; -namespace System.Text.Json +namespace System.Text.Json; + +internal static class Extensions { - internal static class Extensions + public static bool IsValue(this JsonElement src) + { + return src.ValueKind == JsonValueKind.False || + src.ValueKind == JsonValueKind.True || + src.ValueKind == JsonValueKind.String || + src.ValueKind == JsonValueKind.Number || + src.ValueKind == JsonValueKind.Null || + src.ValueKind == JsonValueKind.Undefined; + } + + 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 JsonElement src) { - public static bool IsValue(this JsonElement src) + return src.ValueKind == JsonValueKind.Array || src.ValueKind == JsonValueKind.Object; + } + + public static bool IsContainer(this JsonNode src) + { + var nodeValueKind = src.GetSafeJsonValueKind(); + + return nodeValueKind == JsonValueKind.Array || nodeValueKind == JsonValueKind.Object; + } + + public static bool IsContainer(this JsonElement? src) + { + if (src.HasValue) { - return src.ValueKind == JsonValueKind.False || - src.ValueKind == JsonValueKind.True || - src.ValueKind == JsonValueKind.String || - src.ValueKind == JsonValueKind.Number || - src.ValueKind == JsonValueKind.Null || - src.ValueKind == JsonValueKind.Undefined; + return src.Value.IsContainer(); } - public static bool IsContainer(this JsonElement src) + return false; + } + + public static bool TryGetFirstFromObject(this JsonElement? src, out JsonProperty? element) + { + element = null; + if (src.HasValue) { - return src.ValueKind == JsonValueKind.Array || src.ValueKind == JsonValueKind.Object; + return src.Value.TryGetFirstFromObject(out element); } - public static bool IsContainer(this JsonElement? src) + return false; + } + + 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) { - if (src.HasValue) + var currentObject = src.AsObject(); + var enumerator = currentObject.GetEnumerator(); + if (enumerator.MoveNext()) { - return src.Value.IsContainer(); + element = enumerator.Current.Value; + return true; } - return false; } + return false; + } - public static bool TryGetFirstFromObject(this JsonElement? src, out JsonProperty? element) + public static bool TryMoveNextFromObject(this JsonElement src, int cycle, out JsonProperty? element) + { + element = null; + if (src.ValueKind == JsonValueKind.Object) { - element = null; - if (src.HasValue) + var currentObject = src.EnumerateObject(); + for (int i = 0; i < cycle; i++) { - return src.Value.TryGetFirstFromObject(out element); + currentObject.MoveNext(); } + element = currentObject.Current; + 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 bool TryMoveNextFromObject(this JsonElement src, int cycle, out JsonProperty? element) + public static bool TryGetFirstFromObject(this JsonElement src, out JsonProperty? element) + { + element = null; + if (src.ValueKind == JsonValueKind.Object) { - element = null; - if (src.ValueKind == JsonValueKind.Object) + var currentObject = src.EnumerateObject(); + if (currentObject.MoveNext()) { - var currentObject = src.EnumerateObject(); - for (int i = 0; i < cycle; i++) - { - currentObject.MoveNext(); - } element = currentObject.Current; return true; } - return false; } + return false; + } + + public static bool TryGetFirstFromArray(this JsonElement? src, out JsonElement? element) + { + element = null; + if (src.HasValue) + { + return src.Value.TryGetFirstFromArray(out element); + } + return false; + } - public static bool TryGetFirstFromObject(this JsonElement src, out JsonProperty? element) + public static bool TryGetFirstFromArray(this JsonElement src, out JsonElement? element) + { + element = null; + if (src.ValueKind == JsonValueKind.Array && src.GetArrayLength() > 0) { - element = null; - if (src.ValueKind == JsonValueKind.Object) + if (src.EnumerateArray().MoveNext()) { - var currentObject = src.EnumerateObject(); - if (currentObject.MoveNext()) - { - element = currentObject.Current; - return true; - } + element = src.EnumerateArray().Current; + return true; } - return false; } + return false; + } + + public static IEnumerable DescendantsAndSelf(this IEnumerable source) + { + return source.SelectMany(j => j.DescendantsAndSelf()); + } + + public static IEnumerable DescendantElements(this JsonElement src) + { + return GetDescendantElementsCore(src, false); + } + + public static IEnumerable DescendantsAndSelf(this JsonElement src) + { + return GetDescendantElementsCore(src, true); + } - public static bool TryGetFirstFromArray(this JsonElement? src, out JsonElement? element) + public static IEnumerable ChildrenTokens(this JsonElement src) + { + if (src.ValueKind == JsonValueKind.Object) { - element = null; - if (src.HasValue) + foreach (var item in src.EnumerateObject()) { - return src.Value.TryGetFirstFromArray(out element); + yield return item.Value; } - return false; } - public static bool TryGetFirstFromArray(this JsonElement src, out JsonElement? element) + if (src.ValueKind == JsonValueKind.Array) { - element = null; - if (src.ValueKind == JsonValueKind.Array && src.GetArrayLength() > 0) + foreach (var item in src.EnumerateArray()) { - if (src.EnumerateArray().MoveNext()) - { - element = src.EnumerateArray().Current; - return true; - } + yield return item; } - return false; } + } + + public static IEnumerable ChildrenNodes(this JsonNode src) + { + var srcValueKind = src.GetSafeJsonValueKind(); - public static IEnumerable DescendantsAndSelf(this IEnumerable source) + if (srcValueKind == JsonValueKind.Object) { - return source.SelectMany(j => j.DescendantsAndSelf()); + var srcObject = src.AsObject(); + foreach (var item in srcObject) + { + yield return item.Value; + } } - public static IEnumerable DescendantElements(this JsonElement src) + if (srcValueKind == JsonValueKind.Array) { - return GetDescendantElementsCore(src, false); + var srcArray = src.AsArray(); + foreach (var item in srcArray) + { + yield return item; + } } + } - public static IEnumerable DescendantsAndSelf(this JsonElement src) + internal static IEnumerable GetDescendantElementsCore(JsonElement src, bool self) + { + if (self) { - return GetDescendantElementsCore(src, true); + yield return src; } - public static IEnumerable ChildrenTokens(this JsonElement src) + foreach (JsonElement o in src.ChildrenTokens()) { - if (src.ValueKind == JsonValueKind.Object) + yield return o; + if (o.IsContainer()) { - foreach (var item in src.EnumerateObject()) + foreach (JsonElement d in o.DescendantElements()) { - yield return item.Value; + yield return d; } } + } + } - if (src.ValueKind == JsonValueKind.Array) + public static IEnumerable GetDescendantProperties(this JsonElement src) + { + return GetDescendantPropertiesCore(src); + } + + internal static IEnumerable GetDescendantPropertiesCore(JsonElement src) + { + foreach (JsonProperty o in src.ChildrenPropertiesCore()) + { + yield return o; + if (o.Value.IsContainer()) { - foreach (var item in src.EnumerateArray()) + foreach (JsonProperty d in o.Value.GetDescendantProperties()) { - yield return item; + yield return d; } } } + } - internal static IEnumerable GetDescendantElementsCore(JsonElement src, bool self) + internal static IEnumerable ChildrenPropertiesCore(this JsonElement src) + { + if (src.ValueKind == JsonValueKind.Object) { - if (self) + foreach (var item in src.EnumerateObject()) { - yield return src; + yield return item; } + } - foreach (JsonElement o in src.ChildrenTokens()) + if (src.ValueKind == JsonValueKind.Array) + { + foreach (var item in src.EnumerateArray()) { - yield return o; - if (o.IsContainer()) + foreach (JsonProperty o in item.ChildrenPropertiesCore()) { - foreach (JsonElement d in o.DescendantElements()) - { - yield return d; - } + yield return o; } } } + } - public static IEnumerable GetDescendantProperties(this JsonElement src) + public static int CompareTo(this JsonElement value, JsonElement queryValue) + { + JsonValueKind comparisonType = (value.ValueKind == JsonValueKind.String && value.ValueKind != queryValue.ValueKind) + ? queryValue.ValueKind + : value.ValueKind; + return Compare(comparisonType, value, queryValue); + } + + 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, JsonElement objA, JsonElement objB) + { + /*Same types*/ + if (objA.ValueKind == JsonValueKind.Null && objB.ValueKind == JsonValueKind.Null) { - return GetDescendantPropertiesCore(src); + return 0; } - - internal static IEnumerable GetDescendantPropertiesCore(JsonElement src) + if (objA.ValueKind == JsonValueKind.Undefined && objB.ValueKind == JsonValueKind.Undefined) { - foreach (JsonProperty o in src.ChildrenPropertiesCore()) - { - yield return o; - if (o.Value.IsContainer()) - { - foreach (JsonProperty d in o.Value.GetDescendantProperties()) - { - yield return d; - } - } - } + return 0; } - - internal static IEnumerable ChildrenPropertiesCore(this JsonElement src) + if (objA.ValueKind == JsonValueKind.True && objB.ValueKind == JsonValueKind.True) + { + return 0; + } + if (objA.ValueKind == JsonValueKind.False && objB.ValueKind == JsonValueKind.False) + { + return 0; + } + if (objA.ValueKind == JsonValueKind.Number && objB.ValueKind == JsonValueKind.Number) + { + return objA.GetDouble().CompareTo(objB.GetDouble()); + } + if (objA.ValueKind == JsonValueKind.String && objB.ValueKind == JsonValueKind.String) + { + return objA.GetString().CompareTo(objB.GetString()); + } + //When objA is a number and objB is not. + if (objA.ValueKind == JsonValueKind.Number) { - if (src.ValueKind == JsonValueKind.Object) + var valueObjA = objA.GetDouble(); + if (objB.ValueKind == JsonValueKind.String) { - foreach (var item in src.EnumerateObject()) + if (double.TryParse(objB.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) { - yield return item; + return valueObjA.CompareTo(queryValueTyped); } } - - if (src.ValueKind == JsonValueKind.Array) + } + //When objA is a string and objB is not. + if (objA.ValueKind == JsonValueKind.String) + { + if (objB.ValueKind == JsonValueKind.Number) { - foreach (var item in src.EnumerateArray()) + if (double.TryParse(objA.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) { - foreach (JsonProperty o in item.ChildrenPropertiesCore()) - { - yield return o; - } + return valueTyped.CompareTo(objB.GetDouble()); } } } + return -1; + } - public static int CompareTo(this JsonElement value, JsonElement 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) { - JsonValueKind comparisonType = (value.ValueKind == JsonValueKind.String && value.ValueKind != queryValue.ValueKind) - ? queryValue.ValueKind - : value.ValueKind; - return Compare(comparisonType, value, queryValue); + return 0; } - - private static int Compare(JsonValueKind valueType, JsonElement objA, JsonElement objB) + if (aValueKind == JsonValueKind.Undefined && bValueKind == JsonValueKind.Undefined) { - /*Same types*/ - if (objA.ValueKind == JsonValueKind.Null && objB.ValueKind == JsonValueKind.Null) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.Undefined && objB.ValueKind == JsonValueKind.Undefined) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.True && objB.ValueKind == JsonValueKind.True) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.False && objB.ValueKind == JsonValueKind.False) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.Number && objB.ValueKind == JsonValueKind.Number) - { - return objA.GetDouble().CompareTo(objB.GetDouble()); - } - if (objA.ValueKind == JsonValueKind.String && objB.ValueKind == JsonValueKind.String) - { - return objA.GetString().CompareTo(objB.GetString()); - } - //When objA is a number and objB is not. - if (objA.ValueKind == JsonValueKind.Number) + 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) { - var valueObjA = objA.GetDouble(); - if (objB.ValueKind == JsonValueKind.String) + if (double.TryParse(objB.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) { - if (double.TryParse(objB.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) - { - return valueObjA.CompareTo(queryValueTyped); - } + return valueObjA.CompareTo(queryValueTyped); } } - //When objA is a string and objB is not. - if (objA.ValueKind == JsonValueKind.String) + } + //When objA is a string and objB is not. + if (aValueKind == JsonValueKind.String) + { + if (bValueKind == JsonValueKind.Number) { - if (objB.ValueKind == JsonValueKind.Number) + if (double.TryParse(objA.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) { - if (double.TryParse(objA.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) - { - return valueTyped.CompareTo(objB.GetDouble()); - } + return valueTyped.CompareTo(objB.GetDouble()); } } - return -1; } + return -1; } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs b/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs index c48a64a..0dc03c8 100644 --- a/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs +++ b/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -38,5 +39,39 @@ internal class ArrayIndexFilter : PathFilter } } } + + 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}."); + } + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs index bbcf52c..e83244a 100644 --- a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs +++ b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -26,5 +27,21 @@ public ArrayMultipleIndexFilter(List indexes) } } } + + 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; + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ArraySliceFilter.cs b/src/JsonDocumentPath/Filters/ArraySliceFilter.cs index 3012c59..5c0feda 100644 --- a/src/JsonDocumentPath/Filters/ArraySliceFilter.cs +++ b/src/JsonDocumentPath/Filters/ArraySliceFilter.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { internal 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) @@ -70,6 +73,68 @@ internal class ArraySliceFilter : PathFilter } } + 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}."); + } + } + } + } + private bool IsValid(int index, int stopIndex, bool positiveStep) { if (positiveStep) @@ -80,4 +145,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.cs b/src/JsonDocumentPath/Filters/FieldFilter.cs index 1087b98..2b48e0a 100644 --- a/src/JsonDocumentPath/Filters/FieldFilter.cs +++ b/src/JsonDocumentPath/Filters/FieldFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -48,5 +49,44 @@ public FieldFilter(string? name) } } } + + 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 t.ChildrenNodes()) + { + yield return p; + } + } + } + else + { + if (errorWhenNoMatch) + { + throw new JsonException($"Property '{Name ?? "*"}' not valid on {t.GetType().Name}."); + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs b/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs index 538b060..156374a 100644 --- a/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs +++ b/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -42,5 +43,37 @@ public FieldMultipleFilter(List names) } } } + + 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}."); + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/QueryFilter.cs b/src/JsonDocumentPath/Filters/QueryFilter.cs index 2b05f4f..d0a52cf 100644 --- a/src/JsonDocumentPath/Filters/QueryFilter.cs +++ b/src/JsonDocumentPath/Filters/QueryFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -37,5 +38,34 @@ public QueryFilter(QueryExpression expression) } } } + + 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; + } + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/QueryScanFilter.cs b/src/JsonDocumentPath/Filters/QueryScanFilter.cs index 810f5db..d2e9dab 100644 --- a/src/JsonDocumentPath/Filters/QueryScanFilter.cs +++ b/src/JsonDocumentPath/Filters/QueryScanFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -24,5 +25,19 @@ public QueryScanFilter(QueryExpression expression) } } } + + 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; + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/RootFilter.cs b/src/JsonDocumentPath/Filters/RootFilter.cs index f2a37ae..06b22ba 100644 --- a/src/JsonDocumentPath/Filters/RootFilter.cs +++ b/src/JsonDocumentPath/Filters/RootFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -14,5 +15,10 @@ private RootFilter() { return new JsonElement?[1] { root }; } + + public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) + { + return [root]; + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ScanFilter.cs b/src/JsonDocumentPath/Filters/ScanFilter.cs index 3b75ef7..6bba789 100644 --- a/src/JsonDocumentPath/Filters/ScanFilter.cs +++ b/src/JsonDocumentPath/Filters/ScanFilter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Linq; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -25,5 +25,19 @@ public ScanFilter(string? name) } } } + + 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; + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs b/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs index dc7d35a..3d1b384 100644 --- a/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs +++ b/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -32,5 +33,27 @@ public ScanMultipleFilter(List names) } } } + + 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; + } + } + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonDocumentPath.cs b/src/JsonDocumentPath/JsonDocumentPath.cs index 27f8b7c..d3a45f2 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.cs +++ b/src/JsonDocumentPath/JsonDocumentPath.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Globalization; -using System.Text.RegularExpressions; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -97,16 +97,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 +132,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 +663,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 +759,7 @@ private string ReadRegexString() } _currentIndex++; - sb.Append(currentChar); + sb.Append(currentChar); } else { @@ -902,6 +913,11 @@ private void EnsureLength(string message) return Evaluate(Filters, root, t, errorWhenNoMatch); } + internal IEnumerable Evaluate(JsonNode root, JsonNode t, bool errorWhenNoMatch) + { + return Evaluate(Filters, root, t, errorWhenNoMatch); + } + internal static IEnumerable Evaluate(List filters, JsonElement root, JsonElement t, bool errorWhenNoMatch) { IEnumerable current = new JsonElement?[] { t }; @@ -912,5 +928,16 @@ private void EnsureLength(string message) return current; } + + 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..089feee 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.csproj +++ b/src/JsonDocumentPath/JsonDocumentPath.csproj @@ -1,7 +1,7 @@ - netstandard2.1;net6.0 + net8.0 1.0.3 1.0.0.3 1.0.0.3 @@ -15,6 +15,7 @@ JsonDocumentPath, Json, JsonDocument, JsonPath MIT https://github.com/azambrano/JsonDocumentPath + enable diff --git a/src/JsonDocumentPath/JsonDocumentPathExtensions.cs b/src/JsonDocumentPath/JsonDocumentPathExtensions.cs index 9d93404..5dc14f7 100644 --- a/src/JsonDocumentPath/JsonDocumentPathExtensions.cs +++ b/src/JsonDocumentPath/JsonDocumentPathExtensions.cs @@ -1,155 +1,192 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; -namespace System.Text.Json +namespace System.Text.Json; + +public static class JsonDocumentPathExtensions { - public static class JsonDocumentPathExtensions + /// + /// Selects a collection of elements using a JSONPath expression. + /// + /// + /// A that contains a JSONPath expression. + /// + /// An of that contains the selected elements. + + public static IEnumerable SelectElements(this JsonDocument src, string path) { - /// - /// Selects a collection of elements using a JSONPath expression. - /// - /// - /// A that contains a JSONPath expression. - /// - /// An of that contains the selected elements. + return SelectElements(src.RootElement, path, false); + } - public static IEnumerable SelectElements(this JsonDocument src, string path) - { - return SelectElements(src.RootElement, path, false); - } + /// + /// Selects a collection of elements using a JSONPath expression. + /// + /// + /// A that contains a JSONPath expression. + /// + /// An of that contains the selected elements. - /// - /// Selects a collection of elements using a JSONPath expression. - /// - /// - /// A that contains a JSONPath expression. - /// - /// An of that contains the selected elements. + public static IEnumerable SelectElements(this JsonElement src, string path) + { + return SelectElements(src, path, false); + } - public static IEnumerable SelectElements(this JsonElement src, string path) - { - return SelectElements(src, path, false); - } + /// + /// 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 SelectElements(this JsonElement src, string path, bool errorWhenNoMatch) + { + var parser = new JsonDocumentPath(path); + return parser.Evaluate(src, src, errorWhenNoMatch); + } - /// - /// 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 SelectElements(this JsonElement src, string path, bool errorWhenNoMatch) - { - var parser = new JsonDocumentPath(path); - return parser.Evaluate(src, src, errorWhenNoMatch); - } + /// + /// 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 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 SelectElements(this JsonDocument src, string path, bool 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)) { - var parser = new JsonDocumentPath(path); - return parser.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch); + if (el != null) + { + throw new JsonException("Path returned multiple tokens."); + } + el = t; } + return el; + } - /// - /// Selects a using a JSONPath expression. Selects the token that matches the object path. - /// - /// - /// A that contains a JSONPath expression. - /// - /// A , or null. - public static JsonElement? SelectElement(this JsonDocument src, string path) - { - return SelectElement(src.RootElement, path, false); - } + /// + /// 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 SelectElements(this JsonDocument src, string path, bool errorWhenNoMatch) + { + var parser = new JsonDocumentPath(path); + return parser.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch); + } - /// - /// Selects a using a JSONPath expression. Selects the token that matches the object path. - /// - /// - /// A that contains a JSONPath expression. - /// - /// A , or null. - public static JsonElement? SelectElement(this JsonElement src, string path) - { - return SelectElement(src, path, false); - } + /// + /// Selects a using a JSONPath expression. Selects the token that matches the object path. + /// + /// + /// A that contains a JSONPath expression. + /// + /// A , or null. + public static JsonElement? SelectElement(this JsonDocument src, string path) + { + return SelectElement(src.RootElement, path, false); + } + + /// + /// Selects a using a JSONPath expression. Selects the token that matches the object path. + /// + /// + /// A that contains a JSONPath expression. + /// + /// A , or null. + public static JsonElement? SelectElement(this JsonElement src, string path) + { + return SelectElement(src, path, false); + } - /// - /// 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 JsonElement? SelectElement(this JsonDocument src, string path, bool 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 JsonElement? SelectElement(this JsonDocument src, string path, bool errorWhenNoMatch) + { + var p = new JsonDocumentPath(path); + JsonElement? el = null; + foreach (JsonElement t in p.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch)) { - var p = new JsonDocumentPath(path); - JsonElement? el = null; - foreach (JsonElement t in p.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch)) + if (el != null) { - if (el != null) - { - throw new JsonException("Path returned multiple tokens."); - } - el = t; + throw new JsonException("Path returned multiple tokens."); } - return el; + el = t; } + return el; + } - /// - /// 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 JsonElement? SelectElement(this JsonElement src, string path, bool 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 JsonElement? SelectElement(this JsonElement src, string path, bool errorWhenNoMatch) + { + var p = new JsonDocumentPath(path); + JsonElement? el = null; + foreach (JsonElement t in p.Evaluate(src, src, errorWhenNoMatch)) { - var p = new JsonDocumentPath(path); - JsonElement? el = null; - foreach (JsonElement t in p.Evaluate(src, src, errorWhenNoMatch)) + if (el != null) { - if (el != null) - { - throw new JsonException("Path returned multiple tokens."); - } - el = t; + throw new JsonException("Path returned multiple tokens."); } - return el; + el = t; } + return el; + } - /// - /// Gets the value of the element as a System.Object. - /// - public static object GetObjectValue(this JsonElement src) + /// + /// Gets the value of the element as a System.Object. + /// + public static object GetObjectValue(this JsonElement src) + { + if (src.ValueKind == JsonValueKind.Null) { - if (src.ValueKind == JsonValueKind.Null) - { - return null; - } - if (src.ValueKind == JsonValueKind.String) - { - return src.GetString(); - } - if (src.ValueKind == JsonValueKind.False || src.ValueKind == JsonValueKind.True) - { - return src.GetBoolean(); - } - if (src.ValueKind == JsonValueKind.Number) - { - return src.GetDouble(); - } - return src.GetRawText(); + return null; + } + if (src.ValueKind == JsonValueKind.String) + { + return src.GetString(); + } + if (src.ValueKind == JsonValueKind.False || src.ValueKind == JsonValueKind.True) + { + return src.GetBoolean(); + } + if (src.ValueKind == JsonValueKind.Number) + { + return src.GetDouble(); } + 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..c202c26 --- /dev/null +++ b/src/JsonDocumentPath/JsonNodeExtensions.cs @@ -0,0 +1,54 @@ +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(); + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonNodePath.cs b/src/JsonDocumentPath/JsonNodePath.cs new file mode 100644 index 0000000..bf26354 --- /dev/null +++ b/src/JsonDocumentPath/JsonNodePath.cs @@ -0,0 +1,926 @@ +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; + } +} \ No newline at end of file diff --git a/src/JsonDocumentPath/PathFilter.cs b/src/JsonDocumentPath/PathFilter.cs index bc804a5..b71f18c 100644 --- a/src/JsonDocumentPath/PathFilter.cs +++ b/src/JsonDocumentPath/PathFilter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Nodes; namespace System.Text.Json { @@ -6,6 +7,8 @@ public abstract class PathFilter { public abstract IEnumerable ExecuteFilter(JsonElement root, IEnumerable current, bool errorWhenNoMatch); + public abstract IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch); + protected static JsonElement? GetTokenIndex(JsonElement t, bool errorWhenNoMatch, int index) { if (t.ValueKind == JsonValueKind.Array) @@ -60,5 +63,63 @@ public abstract class PathFilter } } } + + 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; + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/JsonDocumentPath/QueryExpression.cs b/src/JsonDocumentPath/QueryExpression.cs index ba7a6ef..1e3a592 100644 --- a/src/JsonDocumentPath/QueryExpression.cs +++ b/src/JsonDocumentPath/QueryExpression.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; namespace System.Text.Json @@ -31,6 +32,8 @@ public QueryExpression(QueryOperator @operator) } public abstract bool IsMatch(JsonElement root, JsonElement t); + + public abstract bool IsMatch(JsonNode root, JsonNode t); } internal class CompositeExpression : QueryExpression @@ -55,6 +58,7 @@ public override bool IsMatch(JsonElement root, JsonElement t) } } return true; + case QueryOperator.Or: foreach (QueryExpression e in Expressions) { @@ -64,6 +68,36 @@ public override bool IsMatch(JsonElement root, JsonElement t) } } return false; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + 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(); } @@ -96,6 +130,30 @@ public BooleanQueryExpression(QueryOperator @operator, object left, object? righ return Enumerable.Empty(); } + 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(JsonElement root, JsonElement t) { if (Operator == QueryOperator.Exists) @@ -127,6 +185,37 @@ public override bool IsMatch(JsonElement root, JsonElement t) return false; } + 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(JsonElement leftResult, JsonElement rightResult) { if (leftResult.IsValue() && rightResult.IsValue()) @@ -139,54 +228,151 @@ 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; + } + } + 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 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; } @@ -222,6 +408,24 @@ private static bool RegexEquals(JsonElement input, JsonElement pattern) return Regex.IsMatch(input.GetString(), patternText, GetRegexOptions(optionsText)); } + 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(JsonElement value, JsonElement queryValue) { if (value.Equals(queryValue)) @@ -244,38 +448,111 @@ internal static bool EqualsWithStringCoercion(JsonElement value, JsonElement que return string.Equals(value.ToString(), queryValue.GetString(), StringComparison.Ordinal); } + 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); + } + 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 +571,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 +589,4 @@ internal static RegexOptions GetRegexOptions(string optionsText) return options; } } -} +} \ No newline at end of file From 079a1da584853bbd71bc78e2f20bb4cd5eae75c9 Mon Sep 17 00:00:00 2001 From: yingpanwang <49632937+yingpanwang@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:26:20 +0800 Subject: [PATCH 2/5] support JsonNode jsonpath query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * support JsonNode jsonpath query * fix unit tests bugs * fix unit test bugs * add JsonNodeRef test * nullable markup * splite JsonNode support Imp to partial class * 解决合并冲突 --- src/JsonDocumentPath/Extensions.cs | 581 +++++++++--------- .../Filters/ArrayIndexFilter.JsonNode.cs | 46 ++ .../Filters/ArrayIndexFilter.cs | 37 +- .../ArrayMultipleIndexFilter.JsonNode.cs | 28 + .../Filters/ArrayMultipleIndexFilter.cs | 19 +- .../Filters/ArraySliceFilter.JsonNode.cs | 74 +++ .../Filters/ArraySliceFilter.cs | 65 +- .../Filters/FieldFilter.JsonNode.cs | 51 ++ src/JsonDocumentPath/Filters/FieldFilter.cs | 42 +- .../Filters/FieldMultipleFilter.JsonNode.cs | 45 ++ .../Filters/FieldMultipleFilter.cs | 35 +- .../Filters/QueryFilter.JsonNode.cs | 41 ++ src/JsonDocumentPath/Filters/QueryFilter.cs | 32 +- .../Filters/QueryScanFilter.JsonNode.cs | 26 + .../Filters/QueryScanFilter.cs | 17 +- .../Filters/RootFilter.JsonNode.cs | 17 + src/JsonDocumentPath/Filters/RootFilter.cs | 8 +- .../Filters/ScanFilter.JsonNode.cs | 26 + src/JsonDocumentPath/Filters/ScanFilter.cs | 17 +- .../Filters/ScanMultipleFilter.JsonNode.cs | 35 ++ .../Filters/ScanMultipleFilter.cs | 25 +- src/JsonDocumentPath/JsonDocumentPath.cs | 33 +- src/JsonDocumentPath/JsonDocumentPath.csproj | 2 + .../JsonDocumentPathExtensions.cs | 293 ++++----- src/JsonDocumentPath/JsonNodeExtensions.cs | 8 +- src/JsonDocumentPath/JsonNodePath.cs | 16 +- .../JsonNodePathExtensions.cs | 51 ++ src/JsonDocumentPath/PathFilter.cs | 18 +- .../QueryExpression.JsonNode.cs | 281 +++++++++ src/JsonDocumentPath/QueryExpression.cs | 268 +------- 30 files changed, 1208 insertions(+), 1029 deletions(-) create mode 100644 src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs create mode 100644 src/JsonDocumentPath/JsonNodePathExtensions.cs create mode 100644 src/JsonDocumentPath/QueryExpression.JsonNode.cs diff --git a/src/JsonDocumentPath/Extensions.cs b/src/JsonDocumentPath/Extensions.cs index b50803a..3a76554 100644 --- a/src/JsonDocumentPath/Extensions.cs +++ b/src/JsonDocumentPath/Extensions.cs @@ -1,408 +1,421 @@ using System.Collections.Generic; using System.Linq; -using System.Text.Json.Nodes; - -namespace System.Text.Json; - -internal static class Extensions -{ - public static bool IsValue(this JsonElement src) - { - return src.ValueKind == JsonValueKind.False || - src.ValueKind == JsonValueKind.True || - src.ValueKind == JsonValueKind.String || - src.ValueKind == JsonValueKind.Number || - src.ValueKind == JsonValueKind.Null || - src.ValueKind == JsonValueKind.Undefined; - } - 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 JsonElement src) - { - return src.ValueKind == JsonValueKind.Array || src.ValueKind == JsonValueKind.Object; - } +#if NET8_0_OR_GREATER - public static bool IsContainer(this JsonNode src) - { - var nodeValueKind = src.GetSafeJsonValueKind(); +using System.Text.Json.Nodes; - return nodeValueKind == JsonValueKind.Array || nodeValueKind == JsonValueKind.Object; - } +#endif - public static bool IsContainer(this JsonElement? src) +namespace System.Text.Json +{ + internal static partial class Extensions { - if (src.HasValue) + public static bool IsValue(this JsonElement src) { - return src.Value.IsContainer(); + return src.ValueKind == JsonValueKind.False || + src.ValueKind == JsonValueKind.True || + src.ValueKind == JsonValueKind.String || + src.ValueKind == JsonValueKind.Number || + src.ValueKind == JsonValueKind.Null || + src.ValueKind == JsonValueKind.Undefined; } - return false; - } - public static bool TryGetFirstFromObject(this JsonElement? src, out JsonProperty? element) - { - element = null; - if (src.HasValue) + public static bool IsContainer(this JsonElement src) { - return src.Value.TryGetFirstFromObject(out element); + return src.ValueKind == JsonValueKind.Array || src.ValueKind == JsonValueKind.Object; } - return false; - } - 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) + public static bool IsContainer(this JsonElement? src) { - var currentObject = src.AsObject(); - var enumerator = currentObject.GetEnumerator(); - if (enumerator.MoveNext()) + if (src.HasValue) { - element = enumerator.Current.Value; - return true; + return src.Value.IsContainer(); } + return false; } - return false; - } - public static bool TryMoveNextFromObject(this JsonElement src, int cycle, out JsonProperty? element) - { - element = null; - if (src.ValueKind == JsonValueKind.Object) + public static bool TryGetFirstFromObject(this JsonElement? src, out JsonProperty? element) { - var currentObject = src.EnumerateObject(); - for (int i = 0; i < cycle; i++) + element = null; + if (src.HasValue) { - currentObject.MoveNext(); + return src.Value.TryGetFirstFromObject(out element); } - element = currentObject.Current; - 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) + public static bool TryMoveNextFromObject(this JsonElement src, int cycle, out JsonProperty? element) { - var currentObject = src.AsObject().GetEnumerator(); - for (int i = 0; i < cycle; i++) + element = null; + if (src.ValueKind == JsonValueKind.Object) { - currentObject.MoveNext(); + var currentObject = src.EnumerateObject(); + for (int i = 0; i < cycle; i++) + { + currentObject.MoveNext(); + } + element = currentObject.Current; + return true; } - element = currentObject.Current.Value; - return true; + return false; } - return false; - } - public static bool TryGetFirstFromObject(this JsonElement src, out JsonProperty? element) - { - element = null; - if (src.ValueKind == JsonValueKind.Object) + public static bool TryGetFirstFromObject(this JsonElement src, out JsonProperty? element) { - var currentObject = src.EnumerateObject(); - if (currentObject.MoveNext()) + element = null; + if (src.ValueKind == JsonValueKind.Object) { - element = currentObject.Current; - return true; + var currentObject = src.EnumerateObject(); + if (currentObject.MoveNext()) + { + element = currentObject.Current; + return true; + } } + return false; } - return false; - } - public static bool TryGetFirstFromArray(this JsonElement? src, out JsonElement? element) - { - element = null; - if (src.HasValue) + public static bool TryGetFirstFromArray(this JsonElement? src, out JsonElement? element) { - return src.Value.TryGetFirstFromArray(out element); + element = null; + if (src.HasValue) + { + return src.Value.TryGetFirstFromArray(out element); + } + return false; } - return false; - } - public static bool TryGetFirstFromArray(this JsonElement src, out JsonElement? element) - { - element = null; - if (src.ValueKind == JsonValueKind.Array && src.GetArrayLength() > 0) + public static bool TryGetFirstFromArray(this JsonElement src, out JsonElement? element) { - if (src.EnumerateArray().MoveNext()) + element = null; + if (src.ValueKind == JsonValueKind.Array && src.GetArrayLength() > 0) { - element = src.EnumerateArray().Current; - return true; + if (src.EnumerateArray().MoveNext()) + { + element = src.EnumerateArray().Current; + return true; + } } + return false; } - return false; - } - public static IEnumerable DescendantsAndSelf(this IEnumerable source) - { - return source.SelectMany(j => j.DescendantsAndSelf()); - } + public static IEnumerable DescendantsAndSelf(this IEnumerable source) + { + return source.SelectMany(j => j.DescendantsAndSelf()); + } - public static IEnumerable DescendantElements(this JsonElement src) - { - return GetDescendantElementsCore(src, false); - } + public static IEnumerable DescendantElements(this JsonElement src) + { + return GetDescendantElementsCore(src, false); + } - public static IEnumerable DescendantsAndSelf(this JsonElement src) - { - return GetDescendantElementsCore(src, true); - } + public static IEnumerable DescendantsAndSelf(this JsonElement src) + { + return GetDescendantElementsCore(src, true); + } - public static IEnumerable ChildrenTokens(this JsonElement src) - { - if (src.ValueKind == JsonValueKind.Object) + public static IEnumerable ChildrenTokens(this JsonElement src) { - foreach (var item in src.EnumerateObject()) + if (src.ValueKind == JsonValueKind.Object) { - yield return item.Value; + foreach (var item in src.EnumerateObject()) + { + yield return item.Value; + } } - } - if (src.ValueKind == JsonValueKind.Array) - { - foreach (var item in src.EnumerateArray()) + if (src.ValueKind == JsonValueKind.Array) { - yield return item; + foreach (var item in src.EnumerateArray()) + { + yield return item; + } } } - } - public static IEnumerable ChildrenNodes(this JsonNode src) - { - var srcValueKind = src.GetSafeJsonValueKind(); - - if (srcValueKind == JsonValueKind.Object) + internal static IEnumerable GetDescendantElementsCore(JsonElement src, bool self) { - var srcObject = src.AsObject(); - foreach (var item in srcObject) + if (self) { - yield return item.Value; + yield return src; } - } - if (srcValueKind == JsonValueKind.Array) - { - var srcArray = src.AsArray(); - foreach (var item in srcArray) + foreach (JsonElement o in src.ChildrenTokens()) { - yield return item; + yield return o; + if (o.IsContainer()) + { + foreach (JsonElement d in o.DescendantElements()) + { + yield return d; + } + } } } - } - internal static IEnumerable GetDescendantElementsCore(JsonElement src, bool self) - { - if (self) + public static IEnumerable GetDescendantProperties(this JsonElement src) { - yield return src; + return GetDescendantPropertiesCore(src); } - foreach (JsonElement o in src.ChildrenTokens()) + internal static IEnumerable GetDescendantPropertiesCore(JsonElement src) { - yield return o; - if (o.IsContainer()) + foreach (JsonProperty o in src.ChildrenPropertiesCore()) { - foreach (JsonElement d in o.DescendantElements()) + yield return o; + if (o.Value.IsContainer()) { - yield return d; + foreach (JsonProperty d in o.Value.GetDescendantProperties()) + { + yield return d; + } } } } - } - public static IEnumerable GetDescendantProperties(this JsonElement src) - { - return GetDescendantPropertiesCore(src); - } - - internal static IEnumerable GetDescendantPropertiesCore(JsonElement src) - { - foreach (JsonProperty o in src.ChildrenPropertiesCore()) + internal static IEnumerable ChildrenPropertiesCore(this JsonElement src) { - yield return o; - if (o.Value.IsContainer()) + if (src.ValueKind == JsonValueKind.Object) { - foreach (JsonProperty d in o.Value.GetDescendantProperties()) + foreach (var item in src.EnumerateObject()) { - yield return d; + yield return item; } } - } - } - internal static IEnumerable ChildrenPropertiesCore(this JsonElement src) - { - if (src.ValueKind == JsonValueKind.Object) - { - foreach (var item in src.EnumerateObject()) + if (src.ValueKind == JsonValueKind.Array) { - yield return item; + foreach (var item in src.EnumerateArray()) + { + foreach (JsonProperty o in item.ChildrenPropertiesCore()) + { + yield return o; + } + } } } - if (src.ValueKind == JsonValueKind.Array) + public static int CompareTo(this JsonElement value, JsonElement queryValue) + { + JsonValueKind comparisonType = (value.ValueKind == JsonValueKind.String && value.ValueKind != queryValue.ValueKind) + ? queryValue.ValueKind + : value.ValueKind; + return Compare(comparisonType, value, queryValue); + } + + private static int Compare(JsonValueKind valueType, JsonElement objA, JsonElement objB) { - foreach (var item in src.EnumerateArray()) + /*Same types*/ + if (objA.ValueKind == JsonValueKind.Null && objB.ValueKind == JsonValueKind.Null) + { + return 0; + } + if (objA.ValueKind == JsonValueKind.Undefined && objB.ValueKind == JsonValueKind.Undefined) + { + return 0; + } + if (objA.ValueKind == JsonValueKind.True && objB.ValueKind == JsonValueKind.True) + { + return 0; + } + if (objA.ValueKind == JsonValueKind.False && objB.ValueKind == JsonValueKind.False) + { + return 0; + } + if (objA.ValueKind == JsonValueKind.Number && objB.ValueKind == JsonValueKind.Number) + { + return objA.GetDouble().CompareTo(objB.GetDouble()); + } + if (objA.ValueKind == JsonValueKind.String && objB.ValueKind == JsonValueKind.String) + { + return objA.GetString().CompareTo(objB.GetString()); + } + //When objA is a number and objB is not. + if (objA.ValueKind == JsonValueKind.Number) + { + var valueObjA = objA.GetDouble(); + if (objB.ValueKind == JsonValueKind.String) + { + if (double.TryParse(objB.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) + { + return valueObjA.CompareTo(queryValueTyped); + } + } + } + //When objA is a string and objB is not. + if (objA.ValueKind == JsonValueKind.String) { - foreach (JsonProperty o in item.ChildrenPropertiesCore()) + if (objB.ValueKind == JsonValueKind.Number) { - yield return o; + if (double.TryParse(objA.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) + { + return valueTyped.CompareTo(objB.GetDouble()); + } } } + return -1; } } - public static int CompareTo(this JsonElement value, JsonElement queryValue) - { - JsonValueKind comparisonType = (value.ValueKind == JsonValueKind.String && value.ValueKind != queryValue.ValueKind) - ? queryValue.ValueKind - : value.ValueKind; - return Compare(comparisonType, value, queryValue); - } +#if NET8_0_OR_GREATER - public static int CompareTo(this JsonNode value, JsonNode queryValue) + internal static partial class Extensions { - JsonValueKind comparisonType = (value.GetSafeJsonValueKind() == JsonValueKind.String && value.GetSafeJsonValueKind() != queryValue.GetSafeJsonValueKind()) - ? queryValue.GetSafeJsonValueKind() - : value.GetSafeJsonValueKind(); + public static bool IsValue(this JsonNode? src) + { + // JsonNode 无法表示 JsonValueKind.Null 但 null 是正常 JsonValue + if (src == null) + return true; - return Compare(comparisonType, value, queryValue); - } + var nodeValueKind = src.GetSafeJsonValueKind(); - private static int Compare(JsonValueKind valueType, JsonElement objA, JsonElement objB) - { - /*Same types*/ - if (objA.ValueKind == JsonValueKind.Null && objB.ValueKind == JsonValueKind.Null) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.Undefined && objB.ValueKind == JsonValueKind.Undefined) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.True && objB.ValueKind == JsonValueKind.True) - { - return 0; + return nodeValueKind == JsonValueKind.False || + nodeValueKind == JsonValueKind.True || + nodeValueKind == JsonValueKind.String || + nodeValueKind == JsonValueKind.Number || + nodeValueKind == JsonValueKind.Null || + nodeValueKind == JsonValueKind.Undefined; } - if (objA.ValueKind == JsonValueKind.False && objB.ValueKind == JsonValueKind.False) - { - return 0; - } - if (objA.ValueKind == JsonValueKind.Number && objB.ValueKind == JsonValueKind.Number) - { - return objA.GetDouble().CompareTo(objB.GetDouble()); - } - if (objA.ValueKind == JsonValueKind.String && objB.ValueKind == JsonValueKind.String) + + public static bool IsContainer(this JsonNode src) { - return objA.GetString().CompareTo(objB.GetString()); + var nodeValueKind = src.GetSafeJsonValueKind(); + + return nodeValueKind == JsonValueKind.Array || nodeValueKind == JsonValueKind.Object; } - //When objA is a number and objB is not. - if (objA.ValueKind == JsonValueKind.Number) + + public static bool TryGetFirstFromObject(this JsonNode? src, out JsonNode? element) { - var valueObjA = objA.GetDouble(); - if (objB.ValueKind == JsonValueKind.String) + element = null; + + if (src == null) + return false; + + var nodeValueKind = src.GetSafeJsonValueKind(); + if (nodeValueKind == JsonValueKind.Object) { - if (double.TryParse(objB.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) + var currentObject = src.AsObject(); + var enumerator = currentObject.GetEnumerator(); + if (enumerator.MoveNext()) { - return valueObjA.CompareTo(queryValueTyped); + element = enumerator.Current.Value; + return true; } } + return false; } - //When objA is a string and objB is not. - if (objA.ValueKind == JsonValueKind.String) + + public static bool TryMoveNextFromObject(this JsonNode? src, int cycle, out JsonNode? element) { - if (objB.ValueKind == JsonValueKind.Number) + element = null; + + if (src == null) + return false; + + var nodeValueKind = src.GetSafeJsonValueKind(); + + if (nodeValueKind == JsonValueKind.Object) { - if (double.TryParse(objA.GetRawText().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) + var currentObject = src.AsObject().GetEnumerator(); + for (int i = 0; i < cycle; i++) { - return valueTyped.CompareTo(objB.GetDouble()); + currentObject.MoveNext(); } + element = currentObject.Current.Value; + return true; } + return false; } - return -1; - } - 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) + public static IEnumerable ChildrenNodes(this JsonNode src) { - return 0; - } - if (aValueKind == JsonValueKind.False && bValueKind == JsonValueKind.False) - { - return 0; - } - if (aValueKind == JsonValueKind.Number && bValueKind == JsonValueKind.Number) - { - return objA.GetDouble().CompareTo(objB.GetDouble()); + 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; + } + } } - if (aValueKind == JsonValueKind.String && bValueKind == JsonValueKind.String) + + public static int CompareTo(this JsonNode value, JsonNode queryValue) { - return objA.GetString().CompareTo(objB.GetString()); + JsonValueKind comparisonType = (value.GetSafeJsonValueKind() == JsonValueKind.String && value.GetSafeJsonValueKind() != queryValue.GetSafeJsonValueKind()) + ? queryValue.GetSafeJsonValueKind() + : value.GetSafeJsonValueKind(); + + return Compare(comparisonType, value, queryValue); } - //When objA is a number and objB is not. - if (aValueKind == JsonValueKind.Number) + + private static int Compare(JsonValueKind valueType, JsonNode objA, JsonNode objB) { - var valueObjA = objA.GetDouble(); - if (bValueKind == JsonValueKind.String) + 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) { - if (double.TryParse(objB.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double queryValueTyped)) + 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) { - return valueObjA.CompareTo(queryValueTyped); + 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) + //When objA is a string and objB is not. + if (aValueKind == JsonValueKind.String) { - if (double.TryParse(objA.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) + if (bValueKind == JsonValueKind.Number) { - return valueTyped.CompareTo(objB.GetDouble()); + if (double.TryParse(objA.GetValue().AsSpan().TrimStart('"').TrimEnd('"'), out double valueTyped)) + { + return valueTyped.CompareTo(objB.GetDouble()); + } } } + return -1; } - 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..74af9c2 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs @@ -0,0 +1,46 @@ +#if NET8_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 0dc03c8..739bc2e 100644 --- a/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs +++ b/src/JsonDocumentPath/Filters/ArrayIndexFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class ArrayIndexFilter : PathFilter + internal partial class ArrayIndexFilter : PathFilter { public int? Index { get; set; } @@ -39,39 +38,5 @@ internal class ArrayIndexFilter : PathFilter } } } - - 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}."); - } - } - } - } - } } } \ 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..3ef7bb2 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs @@ -0,0 +1,28 @@ +#if NET8_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 e83244a..b523c21 100644 --- a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs +++ b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class ArrayMultipleIndexFilter : PathFilter + internal partial class ArrayMultipleIndexFilter : PathFilter { internal List Indexes; @@ -27,21 +26,5 @@ public ArrayMultipleIndexFilter(List indexes) } } } - - 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; - } - } - } - } } } \ 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..772f73a --- /dev/null +++ b/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs @@ -0,0 +1,74 @@ +#if NET8_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 5c0feda..6516c14 100644 --- a/src/JsonDocumentPath/Filters/ArraySliceFilter.cs +++ b/src/JsonDocumentPath/Filters/ArraySliceFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class ArraySliceFilter : PathFilter + internal partial class ArraySliceFilter : PathFilter { public int? Start { get; set; } @@ -73,68 +72,6 @@ internal class ArraySliceFilter : PathFilter } } - 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}."); - } - } - } - } - private bool IsValid(int index, int stopIndex, bool positiveStep) { if (positiveStep) diff --git a/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs new file mode 100644 index 0000000..58be7c4 --- /dev/null +++ b/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs @@ -0,0 +1,51 @@ +#if NET8_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 t.ChildrenNodes()) + { + 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 2b48e0a..a376ac3 100644 --- a/src/JsonDocumentPath/Filters/FieldFilter.cs +++ b/src/JsonDocumentPath/Filters/FieldFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class FieldFilter : PathFilter + internal partial class FieldFilter : PathFilter { internal string? Name; @@ -49,44 +48,5 @@ public FieldFilter(string? name) } } } - - 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 t.ChildrenNodes()) - { - yield return p; - } - } - } - else - { - if (errorWhenNoMatch) - { - throw new JsonException($"Property '{Name ?? "*"}' not valid on {t.GetType().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..c9f652e --- /dev/null +++ b/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs @@ -0,0 +1,45 @@ +#if NET8_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 156374a..c0607d1 100644 --- a/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs +++ b/src/JsonDocumentPath/Filters/FieldMultipleFilter.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.Linq; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class FieldMultipleFilter : PathFilter + internal partial class FieldMultipleFilter : PathFilter { internal List Names; @@ -43,37 +42,5 @@ public FieldMultipleFilter(List names) } } } - - 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}."); - } - } - } - } } } \ 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..9a904a4 --- /dev/null +++ b/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs @@ -0,0 +1,41 @@ +#if NET8_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 d0a52cf..ed144f5 100644 --- a/src/JsonDocumentPath/Filters/QueryFilter.cs +++ b/src/JsonDocumentPath/Filters/QueryFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class QueryFilter : PathFilter + internal partial class QueryFilter : PathFilter { internal QueryExpression Expression; @@ -38,34 +37,5 @@ public QueryFilter(QueryExpression expression) } } } - - 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; - } - } - } - } - } } } \ 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..b396cd3 --- /dev/null +++ b/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs @@ -0,0 +1,26 @@ +#if NET8_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 d2e9dab..2d4b4e8 100644 --- a/src/JsonDocumentPath/Filters/QueryScanFilter.cs +++ b/src/JsonDocumentPath/Filters/QueryScanFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class QueryScanFilter : PathFilter + internal partial class QueryScanFilter : PathFilter { internal QueryExpression Expression; @@ -25,19 +24,5 @@ public QueryScanFilter(QueryExpression expression) } } } - - 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; - } - } - } - } } } \ 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..2dccac4 --- /dev/null +++ b/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs @@ -0,0 +1,17 @@ +#if NET8_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 06b22ba..d2ac134 100644 --- a/src/JsonDocumentPath/Filters/RootFilter.cs +++ b/src/JsonDocumentPath/Filters/RootFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class RootFilter : PathFilter + internal partial class RootFilter : PathFilter { public static readonly RootFilter Instance = new RootFilter(); @@ -15,10 +14,5 @@ private RootFilter() { return new JsonElement?[1] { root }; } - - public override IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch) - { - return [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..671845d --- /dev/null +++ b/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs @@ -0,0 +1,26 @@ +#if NET8_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 6bba789..7ae213c 100644 --- a/src/JsonDocumentPath/Filters/ScanFilter.cs +++ b/src/JsonDocumentPath/Filters/ScanFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class ScanFilter : PathFilter + internal partial class ScanFilter : PathFilter { internal string? Name; @@ -25,19 +24,5 @@ public ScanFilter(string? name) } } } - - 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; - } - } - } - } } } \ 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..8aaea99 --- /dev/null +++ b/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs @@ -0,0 +1,35 @@ +#if NET8_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 3d1b384..fd57a5b 100644 --- a/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs +++ b/src/JsonDocumentPath/Filters/ScanMultipleFilter.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; namespace System.Text.Json { - internal class ScanMultipleFilter : PathFilter + internal partial class ScanMultipleFilter : PathFilter { private List _names; @@ -33,27 +32,5 @@ public ScanMultipleFilter(List names) } } } - - 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; - } - } - } - } - } - } } } \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonDocumentPath.cs b/src/JsonDocumentPath/JsonDocumentPath.cs index d3a45f2..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.Json.Nodes; +#pragma warning disable IDE0130 // 命名空间与文件夹结构不匹配 namespace System.Text.Json +#pragma warning restore IDE0130 // 命名空间与文件夹结构不匹配 { internal class JsonDocumentPath { @@ -913,11 +914,6 @@ private void EnsureLength(string message) return Evaluate(Filters, root, t, errorWhenNoMatch); } - internal IEnumerable Evaluate(JsonNode root, JsonNode t, bool errorWhenNoMatch) - { - return Evaluate(Filters, root, t, errorWhenNoMatch); - } - internal static IEnumerable Evaluate(List filters, JsonElement root, JsonElement t, bool errorWhenNoMatch) { IEnumerable current = new JsonElement?[] { t }; @@ -929,15 +925,20 @@ private void EnsureLength(string message) return current; } - 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; - } + //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 089feee..6460b4f 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.csproj +++ b/src/JsonDocumentPath/JsonDocumentPath.csproj @@ -1,6 +1,7 @@ + netstandard2.1;net8.0;net9.0 net8.0 1.0.3 1.0.0.3 @@ -16,6 +17,7 @@ MIT https://github.com/azambrano/JsonDocumentPath enable + latest diff --git a/src/JsonDocumentPath/JsonDocumentPathExtensions.cs b/src/JsonDocumentPath/JsonDocumentPathExtensions.cs index 5dc14f7..ad405c5 100644 --- a/src/JsonDocumentPath/JsonDocumentPathExtensions.cs +++ b/src/JsonDocumentPath/JsonDocumentPathExtensions.cs @@ -1,192 +1,157 @@ using System.Collections.Generic; -using System.Text.Json.Nodes; -namespace System.Text.Json; - -public static class JsonDocumentPathExtensions +#pragma warning disable IDE0130 // 命名空间与文件夹结构不匹配 +namespace System.Text.Json +#pragma warning restore IDE0130 // 命名空间与文件夹结构不匹配 { - /// - /// Selects a collection of elements using a JSONPath expression. - /// - /// - /// A that contains a JSONPath expression. - /// - /// An of that contains the selected elements. - - public static IEnumerable SelectElements(this JsonDocument src, string path) + public static class JsonDocumentPathExtensions { - return SelectElements(src.RootElement, path, false); - } - - /// - /// Selects a collection of elements using a JSONPath expression. - /// - /// - /// A that contains a JSONPath expression. - /// - /// An of that contains the selected elements. + /// + /// Selects a collection of elements using a JSONPath expression. + /// + /// + /// A that contains a JSONPath expression. + /// + /// An of that contains the selected elements. - public static IEnumerable SelectElements(this JsonElement src, string path) - { - return SelectElements(src, path, false); - } + public static IEnumerable SelectElements(this JsonDocument src, string path) + { + return SelectElements(src.RootElement, path, false); + } - /// - /// 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 SelectElements(this JsonElement src, string path, bool errorWhenNoMatch) - { - var parser = new JsonDocumentPath(path); - return parser.Evaluate(src, src, errorWhenNoMatch); - } + /// + /// Selects a collection of elements using a JSONPath expression. + /// + /// + /// A that contains a JSONPath expression. + /// + /// An of that contains the selected elements. - /// - /// 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); - } + public static IEnumerable SelectElements(this JsonElement src, string path) + { + return SelectElements(src, path, false); + } - /// - /// 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)) + /// + /// 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 SelectElements(this JsonElement src, string path, bool errorWhenNoMatch) { - if (el != null) - { - throw new JsonException("Path returned multiple tokens."); - } - el = t; + var parser = new JsonDocumentPath(path); + return parser.Evaluate(src, src, errorWhenNoMatch); } - return el; - } - /// - /// 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 SelectElements(this JsonDocument src, string path, bool errorWhenNoMatch) - { - var parser = new JsonDocumentPath(path); - return parser.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch); - } + /// + /// 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 SelectElements(this JsonDocument src, string path, bool errorWhenNoMatch) + { + var parser = new JsonDocumentPath(path); + return parser.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch); + } - /// - /// Selects a using a JSONPath expression. Selects the token that matches the object path. - /// - /// - /// A that contains a JSONPath expression. - /// - /// A , or null. - public static JsonElement? SelectElement(this JsonDocument src, string path) - { - return SelectElement(src.RootElement, path, false); - } + /// + /// Selects a using a JSONPath expression. Selects the token that matches the object path. + /// + /// + /// A that contains a JSONPath expression. + /// + /// A , or null. + public static JsonElement? SelectElement(this JsonDocument src, string path) + { + return SelectElement(src.RootElement, path, false); + } - /// - /// Selects a using a JSONPath expression. Selects the token that matches the object path. - /// - /// - /// A that contains a JSONPath expression. - /// - /// A , or null. - public static JsonElement? SelectElement(this JsonElement src, string path) - { - return SelectElement(src, path, false); - } + /// + /// Selects a using a JSONPath expression. Selects the token that matches the object path. + /// + /// + /// A that contains a JSONPath expression. + /// + /// A , or null. + public static JsonElement? SelectElement(this JsonElement src, string path) + { + return SelectElement(src, path, false); + } - /// - /// 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 JsonElement? SelectElement(this JsonDocument src, string path, bool errorWhenNoMatch) - { - var p = new JsonDocumentPath(path); - JsonElement? el = null; - foreach (JsonElement t in p.Evaluate(src.RootElement, src.RootElement, 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 JsonElement? SelectElement(this JsonDocument src, string path, bool errorWhenNoMatch) { - if (el != null) + var p = new JsonDocumentPath(path); + JsonElement? el = null; + foreach (JsonElement t in p.Evaluate(src.RootElement, src.RootElement, errorWhenNoMatch)) { - throw new JsonException("Path returned multiple tokens."); + if (el != null) + { + throw new JsonException("Path returned multiple tokens."); + } + el = t; } - el = t; + return el; } - return el; - } - /// - /// 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 JsonElement? SelectElement(this JsonElement src, string path, bool errorWhenNoMatch) - { - var p = new JsonDocumentPath(path); - JsonElement? el = null; - foreach (JsonElement t in p.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 JsonElement? SelectElement(this JsonElement src, string path, bool errorWhenNoMatch) { - if (el != null) + var p = new JsonDocumentPath(path); + JsonElement? el = null; + foreach (JsonElement t in p.Evaluate(src, src, errorWhenNoMatch)) { - throw new JsonException("Path returned multiple tokens."); + if (el != null) + { + throw new JsonException("Path returned multiple tokens."); + } + el = t; } - el = t; + return el; } - return el; - } - /// - /// Gets the value of the element as a System.Object. - /// - public static object GetObjectValue(this JsonElement src) - { - if (src.ValueKind == JsonValueKind.Null) - { - return null; - } - if (src.ValueKind == JsonValueKind.String) + /// + /// Gets the value of the element as a System.Object. + /// + public static object GetObjectValue(this JsonElement src) { - return src.GetString(); - } - if (src.ValueKind == JsonValueKind.False || src.ValueKind == JsonValueKind.True) - { - return src.GetBoolean(); - } - if (src.ValueKind == JsonValueKind.Number) - { - return src.GetDouble(); + if (src.ValueKind == JsonValueKind.Null) + { + return null; + } + if (src.ValueKind == JsonValueKind.String) + { + return src.GetString(); + } + if (src.ValueKind == JsonValueKind.False || src.ValueKind == JsonValueKind.True) + { + return src.GetBoolean(); + } + if (src.ValueKind == JsonValueKind.Number) + { + return src.GetDouble(); + } + return src.GetRawText(); } - return src.GetRawText(); } } \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonNodeExtensions.cs b/src/JsonDocumentPath/JsonNodeExtensions.cs index c202c26..c787360 100644 --- a/src/JsonDocumentPath/JsonNodeExtensions.cs +++ b/src/JsonDocumentPath/JsonNodeExtensions.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +#if NET8_0_OR_GREATER + +using System.Text.Json.Nodes; namespace System.Text.Json; @@ -51,4 +53,6 @@ public static JsonValueKind GetSafeJsonValueKind(this JsonNode? node) return node.GetValueKind(); } -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/src/JsonDocumentPath/JsonNodePath.cs b/src/JsonDocumentPath/JsonNodePath.cs index bf26354..2924348 100644 --- a/src/JsonDocumentPath/JsonNodePath.cs +++ b/src/JsonDocumentPath/JsonNodePath.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +#if NET8_0_OR_GREATER + +using System.Collections.Generic; using System.Globalization; using System.Text.Json.Nodes; @@ -438,7 +440,7 @@ private JsonException CreateUnexpectedCharacterException() return new JsonException("Unexpected character while parsing path query: " + _expression[_currentIndex]); } - private object ParseSide() + private object? ParseSide() { EatWhitespace(); @@ -461,7 +463,7 @@ private object ParseSide() throw CreateUnexpectedCharacterException(); } - private JsonNode SafeValue(object? value) + private JsonNode? SafeValue(object? value) { if (value == null) { @@ -469,7 +471,7 @@ private JsonNode SafeValue(object? value) } if (value is string) { - return JsonNode.Parse(string.Concat("\"", value, "\"")).GetValue(); + return JsonNode.Parse(string.Concat("\"", value, "\""))?.GetValue(); } if (value is bool) { @@ -489,7 +491,7 @@ private QueryExpression ParseExpression() while (_currentIndex < _expression.Length) { - object left = ParseSide(); + object left = ParseSide()!; object? right = null; QueryOperator op; @@ -923,4 +925,6 @@ private void EnsureLength(string message) return current; } -} \ No newline at end of file +} + +#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..7eaacbb --- /dev/null +++ b/src/JsonDocumentPath/JsonNodePathExtensions.cs @@ -0,0 +1,51 @@ +#if NET8_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 b71f18c..7356952 100644 --- a/src/JsonDocumentPath/PathFilter.cs +++ b/src/JsonDocumentPath/PathFilter.cs @@ -1,14 +1,16 @@ using System.Collections.Generic; + +#if NET8_0_OR_GREATER + using System.Text.Json.Nodes; +#endif namespace System.Text.Json { public abstract class PathFilter { public abstract IEnumerable ExecuteFilter(JsonElement root, IEnumerable current, bool errorWhenNoMatch); - public abstract IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch); - protected static JsonElement? GetTokenIndex(JsonElement t, bool errorWhenNoMatch, int index) { if (t.ValueKind == JsonValueKind.Array) @@ -64,6 +66,10 @@ public abstract class PathFilter } } +#if NET8_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) @@ -92,13 +98,13 @@ public abstract class PathFilter } } - protected static IEnumerable<(string Name, JsonNode Value)> GetNextScanValue(JsonNode value) + 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 e in value?.AsArray()) { foreach (var c in GetNextScanValue(e)) { @@ -108,7 +114,7 @@ public abstract class PathFilter } else if (value.GetSafeJsonValueKind() == JsonValueKind.Object) { - var propertyEnumerator = value.AsObject().GetEnumerator(); + var propertyEnumerator = value?.AsObject().GetEnumerator(); while (propertyEnumerator.MoveNext()) { @@ -121,5 +127,7 @@ public abstract class PathFilter } } } + +#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..425569a --- /dev/null +++ b/src/JsonDocumentPath/QueryExpression.JsonNode.cs @@ -0,0 +1,281 @@ +#if NET8_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 1e3a592..ede3769 100644 --- a/src/JsonDocumentPath/QueryExpression.cs +++ b/src/JsonDocumentPath/QueryExpression.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Text.Json.Nodes; using System.Text.RegularExpressions; namespace System.Text.Json @@ -22,7 +21,7 @@ internal enum QueryOperator StrictNotEquals = 12 } - internal abstract class QueryExpression + internal abstract partial class QueryExpression { internal QueryOperator Operator; @@ -32,11 +31,9 @@ public QueryExpression(QueryOperator @operator) } public abstract bool IsMatch(JsonElement root, JsonElement t); - - public abstract bool IsMatch(JsonNode root, JsonNode t); } - internal class CompositeExpression : QueryExpression + internal partial class CompositeExpression : QueryExpression { public List Expressions { get; set; } @@ -73,38 +70,9 @@ public override bool IsMatch(JsonElement root, JsonElement t) throw new ArgumentOutOfRangeException(); } } - - 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 class BooleanQueryExpression : QueryExpression + internal partial class BooleanQueryExpression : QueryExpression { public readonly object Left; public readonly object? Right; @@ -130,30 +98,6 @@ public BooleanQueryExpression(QueryOperator @operator, object left, object? righ return Enumerable.Empty(); } - 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(JsonElement root, JsonElement t) { if (Operator == QueryOperator.Exists) @@ -185,37 +129,6 @@ public override bool IsMatch(JsonElement root, JsonElement t) return false; } - 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(JsonElement leftResult, JsonElement rightResult) { if (leftResult.IsValue() && rightResult.IsValue()) @@ -304,94 +217,6 @@ private bool MatchTokens(JsonElement leftResult, JsonElement rightResult) 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(JsonElement input, JsonElement pattern) { if (input.ValueKind != JsonValueKind.String || pattern.ValueKind != JsonValueKind.String) @@ -408,24 +233,6 @@ private static bool RegexEquals(JsonElement input, JsonElement pattern) return Regex.IsMatch(input.GetString(), patternText, GetRegexOptions(optionsText)); } - 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(JsonElement value, JsonElement queryValue) { if (value.Equals(queryValue)) @@ -448,75 +255,6 @@ internal static bool EqualsWithStringCoercion(JsonElement value, JsonElement que return string.Equals(value.ToString(), queryValue.GetString(), StringComparison.Ordinal); } - 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); - } - internal static bool EqualsWithStrictMatch(JsonElement value, JsonElement queryValue) { // we handle floats and integers the exact same way, so they are pseudo equivalent From 2c3e33291c394e5dfd0c957de266e246cd0b2695 Mon Sep 17 00:00:00 2001 From: yingpanwang <49632937+yingpanwang@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:28:36 +0800 Subject: [PATCH 3/5] Feature/jsonnodepath (#2) * support JsonNode jsonpath query * fix unit tests bugs * fix unit test bugs * add JsonNodeRef test * nullable markup * splite JsonNode support Imp to partial class --- src/JsonDocumentPath/JsonDocumentPath.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonDocumentPath/JsonDocumentPath.csproj b/src/JsonDocumentPath/JsonDocumentPath.csproj index 6460b4f..2408c28 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.csproj +++ b/src/JsonDocumentPath/JsonDocumentPath.csproj @@ -2,7 +2,6 @@ netstandard2.1;net8.0;net9.0 - net8.0 1.0.3 1.0.0.3 1.0.0.3 From 18144ea8b6a5094b7b76ddaab26b80f0b2ad7328 Mon Sep 17 00:00:00 2001 From: yingpanwang <49632937+yingpanwang@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:10:31 +0800 Subject: [PATCH 4/5] fix net6/7 support (#4) --- .../JsonDocumentPath.Test.csproj | 5 +++-- src/JsonDocumentPath/Extensions.cs | 19 +++++++++++++++---- .../Filters/ArrayIndexFilter.JsonNode.cs | 2 +- .../ArrayMultipleIndexFilter.JsonNode.cs | 2 +- .../Filters/ArraySliceFilter.JsonNode.cs | 2 +- .../Filters/FieldFilter.JsonNode.cs | 10 ++++++++-- .../Filters/FieldMultipleFilter.JsonNode.cs | 2 +- .../Filters/QueryFilter.JsonNode.cs | 2 +- .../Filters/QueryScanFilter.JsonNode.cs | 2 +- .../Filters/RootFilter.JsonNode.cs | 2 +- .../Filters/ScanFilter.JsonNode.cs | 2 +- .../Filters/ScanMultipleFilter.JsonNode.cs | 2 +- src/JsonDocumentPath/JsonDocumentPath.csproj | 2 +- src/JsonDocumentPath/JsonNodeExtensions.cs | 2 +- src/JsonDocumentPath/JsonNodePath.cs | 2 +- .../JsonNodePathExtensions.cs | 2 +- src/JsonDocumentPath/PathFilter.cs | 6 +++--- .../QueryExpression.JsonNode.cs | 2 +- 18 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj b/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj index 2080b28..01dc065 100644 --- a/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj +++ b/src/JsonDocumentPath.Test/JsonDocumentPath.Test.csproj @@ -1,9 +1,10 @@ - + - net8.0 + net6.0; false + latest diff --git a/src/JsonDocumentPath/Extensions.cs b/src/JsonDocumentPath/Extensions.cs index 3a76554..d7e799c 100644 --- a/src/JsonDocumentPath/Extensions.cs +++ b/src/JsonDocumentPath/Extensions.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; - -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Text.Json.Nodes; @@ -258,7 +257,19 @@ private static int Compare(JsonValueKind valueType, JsonElement objA, JsonElemen } } -#if NET8_0_OR_GREATER +#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 { diff --git a/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs index 74af9c2..881153d 100644 --- a/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/ArrayIndexFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs index 3ef7bb2..93710e2 100644 --- a/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/ArrayMultipleIndexFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs index 772f73a..fea1df2 100644 --- a/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/ArraySliceFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs index 58be7c4..7f4dd6c 100644 --- a/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/FieldFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; @@ -30,7 +30,13 @@ internal partial class FieldFilter } else { - foreach (var p in t.ChildrenNodes()) + foreach (var p in +#if NET6_0 || NET7_0 + Extensions.ChildrenNodes(t) +#else + t.ChildrenNodes() +#endif + ) { yield return p; } diff --git a/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs index c9f652e..3cfaf16 100644 --- a/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/FieldMultipleFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Linq; diff --git a/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs index 9a904a4..47d98bf 100644 --- a/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/QueryFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs index b396cd3..e8de62c 100644 --- a/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/QueryScanFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs index 2dccac4..a5598c7 100644 --- a/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/RootFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs index 671845d..b81d993 100644 --- a/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/ScanFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs b/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs index 8aaea99..4762465 100644 --- a/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs +++ b/src/JsonDocumentPath/Filters/ScanMultipleFilter.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/JsonDocumentPath.csproj b/src/JsonDocumentPath/JsonDocumentPath.csproj index 2408c28..c43a9dd 100644 --- a/src/JsonDocumentPath/JsonDocumentPath.csproj +++ b/src/JsonDocumentPath/JsonDocumentPath.csproj @@ -1,7 +1,7 @@ - netstandard2.1;net8.0;net9.0 + netstandard2.1;net6.0;net7.0;net8.0;net9.0 1.0.3 1.0.0.3 1.0.0.3 diff --git a/src/JsonDocumentPath/JsonNodeExtensions.cs b/src/JsonDocumentPath/JsonNodeExtensions.cs index c787360..e790986 100644 --- a/src/JsonDocumentPath/JsonNodeExtensions.cs +++ b/src/JsonDocumentPath/JsonNodeExtensions.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/JsonNodePath.cs b/src/JsonDocumentPath/JsonNodePath.cs index 2924348..ef7c5ef 100644 --- a/src/JsonDocumentPath/JsonNodePath.cs +++ b/src/JsonDocumentPath/JsonNodePath.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Globalization; diff --git a/src/JsonDocumentPath/JsonNodePathExtensions.cs b/src/JsonDocumentPath/JsonNodePathExtensions.cs index 7eaacbb..12c1706 100644 --- a/src/JsonDocumentPath/JsonNodePathExtensions.cs +++ b/src/JsonDocumentPath/JsonNodePathExtensions.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Text.Json.Nodes; diff --git a/src/JsonDocumentPath/PathFilter.cs b/src/JsonDocumentPath/PathFilter.cs index 7356952..ac1e049 100644 --- a/src/JsonDocumentPath/PathFilter.cs +++ b/src/JsonDocumentPath/PathFilter.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Text.Json.Nodes; @@ -66,7 +66,7 @@ public abstract class PathFilter } } -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER public abstract IEnumerable ExecuteFilter(JsonNode root, IEnumerable current, bool errorWhenNoMatch); diff --git a/src/JsonDocumentPath/QueryExpression.JsonNode.cs b/src/JsonDocumentPath/QueryExpression.JsonNode.cs index 425569a..61934ab 100644 --- a/src/JsonDocumentPath/QueryExpression.JsonNode.cs +++ b/src/JsonDocumentPath/QueryExpression.JsonNode.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Collections.Generic; using System.Linq; From c77b74300452ba1a49c2ee725bfb4d212562e153 Mon Sep 17 00:00:00 2001 From: yingpanwang <49632937+yingpanwang@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:13:41 +0800 Subject: [PATCH 5/5] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) 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