From ffe8a3ba11e6096223174d076c82305653fc8b11 Mon Sep 17 00:00:00 2001 From: John Lewin Date: Fri, 8 Jun 2018 08:41:15 -0700 Subject: [PATCH] Components prototype --- DesignTools/Primitives/ComponentObject3D.cs | 48 ++ DesignTools/Primitives/CylinderObject3D.cs | 3 - DesignTools/PublicPropertyEditor.cs | 11 +- MatterControl.csproj | 2 + PartPreviewWindow/SelectedObjectPanel.cs | 92 +++- Utilities/JsonPath.cs | 577 ++++++++++++++++++++ 6 files changed, 706 insertions(+), 27 deletions(-) create mode 100644 DesignTools/Primitives/ComponentObject3D.cs create mode 100644 Utilities/JsonPath.cs diff --git a/DesignTools/Primitives/ComponentObject3D.cs b/DesignTools/Primitives/ComponentObject3D.cs new file mode 100644 index 000000000..6d0f83b07 --- /dev/null +++ b/DesignTools/Primitives/ComponentObject3D.cs @@ -0,0 +1,48 @@ +/* +Copyright (c) 2018, Lars Brubaker, John Lewin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +using System.Collections.Generic; +using MatterHackers.DataConverters3D; + +namespace MatterHackers.MatterControl.DesignTools +{ + public class ComponentObject3D : Object3D + { + public ComponentObject3D() + { + } + + public ComponentObject3D(IEnumerable children) + : base (children) + { + } + + public List SurfacedEditors { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/DesignTools/Primitives/CylinderObject3D.cs b/DesignTools/Primitives/CylinderObject3D.cs index fd0581656..5890397c0 100644 --- a/DesignTools/Primitives/CylinderObject3D.cs +++ b/DesignTools/Primitives/CylinderObject3D.cs @@ -27,14 +27,11 @@ of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ -using System.Threading; -using MatterHackers.Agg; using MatterHackers.Agg.UI; using MatterHackers.Agg.VertexSource; using MatterHackers.DataConverters3D; using MatterHackers.Localizations; using MatterHackers.MatterControl.DesignTools.Operations; -using MatterHackers.PolygonMesh; using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.DesignTools diff --git a/DesignTools/PublicPropertyEditor.cs b/DesignTools/PublicPropertyEditor.cs index 51b021b3b..d6997237c 100644 --- a/DesignTools/PublicPropertyEditor.cs +++ b/DesignTools/PublicPropertyEditor.cs @@ -51,11 +51,14 @@ namespace MatterHackers.MatterControl.DesignTools public class EditableProperty { public IObject3D Item { get; private set; } + + public object source; public PropertyInfo PropertyInfo { get; private set; } - public EditableProperty(PropertyInfo p, IObject3D item) + public EditableProperty(PropertyInfo p, object source) { - this.Item = item; + this.source = source; + this.Item = source as IObject3D; this.PropertyInfo = p; } @@ -71,7 +74,7 @@ namespace MatterHackers.MatterControl.DesignTools return nameAttribute?.DisplayName ?? prop.Name.SplitCamelCase(); } - public object Value => PropertyInfo.GetGetMethod().Invoke(Item, null); + public object Value => PropertyInfo.GetGetMethod().Invoke(source, null); /// /// Use reflection to set property value @@ -79,7 +82,7 @@ namespace MatterHackers.MatterControl.DesignTools /// public void SetValue(object value) { - this.PropertyInfo.GetSetMethod().Invoke(this.Item, new Object[] { value }); + this.PropertyInfo.GetSetMethod().Invoke(source, new Object[] { value }); } public string DisplayName => GetDisplayName(PropertyInfo); diff --git a/MatterControl.csproj b/MatterControl.csproj index 749b92544..19c158dee 100644 --- a/MatterControl.csproj +++ b/MatterControl.csproj @@ -100,6 +100,7 @@ + @@ -425,6 +426,7 @@ + diff --git a/PartPreviewWindow/SelectedObjectPanel.cs b/PartPreviewWindow/SelectedObjectPanel.cs index 48cdbfc66..740123442 100644 --- a/PartPreviewWindow/SelectedObjectPanel.cs +++ b/PartPreviewWindow/SelectedObjectPanel.cs @@ -30,6 +30,8 @@ either expressed or implied, of the FreeBSD Project. using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using JsonPath; using MatterHackers.Agg; using MatterHackers.Agg.Platform; using MatterHackers.Agg.UI; @@ -39,6 +41,7 @@ using MatterHackers.MatterControl.CustomWidgets; using MatterHackers.MatterControl.DesignTools; using MatterHackers.MatterControl.Library; using MatterHackers.VectorMath; +using static JsonPath.JsonPathContext.ReflectionValueSystem; namespace MatterHackers.MatterControl.PartPreviewWindow { @@ -280,9 +283,10 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public GuiWidget ContentPanel { get; set; } + JsonPathContext xpathLikeResolver = new JsonPathContext(); + public void SetActiveItem(IObject3D selectedItem) { - var selectedItemType = selectedItem.GetType(); editButton.Enabled = (selectedItem.Children.Count > 0); @@ -295,7 +299,54 @@ namespace MatterHackers.MatterControl.PartPreviewWindow var activeEditors = new List<(IObject3DEditor, IObject3D, string)>(); - foreach (var child in new[] { selectedItem }) + var editableItems = new List { selectedItem }; + + var undoBuffer = sceneContext.Scene.UndoBuffer; + + editorPanel.CloseAllChildren(); + + bool allowOperations = true; + + if (selectedItem is ComponentObject3D componentObject) + { + allowOperations = false; + + foreach (var selector in componentObject.SurfacedEditors) + { + // Get the named property via reflection + // Selector example: '$.Children' + var match = xpathLikeResolver.Select(componentObject, selector).ToList(); + + //// TODO: Create editor row for each property + //// - Use the type of the property to find a matching editor (ideally all datatype -> editor functionality would resolve consistently) + //// - Add editor row for each + + foreach (var instance in match) + { + if (instance is IObject3D object3D) + { + editableItems.Add(object3D); + } + else if (JsonPath.JsonPathContext.ReflectionValueSystem.LastMemberValue is ReflectionTarget reflectionTarget) + { + var context = new PPEContext() + { + item = item + }; + + var editableProperty = new EditableProperty(reflectionTarget.PropertyInfo, reflectionTarget.Source); + + var editor = PublicPropertyEditor.CreatePropertyEditor(editableProperty, undoBuffer, context, theme); + if (editor != null) + { + editorPanel.AddChild(editor); + } + } + } + } + } + + foreach (var child in editableItems) { if (ApplicationController.Instance.GetEditorsForType(child.GetType())?.FirstOrDefault() is IObject3DEditor editor) { @@ -303,7 +354,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } - ShowObjectEditor(activeEditors, selectedItem); + ShowObjectEditor(activeEditors, selectedItem, allowOperations: allowOperations); } private class OperationButton :TextButton @@ -324,10 +375,8 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } - private void ShowObjectEditor(IEnumerable<(IObject3DEditor editor, IObject3D item, string displayName)> scope, IObject3D rootSelection) + private void ShowObjectEditor(IEnumerable<(IObject3DEditor editor, IObject3D item, string displayName)> scope, IObject3D rootSelection, bool allowOperations = true) { - editorPanel.CloseAllChildren(); - if (scope == null) { return; @@ -366,25 +415,28 @@ namespace MatterHackers.MatterControl.PartPreviewWindow var buttons = new List(); - foreach (var nodeOperation in ApplicationController.Instance.Graph.Operations) + if (allowOperations) { - foreach (var type in nodeOperation.MappedTypes) + foreach (var nodeOperation in ApplicationController.Instance.Graph.Operations) { - if (type.IsAssignableFrom(selectedItemType) - && (nodeOperation.IsVisible == null || nodeOperation.IsVisible(selectedItem))) + foreach (var type in nodeOperation.MappedTypes) { - var button = new OperationButton(nodeOperation, selectedItem, theme) + if (type.IsAssignableFrom(selectedItemType) + && (nodeOperation.IsVisible == null || nodeOperation.IsVisible(selectedItem))) { - BackgroundColor = theme.MinimalShade, - Margin = theme.ButtonSpacing - }; - button.EnsureAvailablity(); - button.Click += (s, e) => - { - nodeOperation.Operation(selectedItem, sceneContext.Scene).ConfigureAwait(false); - }; + var button = new OperationButton(nodeOperation, selectedItem, theme) + { + BackgroundColor = theme.MinimalShade, + Margin = theme.ButtonSpacing + }; + button.EnsureAvailablity(); + button.Click += (s, e) => + { + nodeOperation.Operation(selectedItem, sceneContext.Scene).ConfigureAwait(false); + }; - buttons.Add(button); + buttons.Add(button); + } } } } diff --git a/Utilities/JsonPath.cs b/Utilities/JsonPath.cs new file mode 100644 index 000000000..c8b21696d --- /dev/null +++ b/Utilities/JsonPath.cs @@ -0,0 +1,577 @@ +#region Copyright (c) 2007 Atif Aziz. All rights reserved. +// https://github.com/atifaziz/JSONPath +// +// 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.Reflection; + using System.Text; + using System.Text.RegularExpressions; + using MatterHackers.DataConverters3D; + using Newtonsoft.Json.Linq; + + #endregion + + public 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); + } + + public sealed 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 ReflectionValueSystem(); // 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); + } + + public class ReflectionDetails + { + public List Properties { get; set; } + } + + public class ReflectionValueSystem : IJsonPathValueSystem + { + //private static Dictionary members = new Dictionary(); + + public const BindingFlags PublicInstanceProperties = BindingFlags.Public | BindingFlags.Instance; + + public static ReflectionDetails GetMember(object item, string path) + { + //if (members.TryGetValue(path, out ReflectionDetails memberInfo)) + //{ + // return memberInfo; + //} + + var reflectionDetails = new ReflectionDetails() + { + Properties = item.GetType().GetProperties(PublicInstanceProperties).Where(pi => pi.GetGetMethod() != null && pi.GetSetMethod() != null).ToList() + }; + +// members[path] = reflectionDetails; + + return reflectionDetails; + } + + public class ReflectionTarget + { + public PropertyInfo PropertyInfo { get; } + public object Source { get; } + + public ReflectionTarget(PropertyInfo propertyInfo, object source) + { + this.PropertyInfo = propertyInfo; + this.Source = source; + } + } + + public bool HasMember(object value, string member) + { + if (!IsPrimitive(value) + && value is IDictionary dict) + { + return dict.Contains(member); + } + + if (value is IList list) + { + return TryParseInt(member) is int i + && i >= 0 + && i < list.Count; + } + + string typeFilter; + + // Separate member and typeFilter from member field + (member, typeFilter) = StripTypeFilter(member); + + // IEnumerable field must be iterated to check + if (value is IEnumerable enumerable) + { + if (TryParseInt(member) is int) + { + return true; + } + + // Handle the typeFilter case + foreach (var n in enumerable) + { + if (n.GetType().Name == typeFilter) + { + return true; + } + } + } + + // TODO: Inline once troubleshooting is complete + var hasMember = GetMember(value, member).Properties.Any(p => p.Name == member); + return hasMember; + } + + public static ReflectionTarget LastMemberValue { get; private set; } + + public object GetMemberValue(object value, string member) + { + // Find and invoke property to get value + LastMemberValue = new ReflectionTarget( + GetMember(value, member).Properties.Where(p => p.Name == member).FirstOrDefault(), + value); + + if (IsPrimitive(value)) + { + throw new ArgumentException(null, nameof(value)); + } + + if (value is IDictionary dict) + { + return dict[member]; + } + + if (value is IList list + && TryParseInt(member) is int i + && i >= 0 + && i < list.Count) + { + return list[i]; + } + + string typeFilter; + + // Separate member and typeFilter from member field + (member, typeFilter) = StripTypeFilter(member); + + var propertyInfo = GetMember(value, member).Properties.Where(p => p.Name == member).FirstOrDefault(); + + LastMemberValue = new ReflectionTarget(propertyInfo, value); + + if (value is IEnumerable enumerable) + { + if (TryParseInt(member) is int k) + { + var v = 0; + foreach (var n in enumerable) + { + if (v++ == k) + { + return n; + } + } + } + + foreach (var n in enumerable) + { + if (n.GetType().Name == typeFilter) + { + return n; + } + } + } + + var propertyValue = propertyInfo.GetGetMethod().Invoke(value, null); + + if (!string.IsNullOrEmpty(typeFilter) && propertyValue is IEnumerable items) + { + foreach(var item in items) + { + if (item.GetType().Name == typeFilter) + { + return item; + } + } + } + + return propertyValue; + } + + 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; + + private static (string member, string filter) StripTypeFilter(string member) + { + int startFilter = member.IndexOf('<'); + int endFilter = member.IndexOf('>'); + int length = endFilter - startFilter; + string typeFilter = ""; + + if (startFilter != -1 && endFilter != -1 && length > 1) + { + typeFilter = member.Substring(startFilter + 1, length - 1); + + member = member.Substring(0, startFilter); + } + + return (member, typeFilter); + } + + } + + sealed class BasicValueSystem : IJsonPathValueSystem + { + public bool HasMember(object value, string member) + { + if (!IsPrimitive(value) + && value is IDictionary dict) + { + return dict.Contains(member); + } + + if (value is JArray array) + { + return TryParseInt(member) is int j + && j >= 0 + && j < array.Count; + } + + if (value is JToken token) + { + return token[member] != null; + } + + return value is IList list + && TryParseInt(member) is int i + && i >= 0 + && i < list.Count; + } + + public object GetMemberValue(object value, string member) + { + if (IsPrimitive(value)) + { + throw new ArgumentException(null, nameof(value)); + } + + if (value is JArray array + && TryParseInt(member) is int j + && j >= 0 + && j < array.Count) + { + return array[j]; + } + + if (value is JToken token) + { + return token[member]; + } + + if (value is IDictionary dict) + { + return dict[member]; + } + + if (!(value is IList list)) + { + throw new ArgumentException(nameof(value)); + } + + if (TryParseInt(member) is int i + && i >= 0 + && i < list.Count) + { + return list[i]; + } + + return 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; + } + } +}