/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
//This is nancyfx's dynamicdictionary
//it is slightly modified to add the ability to chain dynamic property access of arbitrary depth
//without binding on null ref errors in between.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text.RegularExpressions;
using OpenSearch.Net.Utf8Json;
// ReSharper disable ArrangeMethodOrOperatorBody
// ReSharper disable RemoveRedundantBraces
// ReSharper disable ArrangeAccessorOwnerBody
namespace OpenSearch.Net
{
///
/// A dictionary that supports dynamic access.
///
[JsonFormatter(typeof(DynamicDictionaryFormatter))]
public class DynamicDictionary
: DynamicObject,
IEquatable,
IEnumerable,
IDictionary
{
private readonly IDictionary _backingDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
///
/// Gets the number of elements contained in the .
///
/// The number of elements contained in the .
public int Count
{
get { return _backingDictionary.Count; }
}
///
/// Creates a new instance of Dictionary{String,Object} using the keys and underlying object values of this DynamicDictionary instance's key values.
///
///
public Dictionary ToDictionary() => _backingDictionary.ToDictionary(kv => kv.Key, kv => kv.Value.Value);
///
/// Returns an empty dynamic dictionary.
///
/// A instance.
public static DynamicDictionary Empty => new DynamicDictionary();
///
/// Gets a value indicating whether the is read-only.
///
/// Always returns .
public bool IsReadOnly
{
get { return false; }
}
private static Regex SplitRegex = new Regex(@"(?
/// Traverses data using path notation.
/// e.g some.deep.nested.json.path
///
/// A special lookup is available for ANY key _arbitrary_key_ e.g some.deep._arbitrary_key_.json.path which will traverse into the first key
/// If _arbitrary_key_ is the last value it will return the key name
///
///
/// path into the stored object, keys are separated with a dot and the last key is returned as T
///
/// T or default
public T Get(string path)
{
if (path == null) return default;
var split = SplitRegex.Split(path);
var queue = new Queue(split);
if (queue.Count == 0) return default;
var d = new DynamicValue(_backingDictionary);
while (queue.Count > 0)
{
var key = queue.Dequeue().Replace(@"\.", ".");
if (key == "_arbitrary_key_")
{
if (queue.Count > 0) d = d[0];
else
{
var v = d?.ToDictionary()?.Keys?.FirstOrDefault();
d = v != null ? new DynamicValue(v) : DynamicValue.NullValue;
}
}
else if (int.TryParse(key, out var i)) d = d[i];
else d = d[key];
}
return d.TryParse();
}
///
/// Gets or sets the with the specified name.
///
/// A instance containing a value.
public DynamicValue this[string name]
{
get
{
name = GetNeutralKey(name);
if (!_backingDictionary.TryGetValue(name, out var member))
{
member = new DynamicValue(null);
}
return member;
}
set
{
name = GetNeutralKey(name);
_backingDictionary[name] = value is DynamicValue ? value : new DynamicValue(value);
}
}
///
/// Gets an containing the keys of the .
///
/// An containing the keys of the .
public ICollection Keys => _backingDictionary.Keys;
///
/// Gets an containing the values in the .
///
/// An containing the values in the .
public ICollection Values
{
get { return _backingDictionary.Values; }
}
///
/// Adds an item to the .
///
/// The object to add to the .
public void Add(KeyValuePair item)
{
this[item.Key] = item.Value;
}
///
/// Removes all items from the .
///
public void Clear() => _backingDictionary.Clear();
///
/// Determines whether the contains a specific value.
///
///
/// if is found in the ; otherwise, .
///
/// The object to locate in the .
public bool Contains(KeyValuePair item)
{
return _backingDictionary.Contains(item);
}
///
/// Copies the elements of the to an , starting at a particular
/// index.
///
///
/// The one-dimensional that is the destination of the elements copied from the
/// . The must have zero-based indexing.
///
/// The zero-based index in at which copying begins.
public void CopyTo(KeyValuePair[] array, int arrayIndex)
{
_backingDictionary.CopyTo(array, arrayIndex);
}
///
/// Removes the first occurrence of a specific object from the .
///
///
/// if was successfully removed from the ; otherwise,
/// .
///
/// The object to remove from the .
public bool Remove(KeyValuePair item)
{
return _backingDictionary.Remove(item);
}
///
/// Adds an element with the provided key and value to the .
///
/// The object to use as the key of the element to add.
/// The object to use as the value of the element to add.
public void Add(string key, DynamicValue value)
{
this[key] = value;
}
///
/// Determines whether the contains an element with the specified key.
///
///
/// if the contains an element with the key; otherwise, .
///
/// The key to locate in the .
public bool ContainsKey(string key)
{
return _backingDictionary.ContainsKey(key);
}
///
/// Removes the element with the specified key from the .
///
/// if the element is successfully removed; otherwise, .
/// The key of the element to remove.
public bool Remove(string key)
{
key = GetNeutralKey(key);
return _backingDictionary.Remove(key);
}
///
/// Gets the value associated with the specified key.
///
///
/// if the contains an element with the specified key; otherwise,
/// .
///
/// The key whose value to get.
///
/// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default
/// value for the type of the parameter. This parameter is passed uninitialized.
///
public bool TryGetValue(string key, out DynamicValue value)
{
if (_backingDictionary.TryGetValue(key, out value)) return true;
return false;
}
///
/// Returns the enumeration of all dynamic member names.
///
/// A that contains dynamic member names.
IEnumerator IEnumerable.GetEnumerator()
{
return _backingDictionary.Keys.GetEnumerator();
}
///
/// Returns an enumerator that iterates through the collection.
///
/// A that can be used to iterate through the collection.
IEnumerator> IEnumerable>.GetEnumerator()
{
return _backingDictionary.GetEnumerator();
}
///
/// Backwards compatible access to all the KeyValue pairs, in the next release you will be able to foreach directly
///
public IEnumerable> GetKeyValues()
{
return _backingDictionary;
}
///
/// Returns the enumeration of all dynamic member names.
///
/// A that contains dynamic member names.
public IEnumerator GetEnumerator()
{
return _backingDictionary.Keys.GetEnumerator();
}
///
/// Indicates whether the current is equal to another object of the same type.
///
///
/// if the current instance is equal to the parameter; otherwise,
/// .
///
/// An instance to compare with this instance.
public bool Equals(DynamicDictionary other)
{
if (ReferenceEquals(null, other))
{
return false;
}
return ReferenceEquals(this, other) || Equals(other._backingDictionary, _backingDictionary);
}
///
/// Creates a dynamic dictionary from an instance.
///
/// An instance, that the dynamic dictionary should be created from.
/// An instance.
public static DynamicDictionary Create(IDictionary values)
{
var instance = new DynamicDictionary();
foreach (var key in values.Keys)
{
var v = values[key];
instance[key] = v is DynamicValue av ? av : new DynamicValue(v);
}
return instance;
}
///
/// Provides the implementation for operations that set member values. Classes derived from the
/// class can override this method to specify dynamic behavior for operations such as setting a value for a property.
///
///
/// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language
/// determines the behavior. (In most cases, a language-specific run-time exception is thrown.)
///
///
/// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of
/// the member to which the value is being assigned. For example, for the statement sampleObject.SampleProperty = "Test", where sampleObject is
/// an instance of the class derived from the class, binder.Name returns "SampleProperty". The
/// binder.IgnoreCase property specifies whether the member name is case-sensitive.
///
///
/// The value to set to the member. For example, for sampleObject.SampleProperty = "Test", where sampleObject is an
/// instance of the class derived from the class, the is "Test".
///
public override bool TrySetMember(SetMemberBinder binder, object value)
{
this[binder.Name] = new DynamicValue(value);
return true;
}
///
/// Provides the implementation for operations that get member values. Classes derived from the
/// class can override this method to specify dynamic behavior for operations such as getting a value for a property.
///
///
/// true if the operation is successful; otherwise, false. If this method returns false, the run-time binder of the language
/// determines the behavior. (In most cases, a run-time exception is thrown.)
///
///
/// Provides information about the object that called the dynamic operation. The binder.Name property provides the name of
/// the member on which the dynamic operation is performed. For example, for the Console.WriteLine(sampleObject.SampleProperty) statement,
/// where sampleObject is an instance of the class derived from the class, binder.Name returns
/// "SampleProperty". The binder.IgnoreCase property specifies whether the member name is case-sensitive.
///
///
/// The result of the get operation. For example, if the method is called for a property, you can assign the property
/// value to .
///
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (!_backingDictionary.TryGetValue(binder.Name, out var v))
{
result = new DynamicValue(null);
}
else result = v;
return true;
}
///
/// Returns the enumeration of all dynamic member names.
///
/// A that contains dynamic member names.
public override IEnumerable GetDynamicMemberNames()
{
return _backingDictionary.Keys;
}
///
/// Determines whether the specified is equal to this instance.
///
/// The to compare with this instance.
///
/// if the specified is equal to this instance; otherwise,
/// .
///
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
return obj.GetType() == typeof(DynamicDictionary) && Equals((DynamicDictionary)obj);
}
///
/// Returns a hash code for this .
///
/// A hash code for this , suitable for use in hashing algorithms and data structures like a hash table.
public override int GetHashCode() => _backingDictionary?.GetHashCode() ?? 0;
private static KeyValuePair GetDynamicKeyValuePair(KeyValuePair item)
{
var dynamicValueKeyValuePair =
new KeyValuePair(item.Key, item.Value);
return dynamicValueKeyValuePair;
}
private static string GetNeutralKey(string key)
{
return key;
}
}
}