Jsons.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. #region Copyright (c) 2007 Atif Aziz. All rights reserved.
  2. //
  3. // C# implementation of JSONPath[1]
  4. // [1] http://goessner.net/articles/JsonPath/
  5. //
  6. // The MIT License
  7. //
  8. // Copyright (c) 2007 Atif Aziz . All rights reserved.
  9. // Portions Copyright (c) 2007 Stefan Goessner (goessner.net)
  10. //
  11. // Permission is hereby granted, free of charge, to any person obtaining a copy
  12. // of this software and associated documentation files (the "Software"), to deal
  13. // in the Software without restriction, including without limitation the rights
  14. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. // copies of the Software, and to permit persons to whom the Software is
  16. // furnished to do so, subject to the following conditions:
  17. //
  18. // The above copyright notice and this permission notice shall be included in
  19. // all copies or substantial portions of the Software.
  20. //
  21. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. // THE SOFTWARE.
  28. //
  29. #endregion
  30. namespace JsonPath
  31. {
  32. #region Imports
  33. using System;
  34. using System.Collections;
  35. using System.Collections.Generic;
  36. using System.Globalization;
  37. using System.Linq;
  38. using System.Text;
  39. using System.Text.RegularExpressions;
  40. #endregion
  41. partial interface IJsonPathValueSystem
  42. {
  43. bool HasMember(object value, string member);
  44. object GetMemberValue(object value, string member);
  45. IEnumerable<string> GetMembers(object value);
  46. bool IsObject(object value);
  47. bool IsArray(object value);
  48. bool IsPrimitive(object value);
  49. }
  50. sealed partial class JsonPathContext
  51. {
  52. public static readonly JsonPathContext Default = new JsonPathContext();
  53. public Func<string /* script */,
  54. object /* value */,
  55. string /* context */,
  56. object /* result */>
  57. ScriptEvaluator
  58. { get; set; }
  59. public IJsonPathValueSystem ValueSystem { get; set; }
  60. public IEnumerable<object> Select(object obj, string expr) =>
  61. SelectNodes(obj, expr, (v, _) => v);
  62. public IEnumerable<T> SelectNodes<T>(object obj, string expr, Func<object, string, T> resultor)
  63. {
  64. if (obj == null) throw new ArgumentNullException(nameof(obj));
  65. if (resultor == null) throw new ArgumentNullException(nameof(resultor));
  66. var i = new Interpreter(ValueSystem, ScriptEvaluator);
  67. expr = Normalize(expr);
  68. if (expr.Length >= 1 && expr[0] == '$') // ^\$:?
  69. expr = expr.Substring(expr.Length >= 2 && expr[1] == ';' ? 2 : 1);
  70. return i.Trace(expr, obj, "$", (value, path) => resultor(value, AsBracketNotation(path)));
  71. }
  72. static string Normalize(string expr)
  73. {
  74. var subx = new List<string>();
  75. expr = RegExp.Replace(expr, @"[\['](\??\(.*?\))[\]']", m =>
  76. {
  77. subx.Add(m.Groups[1].Value);
  78. return "[#" + (subx.Count - 1).ToString(CultureInfo.InvariantCulture) + "]";
  79. });
  80. expr = RegExp.Replace(expr, @"'?\.'?|\['?", ";");
  81. expr = RegExp.Replace(expr, @";;;|;;", ";..;");
  82. expr = RegExp.Replace(expr, @";$|'?\]|'$", string.Empty);
  83. expr = RegExp.Replace(expr, @"#([0-9]+)", m =>
  84. {
  85. var index = int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture);
  86. return subx[index];
  87. });
  88. return expr;
  89. }
  90. public static string AsBracketNotation(string[] indicies)
  91. {
  92. if (indicies == null)
  93. throw new ArgumentNullException(nameof(indicies));
  94. var sb = new StringBuilder();
  95. foreach (var index in indicies)
  96. {
  97. if (sb.Length == 0)
  98. {
  99. sb.Append('$');
  100. }
  101. else
  102. {
  103. sb.Append('[');
  104. if (RegExp.IsMatch(index, @"^[0-9*]+$"))
  105. sb.Append(index);
  106. else
  107. sb.Append('\'').Append(index).Append('\'');
  108. sb.Append(']');
  109. }
  110. }
  111. return sb.ToString();
  112. }
  113. static int? TryParseInt(string str) =>
  114. int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n)
  115. ? n : (int?)null;
  116. sealed class Interpreter
  117. {
  118. readonly Func<string, object, string, object> _eval;
  119. readonly IJsonPathValueSystem _system;
  120. static readonly IJsonPathValueSystem DefaultValueSystem = new BasicValueSystem();
  121. static readonly char[] Colon = { ':' };
  122. static readonly char[] Semicolon = { ';' };
  123. delegate void WalkCallback(object member, string loc, string expr, object value, string path);
  124. public Interpreter(IJsonPathValueSystem valueSystem, Func<string, object, string, object> eval)
  125. {
  126. _eval = eval ?? delegate
  127. {
  128. // @ symbol in expr must be interpreted specially to resolve
  129. // to value. In JavaScript, the implementation would look
  130. // like:
  131. //
  132. // return obj && value && eval(expr.replace(/@/g, "value"));
  133. return null;
  134. };
  135. _system = valueSystem ?? DefaultValueSystem;
  136. }
  137. sealed class TraceArgs
  138. {
  139. public readonly string Expr;
  140. public readonly object Value;
  141. public readonly string Path;
  142. public TraceArgs(string expr, object value, string path)
  143. {
  144. Expr = expr;
  145. Value = value;
  146. Path = path;
  147. }
  148. }
  149. public IEnumerable<T> Trace<T>(string expr, object value, string path, Func<object, string[], T> resultor) =>
  150. Trace(Args(expr, value, path), resultor);
  151. static TraceArgs Args(string expr, object value, string path) =>
  152. new TraceArgs(expr, value, path);
  153. IEnumerable<T> Trace<T>(TraceArgs args, Func<object, string[], T> resultor)
  154. {
  155. var stack = new Stack<TraceArgs>();
  156. stack.Push(args);
  157. while (stack.Count > 0)
  158. {
  159. var popped = stack.Pop();
  160. var expr = popped.Expr;
  161. var value = popped.Value;
  162. var path = popped.Path;
  163. if (string.IsNullOrEmpty(expr))
  164. {
  165. if (path != null)
  166. yield return resultor(value, path.Split(Semicolon));
  167. continue;
  168. }
  169. var i = expr.IndexOf(';');
  170. var atom = i >= 0 ? expr.Substring(0, i) : expr;
  171. var tail = i >= 0 ? expr.Substring(i + 1) : string.Empty;
  172. if (value != null && _system.HasMember(value, atom))
  173. {
  174. stack.Push(Args(tail, Index(value, atom), path + ";" + atom));
  175. }
  176. else if (atom == "*")
  177. {
  178. Walk(atom, tail, value, path, (m, l, x, v, p) => stack.Push(Args(m + ";" + x, v, p)));
  179. }
  180. else if (atom == "..")
  181. {
  182. Walk(atom, tail, value, path, (m, l, x, v, p) =>
  183. {
  184. var result = Index(v, m.ToString());
  185. if (result != null && !_system.IsPrimitive(result))
  186. stack.Push(Args("..;" + x, result, p + ";" + m));
  187. });
  188. stack.Push(Args(tail, value, path));
  189. }
  190. else if (atom.Length > 2 && atom[0] == '(' && atom[atom.Length - 1] == ')') // [(exp)]
  191. {
  192. stack.Push(Args(_eval(atom, value, path.Substring(path.LastIndexOf(';') + 1)) + ";" + tail, value, path));
  193. }
  194. else if (atom.Length > 3 && atom[0] == '?' && atom[1] == '(' && atom[atom.Length - 1] == ')') // [?(exp)]
  195. {
  196. Walk(atom, tail, value, path, (m, l, x, v, p) =>
  197. {
  198. var result = _eval(RegExp.Replace(l, @"^\?\((.*?)\)$", "$1"),
  199. Index(v, m.ToString()), m.ToString());
  200. if (Convert.ToBoolean(result, CultureInfo.InvariantCulture))
  201. stack.Push(Args(m + ";" + x, v, p));
  202. });
  203. }
  204. else if (RegExp.IsMatch(atom, @"^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$")) // [start:end:step] Phyton slice syntax
  205. {
  206. foreach (var a in Slice(atom, tail, value, path).Reverse())
  207. stack.Push(a);
  208. }
  209. else if (atom.IndexOf(',') >= 0) // [name1,name2,...]
  210. {
  211. foreach (var part in RegExp.Split(atom, @"'?,'?").Reverse())
  212. stack.Push(Args(part + ";" + tail, value, path));
  213. }
  214. }
  215. }
  216. void Walk(string loc, string expr, object value, string path, WalkCallback callback)
  217. {
  218. if (_system.IsPrimitive(value))
  219. return;
  220. if (_system.IsArray(value))
  221. {
  222. var list = (IList)value;
  223. for (var i = list.Count - 1; i >= 0; i--)
  224. callback(i, loc, expr, value, path);
  225. }
  226. else if (_system.IsObject(value))
  227. {
  228. foreach (var key in _system.GetMembers(value).Reverse())
  229. callback(key, loc, expr, value, path);
  230. }
  231. }
  232. static IEnumerable<TraceArgs> Slice(string loc, string expr, object value, string path)
  233. {
  234. if (!(value is IList list))
  235. yield break;
  236. var length = list.Count;
  237. var parts = loc.Split(Colon);
  238. var start = TryParseInt(parts[0]) ?? 0;
  239. var end = TryParseInt(parts[1]) ?? list.Count;
  240. var step = parts.Length > 2 ? TryParseInt(parts[2]) ?? 1 : 1;
  241. start = (start < 0) ? Math.Max(0, start + length) : Math.Min(length, start);
  242. end = (end < 0) ? Math.Max(0, end + length) : Math.Min(length, end);
  243. for (var i = start; i < end; i += step)
  244. yield return Args(i + ";" + expr, value, path);
  245. }
  246. object Index(object obj, string member) =>
  247. _system.GetMemberValue(obj, member);
  248. }
  249. static class RegExp
  250. {
  251. const RegexOptions Options = RegexOptions.ECMAScript;
  252. public static bool IsMatch(string input, string pattern) =>
  253. Regex.IsMatch(input, pattern, Options);
  254. public static string Replace(string input, string pattern, string replacement) =>
  255. Regex.Replace(input, pattern, replacement, Options);
  256. public static string Replace(string input, string pattern, MatchEvaluator evaluator) =>
  257. Regex.Replace(input, pattern, evaluator, Options);
  258. public static IEnumerable<string> Split(string input, string pattern) =>
  259. Regex.Split(input, pattern, Options);
  260. }
  261. sealed class BasicValueSystem : IJsonPathValueSystem
  262. {
  263. public bool HasMember(object value, string member) =>
  264. !IsPrimitive(value)
  265. && (value is IDictionary dict
  266. ? dict.Contains(member)
  267. : value is IList list
  268. && TryParseInt(member) is int i && i >= 0 && i < list.Count);
  269. public object GetMemberValue(object value, string member)
  270. => IsPrimitive(value) ? throw new ArgumentException(null, nameof(value))
  271. : value is IDictionary dict ? dict[member]
  272. : !(value is IList list) ? throw new ArgumentException(nameof(value))
  273. : TryParseInt(member) is int i && i >= 0 && i < list.Count ? list[i]
  274. : null;
  275. public IEnumerable<string> GetMembers(object value) =>
  276. ((IDictionary)value).Keys.Cast<string>();
  277. public bool IsObject(object value) => value is IDictionary;
  278. public bool IsArray(object value) => value is IList;
  279. public bool IsPrimitive(object value) =>
  280. value == null
  281. ? throw new ArgumentNullException(nameof(value))
  282. : Type.GetTypeCode(value.GetType()) != TypeCode.Object;
  283. }
  284. }
  285. }