#region Copyright (c) 2007 Atif Aziz. All rights reserved. // // C# implementation of JSONPath[1] // [1] http://goessner.net/articles/JsonPath/ // // The MIT License // // Copyright (c) 2007 Atif Aziz . All rights reserved. // Portions Copyright (c) 2007 Stefan Goessner (goessner.net) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #endregion namespace JsonPath { #region Imports using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; #endregion partial interface IJsonPathValueSystem { bool HasMember(object value, string member); object GetMemberValue(object value, string member); IEnumerable GetMembers(object value); bool IsObject(object value); bool IsArray(object value); bool IsPrimitive(object value); } sealed partial class JsonPathContext { public static readonly JsonPathContext Default = new JsonPathContext(); public Func ScriptEvaluator { get; set; } public IJsonPathValueSystem ValueSystem { get; set; } public IEnumerable Select(object obj, string expr) => SelectNodes(obj, expr, (v, _) => v); public IEnumerable SelectNodes(object obj, string expr, Func resultor) { if (obj == null) throw new ArgumentNullException(nameof(obj)); if (resultor == null) throw new ArgumentNullException(nameof(resultor)); var i = new Interpreter(ValueSystem, ScriptEvaluator); expr = Normalize(expr); if (expr.Length >= 1 && expr[0] == '$') // ^\$:? expr = expr.Substring(expr.Length >= 2 && expr[1] == ';' ? 2 : 1); return i.Trace(expr, obj, "$", (value, path) => resultor(value, AsBracketNotation(path))); } static string Normalize(string expr) { var subx = new List(); expr = RegExp.Replace(expr, @"[\['](\??\(.*?\))[\]']", m => { subx.Add(m.Groups[1].Value); return "[#" + (subx.Count - 1).ToString(CultureInfo.InvariantCulture) + "]"; }); expr = RegExp.Replace(expr, @"'?\.'?|\['?", ";"); expr = RegExp.Replace(expr, @";;;|;;", ";..;"); expr = RegExp.Replace(expr, @";$|'?\]|'$", string.Empty); expr = RegExp.Replace(expr, @"#([0-9]+)", m => { var index = int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture); return subx[index]; }); return expr; } public static string AsBracketNotation(string[] indicies) { if (indicies == null) throw new ArgumentNullException(nameof(indicies)); var sb = new StringBuilder(); foreach (var index in indicies) { if (sb.Length == 0) { sb.Append('$'); } else { sb.Append('['); if (RegExp.IsMatch(index, @"^[0-9*]+$")) sb.Append(index); else sb.Append('\'').Append(index).Append('\''); sb.Append(']'); } } return sb.ToString(); } static int? TryParseInt(string str) => int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n : (int?)null; sealed class Interpreter { readonly Func _eval; readonly IJsonPathValueSystem _system; static readonly IJsonPathValueSystem DefaultValueSystem = new BasicValueSystem(); static readonly char[] Colon = { ':' }; static readonly char[] Semicolon = { ';' }; delegate void WalkCallback(object member, string loc, string expr, object value, string path); public Interpreter(IJsonPathValueSystem valueSystem, Func eval) { _eval = eval ?? delegate { // @ symbol in expr must be interpreted specially to resolve // to value. In JavaScript, the implementation would look // like: // // return obj && value && eval(expr.replace(/@/g, "value")); return null; }; _system = valueSystem ?? DefaultValueSystem; } sealed class TraceArgs { public readonly string Expr; public readonly object Value; public readonly string Path; public TraceArgs(string expr, object value, string path) { Expr = expr; Value = value; Path = path; } } public IEnumerable Trace(string expr, object value, string path, Func resultor) => Trace(Args(expr, value, path), resultor); static TraceArgs Args(string expr, object value, string path) => new TraceArgs(expr, value, path); IEnumerable Trace(TraceArgs args, Func resultor) { var stack = new Stack(); stack.Push(args); while (stack.Count > 0) { var popped = stack.Pop(); var expr = popped.Expr; var value = popped.Value; var path = popped.Path; if (string.IsNullOrEmpty(expr)) { if (path != null) yield return resultor(value, path.Split(Semicolon)); continue; } var i = expr.IndexOf(';'); var atom = i >= 0 ? expr.Substring(0, i) : expr; var tail = i >= 0 ? expr.Substring(i + 1) : string.Empty; if (value != null && _system.HasMember(value, atom)) { stack.Push(Args(tail, Index(value, atom), path + ";" + atom)); } else if (atom == "*") { Walk(atom, tail, value, path, (m, l, x, v, p) => stack.Push(Args(m + ";" + x, v, p))); } else if (atom == "..") { Walk(atom, tail, value, path, (m, l, x, v, p) => { var result = Index(v, m.ToString()); if (result != null && !_system.IsPrimitive(result)) stack.Push(Args("..;" + x, result, p + ";" + m)); }); stack.Push(Args(tail, value, path)); } else if (atom.Length > 2 && atom[0] == '(' && atom[atom.Length - 1] == ')') // [(exp)] { stack.Push(Args(_eval(atom, value, path.Substring(path.LastIndexOf(';') + 1)) + ";" + tail, value, path)); } else if (atom.Length > 3 && atom[0] == '?' && atom[1] == '(' && atom[atom.Length - 1] == ')') // [?(exp)] { Walk(atom, tail, value, path, (m, l, x, v, p) => { var result = _eval(RegExp.Replace(l, @"^\?\((.*?)\)$", "$1"), Index(v, m.ToString()), m.ToString()); if (Convert.ToBoolean(result, CultureInfo.InvariantCulture)) stack.Push(Args(m + ";" + x, v, p)); }); } else if (RegExp.IsMatch(atom, @"^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$")) // [start:end:step] Phyton slice syntax { foreach (var a in Slice(atom, tail, value, path).Reverse()) stack.Push(a); } else if (atom.IndexOf(',') >= 0) // [name1,name2,...] { foreach (var part in RegExp.Split(atom, @"'?,'?").Reverse()) stack.Push(Args(part + ";" + tail, value, path)); } } } void Walk(string loc, string expr, object value, string path, WalkCallback callback) { if (_system.IsPrimitive(value)) return; if (_system.IsArray(value)) { var list = (IList)value; for (var i = list.Count - 1; i >= 0; i--) callback(i, loc, expr, value, path); } else if (_system.IsObject(value)) { foreach (var key in _system.GetMembers(value).Reverse()) callback(key, loc, expr, value, path); } } static IEnumerable Slice(string loc, string expr, object value, string path) { if (!(value is IList list)) yield break; var length = list.Count; var parts = loc.Split(Colon); var start = TryParseInt(parts[0]) ?? 0; var end = TryParseInt(parts[1]) ?? list.Count; var step = parts.Length > 2 ? TryParseInt(parts[2]) ?? 1 : 1; start = (start < 0) ? Math.Max(0, start + length) : Math.Min(length, start); end = (end < 0) ? Math.Max(0, end + length) : Math.Min(length, end); for (var i = start; i < end; i += step) yield return Args(i + ";" + expr, value, path); } object Index(object obj, string member) => _system.GetMemberValue(obj, member); } static class RegExp { const RegexOptions Options = RegexOptions.ECMAScript; public static bool IsMatch(string input, string pattern) => Regex.IsMatch(input, pattern, Options); public static string Replace(string input, string pattern, string replacement) => Regex.Replace(input, pattern, replacement, Options); public static string Replace(string input, string pattern, MatchEvaluator evaluator) => Regex.Replace(input, pattern, evaluator, Options); public static IEnumerable Split(string input, string pattern) => Regex.Split(input, pattern, Options); } sealed class BasicValueSystem : IJsonPathValueSystem { public bool HasMember(object value, string member) => !IsPrimitive(value) && (value is IDictionary dict ? dict.Contains(member) : value is IList list && TryParseInt(member) is int i && i >= 0 && i < list.Count); public object GetMemberValue(object value, string member) => IsPrimitive(value) ? throw new ArgumentException(null, nameof(value)) : value is IDictionary dict ? dict[member] : !(value is IList list) ? throw new ArgumentException(nameof(value)) : TryParseInt(member) is int i && i >= 0 && i < list.Count ? list[i] : null; public IEnumerable GetMembers(object value) => ((IDictionary)value).Keys.Cast(); public bool IsObject(object value) => value is IDictionary; public bool IsArray(object value) => value is IList; public bool IsPrimitive(object value) => value == null ? throw new ArgumentNullException(nameof(value)) : Type.GetTypeCode(value.GetType()) != TypeCode.Object; } } }