From effe7780f729823118ab53e762448ee999368c1e Mon Sep 17 00:00:00 2001 From: Brenton Farmer Date: Fri, 7 Jun 2024 16:01:13 -0700 Subject: [PATCH] Initial work on JsonPathBuilder. --- .../JsonElementPositionComparer.cs | 69 +++++++------------ src/Hyperbee.Json/JsonPathBuilder.cs | 59 ++++++++++++++++ 2 files changed, 82 insertions(+), 46 deletions(-) create mode 100644 src/Hyperbee.Json/JsonPathBuilder.cs diff --git a/src/Hyperbee.Json/JsonElementPositionComparer.cs b/src/Hyperbee.Json/JsonElementPositionComparer.cs index b3c8d5b6..18910fd8 100644 --- a/src/Hyperbee.Json/JsonElementPositionComparer.cs +++ b/src/Hyperbee.Json/JsonElementPositionComparer.cs @@ -11,6 +11,18 @@ internal class JsonElementPositionComparer : IEqualityComparer static JsonElementPositionComparer() { + // We want a fast comparer that will tell us if two JsonElements point to the same exact + // backing data in the parent JsonDocument. JsonElement is a struct, and a value comparison + // for equality won't give us reliable results and would be expensive. + // + // The internal JsonElement constructor takes parent and idx arguments that are saves as fields. + // + // idx: is an index used to get the position of the JsonElement in the backing data. + // parent: is the owning JsonDocument (could be null in an enumeration). + // + // These arguments are stored in private fields and are not exposed. While note ideal, we + // will access these fields through dynamic methods to use for our comparison. + // Create DynamicMethod for _idx field var idxField = typeof( JsonElement ).GetField( "_idx", BindingFlags.NonPublic | BindingFlags.Instance ); @@ -26,7 +38,7 @@ static JsonElementPositionComparer() __getIdx = (Func) getIdxDynamicMethod.CreateDelegate( typeof( Func ) ); - // Create DynamicMethod for _parent field + // Create DynamicMethod for _parent field var parentField = typeof( JsonElement ).GetField( "_parent", BindingFlags.NonPublic | BindingFlags.Instance ); @@ -44,15 +56,24 @@ static JsonElementPositionComparer() public bool Equals( JsonElement x, JsonElement y ) { + // check for quick out + if ( x.ValueKind != y.ValueKind ) return false; + // check parent documents + + // BF: JsonElement ctor notes that parent may be null in some enumeration conditions. + // This check may not be reliable. If so, should be ok to remove the parent check. + var xParent = __getParent( x ); var yParent = __getParent( y ); - if ( !ReferenceEquals( xParent, yParent ) ) + if ( !ReferenceEquals( xParent, yParent ) ) return false; + // check idx values + return __getIdx( x ) == __getIdx( y ); } @@ -64,47 +85,3 @@ public int GetHashCode( JsonElement obj ) return HashCode.Combine( parent, idx ); } } - -public class JsonPathFinder -{ - public static string FindJsonPath( JsonElement rootElement, JsonElement targetElement ) - { - var comparer = new JsonElementPositionComparer(); - - var stack = new Stack<(JsonElement element, string path)>(); - stack.Push( (rootElement, string.Empty) ); - - while ( stack.Count > 0 ) - { - var (currentElement, currentPath) = stack.Pop(); - - if ( comparer.Equals( currentElement, targetElement ) ) - return currentPath; - - switch ( currentElement.ValueKind ) - { - case JsonValueKind.Object: - foreach ( JsonProperty property in currentElement.EnumerateObject() ) - { - var newPath = string.IsNullOrEmpty( currentPath ) ? property.Name : $"{currentPath}.{property.Name}"; - stack.Push( (property.Value, newPath) ); - } - - break; - - case JsonValueKind.Array: - var index = 0; - foreach ( JsonElement element in currentElement.EnumerateArray() ) - { - var newPath = $"{currentPath}[{index}]"; - stack.Push( (element, newPath) ); - index++; - } - - break; - } - } - - return null; // Target element not found in the JSON document - } -} diff --git a/src/Hyperbee.Json/JsonPathBuilder.cs b/src/Hyperbee.Json/JsonPathBuilder.cs new file mode 100644 index 00000000..8cf461b0 --- /dev/null +++ b/src/Hyperbee.Json/JsonPathBuilder.cs @@ -0,0 +1,59 @@ +using System.Text.Json; + +namespace Hyperbee.Json; + +public class JsonPathBuilder +{ + private readonly JsonElement _rootElement; + + public JsonPathBuilder( JsonDocument rootDocument ) + { + _rootElement = rootDocument.RootElement; + } + + public JsonPathBuilder( JsonElement rootElement ) + { + _rootElement = rootElement; + } + + public string GetPath( JsonElement targetElement ) + { + var comparer = new JsonElementPositionComparer(); + + var stack = new Stack<(JsonElement element, string path)>( 4 ); + stack.Push( (_rootElement, string.Empty) ); + + while ( stack.Count > 0 ) + { + var (currentElement, currentPath) = stack.Pop(); + + if ( comparer.Equals( currentElement, targetElement ) ) + return currentPath; + + switch ( currentElement.ValueKind ) + { + case JsonValueKind.Object: + foreach ( JsonProperty property in currentElement.EnumerateObject() ) + { + var newPath = string.IsNullOrEmpty( currentPath ) ? property.Name : $"{currentPath}.{property.Name}"; + stack.Push( (property.Value, newPath) ); + } + + break; + + case JsonValueKind.Array: + var index = 0; + foreach ( JsonElement element in currentElement.EnumerateArray() ) + { + var newPath = $"{currentPath}[{index}]"; + stack.Push( (element, newPath) ); + index++; + } + + break; + } + } + + return null; // Target element not found in the JSON document + } +}