diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2fb6147 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "Json-Path-Test-Suite"] + path = Json-Path-Test-Suite + url = https://github.com/gregsdennis/JSON-Path-Test-Suite.git +[submodule "Json-Schema-Test-Suite"] + path = Json-Schema-Test-Suite + url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git diff --git a/JSON-Schema-Test-Suite/README.md b/JSON-Schema-Test-Suite/README.md index 2958ee6..5adf569 100644 --- a/JSON-Schema-Test-Suite/README.md +++ b/JSON-Schema-Test-Suite/README.md @@ -118,6 +118,7 @@ for more information. ### .NET ### * [Newtonsoft.Json.Schema](https://github.com/JamesNK/Newtonsoft.Json.Schema) +* [Manatee.Json](https://github.com/gregsdennis/Manatee.Json) ### PHP ### diff --git a/JSON-Schema-Test-Suite/tests/draft4/ref.json b/JSON-Schema-Test-Suite/tests/draft4/ref.json index 7e80552..fe5aa90 100644 --- a/JSON-Schema-Test-Suite/tests/draft4/ref.json +++ b/JSON-Schema-Test-Suite/tests/draft4/ref.json @@ -155,5 +155,25 @@ "valid": false } ] + }, + { + "description": "property named $ref that is not a reference", + "schema": { + "properties": { + "$ref": {"type": "string"} + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": {"$ref": "a"}, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": {"$ref": 2}, + "valid": false + } + ] } ] diff --git a/Json-Path-Test-Suite b/Json-Path-Test-Suite new file mode 160000 index 0000000..c5afa49 --- /dev/null +++ b/Json-Path-Test-Suite @@ -0,0 +1 @@ +Subproject commit c5afa49488b5a659ab7f5569745d9840ee4a12e1 diff --git a/Json-Schema-Test-Suite/.gitignore b/Json-Schema-Test-Suite/.gitignore new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/Json-Schema-Test-Suite/.gitignore @@ -0,0 +1 @@ +TODO diff --git a/Manatee.Json.Tests/DevTest.cs b/Manatee.Json.Tests/DevTest.cs index 74781e5..2b81b5f 100644 --- a/Manatee.Json.Tests/DevTest.cs +++ b/Manatee.Json.Tests/DevTest.cs @@ -28,7 +28,7 @@ namespace Manatee.Json.Tests { [TestClass] - [Ignore] + [Ignore] public class DevTest { [TestMethod] diff --git a/Manatee.Json.Tests/Manatee.Json.Tests.csproj b/Manatee.Json.Tests/Manatee.Json.Tests.csproj index e19db5f..74ad343 100644 --- a/Manatee.Json.Tests/Manatee.Json.Tests.csproj +++ b/Manatee.Json.Tests/Manatee.Json.Tests.csproj @@ -139,10 +139,6 @@ Manatee.Json.snk - - ..\packages\Manatee.StateMachine.1.1.2\lib\net40\Manatee.StateMachine.dll - True - False @@ -174,6 +170,14 @@ + + + + + + + + @@ -223,7 +227,6 @@ - Always diff --git a/Manatee.Json.Tests/Path/EvaluationTest.cs b/Manatee.Json.Tests/Path/EvaluationTest.cs new file mode 100644 index 0000000..586481a --- /dev/null +++ b/Manatee.Json.Tests/Path/EvaluationTest.cs @@ -0,0 +1,37 @@ +using Manatee.Json.Path; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Manatee.Json.Tests.Path +{ + [TestClass] + public class EvaluationTest + { + [TestMethod] + public void ArrayLengthExpression_LastItem() + { + var json = new JsonArray {1, 2, 3}; + var path = JsonPathWith.Array(jv => jv.Length() - 1); + var expected = new JsonArray {3}; + + var actual = path.Evaluate(json); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void ObjectNameLengthExpression_LastItem() + { + var json = new JsonObject + { + {"name", new JsonArray {1, 2, 3}}, + {"name2", new JsonArray {1, 2, 3}}, + {"test", new JsonArray {1, 2}}, + }; + var path = JsonPathWith.Array(jv => JsonPathRoot.Name("test").Length()); + var expected = new JsonArray {new JsonArray {1, 2}}; + + var actual = path.Evaluate(json); + + Assert.AreEqual(expected, actual); + } + } +} diff --git a/Manatee.Json.Tests/Path/FilterExpressionTest.cs b/Manatee.Json.Tests/Path/FilterExpressionTest.cs new file mode 100644 index 0000000..a9fb9c1 --- /dev/null +++ b/Manatee.Json.Tests/Path/FilterExpressionTest.cs @@ -0,0 +1,217 @@ +using Manatee.Json.Path; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Manatee.Json.Tests.Path +{ + [TestClass] + public class FilterExpressionTest + { + private void Run(JsonPath expected, string text) + { + var actual = JsonPath.Parse(text); + Assert.AreEqual(expected, actual); + } + + private void CompareEval(JsonPath expected, string text) + { + var data = new JsonArray + { + 1, + new JsonObject + { + ["bool"] = false, + ["int"] = 20, + ["string"] = "value", + }, + "hello", + true, + 5, + new JsonArray {1,2,3 } + }; + + var actual = JsonPath.Parse(text); + + var expectedResult = expected.Evaluate(data); + var actualResult = actual.Evaluate(data); + + Assert.AreEqual(expectedResult, actualResult); + } + + [TestMethod] + public void PropertyEqualsValue() + { + Run(JsonPathWith.Array(jv => jv.Name("test") == 5), "$[?(@.test == 5)]"); + } + [TestMethod] + public void PropertyNotEqualToValue() + { + Run(JsonPathWith.Array(jv => jv.Name("test") != 5), "$[?(@.test != 5)]"); + } + [TestMethod] + public void PropertyGreaterThanValue() + { + Run(JsonPathWith.Array(jv => jv.Name("test") > 5), "$[?(@.test > 5)]"); + } + [TestMethod] + public void PropertyGreaterThanEqualToValue() + { + Run(JsonPathWith.Array(jv => jv.Name("test") >= 5), "$[?(@.test >= 5)]"); + } + [TestMethod] + public void PropertyLessThanValue() + { + Run(JsonPathWith.Array(jv => jv.Name("test") < 5), "$[?(@.test < 5)]"); + } + [TestMethod] + public void PropertyLessThanEqualToValue() + { + Run(JsonPathWith.Array(jv => jv.Name("test") <= 5), "$[?(@.test <= 5)]"); + } + [TestMethod] + public void ArrayIndexEqualsValue() + { + Run(JsonPathWith.Array(jv => jv.ArrayIndex(1) == 5), "$[?(@[1] == 5)]"); + } + [TestMethod] + [ExpectedException(typeof(JsonPathSyntaxException))] + public void ArrayMultiIndex() + { + var text = "$[?(@[1,3] == 5)]"; + + try + { + var actual = JsonPath.Parse(text); + } + catch (JsonPathSyntaxException e) + { + Assert.IsTrue(e.Message.StartsWith("JSON Path expression indexers only support single constant values.")); + throw; + } + } + [TestMethod] + [ExpectedException(typeof(JsonPathSyntaxException))] + public void ArraySliceEqualsValue() + { + var text = "$[?(@[1:3] == 5)]"; + + try + { + var actual = JsonPath.Parse(text); + } + catch (JsonPathSyntaxException e) + { + Assert.IsTrue(e.Message.StartsWith("JSON Path expression indexers only support single constant values.")); + throw; + } + } + [TestMethod] + [ExpectedException(typeof(JsonPathSyntaxException))] + public void ArrayIndexExpressionEqualsValue() + { + var text = "$[?(@[(@.length-1))]"; + + try + { + var actual = JsonPath.Parse(text); + } + catch (JsonPathSyntaxException e) + { + Assert.IsTrue(e.Message.StartsWith("JSON Path expression indexers only support single constant values.")); + throw; + } + } + [TestMethod] + [ExpectedException(typeof(JsonPathSyntaxException))] + public void ArrayFilterExpressionEqualsValue() + { + var text = "$[?(@[?(@.name == 5))]"; + + try + { + var actual = JsonPath.Parse(text); + } + catch (JsonPathSyntaxException e) + { + Assert.IsTrue(e.Message.StartsWith("JSON Path expression indexers only support single constant values.")); + throw; + } + } + [TestMethod] + public void HasProperty() + { + Run(JsonPathWith.Array(jv => jv.HasProperty("test")), "$[?(@.test)]"); + } + [TestMethod] + public void DoesNotHaveProperty() + { + Run(JsonPathWith.Array(jv => !jv.HasProperty("test")), "$[?(!@.test)]"); + } + [TestMethod] + public void GroupedNot() + { + Run(JsonPathWith.Array(jv => !(jv.HasProperty("test") && jv.Name("name") == 5)), "$[?(!(@.test && @.name == 5))]"); + } + [TestMethod] + public void And() + { + Run(JsonPathWith.Array(jv => jv.HasProperty("test") && jv.Name("name") == 5), "$[?(@.test && @.name == 5)]"); + } + [TestMethod] + public void Or() + { + Run(JsonPathWith.Array(jv => jv.HasProperty("test") || jv.Name("name") == 5),"$[?(@.test || @.name == 5)]"); + } + [TestMethod] + // This won't work the same. Parsing generates an IndexOfExpression with a distinct parameter, + // but when constructing the path, the parameter goes through several castings generating a + // parameter expression. The parsed path would be different in structure but should still + // represent the same thing. We have to test by evaluation. + public void IndexOfNumber() + { + CompareEval(JsonPathWith.Array(jv => jv.IndexOf(5) == 4), "$[?(@.indexOf(5) == 4)]"); + } + [TestMethod] + // This won't work the same. Parsing generates an IndexOfExpression with a distinct parameter, + // but when constructing the path, the parameter goes through several castings generating a + // parameter expression. The parsed path would be different in structure but should still + // represent the same thing. We have to test by evaluation. + public void IndexOfBoolean() + { + CompareEval(JsonPathWith.Array(jv => jv.IndexOf(true) == 3), "$[?(@.indexOf(true) == 3)]"); + } + [TestMethod] + // This won't work the same. Parsing generates an IndexOfExpression with a distinct parameter, + // but when constructing the path, the parameter goes through several castings generating a + // parameter expression. The parsed path would be different in structure but should still + // represent the same thing. We have to test by evaluation. + public void IndexOfString() + { + CompareEval(JsonPathWith.Array(jv => jv.IndexOf("string") == 2), "$[?(@.indexOf(\"hello\") == 2)]"); + } + [TestMethod] + // This won't work the same. Parsing generates a ValueExpression, but the only way to + // construct the path is to pass the field, which generates a FieldExpression. The parsed + // path would be different in structure but should still represent the same thing. + // We have to test by evaluation. + public void IndexOfArray() + { + var arr = new JsonArray {1, 2, 3}; + CompareEval(JsonPathWith.Array(jv => jv.IndexOf(arr) == 6), "$[?(@.indexOf([1,2,3]) == 6)]"); + } + [TestMethod] + // This won't work the same. Parsing generates a ValueExpression, but the only way to + // construct the path is to pass the field, which generates a FieldExpression. The parsed + // path would be different in structure but should still represent the same thing. + // We have to test by evaluation. + public void IndexOfObject() + { + var obj = new JsonObject + { + ["bool"] = false, + ["int"] = 20, + ["string"] = "value", + }; + CompareEval(JsonPathWith.Array(jv => jv.IndexOf(obj) == 1), "$[?(@.indexOf({\"key\":\"value\"}) == 1)]"); + } + } +} diff --git a/Manatee.Json.Tests/Path/GoessnerExamplesTest.cs b/Manatee.Json.Tests/Path/GoessnerExamplesTest.cs index 2fe2f0c..ccb75ee 100644 --- a/Manatee.Json.Tests/Path/GoessnerExamplesTest.cs +++ b/Manatee.Json.Tests/Path/GoessnerExamplesTest.cs @@ -332,7 +332,7 @@ public void GoessnerExample6BParsed() [TestMethod] public void GoessnerExample6BConstructed() { - var path = JsonPathWith.Search("book").ArraySlice(-1, null); + var path = JsonPathWith.Search("book").Array(new Slice(-1, null)); var expected = new JsonArray { new JsonObject @@ -426,7 +426,7 @@ public void GoessnerExample7BParsed() [TestMethod] public void GoessnerExample7BConstructed() { - var path = JsonPathWith.Search("book").ArraySlice(null, 2); + var path = JsonPathWith.Search("book").Array(new Slice(null, 2)); var expected = new JsonArray { new JsonObject diff --git a/Manatee.Json.Tests/Path/IndexExpressionParseTest.cs b/Manatee.Json.Tests/Path/IndexExpressionParseTest.cs new file mode 100644 index 0000000..03aff96 --- /dev/null +++ b/Manatee.Json.Tests/Path/IndexExpressionParseTest.cs @@ -0,0 +1,124 @@ +using Manatee.Json.Path; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Manatee.Json.Tests.Path +{ + [TestClass] + public class IndexExpressionParseTest + { + [TestMethod] + public void Length() + { + var text = "$[(@.length)]"; + var expected = JsonPathWith.Array(jv => jv.Length()); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Length_Root() + { + var text = "$[($.length)]"; + var expected = JsonPathWith.Array(jv => JsonPathRoot.Length()); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void ExtendedLength() + { + var text = "$[(@.name.length)]"; + var expected = JsonPathWith.Array(jv => jv.Name("name").Length()); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Addition() + { + var text = "$[(@.length+1)]"; + var expected = JsonPathWith.Array(jv => jv.Length() + 1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Subtraction() + { + var text = "$[(@.length-1)]"; + var expected = JsonPathWith.Array(jv => jv.Length() - 1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Multiplication() + { + var text = "$[(@.length*1)]"; + var expected = JsonPathWith.Array(jv => jv.Length()*1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Division() + { + var text = "$[(@.length/1)]"; + var expected = JsonPathWith.Array(jv => jv.Length()/1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Modulus() + { + var text = "$[(@.length%1)]"; + var expected = JsonPathWith.Array(jv => jv.Length()%1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + [Ignore] + // The C# ^ operator doesn't have the required exponentiation priority, so + // constructing expressions with this operator results in a strange structure. + // Also not really sure JS supports it as an exponentiation operator, either. + public void Exponent() + { + var text = "$[(@.length^1)]"; + var expected = JsonPathWith.Array(jv => jv.Length() ^ 1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void Add3() + { + var text = "$[(3+@.length+3)]"; + var expected = JsonPathWith.Array(jv => 3 + jv.Length() + 3); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void WhyOnGodsGreenEarthWouldAnyoneDoThis() + { + var text = "$[(4+@.length*($.name.length-2)+5)]"; + var expected = JsonPathWith.Array(jv => 4 + jv.Length()*(JsonPathRoot.Name("name").Length() - 2) + 5); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + } +} diff --git a/Manatee.Json.Tests/Path/ParsingTest.cs b/Manatee.Json.Tests/Path/ParsingTest.cs new file mode 100644 index 0000000..06ab062 --- /dev/null +++ b/Manatee.Json.Tests/Path/ParsingTest.cs @@ -0,0 +1,283 @@ +using Manatee.Json.Path; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Manatee.Json.Tests.Path +{ + [TestClass] + public class ParsingTest + { + private void Run(JsonPath expected, string text) + { + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SingleNamedObject() + { + Run(JsonPathWith.Name("name"), "$.name"); + } + + [TestMethod] + public void SingleQuotedNamedObject() + { + var text = "$.'quoted name'"; + var expected = JsonPathWith.Name("quoted name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void DoubleQuotedNamedObject() + { + var text = "$.\"quoted name\""; + var expected = JsonPathWith.Name("quoted name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SingleWildcardObject() + { + var text = "$.*"; + var expected = JsonPathWith.Wildcard(); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void NamedObjectWithWildcardObject() + { + var text = "$.name.*"; + var expected = JsonPathWith.Name("name").Wildcard(); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void WildcardObjectWithNamedObject() + { + var text = "$.*.name"; + var expected = JsonPathWith.Wildcard().Name("name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + [TestMethod] + public void SingleNamedSearch() + { + var text = "$..name"; + var expected = JsonPathWith.Search("name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SingleQuotedNamedSearch() + { + var text = "$..'quoted name'"; + var expected = JsonPathWith.Search("quoted name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void DoubleQuotedNamedSearch() + { + var text = "$..\"quoted name\""; + var expected = JsonPathWith.Search("quoted name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SingleWildcardSearch() + { + var text = "$..*"; + var expected = JsonPathWith.Search(); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void NamedObjectWithWildcardSearch() + { + var text = "$.name..*"; + var expected = JsonPathWith.Name("name").Search(); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void WildcardObjectWithNamedSearch() + { + var text = "$.*..name"; + var expected = JsonPathWith.Wildcard().Search("name"); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SingleIndexedArray() + { + var text = "$[1]"; + var expected = JsonPathWith.Array(1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SingleSlicedArray() + { + var text = "$[1:5]"; + var expected = JsonPathWith.Array(new Slice(1, 5)); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SteppedSlicedArray() + { + var text = "$[1:5:2]"; + var expected = JsonPathWith.Array(new Slice(1, 5, 2)); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void IndexedSlicedArray() + { + var text = "$[1,5:7]"; + var expected = JsonPathWith.Array(1, new Slice(5, 7)); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SlicedIndexedArray() + { + var text = "$[1:5,7]"; + var expected = JsonPathWith.Array(new Slice(1, 5), 7); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void MultiSlicedArray() + { + var text = "$[1:5,7:11:2]"; + var expected = JsonPathWith.Array(new Slice(1, 5), new Slice(7, 11, 2)); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void MultiIndexedArray() + { + var text = "$[1,3]"; + var expected = JsonPathWith.Array(1, 3); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + [ExpectedException(typeof(JsonPathSyntaxException))] + public void EmptyIndexedArray() + { + var text = "$[]"; + + JsonPath.Parse(text); + } + + [TestMethod] + [ExpectedException(typeof(JsonPathSyntaxException))] + public void EmptyObject() + { + var text = "$."; + + JsonPath.Parse(text); + } + + [TestMethod] + public void WildcardArray() + { + var text = "$[*]"; + var expected = JsonPathWith.Array(); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void SearchIndexedArray() + { + var text = "$..[1]"; + var expected = JsonPathWith.SearchArray(1); + + var actual = JsonPath.Parse(text); + + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ChainedNameIndexedArray() + { + Run(JsonPathWith.Name("name").Array(4), "$.name[4]"); + } + + [TestMethod] + public void ChainedIndexedArrayName() + { + Run(JsonPathWith.Array(4).Name("name"), "$[4].name"); + } + + [TestMethod] + public void ChainedNameName() + { + Run(JsonPathWith.Name("name").Name("test"), "$.name.test"); + } + + [TestMethod] + public void ChainedIndexedArrayIndexedArray() + { + Run(JsonPathWith.Array(2).Array(4), "$[2][4]"); + } + } +} diff --git a/Manatee.Json.Tests/Path/TestSuite/JsonPathTestSuite.cs b/Manatee.Json.Tests/Path/TestSuite/JsonPathTestSuite.cs new file mode 100644 index 0000000..9837781 --- /dev/null +++ b/Manatee.Json.Tests/Path/TestSuite/JsonPathTestSuite.cs @@ -0,0 +1,120 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: JsonPathTestSuite.cs + Namespace: Manatee.Json.Tests.Schema.TestSuite + Class Name: JsonPathTestSuite + Purpose: Runs the series of schema tests defined at + https://github.com/json-schema-org/JSON-Schema-Test-Suite. + +***************************************************************************************/ +using System; +using System.Collections.Generic; +using System.IO; +using Manatee.Json.Serialization; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Manatee.Json.Tests.Path.TestSuite +{ + [TestClass] + public class JsonPathTestSuite + { + private const string TestFolder = @"..\..\..\Json-Path-Test-Suite\tests\"; +#pragma warning disable 649 + private const string FileNameForDebugging = ""; + private const string TestPathForDebugging = ""; +#pragma warning restore 649 + private static readonly JsonSerializer Serializer; + private int _failures; + private int _passes; + + static JsonPathTestSuite() + { + Serializer = new JsonSerializer(); + JsonSerializationAbstractionMap.MapGeneric(typeof(IEnumerable<>), typeof(List<>)); + } + + [TestMethod] + public void RunSuite() + { + // uncomment and paste the filename of a test suite to debug it. + //_fileNameForDebugging = ""; + // uncomment and paste the description of a test to debug it. + //_testNameForDebugging = "ref within ref valid"; + + try + { + var fileNames = Directory.GetFiles(TestFolder); + + foreach (var fileName in fileNames) + { + _RunFile(fileName); + } + + Assert.AreEqual(0, _failures); + } + finally + { + Console.WriteLine(); + Console.WriteLine($"Passes: {_passes}"); + Console.WriteLine($"Failures: {_failures}"); + } + } + + private void _RunFile(string fileName) + { + if (fileName == FileNameForDebugging) + { + System.Diagnostics.Debugger.Break(); + } + + var contents = File.ReadAllText(fileName); + var json = JsonValue.Parse(contents); + + var testSet = Serializer.Deserialize(json); + + Console.WriteLine($"{testSet.Title} ({fileName})"); + _RunTestSet(testSet); + } + + private void _RunTestSet(PathTestSet testSet) + { + foreach (var test in testSet.Tests) + { + var pathString = test.Path.ToString(); + if (pathString == TestPathForDebugging) + { + System.Diagnostics.Debugger.Break(); + } + + var results = test.Path.Evaluate(testSet.Data); + + if (Equals(results, test.Result)) + { + // It's difficult to see the failures when everything shows. + // The Stack Trace Explorer needs to show color output. :/ + //Console.WriteLine($" {test.Description} - Passed"); + _passes++; + } + else + { + Console.WriteLine($" {pathString} - Failed"); + _failures++; + } + } + } + } +} diff --git a/Manatee.Json.Tests/Path/TestSuite/PathTest.cs b/Manatee.Json.Tests/Path/TestSuite/PathTest.cs new file mode 100644 index 0000000..7c85a33 --- /dev/null +++ b/Manatee.Json.Tests/Path/TestSuite/PathTest.cs @@ -0,0 +1,26 @@ +using System; +using Manatee.Json.Path; +using Manatee.Json.Serialization; + +namespace Manatee.Json.Tests.Path.TestSuite +{ + internal class PathTest : IJsonSerializable + { + public JsonPath Path { get; private set; } + public JsonArray Result { get; private set; } + + public void FromJson(JsonValue json, JsonSerializer serializer) + { + Path = JsonPath.Parse(json.Object["path"].String); + var result = json.Object["result"]; + if (result.Type == JsonValueType.Boolean && !result.Boolean) + Result = new JsonArray(); + else + Result = result.Array; + } + public JsonValue ToJson(JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Manatee.Json.Tests/Path/TestSuite/PathTestSet.cs b/Manatee.Json.Tests/Path/TestSuite/PathTestSet.cs new file mode 100644 index 0000000..3ce6623 --- /dev/null +++ b/Manatee.Json.Tests/Path/TestSuite/PathTestSet.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Manatee.Json.Serialization; + +namespace Manatee.Json.Tests.Path.TestSuite +{ + internal class PathTestSet : IJsonSerializable + { + public string Title { get; private set; } + public JsonValue Data { get; private set; } + public IEnumerable Tests { get; private set; } + + public void FromJson(JsonValue json, JsonSerializer serializer) + { + Title = json.Object["title"].String; + Data = json.Object["data"]; + Tests = serializer.Deserialize>(json.Object["tests"]); + } + public JsonValue ToJson(JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} diff --git a/Manatee.Json.Tests/Path/ToStringTest.cs b/Manatee.Json.Tests/Path/ToStringTest.cs new file mode 100644 index 0000000..742e8b5 --- /dev/null +++ b/Manatee.Json.Tests/Path/ToStringTest.cs @@ -0,0 +1,223 @@ +using Manatee.Json.Path; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Manatee.Json.Tests.Path +{ + [TestClass] + public class ToStringTest + { + private void Run(string expected, JsonPath path) + { + var actual = path.ToString(); + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void JustRoot() + { + Run("$", new JsonPath()); + } + + #region Dot Operator + + [TestMethod] + public void ObjectKey() + { + Run("$.name", JsonPathWith.Name("name")); + } + [TestMethod] + public void ObjectChain() + { + Run("$.name.test", JsonPathWith.Name("name").Name("test")); + } + [TestMethod] + public void Length() + { + Run("$.length", JsonPathWith.Length()); + } + + #endregion + + #region Indexed Arrays + + [TestMethod] + public void ArrayIndex_SingleConstant() + { + Run("$[0]", JsonPathWith.Array(0)); + } + [TestMethod] + public void ArrayIndex_SingleSlice() + { + Run("$[0:6]", JsonPathWith.Array(new Slice(0, 6))); + } + [TestMethod] + public void ArrayIndex_SingleSliceStartEndStep() + { + Run("$[0:6:2]", JsonPathWith.Array(new Slice(0, 6, 2))); + } + [TestMethod] + public void ArrayIndex_SingleSliceStart() + { + Run("$[0:]", JsonPathWith.Array(new Slice(0, null))); + } + [TestMethod] + public void ArrayIndex_SingleSliceEnd() + { + Run("$[:6]", JsonPathWith.Array(new Slice(null, 6))); + } + [TestMethod] + public void ArrayIndex_SingleSliceStartStep() + { + Run("$[0::2]", JsonPathWith.Array(new Slice(0, null, 2))); + } + [TestMethod] + public void ArrayIndex_SingleEndStep() + { + Run("$[:6:2]", JsonPathWith.Array(new Slice(null, 6, 2))); + } + [TestMethod] + public void ArrayIndex_SingleSliceStep() + { + Run("$[::2]", JsonPathWith.Array(new Slice(null, null, 2))); + } + [TestMethod] + public void ArrayIndex_ConstantSlice() + { + Run("$[1,4:6]", JsonPathWith.Array(1, new Slice(4, 6))); + } + + #endregion + + #region Index Expression Arrays + + [TestMethod] + public void ArrayIndexExpression_LocalLength() + { + Run("$[(@.length)]", JsonPathWith.Array(jv => jv.Length())); + } + [TestMethod] + public void ArrayIndexExpression_RootLength() + { + Run("$[($.length)]", JsonPathWith.Array(jv => JsonPathRoot.Length())); + } + [TestMethod] + public void ArrayIndexExpression_NameLength() + { + Run("$[(@.name.length)]", JsonPathWith.Array(jv => jv.Name("name").Length())); + } + [TestMethod] + public void ArrayIndexExpression_Addition() + { + Run("$[(@.length+1)]", JsonPathWith.Array(jv => jv.Length() + 1)); + } + [TestMethod] + public void ArrayIndexExpression_Subtraction() + { + Run("$[(@.length-1)]", JsonPathWith.Array(jv => jv.Length() - 1)); + } + [TestMethod] + public void ArrayIndexExpression_Multiplication() + { + Run("$[(@.length*1)]", JsonPathWith.Array(jv => jv.Length() * 1)); + } + [TestMethod] + public void ArrayIndexExpression_Division() + { + Run("$[(@.length/1)]", JsonPathWith.Array(jv => jv.Length() / 1)); + } + [TestMethod] + public void ArrayIndexExpression_Modulus() + { + Run("$[(@.length%1)]", JsonPathWith.Array(jv => jv.Length() % 1)); + } + + #endregion + + #region Filter Expression Arrays + + [TestMethod] + public void ArrayFilterExpression_LocalNameExists() + { + Run("$[?(@.test)]", JsonPathWith.Array(jv => jv.HasProperty("test"))); + } + [TestMethod] + public void ArrayFilterExpression_LocalLengthEqualsInt() + { + Run("$[?(@.length == 1)]", JsonPathWith.Array(jv => jv.Length() == 1)); + } + [TestMethod] + public void ArrayFilterExpression_LocalNameEqualsString() + { + Run("$[?(@.test == \"string\")]", JsonPathWith.Array(jv => jv.Name("test") == "string")); + } + [TestMethod] + public void ArrayFilterExpression_LocalNameChainEqualsBoolean() + { + Run("$[?(@.name.test == false)]", JsonPathWith.Array(jv => jv.Name("name").Name("test") == false)); + } + [TestMethod] + public void ArrayFilterExpression_RootLengthNotEqualsValue() + { + Run("$[?($.length != 1)]", JsonPathWith.Array(jv => JsonPathRoot.Length() != 1)); + } + [TestMethod] + public void ArrayFilterExpression_NameLengthLessThanValue() + { + Run("$[?(@.name.length < 1)]", JsonPathWith.Array(jv => jv.Name("name").Length() < 1)); + } + [TestMethod] + public void ArrayFilterExpression_AdditionLessThanEqualValue() + { + Run("$[?(@.length <= 1)]", JsonPathWith.Array(jv => jv.Length() <= 1)); + } + [TestMethod] + public void ArrayFilterExpression_SubtractionGreaterThanValue() + { + Run("$[?(@.length-1 > 2)]", JsonPathWith.Array(jv => jv.Length() - 1 > 2)); + } + [TestMethod] + public void ArrayFilterExpression_MultiplicationGreaterThanEqualValue() + { + Run("$[?(@.length*1 >= 2)]", JsonPathWith.Array(jv => jv.Length() * 1 >= 2)); + } + [TestMethod] + public void ArrayFilterExpression_DivisionEqualValue() + { + Run("$[?(@.length/1 == 2)]", JsonPathWith.Array(jv => jv.Length() / 1 == 2)); + } + [TestMethod] + public void ArrayFilterExpression_ModulusEqualValue() + { + Run("$[?(@.length%1 == 2)]", JsonPathWith.Array(jv => jv.Length() % 1 == 2)); + } + [TestMethod] + public void ArrayIndexExpression_IndexOfNumber() + { + Run("$[?(@.indexOf(5) == 4)]", JsonPathWith.Array(jv => jv.IndexOf(5) == 4)); + } + [TestMethod] + public void ArrayIndexExpression_IndexOfBoolean() + { + Run("$[?(@.indexOf(false) == 4)]", JsonPathWith.Array(jv => jv.IndexOf(false) == 4)); + } + [TestMethod] + public void ArrayIndexExpression_IndexOfString() + { + Run("$[?(@.indexOf(\"string\") == 4)]", JsonPathWith.Array(jv => jv.IndexOf("string") == 4)); + } + [TestMethod] + public void ArrayIndexExpression_IndexOfArray() + { + var arr = new JsonArray {1, 2, 3}; + Run("$[?(@.indexOf([1,2,3]) == 4)]", JsonPathWith.Array(jv => jv.IndexOf(arr) == 4)); + } + [TestMethod] + public void ArrayIndexExpression_IndexOfObject() + { + var obj = new JsonObject {{"key", "value"}}; + Run("$[?(@.indexOf({\"key\":\"value\"}) == 4)]", JsonPathWith.Array(jv => jv.IndexOf(obj) == 4)); + } + + #endregion + } +} diff --git a/Manatee.Json.Tests/Serialization/JsonDeserializerTest.cs b/Manatee.Json.Tests/Serialization/JsonDeserializerTest.cs index f08d92d..e60233f 100644 --- a/Manatee.Json.Tests/Serialization/JsonDeserializerTest.cs +++ b/Manatee.Json.Tests/Serialization/JsonDeserializerTest.cs @@ -286,6 +286,7 @@ public void InterfaceWithoutMap_Successful() { {"RequiredProp", "test"} }; + JsonSerializationAbstractionMap.RemoveMap(); IInterface expected = new ImplementationClass {RequiredProp = "test"}; var actual = serializer.Deserialize(json); @@ -562,6 +563,7 @@ public void UnimplementedInterface_ReturnsRunTimeImplementation() { var serializer = new JsonSerializer(); var json = new JsonObject {{"RequiredProp", "test"}}; + JsonSerializationAbstractionMap.RemoveMap(); IInterface expected = new ImplementationClass {RequiredProp = "test"}; var actual = serializer.Deserialize(json); diff --git a/Manatee.Json.Tests/packages.config b/Manatee.Json.Tests/packages.config deleted file mode 100644 index 5916821..0000000 --- a/Manatee.Json.Tests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Manatee.Json.sln.DotSettings b/Manatee.Json.sln.DotSettings index 627c71a..f70fddb 100644 --- a/Manatee.Json.sln.DotSettings +++ b/Manatee.Json.sln.DotSettings @@ -17,9 +17,11 @@ TOGETHER_SAME_LINE NEXT_LINE_SHIFTED_2 200 - <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Private methods, properties, and events"><ElementKinds><Kind Name="METHOD" /><Kind Name="EVENT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> True True - True \ No newline at end of file + True + <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Manatee.Json" ModuleVersionMask="*" ClassMask="Manatee.Json.Path.JsonPathRoot" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> \ No newline at end of file diff --git a/Manatee.Json.sln.DotSettings.user b/Manatee.Json.sln.DotSettings.user index 0e7f399..646f45c 100644 --- a/Manatee.Json.sln.DotSettings.user +++ b/Manatee.Json.sln.DotSettings.user @@ -1,5 +1,8 @@  SOLUTION + True + 2016.2 + C:\Users\Gregd\AppData\Local\Temp\ssm.Qageqif.tmp diff --git a/Manatee.Json/Internal/GeneralExtensions.cs b/Manatee.Json/Internal/GeneralExtensions.cs index 8311b84..588b51b 100644 --- a/Manatee.Json/Internal/GeneralExtensions.cs +++ b/Manatee.Json/Internal/GeneralExtensions.cs @@ -267,5 +267,16 @@ public static bool ContentsEqual(this IEnumerable a, IEnumerable b) var listB = b.ToList(); return listA.Count == listB.Count && listA.All(item => listB.Contains(item)); } + public static JsonValue AsJsonValue(this object value) + { + if (value is JsonValue) return (JsonValue) value; + if (value is JsonArray) return (JsonArray) value; + if (value is JsonObject) return (JsonObject) value; + if (value is string) return (string) value; + if (value is bool) return (bool) value; + if (value is IConvertible) return Convert.ToDouble(value); + + return null; + } } } \ No newline at end of file diff --git a/Manatee.Json/JsonArray.cs b/Manatee.Json/JsonArray.cs index f3fab9c..bf7334c 100644 --- a/Manatee.Json/JsonArray.cs +++ b/Manatee.Json/JsonArray.cs @@ -21,8 +21,8 @@ ***************************************************************************************/ +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Manatee.Json.Internal; @@ -72,7 +72,26 @@ public string GetIndentedString(int indentLevel = 0) s += $"{tab1}{this[i].GetIndentedString(indentLevel + 1)}\n{tab0}]"; return s; } - + /// + /// Adds an object to the end of the . + /// + /// The object to be added to the end of the . + /// If the value is null, it will be replaced by . + public new void Add(JsonValue item) + { + base.Add(item ?? JsonValue.Null); + } + /// + /// Adds the elements of the specified collection to the end of the . + /// + /// The collection whose elements should be added to the end of the + /// . The collection itself cannot be null, but it can contain elements + /// that are null. These elements will be replaced by + /// is null. + public new void AddRange(IEnumerable collection) + { + base.AddRange(collection.Select(v => v ?? JsonValue.Null)); + } /// /// Creates a string representation of the JSON data. /// diff --git a/Manatee.Json/JsonObject.cs b/Manatee.Json/JsonObject.cs index be43c75..3a7b22a 100644 --- a/Manatee.Json/JsonObject.cs +++ b/Manatee.Json/JsonObject.cs @@ -88,7 +88,8 @@ public string GetIndentedString(int indentLevel = 0) /// Adds the specified key and value to the dictionary. /// /// The key of the element to add. - /// The value of the element to add. The value can be null for reference types. + /// The value of the element to add. If the value is null, + /// it will be replaced by . public new void Add(string key, JsonValue value) { base.Add(key, value ?? JsonValue.Null); diff --git a/Manatee.Json/Manatee.Json.csproj b/Manatee.Json/Manatee.Json.csproj index 3992d46..196a586 100644 --- a/Manatee.Json/Manatee.Json.csproj +++ b/Manatee.Json/Manatee.Json.csproj @@ -167,36 +167,6 @@ prompt MinimumRecommendedRules.ruleset - - - ..\packages\Manatee.StateMachine.1.1.2\lib\net35\Manatee.StateMachine.dll - True - - - - - ..\packages\Manatee.StateMachine.1.1.2\lib\net35-client\Manatee.StateMachine.dll - True - - - - - ..\packages\Manatee.StateMachine.1.1.2\lib\net40\Manatee.StateMachine.dll - True - - - - - ..\packages\Manatee.StateMachine.1.1.2\lib\net40-client\Manatee.StateMachine.dll - True - - - - - ..\packages\Manatee.StateMachine.1.1.2\lib\net45\Manatee.StateMachine.dll - True - - @@ -233,7 +203,34 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -246,7 +243,6 @@ - @@ -311,6 +307,14 @@ + + + + + + + + @@ -535,7 +539,6 @@ - diff --git a/Manatee.Json/Parsing/ArrayParser.cs b/Manatee.Json/Parsing/ArrayParser.cs index ecda5a2..9798f66 100644 --- a/Manatee.Json/Parsing/ArrayParser.cs +++ b/Manatee.Json/Parsing/ArrayParser.cs @@ -32,7 +32,7 @@ public bool Handles(char c) { return c == '['; } - public string TryParse(string source, ref int index, out JsonValue value) + public string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars) { var array = new JsonArray(); value = array; diff --git a/Manatee.Json/Parsing/BoolParser.cs b/Manatee.Json/Parsing/BoolParser.cs index df9132a..29cb83b 100644 --- a/Manatee.Json/Parsing/BoolParser.cs +++ b/Manatee.Json/Parsing/BoolParser.cs @@ -33,7 +33,7 @@ public bool Handles(char c) { return c.In('t', 'T', 'f', 'F'); } - public string TryParse(string source, ref int index, out JsonValue value) + public string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars) { char[] buffer; int count; diff --git a/Manatee.Json/Parsing/IJsonParser.cs b/Manatee.Json/Parsing/IJsonParser.cs index 2bb06bd..838e6b1 100644 --- a/Manatee.Json/Parsing/IJsonParser.cs +++ b/Manatee.Json/Parsing/IJsonParser.cs @@ -29,7 +29,7 @@ internal interface IJsonParser { bool Handles(char c); // returns error message, if any. Null return implies success. - string TryParse(string source, ref int index, out JsonValue value); + string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars); string TryParse(StreamReader stream, out JsonValue value); } } diff --git a/Manatee.Json/Parsing/JsonParser.cs b/Manatee.Json/Parsing/JsonParser.cs index d2ac7f0..5eeecf4 100644 --- a/Manatee.Json/Parsing/JsonParser.cs +++ b/Manatee.Json/Parsing/JsonParser.cs @@ -58,7 +58,7 @@ public static JsonValue Parse(StreamReader stream) throw new JsonSyntaxException(errorMessage, value); return value; } - public static string Parse(string source, ref int index, out JsonValue value) + public static string Parse(string source, ref int index, out JsonValue value, bool allowExtraChars = false) { var length = source.Length; char c; @@ -74,7 +74,7 @@ public static string Parse(string source, ref int index, out JsonValue value) value = null; return "Cannot determine type."; } - errorMessage = parser.TryParse(source, ref index, out value); + errorMessage = parser.TryParse(source, ref index, out value, allowExtraChars); return errorMessage; } public static string Parse(StreamReader stream, out JsonValue value) diff --git a/Manatee.Json/Parsing/NullParser.cs b/Manatee.Json/Parsing/NullParser.cs index 775f26e..038d02c 100644 --- a/Manatee.Json/Parsing/NullParser.cs +++ b/Manatee.Json/Parsing/NullParser.cs @@ -32,7 +32,7 @@ public bool Handles(char c) { return c.In('n', 'N'); } - public string TryParse(string source, ref int index, out JsonValue value) + public string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars) { var buffer = new char[4]; for (int i = 0; i < 4 && index + i < source.Length; i++) diff --git a/Manatee.Json/Parsing/NumberParser.cs b/Manatee.Json/Parsing/NumberParser.cs index 0d96081..0a2de7b 100644 --- a/Manatee.Json/Parsing/NumberParser.cs +++ b/Manatee.Json/Parsing/NumberParser.cs @@ -37,7 +37,7 @@ public bool Handles(char c) { return c.In('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-'); } - public string TryParse(string source, ref int index, out JsonValue value) + public string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars) { var bufferSize = 0; var bufferLength = FibSequence[bufferSize]; @@ -57,7 +57,9 @@ public string TryParse(string source, ref int index, out JsonValue value) } var c = source[index]; if (char.IsWhiteSpace(c) || c.In(',', ']', '}')) break; - if (!NumberChars.Contains(c)) + var isNumber = NumberChars.Contains(c); + if (!isNumber && allowExtraChars) break; + if (!isNumber) { value = null; return "Expected \',\', \']\', or \'}\'."; diff --git a/Manatee.Json/Parsing/ObjectParser.cs b/Manatee.Json/Parsing/ObjectParser.cs index d6c6aaf..e412412 100644 --- a/Manatee.Json/Parsing/ObjectParser.cs +++ b/Manatee.Json/Parsing/ObjectParser.cs @@ -32,7 +32,7 @@ public bool Handles(char c) { return c == '{'; } - public string TryParse(string source, ref int index, out JsonValue value) + public string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars) { var obj = new JsonObject(); value = obj; diff --git a/Manatee.Json/Parsing/StringParser.cs b/Manatee.Json/Parsing/StringParser.cs index f37e920..16dc195 100644 --- a/Manatee.Json/Parsing/StringParser.cs +++ b/Manatee.Json/Parsing/StringParser.cs @@ -34,7 +34,7 @@ public bool Handles(char c) { return c == '\"'; } - public string TryParse(string source, ref int index, out JsonValue value) + public string TryParse(string source, ref int index, out JsonValue value, bool allowExtraChars) { var bufferSize = 0; var bufferLength = FibSequence[bufferSize]; diff --git a/Manatee.Json/Path/ArrayParameters/FilterExpressionQuery.cs b/Manatee.Json/Path/ArrayParameters/FilterExpressionQuery.cs index 37da1ca..554b04f 100644 --- a/Manatee.Json/Path/ArrayParameters/FilterExpressionQuery.cs +++ b/Manatee.Json/Path/ArrayParameters/FilterExpressionQuery.cs @@ -21,13 +21,14 @@ ***************************************************************************************/ +using System; using System.Collections.Generic; using System.Linq; using Manatee.Json.Path.Expressions; namespace Manatee.Json.Path.ArrayParameters { - internal class FilterExpressionQuery : IJsonPathArrayQuery + internal class FilterExpressionQuery : IJsonPathArrayQuery, IEquatable { private readonly Expression _expression; @@ -44,5 +45,19 @@ public override string ToString() { return $"?({_expression})"; } + public bool Equals(FilterExpressionQuery other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_expression, other._expression); + } + public override bool Equals(object obj) + { + return Equals(obj as FilterExpressionQuery); + } + public override int GetHashCode() + { + return _expression?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/ArrayParameters/IndexExpressionQuery.cs b/Manatee.Json/Path/ArrayParameters/IndexExpressionQuery.cs index 592abaf..9ac1316 100644 --- a/Manatee.Json/Path/ArrayParameters/IndexExpressionQuery.cs +++ b/Manatee.Json/Path/ArrayParameters/IndexExpressionQuery.cs @@ -20,13 +20,14 @@ Purpose: Provides expression-based indexing for array queries. ***************************************************************************************/ +using System; using System.Collections.Generic; using System.Linq; using Manatee.Json.Path.Expressions; namespace Manatee.Json.Path.ArrayParameters { - internal class IndexExpressionQuery : IJsonPathArrayQuery + internal class IndexExpressionQuery : IJsonPathArrayQuery, IEquatable { private readonly Expression _expression; @@ -42,7 +43,21 @@ public IEnumerable Find(JsonArray json, JsonValue root) } public override string ToString() { - return string.Format("({0})", _expression); + return $"({_expression})"; + } + public bool Equals(IndexExpressionQuery other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_expression, other._expression); + } + public override bool Equals(object obj) + { + return Equals(obj as IndexExpressionQuery); + } + public override int GetHashCode() + { + return _expression?.GetHashCode() ?? 0; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/ArrayParameters/IndexQuery.cs b/Manatee.Json/Path/ArrayParameters/IndexQuery.cs deleted file mode 100644 index 1e85170..0000000 --- a/Manatee.Json/Path/ArrayParameters/IndexQuery.cs +++ /dev/null @@ -1,53 +0,0 @@ -/*************************************************************************************** - - Copyright 2016 Greg Dennis - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - File Name: IndexQuery.cs - Namespace: Manatee.Json.Path.ArrayParameters - Class Name: IndexQuery - Purpose: Provides indexed array queries. - -***************************************************************************************/ -using System.Collections.Generic; -using System.Linq; -using Manatee.Json.Internal; - -namespace Manatee.Json.Path.ArrayParameters -{ - internal class IndexQuery : IJsonPathArrayQuery - { - private readonly IEnumerable _indices; - - public IndexQuery(params int[] indices) - { - _indices = indices; - } - - public IEnumerable Find(JsonArray json, JsonValue root) - { - var index = 0; - while (index < json.Count) - { - if (_indices.Contains(index)) - yield return json[index]; - index++; - } - } - public override string ToString() - { - return _indices.Join(","); - } - } -} \ No newline at end of file diff --git a/Manatee.Json/Path/ArrayParameters/SliceQuery.cs b/Manatee.Json/Path/ArrayParameters/SliceQuery.cs index 8c35186..b536732 100644 --- a/Manatee.Json/Path/ArrayParameters/SliceQuery.cs +++ b/Manatee.Json/Path/ArrayParameters/SliceQuery.cs @@ -21,52 +21,43 @@ ***************************************************************************************/ using System; +using Manatee.Json.Internal; using System.Collections.Generic; +using System.Linq; namespace Manatee.Json.Path.ArrayParameters { - internal class SliceQuery : IJsonPathArrayQuery + internal class SliceQuery : IJsonPathArrayQuery, IEquatable { - private int? _start; - private int? _end; - private int? _step; + internal IEnumerable Slices { get; } - public SliceQuery(int? start, int? end, int? step = null) + public SliceQuery(params Slice[] slices) + : this((IEnumerable) slices) {} + public SliceQuery(IEnumerable slices) { - _start = start; - _end = end; - _step = step; + Slices = slices.ToList(); } - public IEnumerable Find(JsonArray json, JsonValue root) { - var start = ResolveIndex(_start ?? 0, json.Count); - var end = ResolveIndex(_end ?? json.Count, json.Count); - var step = Math.Max(_step ?? 1, 1); - - var index = start; - while (index < end) - { - yield return json[index]; - index += step; - } + return Slices.SelectMany(s => s.Find(json, root)).Distinct(); } public override string ToString() { - return _step.HasValue - ? string.Format("{0}:{1}:{2}", - _start.HasValue ? _start.ToString() : string.Empty, - _end.HasValue ? _end.ToString() : string.Empty, - _step) - : string.Format("{0}:{1}", - _start.HasValue ? _start.ToString() : string.Empty, - _end.HasValue ? _end.ToString() : string.Empty); - + return Slices.Join(","); } - - private static int ResolveIndex(int index, int count) + public bool Equals(SliceQuery other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Slices.ContentsEqual(other.Slices); + } + public override bool Equals(object obj) + { + return Equals(obj as SliceQuery); + } + public override int GetHashCode() { - return index < 0 ? count + index : index; + return Slices?.GetCollectionHashCode() ?? 0; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/ArrayParameters/WildCardQuery.cs b/Manatee.Json/Path/ArrayParameters/WildCardQuery.cs index da3220a..fc0fe3d 100644 --- a/Manatee.Json/Path/ArrayParameters/WildCardQuery.cs +++ b/Manatee.Json/Path/ArrayParameters/WildCardQuery.cs @@ -20,11 +20,12 @@ Purpose: Provides a wildcard for array queries. ***************************************************************************************/ +using System; using System.Collections.Generic; namespace Manatee.Json.Path.ArrayParameters { - internal class WildCardQuery : IJsonPathArrayQuery + internal class WildCardQuery : IJsonPathArrayQuery, IEquatable { public static WildCardQuery Instance { get; } @@ -32,6 +33,7 @@ static WildCardQuery() { Instance = new WildCardQuery(); } + private WildCardQuery() {} public IEnumerable Find(JsonArray json, JsonValue root) { @@ -41,5 +43,18 @@ public override string ToString() { return "*"; } + public bool Equals(WildCardQuery other) + { + return !ReferenceEquals(null, other); + } + public override bool Equals(object obj) + { + return Equals(obj as WildCardQuery); + } + public override int GetHashCode() + { + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + return base.GetHashCode(); + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/AddExpression.cs b/Manatee.Json/Path/Expressions/AddExpression.cs index 1fcf53c..87bdf29 100644 --- a/Manatee.Json/Path/Expressions/AddExpression.cs +++ b/Manatee.Json/Path/Expressions/AddExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class AddExpression : ExpressionTreeBranch + internal class AddExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 2; + protected override int BasePriority => 2; public override object Evaluate(T json, JsonValue root) { @@ -44,5 +46,27 @@ public override string ToString() { return $"{Left}+{Right}"; } + public bool Equals(AddExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((AddExpression) obj); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/AndExpression.cs b/Manatee.Json/Path/Expressions/AndExpression.cs index fde7413..ebf61e2 100644 --- a/Manatee.Json/Path/Expressions/AndExpression.cs +++ b/Manatee.Json/Path/Expressions/AndExpression.cs @@ -20,11 +20,13 @@ Purpose: Expresses the intent to perform a boolean AND. ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class AndExpression : ExpressionTreeBranch + internal class AndExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 0; + protected override int BasePriority => 0; public override object Evaluate(T json, JsonValue root) { @@ -34,5 +36,24 @@ public override string ToString() { return $"{Left} && {Right}"; } + public bool Equals(AndExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as AndExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/ArrayIndexExpression.cs b/Manatee.Json/Path/Expressions/ArrayIndexExpression.cs index 7822287..c4a7f16 100644 --- a/Manatee.Json/Path/Expressions/ArrayIndexExpression.cs +++ b/Manatee.Json/Path/Expressions/ArrayIndexExpression.cs @@ -23,18 +23,20 @@ ***************************************************************************************/ using System; using System.Linq; +using Manatee.Json.Internal; namespace Manatee.Json.Path.Expressions { - internal class ArrayIndexExpression : PathExpression + internal class ArrayIndexExpression : PathExpression, IEquatable> { - public override int Priority => 6; public int Index { get; set; } public ExpressionTreeNode IndexExpression { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { - var value = IsLocal ? json as JsonValue : root; + var value = IsLocal ? json.AsJsonValue() : root; if (value == null) throw new NotSupportedException("ArrayIndex requires a JsonValue to evaluate."); var results = Path.Evaluate(value); @@ -56,6 +58,28 @@ public override string ToString() var path = Path == null ? string.Empty : Path.GetRawString(); return string.Format(IsLocal ? "@{0}[{1}]" : "${0}[{1}]", path, GetIndex()); } + public bool Equals(ArrayIndexExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && + Index == other.Index && + Equals(IndexExpression, other.IndexExpression); + } + public override bool Equals(object obj) + { + return Equals(obj as ArrayIndexExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode*397) ^ Index; + hashCode = (hashCode*397) ^ (IndexExpression?.GetHashCode() ?? 0); + return hashCode; + } + } private int GetIndex() { diff --git a/Manatee.Json/Path/Expressions/ConversionExpression.cs b/Manatee.Json/Path/Expressions/ConversionExpression.cs index 63e2e4e..45c4369 100644 --- a/Manatee.Json/Path/Expressions/ConversionExpression.cs +++ b/Manatee.Json/Path/Expressions/ConversionExpression.cs @@ -25,12 +25,13 @@ namespace Manatee.Json.Path.Expressions { - internal class ConversionExpression : ExpressionTreeNode + internal class ConversionExpression : ExpressionTreeNode, IEquatable> { - public override int Priority => 6; public ExpressionTreeNode Root { get; set; } public Type TargetType { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { var value = Root.Evaluate(json, root); @@ -41,6 +42,20 @@ public override string ToString() { return Root.ToString(); } + public bool Equals(ConversionExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Root, other.Root) && TargetType == other.TargetType; + } + public override bool Equals(object obj) + { + return Equals(obj as ConversionExpression); + } + public override int GetHashCode() + { + unchecked { return ((Root?.GetHashCode() ?? 0)*397) ^ (TargetType != null ? TargetType.GetHashCode() : 0); } + } private object CastValue(object value) { @@ -49,7 +64,9 @@ private object CastValue(object value) return new JsonValue((bool)value); if (value is string) return new JsonValue((string)value); - return new JsonValue(Convert.ToDouble(value)); + if (value is IConvertible) + return new JsonValue(Convert.ToDouble(value)); + return value; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/DivideExpression.cs b/Manatee.Json/Path/Expressions/DivideExpression.cs index 7c90c10..e875e2c 100644 --- a/Manatee.Json/Path/Expressions/DivideExpression.cs +++ b/Manatee.Json/Path/Expressions/DivideExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class DivideExpression : ExpressionTreeBranch + internal class DivideExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 3; + protected override int BasePriority => 3; public override object Evaluate(T json, JsonValue root) { @@ -40,5 +42,24 @@ public override string ToString() var right = Right.Priority <= Priority ? $"({Right})" : Right.ToString(); return $"{left}/{right}"; } + public bool Equals(DivideExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as DivideExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/ExponentExpression.cs b/Manatee.Json/Path/Expressions/ExponentExpression.cs index 9c3c5d5..defc193 100644 --- a/Manatee.Json/Path/Expressions/ExponentExpression.cs +++ b/Manatee.Json/Path/Expressions/ExponentExpression.cs @@ -24,9 +24,9 @@ namespace Manatee.Json.Path.Expressions { - internal class ExponentExpression : ExpressionTreeBranch + internal class ExponentExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 4; + protected override int BasePriority => 4; public override object Evaluate(T json, JsonValue root) { @@ -40,5 +40,24 @@ public override string ToString() var right = Right.Priority <= Priority ? $"({Right})" : Right.ToString(); return $"{left}^{right}"; } + public bool Equals(ExponentExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as ExponentExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Expression.cs b/Manatee.Json/Path/Expressions/Expression.cs index 2c3d0ff..cfa8dbb 100644 --- a/Manatee.Json/Path/Expressions/Expression.cs +++ b/Manatee.Json/Path/Expressions/Expression.cs @@ -21,96 +21,33 @@ ***************************************************************************************/ using System; -using System.Collections.Generic; -using System.Linq; -using Manatee.Json.Internal; -using Manatee.Json.Path.Operators; -using Manatee.StateMachine; -using Manatee.StateMachine.Exceptions; namespace Manatee.Json.Path.Expressions { - internal class Expression + internal class Expression : IEquatable> { - private enum State - { - Start, - Value, - NumberOrPath, - Operator, - Comparison, - ValueOrOperator, - BooleanOrComparison, - End, - } - - private static readonly StateMachine _stateMachine = new StateMachine(); - - private List> _nodeList; - private readonly InputStream _stream = new InputStream(); - private bool _done; - private string _source; - private int _index; - private JsonPathExpressionInput? _previousInput; - private int _startIndex; + private readonly ExpressionTreeNode _root; - internal ExpressionTreeNode Root { get; set; } - - static Expression() + public Expression(ExpressionTreeNode root) { - _stateMachine[State.Start, JsonPathExpressionInput.OpenParenth] = GotStart; - _stateMachine[State.Value, JsonPathExpressionInput.OpenParenth] = GotGroup; - _stateMachine[State.Value, JsonPathExpressionInput.Number] = GotNumber; - _stateMachine[State.Value, JsonPathExpressionInput.Minus] = GotNumber; - _stateMachine[State.Value, JsonPathExpressionInput.Root] = GotRoot; - _stateMachine[State.Value, JsonPathExpressionInput.Current] = GotCurrent; - _stateMachine[State.Value, JsonPathExpressionInput.Bang] = GotNot; - _stateMachine[State.Value, JsonPathExpressionInput.Quote] = GotString; - _stateMachine[State.Value, JsonPathExpressionInput.Letter] = GotNamedConstant; - _stateMachine[State.NumberOrPath, JsonPathExpressionInput.OpenParenth] = GotGroup; - _stateMachine[State.NumberOrPath, JsonPathExpressionInput.Number] = GotNumber; - _stateMachine[State.NumberOrPath, JsonPathExpressionInput.Root] = GotRoot; - _stateMachine[State.NumberOrPath, JsonPathExpressionInput.Current] = GotCurrent; - _stateMachine[State.Operator, JsonPathExpressionInput.Plus] = GotPlus; - _stateMachine[State.Operator, JsonPathExpressionInput.Minus] = GotMinus; - _stateMachine[State.Operator, JsonPathExpressionInput.Star] = GotMultiply; - _stateMachine[State.Operator, JsonPathExpressionInput.Slash] = GotDivide; - _stateMachine[State.Operator, JsonPathExpressionInput.Caret] = GotExponent; - _stateMachine[State.Operator, JsonPathExpressionInput.LessThan] = GotLessThan; - _stateMachine[State.Operator, JsonPathExpressionInput.Equal] = GotEqual; - _stateMachine[State.Operator, JsonPathExpressionInput.And] = GotAnd; - _stateMachine[State.Operator, JsonPathExpressionInput.Or] = GotOr; - _stateMachine[State.Operator, JsonPathExpressionInput.GreaterThan] = GotGreaterThan; - _stateMachine[State.Operator, JsonPathExpressionInput.Bang] = GotNot; - _stateMachine[State.Operator, JsonPathExpressionInput.CloseParenth] = CompleteExpression; - _stateMachine[State.Comparison, JsonPathExpressionInput.Equal] = GotEqual; - _stateMachine[State.Comparison, JsonPathExpressionInput.And] = GotAnd; - _stateMachine[State.Comparison, JsonPathExpressionInput.Or] = GotOr; - _stateMachine[State.ValueOrOperator, JsonPathExpressionInput.OpenParenth] = GotGroup; - _stateMachine[State.ValueOrOperator, JsonPathExpressionInput.Number] = GotNumber; - _stateMachine[State.ValueOrOperator, JsonPathExpressionInput.Root] = GotRoot; - _stateMachine[State.ValueOrOperator, JsonPathExpressionInput.Current] = GotCurrent; - _stateMachine[State.ValueOrOperator, JsonPathExpressionInput.Equal] = FinalizeComparison; - _stateMachine[State.ValueOrOperator, JsonPathExpressionInput.Letter] = GotNamedConstant; - _stateMachine[State.BooleanOrComparison, JsonPathExpressionInput.OpenParenth] = GotGroup; - _stateMachine[State.BooleanOrComparison, JsonPathExpressionInput.Root] = GotRoot; - _stateMachine[State.BooleanOrComparison, JsonPathExpressionInput.Current] = GotCurrent; - _stateMachine[State.BooleanOrComparison, JsonPathExpressionInput.Equal] = GotEqual; - _stateMachine[State.BooleanOrComparison, JsonPathExpressionInput.Letter] = GotNamedConstant; - _stateMachine.UpdateFunction = GetNextInput; + _root = root; } public T Evaluate(TIn json, JsonValue root) { - var result = Root.Evaluate(json, root); - if (typeof (T) == typeof (bool) && result == null) + var result = _root.Evaluate(json, root); + if (typeof(T) == typeof(bool) && result == null) return (T) (object) false; - if (typeof (T) == typeof (bool) && result != null && !(result is bool)) + if (typeof(T) == typeof(bool) && result != null && !(result is bool)) return (T) (object) true; - if (typeof (T) == typeof (int) && result == null) + if (typeof(T) == typeof(int) && result == null) return (T) (object) -1; - if (typeof (T) == typeof (JsonValue)) + var resultAsJsonValue = result as JsonValue; + if (typeof(T) == typeof(int) && resultAsJsonValue?.Type == JsonValueType.Number) + return (T) Convert.ChangeType(resultAsJsonValue.Number, typeof(T)); + if (typeof(T) == typeof(JsonValue)) { + if (result is JsonValue) return (T) result; if (result is double) return (T) (object) new JsonValue((double) result); if (result is bool) @@ -122,386 +59,25 @@ public T Evaluate(TIn json, JsonValue root) if (result is JsonObject) return (T) (object) new JsonValue((JsonObject) result); } - return (T)Convert.ChangeType(result, typeof(T)); + return (T) Convert.ChangeType(result, typeof(T)); } public override string ToString() { - return Root.ToString(); - } - - internal int Parse(string source, int i) - { - _source = source; - Parse(i); - return _index; - } - - private void Parse(int i) - { - _stream.Clear(); - _startIndex = i + 1; - _index = i; - _done = false; - try - { - _stateMachine.Run(this, State.Start, _stream); - if (!_done) - throw new JsonPathSyntaxException(GetErrorLocation(), "Found incomplete expression."); - } - catch (InputNotValidForStateException e) - { - switch (e.State) - { - case State.Start: - throw new JsonPathSyntaxException(GetErrorLocation(), "Expression must start with '('."); - case State.Value: - throw new JsonPathSyntaxException(GetErrorLocation(), "Expected an expression, a value, or a path which evaluates to a value."); - case State.NumberOrPath: - throw new JsonPathSyntaxException(GetErrorLocation(), "Expected an expression, a value, or a path which evaluates to a value."); - case State.Operator: - throw new JsonPathSyntaxException(GetErrorLocation(), "Expected an operator."); - case State.Comparison: - throw new JsonPathSyntaxException(GetErrorLocation(), "Invalid comparison operator."); - case State.ValueOrOperator: - if (e.Input == JsonPathExpressionInput.CloseParenth) - throw new JsonPathSyntaxException(GetErrorLocation(), "Found incomplete expression."); - throw new JsonPathSyntaxException(GetErrorLocation(), "Invalid comparison operator."); - case State.BooleanOrComparison: - throw new JsonPathSyntaxException(GetErrorLocation(), "Expected '=', a boolean value, or an expression which evaluates to a boolean."); - default: - throw new ArgumentOutOfRangeException(); - } - } - catch (StateNotValidException) - { - throw new JsonPathSyntaxException(GetErrorLocation(), "An unrecoverable error occurred while parsing a JSON path expression. Please report to littlecrabsolutions@yahoo.com."); - } - catch (ActionNotDefinedForStateAndInputException) - { - throw new JsonPathSyntaxException(GetErrorLocation(), "An unrecoverable error occurred while parsing a JSON path expression. Please report to littlecrabsolutions@yahoo.com."); - } - } - private string GetErrorLocation() - { - var length = _index - _startIndex - 1; - return length < 1 ? string.Empty : _source.Substring(_startIndex, length); - } - private static void GetNextInput(object owner) - { - var obj = owner as Expression; - if (obj == null) return; - if (obj._done || (obj._index == obj._source.Length)) return; - var c = default(char); - try - { - c = obj._source[obj._index++]; - var next = CharacterConverter.ExpressionItem(c); - obj._stream.Add(next); - } - catch (KeyNotFoundException) - { - throw new JsonPathSyntaxException(obj.GetErrorLocation(), "Unrecognized character '{0}' in input string.", c); - } - } - private static State GotStart(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp._nodeList = new List>(); - return State.Value; - } - private static State GotGroup(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - CheckComparison(exp); - var group = new Expression(); - exp._index = group.Parse(exp._source, exp._index - 1); - var last = exp._nodeList.LastOrDefault() as IndexOfExpression; - if (last != null) - last.ParameterExpression = group.Root as ExpressionTreeNode; - else - exp._nodeList.Add(new GroupExpression {Group = group.Root}); - return State.Operator; - } - private static State CompleteExpression(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp.Root = BuildTree(exp._nodeList); - exp._nodeList = null; - exp._done = true; - return State.End; - } - private static State GotNumber(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - CheckComparison(exp); - var numString = new string(exp._source.Skip(exp._index - 1).TakeWhile(char.IsNumber).ToArray()); - double num; - if (!double.TryParse(numString, out num)) - throw new JsonPathSyntaxException("Attempt to parse '{0}' as a number failed.", numString); - exp._index += numString.Length - 1; - exp._nodeList.Add(new ValueExpression {Value = num}); - return State.Operator; - } - private static State GotRoot(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - CheckComparison(exp); - var path = new JsonPath(); - exp._index = path.Parse(exp._source, exp._index - 1) - 1; - var name = path.Operators.Last() as NameOperator; - var indexOf = path.Operators.Last() as IndexOfOperator; - if (name != null && name.Name == "length") - { - path.Operators.Remove(name); - exp._nodeList.Add(new LengthExpression {Path = path}); - } - else if (indexOf != null) - { - path.Operators.Remove(indexOf); - exp._nodeList.Add(new IndexOfExpression { Path = path, IsLocal = true, ParameterExpression = indexOf.Parameter.Root }); - } - else - exp._nodeList.Add(new PathExpression {Path = path}); - return State.Operator; - } - private static State GotCurrent(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - CheckComparison(exp); - var path = new JsonPath(); - exp._index = path.Parse(exp._source, exp._index - 1) - 1; - var name = path.Operators.Last() as NameOperator; - var indexOf = path.Operators.Last() as IndexOfOperator; - if (name != null && name.Name == "length") - { - path.Operators.Remove(name); - exp._nodeList.Add(new LengthExpression {Path = path, IsLocal = true}); - } - else if (indexOf != null) - { - path.Operators.Remove(indexOf); - exp._nodeList.Add(new IndexOfExpression {Path = path, IsLocal = true, ParameterExpression = indexOf.Parameter.Root}); - } - else - exp._nodeList.Add(new PathExpression {Path = path, IsLocal = true}); - return State.Operator; - } - private static State GotString(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - CheckComparison(exp); - var value = new string(exp._source.Skip(exp._index).TakeWhile(c => c != '"').ToArray()); - exp._index += value.Length+1; - exp._nodeList.Add(new ValueExpression {Value = value}); - return State.Operator; + return _root.ToString(); } - private static State GotPlus(object owner, JsonPathExpressionInput input) + public bool Equals(Expression other) { - var exp = owner as Expression; - exp._nodeList.Add(new AddExpression()); - return State.Value; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_root, other._root); } - private static State GotMinus(object owner, JsonPathExpressionInput input) + public override bool Equals(object obj) { - var exp = owner as Expression; - exp._nodeList.Add(new SubtractExpression()); - return State.Value; + return Equals(obj as Expression); } - private static State GotMultiply(object owner, JsonPathExpressionInput input) + public override int GetHashCode() { - var exp = owner as Expression; - exp._nodeList.Add(new MultiplyExpression()); - return State.Value; - } - private static State GotDivide(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp._nodeList.Add(new DivideExpression()); - return State.Value; - } - private static State GotExponent(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp._nodeList.Add(new ExponentExpression()); - return State.Value; - } - private static State GotLessThan(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp._previousInput = JsonPathExpressionInput.LessThan; - return State.ValueOrOperator; - } - private static State GotEqual(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - if (exp._previousInput.HasValue) - { - switch (exp._previousInput) - { - case JsonPathExpressionInput.Equal: - exp._nodeList.Add(new IsEqualExpression()); - break; - case JsonPathExpressionInput.Bang: - exp._nodeList.Add(new IsNotEqualExpression()); - break; - default: - throw new JsonPathSyntaxException(exp.GetErrorLocation(), "Operator '={0}' not recognized.", exp._source[exp._index - 1]); - } - exp._previousInput = null; - return State.Value; - } - exp._previousInput = JsonPathExpressionInput.Equal; - return State.Comparison; - } - private static State GotAnd(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - if (exp._previousInput.HasValue) - { - if (exp._previousInput == JsonPathExpressionInput.And) - exp._nodeList.Add(new AndExpression()); - else - throw new JsonPathSyntaxException(exp.GetErrorLocation(), "Operator '&{0}' not recognized.", exp._source[exp._index - 1]); - exp._previousInput = null; - return State.Value; - } - exp._previousInput = JsonPathExpressionInput.And; - return State.Comparison; - } - private static State GotOr(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - if (exp._previousInput.HasValue) - { - if (exp._previousInput == JsonPathExpressionInput.Or) - exp._nodeList.Add(new OrExpression()); - else - throw new JsonPathSyntaxException(exp.GetErrorLocation(), "Operator '|{0}' not recognized.", exp._source[exp._index - 1]); - exp._previousInput = null; - return State.Value; - } - exp._previousInput = JsonPathExpressionInput.Or; - return State.Comparison; - } - private static State GotGreaterThan(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp._previousInput = JsonPathExpressionInput.GreaterThan; - return State.ValueOrOperator; - } - private static State GotNot(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - exp._previousInput = JsonPathExpressionInput.Bang; - return State.BooleanOrComparison; - } - private static State GotNamedConstant(object owner, JsonPathExpressionInput input) - { - ExpressionTreeNode bang = null; - var exp = owner as Expression; - if (exp._previousInput.HasValue) - { - bang = new NotExpression(); - exp._previousInput = null; - } - object value; - var substring = exp._source.Substring(exp._index - 1).ToLower(); - if (substring.StartsWith("true")) - { - if (bang != null) - exp._nodeList.Add(bang); - value = true; - exp._index += 3; - } - else if (substring.StartsWith("false")) - { - if (bang != null) - exp._nodeList.Add(bang); - value = false; - exp._index += 4; - } - else if (substring.StartsWith("null")) - { - if (bang != null) - throw new JsonPathSyntaxException(exp.GetErrorLocation(), "Cannot apply '!' operator to 'null'."); - value = JsonValue.Null; - exp._index += 3; - } - else - { - var constant = new string(substring.TakeWhile(char.IsLetterOrDigit).ToArray()); - throw new JsonPathSyntaxException(exp.GetErrorLocation(), "Constant value '{0}' not recognized", constant); - } - exp._nodeList.Add(new ValueExpression {Value = value}); - return State.Operator; - } - private static State FinalizeComparison(object owner, JsonPathExpressionInput input) - { - var exp = owner as Expression; - switch (input) - { - case JsonPathExpressionInput.LessThan: - exp._nodeList.Add(new IsLessThanExpression()); - break; - case JsonPathExpressionInput.Equal: - switch (exp._previousInput) - { - case JsonPathExpressionInput.LessThan: - exp._nodeList.Add(new IsLessThanEqualExpression()); - break; - case JsonPathExpressionInput.GreaterThan: - exp._nodeList.Add(new IsGreaterThanEqualExpression()); - break; - case JsonPathExpressionInput.Bang: - exp._nodeList.Add(new IsNotEqualExpression()); - break; - default: - throw new JsonPathSyntaxException(exp.GetErrorLocation(), "Operator '{0}=' not recognized.", exp._source[exp._index - 2]); - } - break; - case JsonPathExpressionInput.GreaterThan: - exp._nodeList.Add(new IsGreaterThanExpression()); - break; - case JsonPathExpressionInput.Bang: - exp._nodeList.Add(new NotExpression()); - break; - default: - throw new ArgumentOutOfRangeException(); - } - exp._previousInput = null; - return State.NumberOrPath; - } - private static void CheckComparison(Expression exp) - { - if (exp._previousInput.HasValue) - { - var previous = exp._previousInput.Value; - exp._previousInput = null; - FinalizeComparison(exp, previous); - } - } - private static ExpressionTreeNode BuildTree(IList> nodes) - { - if (!nodes.Any()) return null; - var minPriority = nodes.Min(n => n.Priority); - var root = nodes.Last(n => n.Priority == minPriority); - var branch = root as ExpressionTreeBranch; - if (branch != null) - { - var split = nodes.IndexOf(root); - var left = nodes.Take(split).ToList(); - var right = nodes.Skip(split + 1).ToList(); - branch.Left = BuildTree(left); - branch.Right = BuildTree(right); - } - var not = root as NotExpression; - if (not != null) - { - var split = nodes.IndexOf(root); - var right = nodes.Skip(split + 1).FirstOrDefault(); - not.Root = right; - } - return root; + return _root?.GetHashCode() ?? 0; } } } diff --git a/Manatee.Json/Path/Expressions/ExpressionTreeBranch.cs b/Manatee.Json/Path/Expressions/ExpressionTreeBranch.cs index 696b9fe..08179d3 100644 --- a/Manatee.Json/Path/Expressions/ExpressionTreeBranch.cs +++ b/Manatee.Json/Path/Expressions/ExpressionTreeBranch.cs @@ -26,5 +26,18 @@ internal abstract class ExpressionTreeBranch : ExpressionTreeNode { public ExpressionTreeNode Left { get; set; } public ExpressionTreeNode Right { get; set; } + + protected bool Equals(ExpressionTreeBranch other) + { + return Equals(Left, other.Left) && Equals(Right, other.Right); + } + public override bool Equals(object obj) + { + return Equals(obj as ExpressionTreeBranch); + } + public override int GetHashCode() + { + unchecked { return ((Left?.GetHashCode() ?? 0)*397) ^ (Right?.GetHashCode() ?? 0); } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/ExpressionTreeNode.cs b/Manatee.Json/Path/Expressions/ExpressionTreeNode.cs index dbe2d84..a4f16d0 100644 --- a/Manatee.Json/Path/Expressions/ExpressionTreeNode.cs +++ b/Manatee.Json/Path/Expressions/ExpressionTreeNode.cs @@ -24,8 +24,17 @@ namespace Manatee.Json.Path.Expressions { internal abstract class ExpressionTreeNode { - public abstract int Priority { get; } + private int _priorityBump; + + public int Priority => BasePriority + _priorityBump; + + protected abstract int BasePriority { get; } public abstract object Evaluate(T json, JsonValue root); + + public void BumpPriority() + { + _priorityBump += 10; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/FieldExpression.cs b/Manatee.Json/Path/Expressions/FieldExpression.cs index 27ddcde..a03197c 100644 --- a/Manatee.Json/Path/Expressions/FieldExpression.cs +++ b/Manatee.Json/Path/Expressions/FieldExpression.cs @@ -26,15 +26,19 @@ namespace Manatee.Json.Path.Expressions { - internal class FieldExpression : ExpressionTreeNode + internal class FieldExpression : ExpressionTreeNode, IEquatable> { - public override int Priority => 6; public FieldInfo Field { get; set; } public object Source { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { - if (Field.FieldType == typeof(string)) + if (Field.FieldType == typeof(string) || + Field.FieldType == typeof(JsonArray) || + Field.FieldType == typeof(JsonObject) || + Field.FieldType == typeof(JsonValue)) return Field.GetValue(Source); return Convert.ToDouble(Field.GetValue(Source)); } @@ -45,5 +49,19 @@ public override string ToString() ? $"\"{value}\"" : value?.ToString() ?? "null"; } + public bool Equals(FieldExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Field, other.Field) && Equals(Source, other.Source); + } + public override bool Equals(object obj) + { + return Equals(obj as FieldExpression); + } + public override int GetHashCode() + { + unchecked { return ((Field != null ? Field.GetHashCode() : 0)*397) ^ (Source?.GetHashCode() ?? 0); } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/HasPropertyExpression.cs b/Manatee.Json/Path/Expressions/HasPropertyExpression.cs index 0b79936..79d7bec 100644 --- a/Manatee.Json/Path/Expressions/HasPropertyExpression.cs +++ b/Manatee.Json/Path/Expressions/HasPropertyExpression.cs @@ -24,11 +24,12 @@ namespace Manatee.Json.Path.Expressions { - internal class HasPropertyExpression : ExpressionTreeNode + internal class HasPropertyExpression : PathExpression, IEquatable> { - public override int Priority => 6; public string Name { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { var value = json as JsonValue; @@ -43,5 +44,24 @@ public override string ToString() { return $"@.{Name}"; } + public bool Equals(HasPropertyExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && string.Equals(Name, other.Name); + } + public override bool Equals(object obj) + { + return Equals(obj as HasPropertyExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ (Name?.GetHashCode() ?? 0); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/IndexOfExpression.cs b/Manatee.Json/Path/Expressions/IndexOfExpression.cs index 3224b0a..65f8f29 100644 --- a/Manatee.Json/Path/Expressions/IndexOfExpression.cs +++ b/Manatee.Json/Path/Expressions/IndexOfExpression.cs @@ -22,18 +22,20 @@ ***************************************************************************************/ using System; using System.Linq; +using Manatee.Json.Internal; namespace Manatee.Json.Path.Expressions { - internal class IndexOfExpression : PathExpression + internal class IndexOfExpression : PathExpression, IEquatable> { - public override int Priority => 6; public JsonValue Parameter { get; set; } public ExpressionTreeNode ParameterExpression { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { - var value = IsLocal ? json as JsonValue : root; + var value = IsLocal ? json.AsJsonValue() : root; if (value == null) throw new NotSupportedException("IndexOf requires a JsonValue to evaluate."); var results = Path.Evaluate(value); @@ -51,6 +53,26 @@ public override string ToString() var parameter = ParameterExpression?.ToString() ?? Parameter.ToString(); return string.Format(IsLocal ? "@{0}.indexOf({1})" : "${0}.indexOf({1})", path, parameter); } + public bool Equals(IndexOfExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Equals(ParameterExpression, other.ParameterExpression); + } + public override bool Equals(object obj) + { + return Equals(obj as IndexOfExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode*397) ^ (Parameter != null ? Parameter.GetHashCode() : 0); + hashCode = (hashCode*397) ^ (ParameterExpression?.GetHashCode() ?? 0); + return hashCode; + } + } private JsonValue GetParameter() { diff --git a/Manatee.Json/Path/Expressions/IsEqualExpression.cs b/Manatee.Json/Path/Expressions/IsEqualExpression.cs index 6a90e1d..3221031 100644 --- a/Manatee.Json/Path/Expressions/IsEqualExpression.cs +++ b/Manatee.Json/Path/Expressions/IsEqualExpression.cs @@ -25,9 +25,9 @@ namespace Manatee.Json.Path.Expressions { - internal class IsEqualExpression : ExpressionTreeBranch + internal class IsEqualExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 1; + protected override int BasePriority => 1; public override object Evaluate(T json, JsonValue root) { @@ -41,5 +41,24 @@ public override string ToString() { return $"{Left} == {Right}"; } + public bool Equals(IsEqualExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as IsEqualExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/IsGreaterThanEqualExpression.cs b/Manatee.Json/Path/Expressions/IsGreaterThanEqualExpression.cs index fa55ed7..8f1e0e4 100644 --- a/Manatee.Json/Path/Expressions/IsGreaterThanEqualExpression.cs +++ b/Manatee.Json/Path/Expressions/IsGreaterThanEqualExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class IsGreaterThanEqualExpression : ExpressionTreeBranch + internal class IsGreaterThanEqualExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 1; + protected override int BasePriority => 1; public override object Evaluate(T json, JsonValue root) { @@ -37,5 +39,24 @@ public override string ToString() { return $"{Left} >= {Right}"; } + public bool Equals(IsGreaterThanEqualExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as IsGreaterThanEqualExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/IsGreaterThanExpression.cs b/Manatee.Json/Path/Expressions/IsGreaterThanExpression.cs index 7f6b819..93d9ba2 100644 --- a/Manatee.Json/Path/Expressions/IsGreaterThanExpression.cs +++ b/Manatee.Json/Path/Expressions/IsGreaterThanExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class IsGreaterThanExpression : ExpressionTreeBranch + internal class IsGreaterThanExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 1; + protected override int BasePriority => 1; public override object Evaluate(T json, JsonValue root) { @@ -37,5 +39,25 @@ public override string ToString() { return $"{Left} > {Right}"; } + public bool Equals(IsGreaterThanExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + if (obj.GetType() != this.GetType()) return false; + return Equals((IsGreaterThanExpression) obj); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/IsLessThanEqualExpression.cs b/Manatee.Json/Path/Expressions/IsLessThanEqualExpression.cs index 5819946..cdfa826 100644 --- a/Manatee.Json/Path/Expressions/IsLessThanEqualExpression.cs +++ b/Manatee.Json/Path/Expressions/IsLessThanEqualExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class IsLessThanEqualExpression : ExpressionTreeBranch + internal class IsLessThanEqualExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 1; + protected override int BasePriority => 1; public override object Evaluate(T json, JsonValue root) { @@ -37,5 +39,24 @@ public override string ToString() { return $"{Left} <= {Right}"; } + public bool Equals(IsLessThanEqualExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as IsLessThanEqualExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/IsLessThanExpression.cs b/Manatee.Json/Path/Expressions/IsLessThanExpression.cs index df9b8e2..a1952d0 100644 --- a/Manatee.Json/Path/Expressions/IsLessThanExpression.cs +++ b/Manatee.Json/Path/Expressions/IsLessThanExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class IsLessThanExpression : ExpressionTreeBranch + internal class IsLessThanExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 1; + protected override int BasePriority => 1; public override object Evaluate(T json, JsonValue root) { @@ -37,5 +39,24 @@ public override string ToString() { return $"{Left} < {Right}"; } + public bool Equals(IsLessThanExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as IsLessThanExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/IsNotEqualExpression.cs b/Manatee.Json/Path/Expressions/IsNotEqualExpression.cs index cc0090f..d7c36cc 100644 --- a/Manatee.Json/Path/Expressions/IsNotEqualExpression.cs +++ b/Manatee.Json/Path/Expressions/IsNotEqualExpression.cs @@ -25,9 +25,9 @@ namespace Manatee.Json.Path.Expressions { - internal class IsNotEqualExpression : ExpressionTreeBranch + internal class IsNotEqualExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 1; + protected override int BasePriority => 1; public override object Evaluate(T json, JsonValue root) { @@ -41,5 +41,24 @@ public override string ToString() { return $"{Left} != {Right}"; } + public bool Equals(IsNotEqualExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as IsNotEqualExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/LengthExpression.cs b/Manatee.Json/Path/Expressions/LengthExpression.cs index e42d716..c50bf9b 100644 --- a/Manatee.Json/Path/Expressions/LengthExpression.cs +++ b/Manatee.Json/Path/Expressions/LengthExpression.cs @@ -25,9 +25,9 @@ namespace Manatee.Json.Path.Expressions { - internal class LengthExpression : PathExpression + internal class LengthExpression : PathExpression, IEquatable> { - public override int Priority => 6; + protected override int BasePriority => 6; public override object Evaluate(T json, JsonValue root) { @@ -66,5 +66,24 @@ public override string ToString() var path = Path == null ? string.Empty : Path.GetRawString(); return (IsLocal ? "@" : "$") + $"{path}.length"; } + public bool Equals(LengthExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as LengthExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/ModuloExpression.cs b/Manatee.Json/Path/Expressions/ModuloExpression.cs index 6e6d6a7..93f846d 100644 --- a/Manatee.Json/Path/Expressions/ModuloExpression.cs +++ b/Manatee.Json/Path/Expressions/ModuloExpression.cs @@ -20,11 +20,13 @@ Purpose: Expresses the intent to calculate the modulo of two numbers. ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class ModuloExpression : ExpressionTreeBranch + internal class ModuloExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 2; + protected override int BasePriority => 2; public override object Evaluate(T json, JsonValue root) { @@ -39,5 +41,24 @@ public override string ToString() var right = Right.Priority <= Priority ? $"({Right})" : Right.ToString(); return $"{left}%{right}"; } + public bool Equals(ModuloExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as ModuloExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/MultiplyExpression.cs b/Manatee.Json/Path/Expressions/MultiplyExpression.cs index 5296dd7..3892ce0 100644 --- a/Manatee.Json/Path/Expressions/MultiplyExpression.cs +++ b/Manatee.Json/Path/Expressions/MultiplyExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class MultiplyExpression : ExpressionTreeBranch + internal class MultiplyExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 3; + protected override int BasePriority => 3; public override object Evaluate(T json, JsonValue root) { @@ -40,5 +42,24 @@ public override string ToString() var right = Right.Priority <= Priority ? $"({Right})" : Right.ToString(); return $"{left}*{right}"; } + public bool Equals(MultiplyExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as MultiplyExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/NameExpresssion.cs b/Manatee.Json/Path/Expressions/NameExpresssion.cs index 2494677..83bdb8b 100644 --- a/Manatee.Json/Path/Expressions/NameExpresssion.cs +++ b/Manatee.Json/Path/Expressions/NameExpresssion.cs @@ -23,18 +23,20 @@ ***************************************************************************************/ using System; using System.Linq; +using Manatee.Json.Internal; namespace Manatee.Json.Path.Expressions { - internal class NameExpression : PathExpression + internal class NameExpression : PathExpression, IEquatable> { - public override int Priority => 6; public string Name { get; set; } public ExpressionTreeNode NameExp { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { - var value = IsLocal ? json as JsonValue : root; + var value = IsLocal ? json.AsJsonValue() : root; if (value == null) throw new NotSupportedException("Name requires a JsonValue to evaluate."); var results = Path.Evaluate(value); @@ -59,5 +61,25 @@ private string GetName() return (string)value; return Name; } + public bool Equals(NameExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && string.Equals(Name, other.Name) && Equals(NameExp, other.NameExp); + } + public override bool Equals(object obj) + { + return Equals(obj as NameExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode*397) ^ (Name?.GetHashCode() ?? 0); + hashCode = (hashCode*397) ^ (NameExp?.GetHashCode() ?? 0); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/NegateExpression.cs b/Manatee.Json/Path/Expressions/NegateExpression.cs index ab21e68..77deaec 100644 --- a/Manatee.Json/Path/Expressions/NegateExpression.cs +++ b/Manatee.Json/Path/Expressions/NegateExpression.cs @@ -24,11 +24,12 @@ namespace Manatee.Json.Path.Expressions { - internal class NegateExpression : ExpressionTreeNode + internal class NegateExpression : ExpressionTreeNode, IEquatable> { - public override int Priority => 6; public ExpressionTreeNode Root { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { return -Convert.ToDouble(Root.Evaluate(json, root)); @@ -39,5 +40,19 @@ public override string ToString() ? $"-({Root})" : $"-{Root}"; } + public bool Equals(NegateExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Root, other.Root); + } + public override bool Equals(object obj) + { + return Equals(obj as NegateExpression); + } + public override int GetHashCode() + { + return Root?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/NotExpression.cs b/Manatee.Json/Path/Expressions/NotExpression.cs index 6a79a21..3c5d550 100644 --- a/Manatee.Json/Path/Expressions/NotExpression.cs +++ b/Manatee.Json/Path/Expressions/NotExpression.cs @@ -20,13 +20,16 @@ Purpose: Expresses the intent to invert a boolean. ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class NotExpression : ExpressionTreeNode + internal class NotExpression : ExpressionTreeNode, IEquatable> { - public override int Priority => 5; public ExpressionTreeNode Root { get; set; } + protected override int BasePriority => 5; + public override object Evaluate(T json, JsonValue root) { var result = Root.Evaluate(json, root); @@ -41,5 +44,19 @@ public override string ToString() ? $"!({Root})" : $"!{Root}"; } + public bool Equals(NotExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Root, other.Root); + } + public override bool Equals(object obj) + { + return Equals(obj as NotExpression); + } + public override int GetHashCode() + { + return Root?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/OrExpression.cs b/Manatee.Json/Path/Expressions/OrExpression.cs index 8676e0e..10f1e12 100644 --- a/Manatee.Json/Path/Expressions/OrExpression.cs +++ b/Manatee.Json/Path/Expressions/OrExpression.cs @@ -20,11 +20,13 @@ Purpose: Expresses the intent to perform a boolean OR. ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class OrExpression : ExpressionTreeBranch + internal class OrExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 0; + protected override int BasePriority => 0; public override object Evaluate(T json, JsonValue root) { @@ -34,5 +36,24 @@ public override string ToString() { return $"{Left} || {Right}"; } + public bool Equals(OrExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as OrExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/AddExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/AddExpressionParser.cs new file mode 100644 index 0000000..ac5d5ca --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/AddExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: AddExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: AddExpressionParser + Purpose: Parses addition expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class AddExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("+"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new AddExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/AndExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/AndExpressionParser.cs new file mode 100644 index 0000000..a7569f4 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/AndExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: AndExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: AndExpressionParser + Purpose: Parses logical-and expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class AndExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("&&"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 2; + node = new AndExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/ConstantBooleanExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/ConstantBooleanExpressionParser.cs new file mode 100644 index 0000000..817dca1 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/ConstantBooleanExpressionParser.cs @@ -0,0 +1,50 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ConstantBooleanExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: ConstantBooleanExpressionParser + Purpose: Parses boolean expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class ConstantBooleanExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("true") || input.StartsWith("false"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + var substring = source.Substring(index); + if (substring.StartsWith("true")) + { + index += 4; + node = new ValueExpression {Value = true}; + return null; + } + if (substring.StartsWith("false")) + { + index += 5; + node = new ValueExpression {Value = false}; + return null; + } + node = null; + return "Boolean value not recognized."; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/ConstantNumberExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/ConstantNumberExpressionParser.cs new file mode 100644 index 0000000..9d86428 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/ConstantNumberExpressionParser.cs @@ -0,0 +1,47 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ConstantNumberExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: ConstantNumberExpressionParser + Purpose: Parses number expressions within JsonPath. + +***************************************************************************************/ +using Manatee.Json.Path.Parsing; + +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class ConstantNumberExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return char.IsDigit(input[0]); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + double? number; + var error = source.GetNumber(ref index, out number); + if (error != null) + { + node = null; + return error; + } + + node = new ValueExpression {Value = number}; + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/ConstantStringExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/ConstantStringExpressionParser.cs new file mode 100644 index 0000000..7b8a2e6 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/ConstantStringExpressionParser.cs @@ -0,0 +1,47 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ConstantStringExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: ConstantStringExpressionParser + Purpose: Parses string expressions within JsonPath. + +***************************************************************************************/ +using Manatee.Json.Internal; +using Manatee.Json.Path.Parsing; + +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class ConstantStringExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input[0].In('"', '\''); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + string value; + var error = source.GetKey(ref index, out value); + if (error != null) + { + node = null; + return error; + } + node = new ValueExpression {Value = value}; + return null; + } + } +} diff --git a/Manatee.Json/Path/Expressions/Parsing/DivideExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/DivideExpressionParser.cs new file mode 100644 index 0000000..fbcc224 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/DivideExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: DivideExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: DivideExpressionParser + Purpose: Parses division expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class DivideExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("/"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new DivideExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/ExponentExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/ExponentExpressionParser.cs new file mode 100644 index 0000000..f572226 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/ExponentExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ExponentExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: ExponentExpressionParser + Purpose: Parses exponential expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class ExponentExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("^"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new ExponentExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/ExpressionEndParser.cs b/Manatee.Json/Path/Expressions/Parsing/ExpressionEndParser.cs new file mode 100644 index 0000000..1639f18 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/ExpressionEndParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ExpressionEndParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: ExpressionEndParser + Purpose: Indicates the end of expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class ExpressionEndParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input[0] == ')'; + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = null; + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/GroupExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/GroupExpressionParser.cs new file mode 100644 index 0000000..5ba72f4 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/GroupExpressionParser.cs @@ -0,0 +1,39 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: GroupExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: GroupExpressionParser + Purpose: Parses parenthetical groups in expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class GroupExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input[0] == '('; + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + var error = JsonPathExpressionParser.Parse(source, ref index, out node); + node?.BumpPriority(); + return error; + } + } +} diff --git a/Manatee.Json/Path/Expressions/Parsing/IJsonPathExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IJsonPathExpressionParser.cs new file mode 100644 index 0000000..37ef179 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IJsonPathExpressionParser.cs @@ -0,0 +1,30 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IJsonPathExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IJsonPathExpressionParser + Purpose: Defines methods to parse JsonPath expressions. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal interface IJsonPathExpressionParser + { + bool Handles(string input); + string TryParse(string source, ref int index, out ExpressionTreeNode node); + } +} diff --git a/Manatee.Json/Path/Expressions/Parsing/IsEqualExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IsEqualExpressionParser.cs new file mode 100644 index 0000000..d9bb1da --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IsEqualExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IsEqualExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IsEqualExpressionParser + Purpose: Parses equality expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class IsEqualExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("=="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 2; + node = new IsEqualExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/IsGreaterThanEqualExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IsGreaterThanEqualExpressionParser.cs new file mode 100644 index 0000000..b69790a --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IsGreaterThanEqualExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IsGreaterThanEqualExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IsGreaterThanEqualExpressionParser + Purpose: Parses greater-than-or-equal inequality expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class IsGreaterThanEqualExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith(">="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 2; + node = new IsGreaterThanEqualExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/IsGreaterThanExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IsGreaterThanExpressionParser.cs new file mode 100644 index 0000000..c1b1e90 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IsGreaterThanExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IsGreaterThanExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IsGreaterThanExpressionParser + Purpose: Parses greater-than inequality expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class IsGreaterThanExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith(">") && !input.StartsWith(">="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new IsGreaterThanExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/IsLessThanEqualExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IsLessThanEqualExpressionParser.cs new file mode 100644 index 0000000..3e4255f --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IsLessThanEqualExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IsLessThanEqualExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IsLessThanEqualExpressionParser + Purpose: Parses less-than-or-equal inequality expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class IsLessThanEqualExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("<="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 2; + node = new IsLessThanEqualExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/IsLessThanExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IsLessThanExpressionParser.cs new file mode 100644 index 0000000..df40e96 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IsLessThanExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IsLessThanExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IsLessThanExpressionParser + Purpose: Parses lees-than inequality expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class IsLessThanExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("<") && !input.StartsWith("<="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new IsLessThanExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/IsNotEqualExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/IsNotEqualExpressionParser.cs new file mode 100644 index 0000000..a22caac --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/IsNotEqualExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IsNotEqualExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: IsNotEqualExpressionParser + Purpose: Parses inequality expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class IsNotEqualExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("!="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 2; + node = new IsNotEqualExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/JsonPathExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/JsonPathExpressionParser.cs new file mode 100644 index 0000000..ebb1a6f --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/JsonPathExpressionParser.cs @@ -0,0 +1,121 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: JsonPathExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: JsonPathExpressionParser + Purpose: Parses JsonPath expressions within JsonPath. + +***************************************************************************************/ +using System; +using System.Collections.Generic; +using System.Linq; +using Manatee.Json.Internal; + +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal static class JsonPathExpressionParser + { + private static readonly List Parsers; + + static JsonPathExpressionParser() + { + Parsers = typeof(JsonPathExpressionParser).Assembly.GetTypes() + .Where(t => typeof(IJsonPathExpressionParser).IsAssignableFrom(t) && t.IsClass) + .Select(Activator.CreateInstance) + .Cast() + .ToList(); + } + + public static string Parse(string source, ref int index, out Expression expr) + { + ExpressionTreeNode root; + var error = Parse(source, ref index, out root); + if (error != null) + { + expr = null; + return error; + } + + expr = new Expression(root); + return null; + } + public static string Parse(string source, ref int index, out ExpressionTreeNode root) + { + var nodes = new List>(); + var length = source.Length; + ExpressionTreeNode node; + root = null; + do + { + char c; + var errorMessage = source.SkipWhiteSpace(ref index, length, out c); + if (errorMessage != null) return errorMessage; + var substring = source.Substring(index); + var parser = Parsers.FirstOrDefault(p => p.Handles(substring)); + if (parser == null) return "Unrecognized JSON Path Expression element."; + errorMessage = parser.TryParse(source, ref index, out node); + if (errorMessage != null) return errorMessage; + if (node != null) + nodes.Add(node); + } while (index < length && node != null); + + root = nodes.Count == 1 + ? CheckNode(nodes[0], null) + : BuildTree(nodes); + return null; + } + + private static ExpressionTreeNode BuildTree(List> nodes) + { + if (!nodes.Any()) return null; + var minPriority = nodes.Min(n => n.Priority); + var root = nodes.Last(n => n.Priority == minPriority); + var branch = root as ExpressionTreeBranch; + if (branch != null && branch.Right == null && branch.Left == null) + { + var split = nodes.LastIndexOf(root); + var left = nodes.Take(split).ToList(); + var right = nodes.Skip(split + 1).ToList(); + branch.Left = CheckNode(BuildTree(left), branch); + branch.Right = CheckNode(BuildTree(right), branch); + } + var not = root as NotExpression; + if (not != null) + { + var split = nodes.IndexOf(root); + var right = nodes.Skip(split + 1).FirstOrDefault(); + not.Root = CheckNode(right, null); + } + return root; + } + + private static ExpressionTreeNode CheckNode(ExpressionTreeNode node, ExpressionTreeBranch root) + { + var named = node as NameExpression; + if (named != null && (root == null || root.Priority == 0)) + { + return new HasPropertyExpression + { + Path = named.Path, + IsLocal = named.IsLocal, + Name = named.Name + }; + } + return node; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/LengthExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/LengthExpressionParser.cs new file mode 100644 index 0000000..7a24b96 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/LengthExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: LengthExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: LengthExpressionParser + Purpose: Parses .length expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class LengthExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith(".length") && input.Length > 7 && !char.IsLetterOrDigit(input[7]); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 7; + node = new LengthExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/ModuloExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/ModuloExpressionParser.cs new file mode 100644 index 0000000..60099f1 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/ModuloExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ModuloExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: ModuloExpressionParser + Purpose: Parses modulo expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class ModuloExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("%"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new ModuloExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/MultiplyExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/MultiplyExpressionParser.cs new file mode 100644 index 0000000..f150384 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/MultiplyExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: MultiplyExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: MultiplyExpressionParser + Purpose: Parses mulitplication expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class MultiplyExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("*"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new MultiplyExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/NotExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/NotExpressionParser.cs new file mode 100644 index 0000000..3f1fe9f --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/NotExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: NotExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: NotExpressionParser + Purpose: Parses logical-not expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class NotExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("!") && !input.StartsWith("!="); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new NotExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/OrExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/OrExpressionParser.cs new file mode 100644 index 0000000..8f1c490 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/OrExpressionParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: OrExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: OrExpressionParser + Purpose: Parses logical-or expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class OrExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input.StartsWith("||"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index += 2; + node = new OrExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/PathExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/PathExpressionParser.cs new file mode 100644 index 0000000..e2f81b6 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/PathExpressionParser.cs @@ -0,0 +1,128 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: PathExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: PathExpressionParser + Purpose: Parses expressions within JsonPath. + +***************************************************************************************/ +using System; +using System.Linq; +using Manatee.Json.Internal; +using Manatee.Json.Parsing; +using Manatee.Json.Path.ArrayParameters; +using Manatee.Json.Path.Operators; +using Manatee.Json.Path.Parsing; + +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class PathExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + return input[0].In('@', '$'); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + var isLocal = source[index] == '@'; + JsonPath path; + var error = JsonPathParser.Parse(source, ref index, out path); + // Swallow this error from the path parser and assume the path just ended. + // If it's really a syntax error, the expression parser should catch it. + if (error != null && error != "Unrecognized JSON Path element.") + { + node = null; + return error; + } + + var name = path.Operators.Last() as NameOperator; + var length = path.Operators.Last() as LengthOperator; + var array = path.Operators.Last() as ArrayOperator; + if (name != null) + { + path.Operators.Remove(name); + if (name.Name == "indexOf") + { + JsonValue parameter; + if (source[index] != '(') + { + node = null; + return "Expected '('. 'indexOf' operator requires a parameter."; + } + index++; + error = JsonParser.Parse(source, ref index, out parameter, true); + // Swallow this error from the JSON parser and assume the value just ended. + // If it's really a syntax error, the expression parser should catch it. + if (error != null && error != "Expected \',\', \']\', or \'}\'.") + { + node = null; + return $"Error parsing parameter for 'indexOf' expression: {error}."; + } + if (source[index] != ')') + { + node = null; + return "Expected ')'."; + } + index++; + node = new IndexOfExpression + { + Path = path, + IsLocal = isLocal, + Parameter = parameter + }; + } + else + node = new NameExpression + { + Path = path, + IsLocal = isLocal, + Name = name.Name + }; + } + else if (length != null) + { + path.Operators.Remove(length); + node = new LengthExpression + { + Path = path, + IsLocal = isLocal + }; + } + else if (array != null) + { + path.Operators.Remove(array); + var query = array.Query as SliceQuery; + var constant = query?.Slices.FirstOrDefault()?.Index; + if (query == null || query.Slices.Count() != 1 || !constant.HasValue) + { + node = null; + return "JSON Path expression indexers only support single constant values."; + } + node = new ArrayIndexExpression + { + Path = path, + IsLocal = isLocal, + Index = constant.Value + }; + } + else + throw new NotImplementedException(); + + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Parsing/SubtractExpressionParser.cs b/Manatee.Json/Path/Expressions/Parsing/SubtractExpressionParser.cs new file mode 100644 index 0000000..1830243 --- /dev/null +++ b/Manatee.Json/Path/Expressions/Parsing/SubtractExpressionParser.cs @@ -0,0 +1,39 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: SubtractExpressionParser.cs + Namespace: Manatee.Json.Path.Expressions.Parsing + Class Name: SubtractExpressionParser + Purpose: Parses subtraction expressions within JsonPath. + +***************************************************************************************/ +namespace Manatee.Json.Path.Expressions.Parsing +{ + internal class SubtractExpressionParser : IJsonPathExpressionParser + { + public bool Handles(string input) + { + // TODO: Determine how to identify negations separately from subtractions. + return input.StartsWith("-"); + } + public string TryParse(string source, ref int index, out ExpressionTreeNode node) + { + index++; + node = new SubtractExpression(); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/PathExpression.cs b/Manatee.Json/Path/Expressions/PathExpression.cs index def753a..d14378a 100644 --- a/Manatee.Json/Path/Expressions/PathExpression.cs +++ b/Manatee.Json/Path/Expressions/PathExpression.cs @@ -25,12 +25,13 @@ namespace Manatee.Json.Path.Expressions { - internal class PathExpression : ExpressionTreeNode + internal class PathExpression : ExpressionTreeNode, IEquatable> { - public override int Priority => 6; public JsonPath Path { get; set; } public bool IsLocal { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { var value = IsLocal ? json as JsonValue : root; @@ -45,5 +46,19 @@ public override string ToString() { return (IsLocal ? "@" : "$") + Path.GetRawString(); } + public bool Equals(PathExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Path, other.Path) && IsLocal == other.IsLocal; + } + public override bool Equals(object obj) + { + return Equals(obj as PathExpression); + } + public override int GetHashCode() + { + unchecked { return ((Path?.GetHashCode() ?? 0)*397) ^ IsLocal.GetHashCode(); } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/SubtractExpression.cs b/Manatee.Json/Path/Expressions/SubtractExpression.cs index 6ad2d33..7da05e7 100644 --- a/Manatee.Json/Path/Expressions/SubtractExpression.cs +++ b/Manatee.Json/Path/Expressions/SubtractExpression.cs @@ -21,11 +21,13 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class SubtractExpression : ExpressionTreeBranch + internal class SubtractExpression : ExpressionTreeBranch, IEquatable> { - public override int Priority => 2; + protected override int BasePriority => 2; public override object Evaluate(T json, JsonValue root) { @@ -36,9 +38,28 @@ public override object Evaluate(T json, JsonValue root) } public override string ToString() { - if (Right.Priority == Priority) + if (Right?.Priority == Priority) return $"{Left}-({Right})"; return $"{Left}-{Right}"; } + public bool Equals(SubtractExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other); + } + public override bool Equals(object obj) + { + return Equals(obj as SubtractExpression); + } + public override int GetHashCode() + { + unchecked + { + int hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ GetType().GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Translation/ArrayIndexExpressionTranslator.cs b/Manatee.Json/Path/Expressions/Translation/ArrayIndexExpressionTranslator.cs index 0975f45..de91ad7 100644 --- a/Manatee.Json/Path/Expressions/Translation/ArrayIndexExpressionTranslator.cs +++ b/Manatee.Json/Path/Expressions/Translation/ArrayIndexExpressionTranslator.cs @@ -36,21 +36,21 @@ public override ExpressionTreeNode Translate(Expression body) if (method == null) throw new InvalidOperationException(); var parameter = method.Arguments.Last() as ConstantExpression; - if (parameter == null || parameter.Type != typeof (int)) + if (parameter == null || parameter.Type != typeof(int)) { return new ArrayIndexExpression + { + Path = BuildPath(method, out isLocal), + IsLocal = isLocal, + IndexExpression = ExpressionTranslator.TranslateNode(method.Arguments.Last()) + }; + } + return new ArrayIndexExpression { Path = BuildPath(method, out isLocal), IsLocal = isLocal, - IndexExpression = ExpressionTranslator.TranslateNode(method.Arguments.Last()) + Index = (int) parameter.Value, }; - } - return new ArrayIndexExpression - { - Path = BuildPath(method, out isLocal), - IsLocal = isLocal, - Index = (int)parameter.Value, - }; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Translation/ExpressionTranslator.cs b/Manatee.Json/Path/Expressions/Translation/ExpressionTranslator.cs index bfe0ced..664c48e 100644 --- a/Manatee.Json/Path/Expressions/Translation/ExpressionTranslator.cs +++ b/Manatee.Json/Path/Expressions/Translation/ExpressionTranslator.cs @@ -32,14 +32,14 @@ namespace Manatee.Json.Path.Expressions.Translation { internal static class ExpressionTranslator { - private static readonly Dictionary> _translators = + private static readonly Dictionary> Translators = new Dictionary> { - {typeof (ConstantExpression), GetValueTranslator}, - {typeof (BinaryExpression), e => GetNodeTypeBasedTranslator(e.NodeType)}, - {typeof (UnaryExpression), e => GetNodeTypeBasedTranslator(e.NodeType)}, - {typeof (MethodCallExpression), GetMethodCallTranslator}, - {typeof (MemberExpression), GetMemberTranslator}, + {typeof(ConstantExpression), GetValueTranslator}, + {typeof(BinaryExpression), e => GetNodeTypeBasedTranslator(e.NodeType)}, + {typeof(UnaryExpression), e => GetNodeTypeBasedTranslator(e.NodeType)}, + {typeof(MethodCallExpression), GetMethodCallTranslator}, + {typeof(MemberExpression), GetMemberTranslator}, }; private static IExpressionTranslator GetValueTranslator(Expression e) @@ -50,7 +50,7 @@ private static IExpressionTranslator GetValueTranslator(Expression e) if (type == typeof(string)) return new StringValueExpressionTranslator(); if (type.In(typeof(sbyte), typeof(byte), typeof(char), typeof(short), typeof(ushort), typeof(int), - typeof (uint), typeof (long), typeof (ulong), typeof (float), typeof (double), typeof (decimal))) + typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal))) return new NumberValueExpressionTranslator(); var constant = (ConstantExpression) e; if (constant.Value == null) @@ -199,10 +199,10 @@ private static IExpressionTranslator GetMemberTranslator(Expression exp) public static ExpressionTreeNode TranslateNode(Expression source) { var type = source.GetType(); - var expressionKey = _translators.Keys.FirstOrDefault(t => t.IsAssignableFrom(type)); + var expressionKey = Translators.Keys.FirstOrDefault(t => t.IsAssignableFrom(type)); if (expressionKey != null) { - var translator = _translators[expressionKey](source); + var translator = Translators[expressionKey](source); if (translator != null) return translator.Translate(source); } @@ -210,11 +210,11 @@ public static ExpressionTreeNode TranslateNode(Expression source) } public static Expression Translate(Expression> source) { - return new Expression {Root = TranslateNode(source.Body)}; + return new Expression(TranslateNode(source.Body)); } public static Expression Translate(Expression> source) { - return new Expression {Root = TranslateNode(source.Body)}; + return new Expression(TranslateNode(source.Body)); } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Translation/HasPropertyExpressionTranslator.cs b/Manatee.Json/Path/Expressions/Translation/HasPropertyExpressionTranslator.cs index c24f5a7..8dec90a 100644 --- a/Manatee.Json/Path/Expressions/Translation/HasPropertyExpressionTranslator.cs +++ b/Manatee.Json/Path/Expressions/Translation/HasPropertyExpressionTranslator.cs @@ -27,17 +27,23 @@ namespace Manatee.Json.Path.Expressions.Translation { - internal class HasPropertyExpressionTranslator : IExpressionTranslator + internal class HasPropertyExpressionTranslator : PathExpressionTranslator { - public ExpressionTreeNode Translate(Expression body) + public override ExpressionTreeNode Translate(Expression body) { + bool isLocal; var method = body as MethodCallExpression; if (method == null) throw new InvalidOperationException(); var parameter = method.Arguments.Last() as ConstantExpression; if (parameter == null || parameter.Type != typeof(string)) throw new NotSupportedException("Only constant string arguments are supported in HasProperty()"); - return new HasPropertyExpression {Name = parameter.Value.ToString()}; + return new HasPropertyExpression + { + Path = BuildPath(method, out isLocal), + IsLocal = isLocal, + Name = parameter.Value.ToString() + }; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/Translation/PathExpressionTranslator.cs b/Manatee.Json/Path/Expressions/Translation/PathExpressionTranslator.cs index 031b1f8..d7fa17d 100644 --- a/Manatee.Json/Path/Expressions/Translation/PathExpressionTranslator.cs +++ b/Manatee.Json/Path/Expressions/Translation/PathExpressionTranslator.cs @@ -36,8 +36,8 @@ internal abstract class PathExpressionTranslator : IExpressionTranslator protected static JsonPath BuildPath(MethodCallExpression method, out bool isLocal) { var path = new JsonPath(); - var currentMethod = method.Arguments.First() as MethodCallExpression; - isLocal = method.Method.Name== "Length" ? method.Arguments.Count != 0 : method.Arguments.Count != 1; + var currentMethod = method.Arguments.FirstOrDefault() as MethodCallExpression; + isLocal = method.Method.Name == "Length" ? method.Arguments.Count != 0 : method.Arguments.Count != 1; while (currentMethod != null) { var parameter = currentMethod.Arguments.Last() as ConstantExpression; @@ -51,11 +51,11 @@ protected static JsonPath BuildPath(MethodCallExpression method, out bool isLoca case "ArrayIndex": if (parameter == null || parameter.Type != typeof (int)) throw new NotSupportedException("Only literal string arguments are supported within JsonPath expressions."); - path.Operators.Insert(0, new ArrayOperator(new IndexQuery((int) parameter.Value))); + path.Operators.Insert(0, new ArrayOperator(new SliceQuery(new Slice((int) parameter.Value)))); break; } isLocal = currentMethod.Arguments.Count != 1; - currentMethod = currentMethod.Arguments.First() as MethodCallExpression; + currentMethod = currentMethod.Arguments.FirstOrDefault() as MethodCallExpression; } return path; } diff --git a/Manatee.Json/Path/Expressions/ValueComparer.cs b/Manatee.Json/Path/Expressions/ValueComparer.cs index 119c6e1..260201d 100644 --- a/Manatee.Json/Path/Expressions/ValueComparer.cs +++ b/Manatee.Json/Path/Expressions/ValueComparer.cs @@ -20,6 +20,8 @@ Purpose: Compares values for JSONPath expressions. ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { internal static class ValueComparer @@ -29,14 +31,14 @@ public static bool Equal(object a, object b) var sleft = TryGetString(a); var sright = TryGetString(b); if (sleft != null && sright != null) - return string.Compare(sleft, sright, System.StringComparison.Ordinal) == 0; - var dleft = TryGetNumber(a); - var dright = TryGetNumber(b); - if (dleft != null && dright != null) - return dleft == dright; + return string.Compare(sleft, sright, StringComparison.Ordinal) == 0; var bleft = TryGetBoolean(a); var bright = TryGetBoolean(b); if (bleft != null && bright != null) + return bleft == bright; + var dleft = TryGetNumber(a); + var dright = TryGetNumber(b); + if (dleft != null && dright != null) return dleft == dright; return false; } @@ -45,7 +47,7 @@ public static bool LessThan(object a, object b) var sleft = TryGetString(a); var sright = TryGetString(b); if (sleft != null && sright != null) - return string.Compare(sleft, sright, System.StringComparison.Ordinal) < 0; + return string.Compare(sleft, sright, StringComparison.Ordinal) < 0; var dleft = TryGetNumber(a); var dright = TryGetNumber(b); if (dleft != null && dright != null) @@ -57,7 +59,7 @@ public static bool GreaterThan(object a, object b) var sleft = TryGetString(a); var sright = TryGetString(b); if (sleft != null && sright != null) - return string.Compare(sleft, sright, System.StringComparison.Ordinal) > 0; + return string.Compare(sleft, sright, StringComparison.Ordinal) > 0; var dleft = TryGetNumber(a); var dright = TryGetNumber(b); if (dleft != null && dright != null) @@ -70,7 +72,7 @@ public static bool LessThanEqual(object a, object b) var sleft = TryGetString(a); var sright = TryGetString(b); if (sleft != null && sright != null) - return string.Compare(sleft, sright, System.StringComparison.Ordinal) <= 0; + return string.Compare(sleft, sright, StringComparison.Ordinal) <= 0; var dleft = TryGetNumber(a); var dright = TryGetNumber(b); if (dleft != null && dright != null) @@ -82,7 +84,7 @@ public static bool GreaterThanEqual(object a, object b) var sleft = TryGetString(a); var sright = TryGetString(b); if (sleft != null && sright != null) - return string.Compare(sleft, sright, System.StringComparison.Ordinal) >= 0; + return string.Compare(sleft, sright, StringComparison.Ordinal) >= 0; var dleft = TryGetNumber(a); var dright = TryGetNumber(b); if (dleft != null && dright != null) @@ -98,7 +100,11 @@ private static string TryGetString(object value) private static double? TryGetNumber(object value) { var jv = value as JsonValue; - return jv != null && jv.Type == JsonValueType.Number ? jv.Number : value as double?; + if (jv != null && jv.Type == JsonValueType.Number) return jv.Number; + if (!(value is bool) && value is IConvertible) + return Convert.ToDouble(value); + // at this point, we have no idea what this is. + return null; } private static bool? TryGetBoolean(object value) { diff --git a/Manatee.Json/Path/Expressions/ValueExpression.cs b/Manatee.Json/Path/Expressions/ValueExpression.cs index 06d622d..d880e21 100644 --- a/Manatee.Json/Path/Expressions/ValueExpression.cs +++ b/Manatee.Json/Path/Expressions/ValueExpression.cs @@ -21,13 +21,16 @@ ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Expressions { - internal class ValueExpression : ExpressionTreeNode + internal class ValueExpression : ExpressionTreeNode, IEquatable> { - public override int Priority => 6; public object Value { get; set; } + protected override int BasePriority => 6; + public override object Evaluate(T json, JsonValue root) { return Value; @@ -36,7 +39,23 @@ public override string ToString() { return Value is string ? $"\"{Value}\"" - : Value.ToString(); + : Value is bool + ? Value.ToString().ToLower() + : Value.ToString(); + } + public bool Equals(ValueExpression other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Value, other.Value); + } + public override bool Equals(object obj) + { + return Equals(obj as ValueExpression); + } + public override int GetHashCode() + { + return Value?.GetHashCode() ?? 0; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/JsonPath.cs b/Manatee.Json/Path/JsonPath.cs index f1e2c47..6f9fac8 100644 --- a/Manatee.Json/Path/JsonPath.cs +++ b/Manatee.Json/Path/JsonPath.cs @@ -24,95 +24,18 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using Manatee.Json.Internal; -using Manatee.Json.Path.ArrayParameters; -using Manatee.Json.Path.Expressions; -using Manatee.Json.Path.Operators; -using Manatee.Json.Path.SearchParameters; -using Manatee.StateMachine; -using Manatee.StateMachine.Exceptions; +using Manatee.Json.Path.Parsing; namespace Manatee.Json.Path { /// /// Provides primary functionality for JSON Path objects. /// - public class JsonPath + public class JsonPath : IEquatable { - private enum State - { - Start, - ObjectOrArray, - ArrayContent, - IndexOrSlice, - Colon, - SliceValue, - Comma, - IndexValue, - CloseArray, - ObjectContent, - Search, - End - } - - private static readonly StateMachine _stateMachine = new StateMachine(); - - private bool _gotObject; - private string _source; - private int _index; - private List _indices; - private Dictionary _slice; - private int _colonCount; - private bool _done; - private readonly InputStream _stream = new InputStream(); - private bool _isSearch; - private bool _allowLocalRoot; - internal List Operators { get; } = new List(); - static JsonPath() - { - _stateMachine[State.Start, JsonPathInput.Dollar] = GotRoot; - _stateMachine[State.Start, JsonPathInput.Current] = GotCurrent; - _stateMachine[State.ObjectOrArray, JsonPathInput.OpenBracket] = GotArray; - _stateMachine[State.ObjectOrArray, JsonPathInput.Period] = GotObject; - _stateMachine[State.ObjectOrArray, JsonPathInput.OpenParenth] = GotFunction; - _stateMachine[State.ObjectOrArray, JsonPathInput.Number] = CompletePath; - _stateMachine[State.ObjectOrArray, JsonPathInput.End] = CompletePath; - _stateMachine[State.ArrayContent, JsonPathInput.Star] = GotArrayWildCard; - _stateMachine[State.ArrayContent, JsonPathInput.OpenParenth] = ParseIndexExpression; - _stateMachine[State.ArrayContent, JsonPathInput.Question] = ParseFilterExpression; - _stateMachine[State.ArrayContent, JsonPathInput.Number] = GotIndexOrSlice; - _stateMachine[State.ArrayContent, JsonPathInput.Colon] = GotSlice; - _stateMachine[State.IndexOrSlice, JsonPathInput.Colon] = GotSlice; - _stateMachine[State.IndexOrSlice, JsonPathInput.Comma] = GotIndex; - _stateMachine[State.IndexOrSlice, JsonPathInput.CloseBracket] = FinalizeIndex; - _stateMachine[State.Colon, JsonPathInput.CloseBracket] = FinalizeSlice; - _stateMachine[State.Colon, JsonPathInput.Number] = GotSliceValue; - _stateMachine[State.Colon, JsonPathInput.Colon] = GotSlice; - _stateMachine[State.SliceValue, JsonPathInput.CloseBracket] = FinalizeSlice; - _stateMachine[State.SliceValue, JsonPathInput.Colon] = GotSlice; - _stateMachine[State.Comma, JsonPathInput.Number] = GotIndexValue; - _stateMachine[State.IndexValue, JsonPathInput.CloseBracket] = FinalizeIndex; - _stateMachine[State.IndexValue, JsonPathInput.Comma] = GotIndex; - _stateMachine[State.CloseArray, JsonPathInput.CloseBracket] = FinalizeArray; - _stateMachine[State.ObjectContent, JsonPathInput.Period] = GotSearch; - _stateMachine[State.ObjectContent, JsonPathInput.Star] = GotObjectWildCard; - _stateMachine[State.ObjectContent, JsonPathInput.Letter] = GotName; - _stateMachine[State.Search, JsonPathInput.OpenBracket] = GotSearchArray; - _stateMachine[State.Search, JsonPathInput.Star] = GotSearchWildCard; - _stateMachine[State.Search, JsonPathInput.Letter] = GotSearchName; - _stateMachine.UpdateFunction = GetNextInput; - } - /// - /// Finalizes memory management responsibilities. - /// - ~JsonPath() - { - _stateMachine.UnregisterOwner(this); - } - /// /// Parses a containing a JSON path. /// @@ -123,13 +46,7 @@ static JsonPath() /// Thrown if contains invalid JSON path syntax. public static JsonPath Parse(string source) { - if (source == null) - throw new ArgumentNullException(nameof(source)); - if (source.IsNullOrWhiteSpace()) - throw new ArgumentException("Source string contains no data."); - var path = new JsonPath {_source = Regex.Replace(source, @"\s+", string.Empty)}; - path.Parse(0); - return path; + return JsonPathParser.Parse(source); } /// /// Evaluates a JSON value using the path. @@ -153,334 +70,34 @@ public override string ToString() { return $"${GetRawString()}"; } - - internal string GetRawString() - { - return Operators.Select(o => o.ToString()).Join(string.Empty); - } - internal int Parse(string source, int i) - { - _source = source; - _allowLocalRoot = true; - Parse(i); - return _index; - } - - private void Parse(int i) - { - _stream.Clear(); - _index = i; - _done = false; - try - { - _stateMachine.Run(this, State.Start, _stream); - if (!_done) - throw new JsonPathSyntaxException(this, "Found incomplete JSON path."); - } - catch (InputNotValidForStateException e) - { - if (e.Input == JsonPathInput.Star && _allowLocalRoot) - { - _done = true; - return; - } - switch (e.State) - { - case State.Start: - throw new JsonPathSyntaxException(this, "Paths must start with a '$', or optionally a '@' if inside an expression."); - case State.ObjectOrArray: - throw new JsonPathSyntaxException(this, "Expected '.', '..', or '['."); - case State.ArrayContent: - throw new JsonPathSyntaxException(this, "Expected '?', '(', ':', or an integer."); - case State.IndexOrSlice: - throw new JsonPathSyntaxException(this, "Expected ',', ':', or ']'."); - case State.Colon: - if (_colonCount == 2) - throw new JsonPathSyntaxException(this, "Expected ']' or an integer."); - throw new JsonPathSyntaxException(this, "Expected ']', ':', or an integer."); - case State.SliceValue: - throw new JsonPathSyntaxException(this, "Expected ':' or ']'."); - case State.Comma: - throw new JsonPathSyntaxException(this, "Expected an integer."); - case State.IndexValue: - throw new JsonPathSyntaxException(this, "Expected ',' or ']'."); - case State.CloseArray: - throw new JsonPathSyntaxException(this, "Expected ']'."); - case State.ObjectContent: - throw new JsonPathSyntaxException(this, "Expected a key name. Note: quoted keys and keys which start with numbers are not currently supported."); - case State.Search: - throw new JsonPathSyntaxException(this, "Expected a key name or '['. Note: quoted keys and keys which start with numbers are not currently supported."); - default: - throw new ArgumentOutOfRangeException(); - } - } - catch (StateNotValidException) - { - throw new JsonPathSyntaxException(this, "An unrecoverable error occurred while parsing a JSON path. Please report to littlecrabsolutions@yahoo.com."); - } - catch (ActionNotDefinedForStateAndInputException) - { - throw new JsonPathSyntaxException(this, "An unrecoverable error occurred while parsing a JSON path. Please report to littlecrabsolutions@yahoo.com."); - } - } - private static void GetNextInput(object owner) - { - var obj = owner as JsonPath; - if (obj == null) return; - if (obj._done) return; - if (obj._index == obj._source.Length) - { - obj._stream.Add(JsonPathInput.End); - return; - } - var c = default(char); - try - { - c = obj._source[obj._index++]; - var next = CharacterConverter.PathItem(c); - obj._stream.Add(next); - } - catch (KeyNotFoundException) - { - throw new JsonPathSyntaxException(obj, "Unrecognized character '{0}' in input string.", c); - } - } - private static State GotRoot(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - return State.ObjectOrArray; - } - private static State GotCurrent(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - if (!path._allowLocalRoot) - throw new JsonPathSyntaxException(path, "Local paths (starting with '@') are only allowed within the context of an expression."); - path._gotObject = false; - return State.ObjectOrArray; - } - private static State GotArray(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - path._colonCount = 0; - return State.ArrayContent; - } - private static State GotObject(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - if (path._gotObject) - { - path._gotObject = false; - return State.Search; - } - path._gotObject = true; - return State.ObjectContent; - } - private static State GotFunction(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var exp = new Expression(); - path._index = exp.Parse(path._source, path._index - 1); - var last = path.Operators.Last() as NameOperator; - if (last != null && last.Name != "indexOf") - throw new NotSupportedException("Currently, 'indexOf()' is the only supported function."); - path.Operators.Remove(last); - path.Operators.Add(new IndexOfOperator(exp)); - return State.ObjectOrArray; - } - private static State CompletePath(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._done = true; - return State.End; - } - private static State GotArrayWildCard(object owner, JsonPathInput input) + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(JsonPath other) { - var path = owner as JsonPath; - path._gotObject = false; - if (path._isSearch) - { - path._isSearch = false; - path.Operators.Add(new SearchOperator(new ArraySearchParameter(WildCardQuery.Instance))); - } - else - path.Operators.Add(new ArrayOperator(WildCardQuery.Instance)); - return State.CloseArray; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Operators.SequenceEqual(other.Operators); } - private static State ParseIndexExpression(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var exp = new Expression(); - path._index = exp.Parse(path._source, path._index - 1); - path.Operators.Add(new ArrayOperator(new IndexExpressionQuery(exp))); - return State.CloseArray; - } - private static State ParseFilterExpression(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var exp = new Expression(); - path._index = exp.Parse(path._source, path._index); - path.Operators.Add(new ArrayOperator(new FilterExpressionQuery(exp))); - return State.CloseArray; - } - private static State GotIndexOrSlice(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var value = GetNumber(path); - path._indices = new List {value}; - return State.IndexOrSlice; - } - private static State GotSliceValue(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var value = GetNumber(path); - path._slice[path._colonCount] = value; - return State.SliceValue; - } - private static State FinalizeSlice(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - if (path._colonCount > 2) - throw new JsonPathSyntaxException(path, "Array slice format incorrect. Parameters are 'start:end:increment'."); - int? start = path._slice.ContainsKey(0) ? path._slice[0] : (int?)null; - int? end = path._slice.ContainsKey(1) ? path._slice[1] : (int?)null; - int? step = path._slice.ContainsKey(2) ? path._slice[2] : (int?)null; - if (path._isSearch) - { - path._isSearch = false; - path.Operators.Add(new SearchOperator(new ArraySearchParameter(new SliceQuery(start, end, step)))); - } - else - path.Operators.Add(new ArrayOperator(new SliceQuery(start, end, step))); - return State.ObjectOrArray; - } - private static State GotSlice(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - if (path._slice == null) - path._slice = new Dictionary(); - if (path._indices != null && path._indices.Any()) - { - path._slice.Add(path._colonCount, path._indices.First()); - path._indices = null; - } - path._colonCount++; - return State.Colon; - } - private static State GotIndexValue(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var value = GetNumber(path); - path._indices.Add(value); - return State.IndexValue; - } - private static State FinalizeIndex(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - if (path._isSearch) - { - path._isSearch = false; - path.Operators.Add(new SearchOperator(new ArraySearchParameter(new IndexQuery(path._indices.ToArray())))); - } - else - path.Operators.Add(new ArrayOperator(new IndexQuery(path._indices.ToArray()))); - return State.ObjectOrArray; - } - private static State GotIndex(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - return State.Comma; - } - private static State FinalizeArray(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - return State.ObjectOrArray; - } - private static State GotSearch(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - return State.Search; - } - private static State GotObjectWildCard(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - path.Operators.Add(WildCardOperator.Instance); - return State.ObjectOrArray; - } - private static State GotName(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - var name = GetName(path); - path.Operators.Add(new NameOperator(name)); - return State.ObjectOrArray; - } - private static State GotSearchArray(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - path._isSearch = true; - return State.ArrayContent; - } - private static State GotSearchWildCard(object owner, JsonPathInput input) - { - var path = owner as JsonPath; - path._gotObject = false; - path.Operators.Add(new SearchOperator(WildCardSearchParameter.Instance)); - return State.ObjectOrArray; - } - private static State GotSearchName(object owner, JsonPathInput input) + /// Determines whether the specified is equal to the current . + /// true if the specified is equal to the current ; otherwise, false. + /// The object to compare with the current object. + /// 2 + public override bool Equals(object obj) { - var path = owner as JsonPath; - path._gotObject = false; - var name = GetName(path); - path.Operators.Add(new SearchOperator(new NameSearchParameter(name))); - return State.ObjectOrArray; + return Equals(obj as JsonPath); } - private static string GetName(JsonPath path) + /// Serves as a hash function for a particular type. + /// A hash code for the current . + /// 2 + public override int GetHashCode() { - var chars = new List(); - path._index--; - while (path._index < path._source.Length && (char.IsLetterOrDigit(path._source[path._index]) || path._source[path._index] == '_')) - { - chars.Add(path._source[path._index]); - path._index++; - } - var name = new string(chars.ToArray()); - return name; + return Operators?.GetCollectionHashCode() ?? 0; } - private static int GetNumber(JsonPath path) + + internal string GetRawString() { - var chars = new List(); - path._index--; - var negate = path._source[path._index] == '-'; - int sign = 1; - if (negate) - { - sign = -1; - path._index++; - } - while (path._index < path._source.Length && char.IsNumber(path._source[path._index])) - { - chars.Add(path._source[path._index]); - path._index++; - } - var name = new string(chars.ToArray()); - return int.Parse(name)*sign; + return Operators.Select(o => o.ToString()).Join(string.Empty); } } } \ No newline at end of file diff --git a/Manatee.Json/Path/JsonPathArray.cs b/Manatee.Json/Path/JsonPathArray.cs index 8826d6f..26f1563 100644 --- a/Manatee.Json/Path/JsonPathArray.cs +++ b/Manatee.Json/Path/JsonPathArray.cs @@ -26,7 +26,7 @@ namespace Manatee.Json.Path /// /// Serves as a stand-in for JsonArray in Path Expressions. /// - public class JsonPathArray + public sealed class JsonPathArray { } diff --git a/Manatee.Json/Path/JsonPathSyntaxException.cs b/Manatee.Json/Path/JsonPathSyntaxException.cs index f824fee..515b7b7 100644 --- a/Manatee.Json/Path/JsonPathSyntaxException.cs +++ b/Manatee.Json/Path/JsonPathSyntaxException.cs @@ -47,11 +47,11 @@ public class JsonPathSyntaxException : Exception /// The error message that explains the reason for the exception, or an empty string(""). /// /// 1 - public override string Message => string.Format(_isExpression ? "{0} Expression up to error: {1}" : "{0} Path up to error: {1}", base.Message, Path); + public override string Message => string.Format(_isExpression ? "{0} Expression up to error: '{1}'" : "{0} Path up to error: '{1}'", base.Message, Path); [StringFormatMethod("format")] - internal JsonPathSyntaxException(JsonPath path, string format, params object[] parameters) - : base(string.Format(format, parameters)) + internal JsonPathSyntaxException(JsonPath path, string message) + : base(message) { Path = path.ToString(); } diff --git a/Manatee.Json/Path/JsonPathValue.cs b/Manatee.Json/Path/JsonPathValue.cs index 955825d..dfaa54e 100644 --- a/Manatee.Json/Path/JsonPathValue.cs +++ b/Manatee.Json/Path/JsonPathValue.cs @@ -28,41 +28,10 @@ namespace Manatee.Json.Path /// /// Serves as a stand-in for JsonValue in Path Expressions. /// - public class JsonPathValue +#pragma warning disable 660,661 + public sealed class JsonPathValue +#pragma warning restore 660,661 { - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified is equal to the current ; otherwise, false. - /// - /// The to compare with the current . 2 - protected bool Equals(JsonPathValue other) - { - throw new InvalidOperationException("This operation is reserved for JsonPath."); - } - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified is equal to the current ; otherwise, false. - /// - /// The to compare with the current . 2 - public override bool Equals(object obj) - { - throw new InvalidOperationException("This operation is reserved for JsonPath."); - } - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// - /// 2 - public override int GetHashCode() - { - throw new InvalidOperationException("This operation is reserved for JsonPath."); - } /// /// /// diff --git a/Manatee.Json/Path/JsonPathWith.cs b/Manatee.Json/Path/JsonPathWith.cs index e8bf218..5dc0a99 100644 --- a/Manatee.Json/Path/JsonPathWith.cs +++ b/Manatee.Json/Path/JsonPathWith.cs @@ -21,6 +21,7 @@ ***************************************************************************************/ using System; +using System.Linq; using System.Linq.Expressions; using Manatee.Json.Path.ArrayParameters; using Manatee.Json.Path.Expressions.Translation; @@ -104,24 +105,16 @@ public static JsonPath SearchLength() return path; } /// - /// Appends a by including all array values. - /// - /// The new . - public static JsonPath SearchArray() - { - var path = new JsonPath(); - path.Operators.Add(new SearchOperator(new ArraySearchParameter(WildCardQuery.Instance))); - return path; - } - /// /// Appends a by specifying a series of array indicies. /// - /// The indices of the s to include. + /// The indices and slices of the s to include. /// The new . - public static JsonPath SearchArray(params int[] indices) + public static JsonPath SearchArray(params Slice[] slices) { var path = new JsonPath(); - path.Operators.Add(new SearchOperator(new ArraySearchParameter(new IndexQuery(indices)))); + path.Operators.Add(new SearchOperator(slices.Any() + ? new ArraySearchParameter(new SliceQuery(slices)) + : new ArraySearchParameter(WildCardQuery.Instance))); return path; } /// @@ -134,11 +127,10 @@ public static JsonPath SearchArray(params int[] indices) /// The format for the array slice is [start:end:step]. All parameters are individually optional, /// however either the start or end must be defines. Negative values for start and end indicate that the /// iterator should begin counting from the end of the array. + [Obsolete("Use the SearchArray(params Slice[]) overload instead.")] public static JsonPath SearchArraySlice(int? start, int? end, int? step = null) { - var path = new JsonPath(); - path.Operators.Add(new SearchOperator(new ArraySearchParameter(new SliceQuery(start, end, step)))); - return path; + return SearchArray(new Slice(start, end, step)); } /// /// Appends a by specifying an expression which evaluates to the index to include. @@ -163,24 +155,16 @@ public static JsonPath SearchArray(Expression> express return path; } /// - /// Appends a by including all array values. - /// - /// The new . - public static JsonPath Array() - { - var path = new JsonPath(); - path.Operators.Add(new ArrayOperator(WildCardQuery.Instance)); - return path; - } - /// /// Appends a by specifying a series of array indicies. /// - /// The indices of the s to include. + /// The indices and slices of the s to include. /// The new . - public static JsonPath Array(params int[] indices) + public static JsonPath Array(params Slice[] slices) { var path = new JsonPath(); - path.Operators.Add(new ArrayOperator(new IndexQuery(indices))); + path.Operators.Add(!slices.Any() + ? new ArrayOperator(WildCardQuery.Instance) + : new ArrayOperator(new SliceQuery(slices))); return path; } /// @@ -193,11 +177,10 @@ public static JsonPath Array(params int[] indices) /// The format for the array slice is [start:end:step]. All parameters are individually optional, /// however either the start or end must be defines. Negative values for start and end indicate that the /// iterator should begin counting from the end of the array. + [Obsolete("Use the Array(params Slice[]) overload instead.")] public static JsonPath ArraySlice(int? start, int? end, int? step = null) { - var path = new JsonPath(); - path.Operators.Add(new ArrayOperator(new SliceQuery(start, end, step))); - return path; + return Array(new Slice(start, end, step)); } /// /// Appends a by specifying an expression which evaluates to the index to include. @@ -306,6 +289,47 @@ public static JsonPath SearchLength(this JsonPath path) return newPath; } /// + /// Appends a by specifying a series of array indicies. + /// + /// The to extend. + /// The indices and slices of the s to include. + /// The new . + public static JsonPath SearchArray(this JsonPath path, params Slice[] slices) + { + var newPath = new JsonPath(); + newPath.Operators.AddRange(path.Operators); + newPath.Operators.Add(new SearchOperator(slices.Any() + ? new ArraySearchParameter(new SliceQuery(slices)) + : new ArraySearchParameter(WildCardQuery.Instance))); + return newPath; + } + /// + /// Appends a by specifying an expression which evaluates to the index to include. + /// + /// The to extend. + /// The expression. + /// The new . + public static JsonPath SearchArray(this JsonPath path, Expression> expression) + { + var newPath = new JsonPath(); + newPath.Operators.AddRange(path.Operators); + newPath.Operators.Add(new SearchOperator(new ArraySearchParameter(new IndexExpressionQuery(ExpressionTranslator.Translate(expression))))); + return newPath; + } + /// + /// Appends a by specifying a predicate expression which filters the values. + /// + /// The to extend. + /// The predicate expression + /// The new . + public static JsonPath SearchArray(this JsonPath path, Expression> expression) + { + var newPath = new JsonPath(); + newPath.Operators.AddRange(path.Operators); + newPath.Operators.Add(new SearchOperator(new ArraySearchParameter(new FilterExpressionQuery(ExpressionTranslator.Translate(expression))))); + return newPath; + } + /// /// Appends a by including all array values. /// /// The to extend. @@ -321,13 +345,13 @@ public static JsonPath Array(this JsonPath path) /// Appends a by specifying a series of array indicies. /// /// The to extend. - /// The indices of the s to include. + /// The indices of the s to include. /// The new . - public static JsonPath Array(this JsonPath path, params int[] indices) + public static JsonPath Array(this JsonPath path, params Slice[] slices) { var newPath = new JsonPath(); newPath.Operators.AddRange(path.Operators); - newPath.Operators.Add(new ArrayOperator(new IndexQuery(indices))); + newPath.Operators.Add(new ArrayOperator(new SliceQuery(slices))); return newPath; } /// @@ -341,12 +365,10 @@ public static JsonPath Array(this JsonPath path, params int[] indices) /// The format for the array slice is [start:end:step]. All parameters are individually optional, /// however either the start or end must be defines. Negative values for start and end indicate that the /// iterator should begin counting from the end of the array. + [Obsolete("Use the Array(this JsonPath, params Slice[]) overload instead.")] public static JsonPath ArraySlice(this JsonPath path, int? start, int? end, int? step = null) { - var newPath = new JsonPath(); - newPath.Operators.AddRange(path.Operators); - newPath.Operators.Add(new ArrayOperator(new SliceQuery(start, end, step))); - return newPath; + return path.Array(new Slice(start, end, step)); } /// /// Appends a by specifying an expression which evaluates to the index to include. diff --git a/Manatee.Json/Path/Operators/ArrayOperator.cs b/Manatee.Json/Path/Operators/ArrayOperator.cs index 5551afc..bd6c30d 100644 --- a/Manatee.Json/Path/Operators/ArrayOperator.cs +++ b/Manatee.Json/Path/Operators/ArrayOperator.cs @@ -20,33 +20,48 @@ Purpose: Indicates that the current value should be an array. ***************************************************************************************/ +using System; using System.Linq; using Manatee.Json.Internal; namespace Manatee.Json.Path.Operators { - internal class ArrayOperator : IJsonPathOperator + internal class ArrayOperator : IJsonPathOperator, IEquatable { - private readonly IJsonPathArrayQuery _query; + public IJsonPathArrayQuery Query { get; } public ArrayOperator(IJsonPathArrayQuery query) { - _query = query; + Query = query; } public JsonArray Evaluate(JsonArray json, JsonValue root) { return new JsonArray(json.SelectMany(v => v.Type == JsonValueType.Array - ? _query.Find(v.Array, root) + ? Query.Find(v.Array, root) : v.Type == JsonValueType.Object - ? _query.Find(v.Object.Values.ToJson(), root) + ? Query.Find(v.Object.Values.ToJson(), root) : Enumerable.Empty()) .NotNull()); } public override string ToString() { - return $"[{_query}]"; + return $"[{Query}]"; + } + public bool Equals(ArrayOperator other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Query, other.Query); + } + public override bool Equals(object obj) + { + return Equals(obj as ArrayOperator); + } + public override int GetHashCode() + { + return Query?.GetHashCode() ?? 0; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Operators/IndexOfOperator.cs b/Manatee.Json/Path/Operators/IndexOfOperator.cs index d2a3021..c3c46c9 100644 --- a/Manatee.Json/Path/Operators/IndexOfOperator.cs +++ b/Manatee.Json/Path/Operators/IndexOfOperator.cs @@ -21,12 +21,13 @@ returns the number of items. ***************************************************************************************/ +using System; using System.Linq; using Manatee.Json.Path.Expressions; namespace Manatee.Json.Path.Operators { - internal class IndexOfOperator : IJsonPathOperator + internal class IndexOfOperator : IJsonPathOperator, IEquatable { public Expression Parameter { get; } @@ -46,5 +47,19 @@ public override string ToString() { return $".indexOf({Parameter})"; } + public bool Equals(IndexOfOperator other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Parameter, other.Parameter); + } + public override bool Equals(object obj) + { + return Equals(obj as IndexOfOperator); + } + public override int GetHashCode() + { + return Parameter?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Operators/LengthOperator.cs b/Manatee.Json/Path/Operators/LengthOperator.cs index 2bbd470..dc3d3ae 100644 --- a/Manatee.Json/Path/Operators/LengthOperator.cs +++ b/Manatee.Json/Path/Operators/LengthOperator.cs @@ -21,11 +21,12 @@ returns the number of items. ***************************************************************************************/ +using System; using System.Linq; namespace Manatee.Json.Path.Operators { - internal class LengthOperator : IJsonPathOperator + internal class LengthOperator : IJsonPathOperator, IEquatable { public static LengthOperator Instance { get; } = new LengthOperator(); @@ -37,7 +38,20 @@ public JsonArray Evaluate(JsonArray json, JsonValue root) } public override string ToString() { - return string.Format(".length"); + return ".length"; + } + public bool Equals(LengthOperator other) + { + return !ReferenceEquals(null, other); + } + public override bool Equals(object obj) + { + return Equals(obj as LengthOperator); + } + public override int GetHashCode() + { + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + return base.GetHashCode(); } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Operators/NameOperator.cs b/Manatee.Json/Path/Operators/NameOperator.cs index 4d3278e..e3e800a 100644 --- a/Manatee.Json/Path/Operators/NameOperator.cs +++ b/Manatee.Json/Path/Operators/NameOperator.cs @@ -21,12 +21,13 @@ a named property should be retrieved. ***************************************************************************************/ +using System; using System.Linq; using Manatee.Json.Internal; namespace Manatee.Json.Path.Operators { - internal class NameOperator : IJsonPathOperator + internal class NameOperator : IJsonPathOperator, IEquatable { public string Name { get; } @@ -43,7 +44,23 @@ public JsonArray Evaluate(JsonArray json, JsonValue root) } public override string ToString() { - return $".{Name}"; + return Name.Any(c => !char.IsLetterOrDigit(c)) + ? $".'{Name}'" + : $".{Name}"; + } + public bool Equals(NameOperator other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Name, other.Name); + } + public override bool Equals(object obj) + { + return Equals(obj as NameOperator); + } + public override int GetHashCode() + { + return Name?.GetHashCode() ?? 0; } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Operators/SearchOperator.cs b/Manatee.Json/Path/Operators/SearchOperator.cs index 1073348..78d92b7 100644 --- a/Manatee.Json/Path/Operators/SearchOperator.cs +++ b/Manatee.Json/Path/Operators/SearchOperator.cs @@ -20,9 +20,11 @@ Purpose: Indicates that the path should search for a named property. ***************************************************************************************/ +using System; + namespace Manatee.Json.Path.Operators { - internal class SearchOperator : IJsonPathOperator + internal class SearchOperator : IJsonPathOperator, IEquatable { private readonly IJsonPathSearchParameter _parameter; @@ -39,5 +41,19 @@ public override string ToString() { return $"..{_parameter}"; } + public bool Equals(SearchOperator other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_parameter, other._parameter); + } + public override bool Equals(object obj) + { + return Equals(obj as SearchOperator); + } + public override int GetHashCode() + { + return _parameter?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Operators/WildCardOperator.cs b/Manatee.Json/Path/Operators/WildCardOperator.cs index 49d1868..2650e50 100644 --- a/Manatee.Json/Path/Operators/WildCardOperator.cs +++ b/Manatee.Json/Path/Operators/WildCardOperator.cs @@ -21,14 +21,21 @@ current level. ***************************************************************************************/ +using System; using System.Linq; using Manatee.Json.Internal; namespace Manatee.Json.Path.Operators { - internal class WildCardOperator : IJsonPathOperator + internal class WildCardOperator : IJsonPathOperator, IEquatable { - public static WildCardOperator Instance { get; } = new WildCardOperator(); + public static WildCardOperator Instance { get; } + + static WildCardOperator() + { + Instance = new WildCardOperator(); + } + private WildCardOperator() {} public JsonArray Evaluate(JsonArray json, JsonValue root) { @@ -49,5 +56,18 @@ public override string ToString() { return ".*"; } + public bool Equals(WildCardOperator other) + { + return !ReferenceEquals(null, other); + } + public override bool Equals(object obj) + { + return Equals(obj as WildCardOperator); + } + public override int GetHashCode() + { + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + return base.GetHashCode(); + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Parsing/ExpressionFilterParser.cs b/Manatee.Json/Path/Parsing/ExpressionFilterParser.cs new file mode 100644 index 0000000..a1db3fa --- /dev/null +++ b/Manatee.Json/Path/Parsing/ExpressionFilterParser.cs @@ -0,0 +1,50 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ExpressionFilterParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: ExpressionFilterParser + Purpose: Parses JSON Path array components with filter expresssions. + +***************************************************************************************/ +using Manatee.Json.Path.ArrayParameters; +using Manatee.Json.Path.Expressions; +using Manatee.Json.Path.Expressions.Parsing; +using Manatee.Json.Path.Operators; + +namespace Manatee.Json.Path.Parsing +{ + internal class ExpressionFilterParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.StartsWith("[?("); + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + index += 3; + Expression expression; + var error = JsonPathExpressionParser.Parse(source, ref index, out expression); + + if (error != null) + return error; + + index++; // consume closing ']' + path.Operators.Add(new ArrayOperator(new FilterExpressionQuery(expression))); + return null; + } + } +} diff --git a/Manatee.Json/Path/Parsing/ExpressionIndexParser.cs b/Manatee.Json/Path/Parsing/ExpressionIndexParser.cs new file mode 100644 index 0000000..4706d0a --- /dev/null +++ b/Manatee.Json/Path/Parsing/ExpressionIndexParser.cs @@ -0,0 +1,50 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ExpressionIndexParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: ExpressionIndexParser + Purpose: Parses JSON Path array components with index expresssions. + +***************************************************************************************/ +using Manatee.Json.Path.ArrayParameters; +using Manatee.Json.Path.Expressions; +using Manatee.Json.Path.Expressions.Parsing; +using Manatee.Json.Path.Operators; + +namespace Manatee.Json.Path.Parsing +{ + internal class ExpressionIndexParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.StartsWith("[("); + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + index += 2; + Expression expression; + var error = JsonPathExpressionParser.Parse(source, ref index, out expression); + + if (error != null) + return error; + + index++; // consume closing ']' + path.Operators.Add(new ArrayOperator(new IndexExpressionQuery(expression))); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Parsing/IJsonPathParser.cs b/Manatee.Json/Path/Parsing/IJsonPathParser.cs new file mode 100644 index 0000000..d34e7e0 --- /dev/null +++ b/Manatee.Json/Path/Parsing/IJsonPathParser.cs @@ -0,0 +1,31 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IJsonPathParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: IJsonPathParser + Purpose: Defines methods used to parse JSON Path components. + +***************************************************************************************/ +namespace Manatee.Json.Path.Parsing +{ + internal interface IJsonPathParser + { + bool Handles(string input); + + string TryParse(string source, ref int index, ref JsonPath path); + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Parsing/IndexedArrayParser.cs b/Manatee.Json/Path/Parsing/IndexedArrayParser.cs new file mode 100644 index 0000000..17045bc --- /dev/null +++ b/Manatee.Json/Path/Parsing/IndexedArrayParser.cs @@ -0,0 +1,48 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IndexedArrayParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: IndexedArrayParser + Purpose: Parses JSON Path array components with discrete indices. + +***************************************************************************************/ +using System.Collections.Generic; +using System.Linq; +using Manatee.Json.Internal; + +namespace Manatee.Json.Path.Parsing +{ + internal class IndexedArrayParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.Length > 1 && input[0] == '[' && (char.IsDigit(input[1]) || input[1].In('-', ':')); + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + if (path == null) + return "Start token not found."; + + IList slices; + var error = source.GetSlices(ref index, out slices); + if (error != null) return error; + + path = path.Array(slices.ToArray()); + return null; + } + } +} diff --git a/Manatee.Json/Path/Parsing/JsonPathParser.cs b/Manatee.Json/Path/Parsing/JsonPathParser.cs new file mode 100644 index 0000000..e16af45 --- /dev/null +++ b/Manatee.Json/Path/Parsing/JsonPathParser.cs @@ -0,0 +1,70 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: JsonPathParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: JsonPathParser + Purpose: Parses JSON Path. + +***************************************************************************************/ +using System; +using System.Collections.Generic; +using System.Linq; +using Manatee.Json.Internal; + +namespace Manatee.Json.Path.Parsing +{ + internal static class JsonPathParser + { + private static readonly List Parsers; + + static JsonPathParser() + { + Parsers = typeof(JsonPathParser).Assembly.GetTypes() + .Where(t => typeof(IJsonPathParser).IsAssignableFrom(t) && t.IsClass) + .Select(Activator.CreateInstance) + .Cast() + .ToList(); + } + + public static JsonPath Parse(string source) + { + var index = 0; + JsonPath path; + var errorMessage = Parse(source, ref index, out path); + if (errorMessage != null) + throw new JsonPathSyntaxException(path, errorMessage); + return path; + } + public static string Parse(string source, ref int index, out JsonPath path) + { + var length = source.Length; + path = null; + while(index < length) + { + char c; + var errorMessage = source.SkipWhiteSpace(ref index, length, out c); + if (errorMessage != null) return errorMessage; + var substring = source.Substring(index); + var parser = Parsers.FirstOrDefault(p => p.Handles(substring)); + if (parser == null) return "Unrecognized JSON Path element."; + errorMessage = parser.TryParse(source, ref index, ref path); + if (errorMessage != null) return errorMessage; + } + return null; + } + } +} diff --git a/Manatee.Json/Path/Parsing/ObjectParser.cs b/Manatee.Json/Path/Parsing/ObjectParser.cs new file mode 100644 index 0000000..3bdb3ea --- /dev/null +++ b/Manatee.Json/Path/Parsing/ObjectParser.cs @@ -0,0 +1,53 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: ObjectParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: ObjectParser + Purpose: Parses JSON Path object components. + +***************************************************************************************/ +namespace Manatee.Json.Path.Parsing +{ + internal class ObjectParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.Length > 1 && input[0] == '.' && input[1] != '.'; + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + if (path == null) + return "Start token not found."; + + index++; + + if (source[index] == '*') + { + path = path.Wildcard(); + index++; + return null; + } + + string key; + var error = source.GetKey(ref index, out key); + if (error != null) return error; + + path = path.Name(key); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Parsing/PathParsingExtensions.cs b/Manatee.Json/Path/Parsing/PathParsingExtensions.cs new file mode 100644 index 0000000..33c0e86 --- /dev/null +++ b/Manatee.Json/Path/Parsing/PathParsingExtensions.cs @@ -0,0 +1,238 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: PathParsingExtensions.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: PathParsingExtensions + Purpose: Extension methods used while parsing JSON Path. + +***************************************************************************************/ +using System; +using System.Collections.Generic; +using System.Linq; +using Manatee.Json.Internal; + +namespace Manatee.Json.Path.Parsing +{ + internal static class PathParsingExtensions + { + #region GetKey + + private static readonly int[] FibSequence = { 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657 }; + + public static string GetKey(this string source, ref int index, out string key) + { + return source[index].In('"', '\'') + ? GetQuotedKey(source, ref index, out key) + : GetBasicKey(source, ref index, out key); + } + + private static string GetBasicKey(string source, ref int index, out string key) + { + var bufferSize = 0; + var bufferLength = FibSequence[bufferSize]; + var buffer = new char[bufferLength]; + var bufferIndex = 0; + var sourceLength = source.Length; + var complete = false; + var c = (char)0; + while (index < sourceLength) + { + if (bufferIndex == bufferLength) + { + var currentLength = bufferLength; + bufferSize++; + bufferLength = FibSequence[bufferSize]; + var newBuffer = new char[bufferLength]; + Buffer.BlockCopy(buffer, 0, newBuffer, 0, currentLength * 2); + buffer = newBuffer; + } + c = source[index]; + index++; + if (!(char.IsLetterOrDigit(c) || c == '_')) + { + complete = true; + index--; + break; + } + buffer[bufferIndex] = c; + bufferIndex++; + } + if (!complete && index + 1 < sourceLength) + { + key = null; + return $"The character '{c}' is not supported for unquoted names."; + } + var result = new string(buffer, 0, bufferIndex); + key = result; + return null; + } + + private static string GetQuotedKey(string source, ref int index, out string key) + { + var bufferSize = 0; + var bufferLength = FibSequence[bufferSize]; + var buffer = new char[bufferLength]; + var bufferIndex = 0; + var sourceLength = source.Length; + var foundEscape = false; + var complete = false; + var quoteChar = source[index]; + index++; + while (index < sourceLength) + { + if (bufferIndex == bufferLength) + { + var currentLength = bufferLength; + bufferSize++; + bufferLength = FibSequence[bufferSize]; + var newBuffer = new char[bufferLength]; + Buffer.BlockCopy(buffer, 0, newBuffer, 0, currentLength * 2); + buffer = newBuffer; + } + var c = source[index]; + index++; + if (c == quoteChar && !foundEscape) + { + complete = true; + break; + } + foundEscape = c == '\\'; + buffer[bufferIndex] = c; + bufferIndex++; + } + if (!complete) + { + key = null; + return "Could not find end of string value."; + } + var result = new string(buffer, 0, bufferIndex); + string escaped; + var errorMessage = result.EvaluateEscapeSequences(out escaped); + key = escaped; + return errorMessage; + } + + #endregion + + #region GetSlice + + public static string GetSlices(this string source, ref int index, out IList slices) + { + string error; + Slice lastSlice; + slices = new List(); + do + { + index++; + error = source.GetSlice(ref index, out lastSlice); + if (lastSlice != null) + slices.Add(lastSlice); + } while (error == null && lastSlice != null); + if (error != null) return error; + + if (!slices.Any()) + return "Index required inside '[]'"; + return null; + } + + private static string GetSlice(this string source, ref int index, out Slice slice) + { + slice = null; + if (source[index - 1] == ']') return null; + + int? n1, n2, n3; + + var error = GetInt(source, ref index, out n1); + if (error != null) return error; + if (n1.HasValue && source[index].In(',', ']')) + { + slice = new Slice(n1.Value); + return null; + } + if (source[index] != ':') + return "Expected ':', ',', or ']'."; + + index++; + error = GetInt(source, ref index, out n2); + if (error != null) return error; + if (source[index].In(',', ']')) + { + slice = new Slice(n1, n2); + return null; + } + if (source[index] != ':') + return "Expected ':', ',', or ']'."; + + index++; + error = GetInt(source, ref index, out n3); + if (error != null) return error; + if (source[index].In(',', ']')) + { + slice = new Slice(n1, n2, n3); + return null; + } + return "Expected ',' or ']'."; + } + public static string GetInt(string source, ref int index, out int? number) + { + int value; + var text = new string(source.Substring(index).TakeWhile(c => char.IsDigit(c) || c == '-').ToArray()); + if (text.Length == 0 && source[index].In(':', ',', ']')) + { + number = null; + return null; + } + if (!int.TryParse(text, out value)) + { + number = null; + return "Expected number."; + } + + index += text.Length; + number = value; + return null; + } + + #endregion + + #region GetNumber + + private const string NumberChars = "0123456789e.-"; + + public static string GetNumber(this string source, ref int index, out double? number) + { + double value; + var text = new string(source.Substring(index).TakeWhile(c => NumberChars.Contains(c)).ToArray()); + if (text.Length == 0 && source[index].In(':', ',', ']')) + { + number = null; + return null; + } + if (!double.TryParse(text, out value)) + { + number = null; + return "Expected number."; + } + + index += text.Length; + number = value; + return null; + } + + #endregion + } +} diff --git a/Manatee.Json/Path/Parsing/SearchIndexedArrayParser.cs b/Manatee.Json/Path/Parsing/SearchIndexedArrayParser.cs new file mode 100644 index 0000000..0fc1891 --- /dev/null +++ b/Manatee.Json/Path/Parsing/SearchIndexedArrayParser.cs @@ -0,0 +1,49 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: IndexedArrayParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: IndexedArrayParser + Purpose: Parses JSON Path array components with discrete indices. + +***************************************************************************************/ +using System.Collections.Generic; +using System.Linq; +using Manatee.Json.Internal; + +namespace Manatee.Json.Path.Parsing +{ + internal class SearchIndexedArrayParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.Length > 4 && input.StartsWith("..[") && (char.IsDigit(input[3]) || input[3].In('-', ':')); + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + if (path == null) + return "Start token not found."; + + index += 2; + IList slices; + string error = source.GetSlices(ref index, out slices); + if (error != null) return error; + + path = path.SearchArray(slices.ToArray()); + return null; + } + } +} diff --git a/Manatee.Json/Path/Parsing/SearchParser.cs b/Manatee.Json/Path/Parsing/SearchParser.cs new file mode 100644 index 0000000..8e1d409 --- /dev/null +++ b/Manatee.Json/Path/Parsing/SearchParser.cs @@ -0,0 +1,53 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: SearchParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: SearchParser + Purpose: Parses JSON Path named and wildcard search components. + +***************************************************************************************/ +namespace Manatee.Json.Path.Parsing +{ + internal class SearchParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.StartsWith("..") && !input.StartsWith("..["); + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + if (path == null) + return "Start token not found."; + + index += 2; + + if (source[index] == '*') + { + path = path.Search(); + index++; + return null; + } + + string key; + var error = source.GetKey(ref index, out key); + if (error != null) return error; + + path = path.Search(key); + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/Expressions/GroupExpression.cs b/Manatee.Json/Path/Parsing/StartParser.cs similarity index 50% rename from Manatee.Json/Path/Expressions/GroupExpression.cs rename to Manatee.Json/Path/Parsing/StartParser.cs index ec2b86d..831b471 100644 --- a/Manatee.Json/Path/Expressions/GroupExpression.cs +++ b/Manatee.Json/Path/Parsing/StartParser.cs @@ -1,4 +1,4 @@ -/*************************************************************************************** +/*************************************************************************************** Copyright 2016 Greg Dennis @@ -14,28 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. - File Name: GroupExpression.cs - Namespace: Manatee.Json.Path.Expressions - Class Name: GroupExpression - Purpose: Expresses the intent to group an expression as a component - in a larger expression. + File Name: StartParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: StartParser + Purpose: Parses JSON Path start symbols. ***************************************************************************************/ - -namespace Manatee.Json.Path.Expressions +namespace Manatee.Json.Path.Parsing { - internal class GroupExpression : ExpressionTreeNode + internal class StartParser : IJsonPathParser { - public override int Priority => 6; - public ExpressionTreeNode Group { get; set; } - - public override object Evaluate(T json, JsonValue root) + public bool Handles(string input) { - return Group.Evaluate(json, root); + return input[0] == '$' || input[0] == '@'; } - public override string ToString() + public string TryParse(string source, ref int index, ref JsonPath path) { - return $"({Group})"; + if (path != null) + return "Start token not valid in the middle of path."; + + path = new JsonPath(); + index++; + return null; } } -} \ No newline at end of file +} diff --git a/Manatee.Json/Path/Parsing/WildcardArrayParser.cs b/Manatee.Json/Path/Parsing/WildcardArrayParser.cs new file mode 100644 index 0000000..b124fc9 --- /dev/null +++ b/Manatee.Json/Path/Parsing/WildcardArrayParser.cs @@ -0,0 +1,38 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: WildcardArrayParser.cs + Namespace: Manatee.Json.Path.Parsing + Class Name: WildcardArrayParser + Purpose: Parses JSON Path array components with wildcard indexers. + +***************************************************************************************/ +namespace Manatee.Json.Path.Parsing +{ + internal class WildcardArrayParser : IJsonPathParser + { + public bool Handles(string input) + { + return input.StartsWith("[*]"); + } + public string TryParse(string source, ref int index, ref JsonPath path) + { + path = path.Array(); + index += 3; + return null; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Path/PathExpressionExtensions.cs b/Manatee.Json/Path/PathExpressionExtensions.cs index a3d56b2..2f8d722 100644 --- a/Manatee.Json/Path/PathExpressionExtensions.cs +++ b/Manatee.Json/Path/PathExpressionExtensions.cs @@ -44,6 +44,16 @@ public static int Length(this JsonPathArray json) /// Specifies the length of a . /// /// The array. + /// The name of the property. + /// The length of the array. + public static JsonPathArray Name(this JsonPathArray json, string name) + { + throw new InvalidOperationException("This operation is reserved for JsonPath."); + } + /// + /// Specifies the length of a . + /// + /// The array. /// The length of the array. public static int Length(this JsonPathValue json) { diff --git a/Manatee.Json/Path/SearchParameters/ArraySearchParameter.cs b/Manatee.Json/Path/SearchParameters/ArraySearchParameter.cs index de6fd91..5be3fa4 100644 --- a/Manatee.Json/Path/SearchParameters/ArraySearchParameter.cs +++ b/Manatee.Json/Path/SearchParameters/ArraySearchParameter.cs @@ -20,12 +20,13 @@ Purpose: Indicates that a search should look for an array parameter. ***************************************************************************************/ +using System; using System.Collections.Generic; using System.Linq; namespace Manatee.Json.Path.SearchParameters { - internal class ArraySearchParameter : IJsonPathSearchParameter + internal class ArraySearchParameter : IJsonPathSearchParameter, IEquatable { private readonly IJsonPathArrayQuery _query; @@ -56,5 +57,19 @@ public override string ToString() { return $"[{_query}]"; } + public bool Equals(ArraySearchParameter other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_query, other._query); + } + public override bool Equals(object obj) + { + return Equals(obj as ArraySearchParameter); + } + public override int GetHashCode() + { + return _query?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/SearchParameters/LengthSearchParameter.cs b/Manatee.Json/Path/SearchParameters/LengthSearchParameter.cs index 15f2160..55cc106 100644 --- a/Manatee.Json/Path/SearchParameters/LengthSearchParameter.cs +++ b/Manatee.Json/Path/SearchParameters/LengthSearchParameter.cs @@ -21,12 +21,13 @@ object and array values. ***************************************************************************************/ +using System; using System.Collections.Generic; using System.Linq; namespace Manatee.Json.Path.SearchParameters { - internal class LengthSearchParameter : IJsonPathSearchParameter + internal class LengthSearchParameter : IJsonPathSearchParameter, IEquatable { public static LengthSearchParameter Instance { get; } = new LengthSearchParameter(); @@ -51,5 +52,18 @@ public override string ToString() { return "length"; } + public bool Equals(LengthSearchParameter other) + { + return !ReferenceEquals(null, other); + } + public override bool Equals(object obj) + { + return Equals(obj as LengthSearchParameter); + } + public override int GetHashCode() + { + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + return base.GetHashCode(); + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/SearchParameters/NameSearchParameter.cs b/Manatee.Json/Path/SearchParameters/NameSearchParameter.cs index 5681615..31f9042 100644 --- a/Manatee.Json/Path/SearchParameters/NameSearchParameter.cs +++ b/Manatee.Json/Path/SearchParameters/NameSearchParameter.cs @@ -20,12 +20,13 @@ Purpose: Indicates that a search should look for a named parameter. ***************************************************************************************/ +using System; using System.Collections.Generic; using System.Linq; namespace Manatee.Json.Path.SearchParameters { - internal class NameSearchParameter : IJsonPathSearchParameter + internal class NameSearchParameter : IJsonPathSearchParameter, IEquatable { private readonly string _name; @@ -57,5 +58,19 @@ public override string ToString() { return _name; } + public bool Equals(NameSearchParameter other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(_name, other._name); + } + public override bool Equals(object obj) + { + return Equals(obj as NameSearchParameter); + } + public override int GetHashCode() + { + return _name?.GetHashCode() ?? 0; + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/SearchParameters/WildCardSearchParameter.cs b/Manatee.Json/Path/SearchParameters/WildCardSearchParameter.cs index ec7c016..d80383e 100644 --- a/Manatee.Json/Path/SearchParameters/WildCardSearchParameter.cs +++ b/Manatee.Json/Path/SearchParameters/WildCardSearchParameter.cs @@ -20,12 +20,13 @@ Purpose: Indicates that a search should return all values. ***************************************************************************************/ +using System; using System.Collections.Generic; using System.Linq; namespace Manatee.Json.Path.SearchParameters { - internal class WildCardSearchParameter : IJsonPathSearchParameter + internal class WildCardSearchParameter : IJsonPathSearchParameter, IEquatable { public static WildCardSearchParameter Instance { get; } = new WildCardSearchParameter(); @@ -50,5 +51,18 @@ public override string ToString() { return "*"; } + public bool Equals(WildCardSearchParameter other) + { + return !ReferenceEquals(null, other); + } + public override bool Equals(object obj) + { + return Equals(obj as WildCardSearchParameter); + } + public override int GetHashCode() + { + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + return base.GetHashCode(); + } } } \ No newline at end of file diff --git a/Manatee.Json/Path/Slice.cs b/Manatee.Json/Path/Slice.cs new file mode 100644 index 0000000..1404e0f --- /dev/null +++ b/Manatee.Json/Path/Slice.cs @@ -0,0 +1,143 @@ +/*************************************************************************************** + + Copyright 2016 Greg Dennis + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + File Name: Slice.cs + Namespace: Manatee.Json.Path + Class Name: Slice + Purpose: Defines an index in a JSON Path array. + +***************************************************************************************/ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Manatee.Json.Path +{ + /// + /// Defines an index in a JSON Path array. + /// + public class Slice : IEquatable + { + private int? _start; + private int? _end; + private int? _step; + + internal int? Index { get; } + + /// + /// Creates a new instance of the class. + /// + /// A single index. + public Slice(int index) + { + Index = index; + } + /// + /// Creates a new instance of the class. + /// + /// The start index of the slice. + /// The end index of the slice. + /// Optional. The increment between each selected index between and . + public Slice(int? start, int? end, int? step = null) + { + _start = start; + _end = end; + _step = step; + } + + /// Returns a string that represents the current object. + /// A string that represents the current object. + /// 2 + public override string ToString() + { + return Index.HasValue + ? Index.ToString() + : _step.HasValue + ? $"{(_start.HasValue ? _start.ToString() : string.Empty)}:{(_end.HasValue ? _end.ToString() : string.Empty)}:{_step}" + : $"{(_start.HasValue ? _start.ToString() : string.Empty)}:{(_end.HasValue ? _end.ToString() : string.Empty)}"; + } + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(Slice other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Index == other.Index && + _start == other._start && + _end == other._end && + _step == other._step; + } + /// Determines whether the specified is equal to the current . + /// true if the specified is equal to the current ; otherwise, false. + /// The object to compare with the current object. + /// 2 + public override bool Equals(object obj) + { + return Equals(obj as Slice); + } + /// Serves as a hash function for a particular type. + /// A hash code for the current . + /// 2 + public override int GetHashCode() + { + unchecked + { + var hashCode = Index.GetHashCode(); + hashCode = (hashCode*397) ^ _start.GetHashCode(); + hashCode = (hashCode*397) ^ _end.GetHashCode(); + hashCode = (hashCode*397) ^ _step.GetHashCode(); + return hashCode; + } + } + /// + /// Implicit conversion from integer to a single-indexed . + /// + /// The index. + public static implicit operator Slice(int i) + { + return new Slice(i); + } + + internal IEnumerable Find(JsonArray json, JsonValue root) + { + if (Index.HasValue) + { + return Index.Value < 0 || json.Count < Index.Value + ? Enumerable.Empty() + : new[] { json[Index.Value] }; + } + + var start = ResolveIndex(_start ?? 0, json.Count); + var end = ResolveIndex(_end ?? json.Count, json.Count); + var step = Math.Max(_step ?? 1, 1); + + var index = start; + var list = new List(); + while (index < end) + { + list.Add(json[index]); + index += step; + } + return list; + } + + private static int ResolveIndex(int index, int count) + { + return index < 0 ? count + index : index; + } + } +} \ No newline at end of file diff --git a/Manatee.Json/Schema/JsonSchemaPropertyDefinition.cs b/Manatee.Json/Schema/JsonSchemaPropertyDefinition.cs index 4b43b57..699f650 100644 --- a/Manatee.Json/Schema/JsonSchemaPropertyDefinition.cs +++ b/Manatee.Json/Schema/JsonSchemaPropertyDefinition.cs @@ -83,17 +83,15 @@ public JsonValue ToJson(JsonSerializer serializer) return new JsonObject {{Name, Type.ToJson(serializer)}}; } + /// Returns a string that represents the current object. + /// A string that represents the current object. + /// 2 public override string ToString() { return ToJson(null).ToString(); } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. /// An object to compare with this object. public bool Equals(JsonSchemaPropertyDefinition other) { @@ -101,13 +99,10 @@ public bool Equals(JsonSchemaPropertyDefinition other) Equals(Type, other.Type) && IsRequired == other.IsRequired; } - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified is equal to the current ; otherwise, false. - /// - /// The object to compare with the current object. 2 + /// Determines whether the specified is equal to the current . + /// true if the specified is equal to the current ; otherwise, false. + /// The object to compare with the current object. + /// 2 public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; @@ -115,12 +110,8 @@ public override bool Equals(object obj) if (obj.GetType() != GetType()) return false; return Equals((JsonSchemaPropertyDefinition) obj); } - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// + /// Serves as a hash function for a particular type. + /// A hash code for the current . /// 2 public override int GetHashCode() { diff --git a/Manatee.Json/packages.config b/Manatee.Json/packages.config deleted file mode 100644 index 66d7e29..0000000 --- a/Manatee.Json/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file