From 0f77d552279a619e33efdb7479ca074fe78ff868 Mon Sep 17 00:00:00 2001 From: Tom Gillbe Date: Sat, 7 Nov 2020 16:04:45 +0000 Subject: [PATCH] Fix the order of YamlMappingNode items --- .../Helpers/OrderedDictionaryTests.cs | 109 +++++++++ YamlDotNet/Helpers/IOrderedDictionary.cs | 55 +++++ YamlDotNet/Helpers/OrderedDictionary.cs | 222 ++++++++++++++++++ .../RepresentationModel/YamlMappingNode.cs | 6 +- 4 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 YamlDotNet.Test/Helpers/OrderedDictionaryTests.cs create mode 100644 YamlDotNet/Helpers/IOrderedDictionary.cs create mode 100644 YamlDotNet/Helpers/OrderedDictionary.cs diff --git a/YamlDotNet.Test/Helpers/OrderedDictionaryTests.cs b/YamlDotNet.Test/Helpers/OrderedDictionaryTests.cs new file mode 100644 index 000000000..0dc854d81 --- /dev/null +++ b/YamlDotNet.Test/Helpers/OrderedDictionaryTests.cs @@ -0,0 +1,109 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors + +// 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. + +using System.Collections.Generic; +using System.Linq; +using Xunit; +using YamlDotNet.Helpers; + +namespace YamlDotNet.Test.Helpers +{ + public class OrderedDictionaryTests + { + [Fact] + public void OrderOfElementsIsMainted() + { + var dict = (IDictionary)new OrderedDictionary + { + { 3, "First" }, + { 2, "Temporary" }, + { 1, "Second" }, + }; + dict.Remove(2); + dict.Add(4, "Inserted"); + dict[4] = "Third"; + + Assert.Equal(3, dict.Count); + Assert.Equal(new KeyValuePair(3, "First"), dict.First()); + Assert.Equal(new KeyValuePair(1, "Second"), dict.Skip(1).First()); + Assert.Equal(new KeyValuePair(4, "Third"), dict.Skip(2).First()); + Assert.Equal(new[] { 3, 1, 4 }, dict.Keys.ToArray()); + Assert.Equal(new[] { "First", "Second", "Third" }, dict.Values.ToArray()); + } + + [Fact] + public void KeysContainsWorks() + { + var dict = new OrderedDictionary + { + { 3, "First item" }, + { 2, "Second item" }, + { 1, "Third item" }, + }; + + Assert.False(dict.Keys.Contains(0)); + Assert.True(dict.Keys.Contains(1)); + Assert.True(dict.Keys.Contains(2)); + Assert.True(dict.Keys.Contains(3)); + Assert.False(dict.Keys.Contains(4)); + } + + [Fact] + public void ValuesContainsWorks() + { + var dict = new OrderedDictionary + { + { 3, "First item" }, + { 2, "Second item" }, + { 1, "Third item" }, + }; + + Assert.False(dict.Values.Contains(null)); + Assert.True(dict.Values.Contains("First item")); + Assert.True(dict.Values.Contains("Second item")); + Assert.True(dict.Values.Contains("Third item")); + Assert.False(dict.Values.Contains("Fourth item")); + } + + [Fact] + public void CanInsertAndRemoveAtIndex() + { + var dict = new OrderedDictionary + { + { 3, "First" }, + { 2, "Temporary" }, + { 1, "Second" }, + }; + dict.RemoveAt(1); + dict.Insert(0, 4, "Zero"); + + Assert.Equal(3, dict.Count); + Assert.Equal(new KeyValuePair(4, "Zero"), dict.First()); + Assert.Equal(new KeyValuePair(3, "First"), dict.Skip(1).First()); + Assert.Equal(new KeyValuePair(1, "Second"), dict.Skip(2).First()); + Assert.Equal(new KeyValuePair(4, "Zero"), dict[0]); + Assert.Equal(new KeyValuePair(3, "First"), dict[1]); + Assert.Equal(new KeyValuePair(1, "Second"), dict[2]); + Assert.Equal(new[] { 4, 3, 1 }, dict.Keys.ToArray()); + Assert.Equal(new[] { "Zero", "First", "Second" }, dict.Values.ToArray()); + } + } +} diff --git a/YamlDotNet/Helpers/IOrderedDictionary.cs b/YamlDotNet/Helpers/IOrderedDictionary.cs new file mode 100644 index 000000000..611518e19 --- /dev/null +++ b/YamlDotNet/Helpers/IOrderedDictionary.cs @@ -0,0 +1,55 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors + +// 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. + +using System.Collections.Generic; + +namespace YamlDotNet.Helpers +{ + public interface IOrderedDictionary : IDictionary + where TKey : notnull + { + /// + /// Gets or sets the element with the specified index. + /// + /// The index of the element to get or set. + /// The element with the specified index. + KeyValuePair this[int index] + { + get; + set; + } + + /// + /// Adds an element with the provided key and value to the + /// at the given index. + /// + /// The zero-based index at which the item should be inserted. + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + void Insert(int index, TKey key, TValue value); + + /// + /// Removes the element at the specified index. + /// + /// The zero-based index of the element to remove. + void RemoveAt(int index); + } +} diff --git a/YamlDotNet/Helpers/OrderedDictionary.cs b/YamlDotNet/Helpers/OrderedDictionary.cs new file mode 100644 index 000000000..eacf59c71 --- /dev/null +++ b/YamlDotNet/Helpers/OrderedDictionary.cs @@ -0,0 +1,222 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors + +// 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. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.Serialization; + +namespace YamlDotNet.Helpers +{ + [Serializable] + internal class OrderedDictionary : IOrderedDictionary + where TKey : notnull + { + [NonSerialized] + private Dictionary dictionary; + private readonly List> list; + private readonly IEqualityComparer comparer; + + public TValue this[TKey key] + { + get => dictionary[key]; + set + { + if (dictionary.ContainsKey(key)) + { + var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); + dictionary[key] = value; + list[index] = new KeyValuePair(key, value); + } + else + { + Add(key, value); + } + } + } + + public ICollection Keys => new KeyCollection(this); + + public ICollection Values => new ValueCollection(this); + + public int Count => dictionary.Count; + + public bool IsReadOnly => false; + + public KeyValuePair this[int index] + { + get => list[index]; + set => list[index] = value; + } + + public OrderedDictionary() : this(EqualityComparer.Default) + { + } + + public OrderedDictionary(IEqualityComparer comparer) + { + list = new List>(); + dictionary = new Dictionary(comparer); + this.comparer = comparer; + } + + public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); + + public void Add(KeyValuePair item) + { + dictionary.Add(item.Key, item.Value); + list.Add(item); + } + + public void Clear() + { + dictionary.Clear(); + list.Clear(); + } + + public bool Contains(KeyValuePair item) => dictionary.Contains(item); + + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => + list.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => list.GetEnumerator(); + + public void Insert(int index, TKey key, TValue value) + { + dictionary.Add(key, value); + list.Insert(index, new KeyValuePair(key, value)); + } + + public bool Remove(TKey key) + { + if (dictionary.ContainsKey(key)) + { + var index = list.FindIndex(kvp => comparer.Equals(kvp.Key, key)); + list.RemoveAt(index); + if (!dictionary.Remove(key)) + { + throw new InvalidOperationException(); + } + return true; + } + else + { + return false; + } + } + + public bool Remove(KeyValuePair item) => Remove(item.Key); + + public void RemoveAt(int index) + { + var key = list[index].Key; + dictionary.Remove(key); + list.RemoveAt(index); + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => + dictionary.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); + + + [OnDeserialized] + internal void OnDeserializedMethod(StreamingContext context) + { + // Reconstruct the dictionary from the serialized list + dictionary = new Dictionary(); + foreach (var kvp in list) + { + dictionary[kvp.Key] = kvp.Value; + } + } + + private class KeyCollection : ICollection + { + private readonly OrderedDictionary orderedDictionary; + + public int Count => orderedDictionary.list.Count; + + public bool IsReadOnly => true; + + public void Add(TKey item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool Contains(TKey item) => orderedDictionary.dictionary.Keys.Contains(item); + + public KeyCollection(OrderedDictionary orderedDictionary) + { + this.orderedDictionary = orderedDictionary; + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + for (int i = 0; i < orderedDictionary.list.Count; i++) + array[i] = orderedDictionary.list[i + arrayIndex].Key; + } + + public IEnumerator GetEnumerator() => + orderedDictionary.list.Select(kvp => kvp.Key).GetEnumerator(); + + public bool Remove(TKey item) => throw new NotSupportedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private class ValueCollection : ICollection + { + private readonly OrderedDictionary orderedDictionary; + + public int Count => orderedDictionary.list.Count; + + public bool IsReadOnly => true; + + public void Add(TValue item) => throw new NotSupportedException(); + + public void Clear() => throw new NotSupportedException(); + + public bool Contains(TValue item) => orderedDictionary.dictionary.Values.Contains(item); + + public ValueCollection(OrderedDictionary orderedDictionary) + { + this.orderedDictionary = orderedDictionary; + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + for (int i = 0; i < orderedDictionary.list.Count; i++) + array[i] = orderedDictionary.list[i + arrayIndex].Value; + } + + public IEnumerator GetEnumerator() => + orderedDictionary.list.Select(kvp => kvp.Value).GetEnumerator(); + + public bool Remove(TValue item) => throw new NotSupportedException(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} diff --git a/YamlDotNet/RepresentationModel/YamlMappingNode.cs b/YamlDotNet/RepresentationModel/YamlMappingNode.cs index b686e2114..be033bc29 100644 --- a/YamlDotNet/RepresentationModel/YamlMappingNode.cs +++ b/YamlDotNet/RepresentationModel/YamlMappingNode.cs @@ -22,10 +22,10 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Text; using YamlDotNet.Core; using YamlDotNet.Core.Events; +using YamlDotNet.Helpers; using YamlDotNet.Serialization; using static YamlDotNet.Core.HashCode; @@ -37,13 +37,13 @@ namespace YamlDotNet.RepresentationModel [Serializable] public sealed class YamlMappingNode : YamlNode, IEnumerable>, IYamlConvertible { - private readonly IDictionary children = new Dictionary(); + private readonly IOrderedDictionary children = new OrderedDictionary(); /// /// Gets the children of the current node. /// /// The children. - public IDictionary Children + public IOrderedDictionary Children { get {