Change ConvertFrom-Json -AsHashtable to use ordered hashtable (#17405)

JSON typically has ordering to make it easier to read. Currently, a Hashtable does not preserve ordering so for complex JSON, the members are not where you would expect in terms of order. This change wraps OrderedDictionary into a OrderedHashtable that derives from Hashtable. This preserves existing compatibility with scripts that expect a Hashtable to be returned, but preserves the order of elements.
This commit is contained in:
Steve Lee
2022-07-13 09:23:03 +05:00
committed by GitHub
parent 0295cc6d07
commit ff0506d632
4 changed files with 420 additions and 1 deletions
@@ -344,7 +344,7 @@ namespace Microsoft.PowerShell.Commands
private static Hashtable PopulateHashTableFromJDictionary(JObject entries, out ErrorRecord error)
{
error = null;
Hashtable result = new(entries.Count);
OrderedHashtable result = new(entries.Count);
foreach (var entry in entries)
{
// Case sensitive duplicates should normally not occur since JsonConvert.DeserializeObject
@@ -0,0 +1,246 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Runtime.Serialization;
#nullable enable
namespace System.Management.Automation
{
/// <summary>
/// OrderedHashtable is a hashtable that preserves the order of the keys.
/// </summary>
public sealed class OrderedHashtable : Hashtable, IEnumerable
{
private readonly OrderedDictionary _orderedDictionary;
/// <summary>
/// Initializes a new instance of the <see cref="OrderedHashtable"/> class.
/// </summary>
public OrderedHashtable()
{
_orderedDictionary = new OrderedDictionary();
}
/// <summary>
/// Initializes a new instance of the <see cref="OrderedHashtable"/> class.
/// </summary>
/// <param name="capacity">The capacity.</param>
public OrderedHashtable(int capacity) : base(capacity)
{
_orderedDictionary = new OrderedDictionary(capacity);
}
/// <summary>
/// Initializes a new instance of the <see cref="OrderedHashtable"/> class.
/// </summary>
/// <param name="dictionary">The dictionary to use for initialization.</param>
public OrderedHashtable(IDictionary dictionary)
{
_orderedDictionary = new OrderedDictionary(dictionary.Count);
foreach (DictionaryEntry entry in dictionary)
{
_orderedDictionary.Add(entry.Key, entry.Value);
}
}
/// <summary>
/// Get the number of items in the hashtable.
/// </summary>
public override int Count
{
get
{
return _orderedDictionary.Count;
}
}
/// <summary>
/// Get if the hashtable is a fixed size.
/// </summary>
public override bool IsFixedSize
{
get
{
return false;
}
}
/// <summary>
/// Get if the hashtable is read-only.
/// </summary>
public override bool IsReadOnly
{
get
{
return false;
}
}
/// <summary>
/// Get if the hashtable is synchronized.
/// </summary>
public override bool IsSynchronized
{
get
{
return false;
}
}
/// <summary>
/// Gets the keys in the hashtable.
/// </summary>
public override ICollection Keys
{
get
{
return _orderedDictionary.Keys;
}
}
/// <summary>
/// Gets the values in the hashtable.
/// </summary>
public override ICollection Values
{
get
{
return _orderedDictionary.Values;
}
}
/// <summary>
/// Gets or sets the value associated with the specified key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The value associated with the key.</returns>
public override object? this[object key]
{
get
{
return _orderedDictionary[key];
}
set
{
_orderedDictionary[key] = value;
}
}
/// <summary>
/// Adds the specified key and value to the hashtable.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
public override void Add(object key, object? value)
{
_orderedDictionary.Add(key, value);
}
/// <summary>
/// Removes all keys and values from the hashtable.
/// </summary>
public override void Clear()
{
_orderedDictionary.Clear();
}
/// <summary>
/// Get a shallow clone of the hashtable.
/// </summary>
/// <returns>A shallow clone of the hashtable.</returns>
public override object Clone()
{
return new OrderedHashtable(_orderedDictionary);
}
/// <summary>
/// Determines whether the hashtable contains a specific key.
/// </summary>
/// <param name="key">The key to locate in the hashtable.</param>
/// <returns>true if the hashtable contains an element with the specified key; otherwise, false.</returns>
public override bool Contains(object key)
{
return _orderedDictionary.Contains(key);
}
/// <summary>
/// Determines whether the hashtable contains a specific key.
/// </summary>
/// <param name="key">The key to locate in the hashtable.</param>
/// <returns>true if the hashtable contains an element with the specified key; otherwise, false.</returns>
public override bool ContainsKey(object key)
{
return _orderedDictionary.Contains(key);
}
/// <summary>
/// Determines whether the hashtable contains a specific value.
/// </summary>
/// <param name="value">The value to locate in the hashtable.</param>
/// <returns>true if the hashtable contains an element with the specified value; otherwise, false.</returns>
public override bool ContainsValue(object? value)
{
foreach (DictionaryEntry entry in _orderedDictionary)
{
if (Equals(entry.Value, value))
{
return true;
}
}
return false;
}
/// <summary>
/// Copies the elements of the hashtable to an array of type object, starting at the specified array index.
/// </summary>
/// <param name="array">The one-dimensional array that is the destination of the elements copied from the hashtable. The array must have zero-based indexing.</param>
/// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
public override void CopyTo(Array array, int arrayIndex)
{
_orderedDictionary.CopyTo(array, arrayIndex);
}
/// <summary>
/// Get the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
public override IDictionaryEnumerator GetEnumerator()
{
return _orderedDictionary.GetEnumerator();
}
/// <summary>
/// Get the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Returns the data needed to seralize the Hashtable.
/// </summary>
/// <param name="info">The serialization info.</param>
/// <param name="context">The serialization context.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
_orderedDictionary.GetObjectData(info, context);
}
/// <summary>
/// Removes the specified key from the hashtable.
/// </summary>
/// <param name="key">The key to remove.</param>
public override void Remove(object key)
{
_orderedDictionary.Remove(key);
}
}
}
@@ -142,6 +142,17 @@ Describe 'ConvertFrom-Json Unit Tests' -tags "CI" {
$out[1] | Should -Be $null
$out[2] | Should -Be 2
}
It 'Order is preserved for a hashtable' {
$json = '{"a":1,"b":2,"c":3}' | ConvertFrom-Json -AsHashtable
($json | Out-String).Trim() | Should -BeExactly @"
Name Value
---- -----
a 1
b 2
c 3
"@
}
}
Describe 'ConvertFrom-Json -Depth Tests' -tags "Feature" {
@@ -0,0 +1,162 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe 'Tests for OrderedHashtable' {
It 'Can create an empty OrderedHashtable' {
$oh =[System.Management.Automation.OrderedHashtable]::new()
$oh.Count | Should -Be 0
$oh.GetType().Name | Should -BeExactly 'OrderedHashtable'
}
It 'Can create an empty OrderedHashtable with a capacity' {
$oh = [System.Management.Automation.OrderedHashtable]::new(10)
$oh.Count | Should -Be 0
$oh.GetType().Name | Should -BeExactly 'OrderedHashtable'
}
It 'Can create an OrderedHashtable with an initial dictionary' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.Count | Should -Be 2
$oh['a'] | Should -Be $h['a']
$oh['b'] | Should -Be $h['b']
}
It 'Can use the Add() method' {
$oh = [System.Management.Automation.OrderedHashtable]::new()
$oh.Add('a', 1)
$oh.Add('b', 2)
$oh.Count | Should -Be 2
$oh['a'] | Should -Be 1
$oh['b'] | Should -Be 2
}
It 'Can use the Clear() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.Count | Should -Be 2
$oh.Clear()
$oh.Count | Should -Be 0
}
It 'Can use the Clone() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh2 = $oh.Clone()
$oh2.Count | Should -Be 2
$oh2['a'] | Should -Be $h['a']
$oh2['b'] | Should -Be $h['b']
}
It 'Can use the Contains() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.Contains('a') | Should -BeTrue
$oh.Contains('b') | Should -BeTrue
$oh.Contains('c') | Should -BeFalse
}
It 'Can use the ContainsKey() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.ContainsKey('a') | Should -BeTrue
$oh.ContainsKey('b') | Should -BeTrue
$oh.ContainsKey('c') | Should -BeFalse
}
It 'Can use the ContainsValue() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.ContainsValue(1) | Should -BeTrue
$oh.ContainsValue(2) | Should -BeTrue
$oh.ContainsValue(3) | Should -BeFalse
}
It 'Can use the CopyTo() method' {
$oh = [System.Management.Automation.OrderedHashtable]::new()
$oh.Add('a', 1)
$oh.Add('b', 2)
$oh.Add('c', 3)
$a = (4, 5, 6, 7)
$oh.CopyTo($a, 1)
$a[0] | Should -Be 4
# OrderedDictionary.CopyTo() doesn't guarantee to preserve order
# so we can't easily test the values
$a[1].GetType().Name | Should -BeExactly 'DictionaryEntry'
$a[2].GetType().Name | Should -BeExactly 'DictionaryEntry'
$a[3].GetType().Name | Should -BeExactly 'DictionaryEntry'
}
It 'Can use the Equals() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh2 = $oh.Clone()
$oh3 = $oh
$oh.Equals($oh2) | Should -BeFalse
$oh.Equals($oh3) | Should -BeTrue
}
It 'Can use the GetEnumerator() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.GetEnumerator().GetType().Name | Should -BeExactly 'OrderedDictionaryEnumerator'
}
It 'Can use the GetHashCode() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.GetHashCode() | Should -BeGreaterThan 0
$oh2 = $oh.Clone()
$oh.GetHashCode() | Should -Not -Be $oh2.GetHashCode()
$oh3 = $oh
$oh.GetHashCode() | Should -Be $oh3.GetHashCode()
}
It 'Can use Remove() method' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.Remove('a')
$oh.Count | Should -Be 1
$oh.Contains('a') | Should -BeFalse
}
It 'Can use Item property' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh['a'] | Should -Be 1
$oh['b'] | Should -Be 2
}
It 'Can use IsFixedSize property' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.IsFixedSize | Should -BeFalse
}
It 'Can use IsReadOnly property' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.IsReadOnly | Should -BeFalse
}
It 'Can use IsSynchronized property' {
$h = @{ a = 1; b = 2 }
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
$oh.IsSynchronized | Should -BeFalse
}
It 'Can use Keys property' {
$oh = [System.Management.Automation.OrderedHashtable]::new()
$oh['a'] = 1
$oh['b'] = 2
$oh.Keys | Should -Be ('a', 'b')
}
It 'Can use Values property' {
$oh = [System.Management.Automation.OrderedHashtable]::new()
$oh['a'] = 1
$oh['b'] = 2
$oh.Values | Should -Be (1, 2)
}
}