Skip to content

Commit

Permalink
Additional JsonNode functionality (#87381)
Browse files Browse the repository at this point in the history
* add deep clone implementation

* Add tests for DeepClone JsonObject method

* add more tests

* Add GetElementIndex, GetPropertyName api

* Add ReplaceWith api

* Add api description

* remove unused code

* add more tests

* resolve comments

* resolve comments.

* resolve comments.

* resolve comments.

* add test cases

* resolve comments.

* resolve comments.

* remove unused namespace.

* resolve comments.

* Resolve comments.

* resolve comments.

* fix failed test.

* enhance comparision.

* resolve comments.

* resolve comments.

* resolve comments.

* resolve comments.

* resolve comments.

* add more test cases.

* resolve comments.

* resolve comments.

* add test cases.
  • Loading branch information
RaymondHuy authored Jun 27, 2023
1 parent 318c0e6 commit dd079f5
Show file tree
Hide file tree
Showing 13 changed files with 1,011 additions and 12 deletions.
10 changes: 10 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ public void Clear() { }
public bool Contains(System.Text.Json.Nodes.JsonNode? item) { throw null; }
public static System.Text.Json.Nodes.JsonArray? Create(System.Text.Json.JsonElement element, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; }
public System.Collections.Generic.IEnumerator<System.Text.Json.Nodes.JsonNode?> GetEnumerator() { throw null; }
public System.Collections.Generic.IEnumerable<T> GetValues<T>() { throw null; }
public int IndexOf(System.Text.Json.Nodes.JsonNode? item) { throw null; }
public void Insert(int index, System.Text.Json.Nodes.JsonNode? item) { }
public bool Remove(System.Text.Json.Nodes.JsonNode? item) { throw null; }
Expand All @@ -679,8 +680,13 @@ internal JsonNode() { }
public System.Text.Json.Nodes.JsonArray AsArray() { throw null; }
public System.Text.Json.Nodes.JsonObject AsObject() { throw null; }
public System.Text.Json.Nodes.JsonValue AsValue() { throw null; }
public System.Text.Json.Nodes.JsonNode DeepClone() { throw null; }
public static bool DeepEquals(System.Text.Json.Nodes.JsonNode? node1, System.Text.Json.Nodes.JsonNode? node2) { throw null; }
public string GetPropertyName() { throw null; }
public string GetPath() { throw null; }
public virtual T GetValue<T>() { throw null; }
public JsonValueKind GetValueKind() { throw null; }
public int GetElementIndex() { throw null; }
public static explicit operator bool (System.Text.Json.Nodes.JsonNode value) { throw null; }
public static explicit operator byte (System.Text.Json.Nodes.JsonNode value) { throw null; }
public static explicit operator char (System.Text.Json.Nodes.JsonNode value) { throw null; }
Expand Down Expand Up @@ -768,6 +774,10 @@ internal JsonNode() { }
public static System.Text.Json.Nodes.JsonNode? Parse(System.ReadOnlySpan<byte> utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; }
public static System.Text.Json.Nodes.JsonNode? Parse([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Json")] string json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; }
public static System.Text.Json.Nodes.JsonNode? Parse(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; }

[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Creating JsonValue instances with non-primitive types requires generating code at runtime.")]
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Creating JsonValue instances with non-primitive types is not compatible with trimming. It can result in non-primitive types being serialized, which may have their members trimmed.")]
public void ReplaceWith<T>(T value) { throw null; }
public string ToJsonString(System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
public override string ToString() { throw null; }
public abstract void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,6 @@ internal bool TextEquals(int index, ReadOnlySpan<char> otherText, bool isPropert
{
CheckNotDisposed();

int matchIndex = isPropertyName ? index - DbRow.Size : index;

byte[]? otherUtf8TextArray = null;

int length = checked(otherText.Length * JsonConstants.MaxExpansionFactorWhileTranscoding);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,86 @@ public JsonArray(params JsonNode?[] items) : base()
InitializeFromArray(items);
}

internal override JsonNode InternalDeepClone()
{
if (_jsonElement.HasValue)
{
return new JsonArray(_jsonElement.Value.Clone(), Options);
}

List<JsonNode?> list = List;

var jsonArray = new JsonArray(Options)
{
_list = new List<JsonNode?>(list.Count)
};

for (int i = 0; i < list.Count; i++)
{
JsonNode? item = list[i];
if (item is null)
{
jsonArray.Add(null);
}
else
{
jsonArray.Add(item.DeepClone());
}
}

return jsonArray;
}

internal override bool DeepEquals(JsonNode? node)
{
switch (node)
{
case null or JsonObject:
return false;
case JsonValue value:
// JsonValueTrimmable/NonTrimmable can hold the array type so calling this method to continue the deep comparision.
return value.DeepEquals(this);
case JsonArray array:
List<JsonNode?> currentList = List;
List<JsonNode?> otherList = array.List;

if (currentList.Count != otherList.Count)
{
return false;
}

for (int i = 0; i < currentList.Count; i++)
{
if (!DeepEquals(currentList[i], otherList[i]))
{
return false;
}
}

return true;
default:
Debug.Fail("Impossible case");
return false;
}
}

internal int GetElementIndex(JsonNode? node)
{
return List.IndexOf(node);
}

/// <summary>
/// Returns enumerator that wraps calls to <see cref="JsonNode.GetValue{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the value to obtain from the <see cref="JsonValue"/>.</typeparam>
public IEnumerable<T> GetValues<T>()
{
foreach (JsonNode? item in List)
{
yield return item is null ? (T)(object?)null! : item.GetValue<T>();
}
}

private void InitializeFromArray(JsonNode?[] items)
{
var list = new List<JsonNode?>(items);
Expand Down
126 changes: 117 additions & 9 deletions src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace System.Text.Json.Nodes
{
Expand Down Expand Up @@ -50,12 +51,14 @@ internal JsonNode(JsonNodeOptions? options = null)
/// </exception>
public JsonArray AsArray()
{
if (this is JsonArray jArray)
JsonArray? jArray = this as JsonArray;

if (jArray is null)
{
return jArray;
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray));
}

throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonArray)));
return jArray;
}

/// <summary>
Expand All @@ -69,12 +72,14 @@ public JsonArray AsArray()
/// </exception>
public JsonObject AsObject()
{
if (this is JsonObject jObject)
JsonObject? jObject = this as JsonObject;

if (jObject is null)
{
return jObject;
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonObject));
}

throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonObject)));
return jObject;
}

/// <summary>
Expand All @@ -88,12 +93,14 @@ public JsonObject AsObject()
/// </exception>
public JsonValue AsValue()
{
if (this is JsonValue jValue)
JsonValue? jValue = this as JsonValue;

if (jValue is null)
{
return jValue;
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonValue));
}

throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonValue)));
return jValue;
}

/// <summary>
Expand Down Expand Up @@ -232,6 +239,107 @@ public JsonNode? this[string propertyName]
}
}

/// <summary>
/// Creates a new instance of the <see cref="JsonNode"/>. All children nodes are recursively cloned.
/// </summary>
public JsonNode DeepClone()
{
return InternalDeepClone();
}

internal abstract JsonNode InternalDeepClone();

/// <summary>
/// Returns <see cref="JsonValueKind"/> of current instance.
/// </summary>
public JsonValueKind GetValueKind()
{
return this switch
{
JsonObject => JsonValueKind.Object,
JsonArray => JsonValueKind.Array,
_ => AsValue().GetInternalValueKind(),
};
}

/// <summary>
/// Returns property name of the current node from the parent object.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The current parent is not a <see cref="JsonObject"/>.
/// </exception>
public string GetPropertyName()
{
JsonObject? jsonObject = _parent as JsonObject;

if (jsonObject is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonObject));
}

return jsonObject.GetPropertyName(this);
}

/// <summary>
/// Returns index of the current node from the parent <see cref="JsonArray" />.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The current parent is not a <see cref="JsonArray"/>.
/// </exception>
public int GetElementIndex()
{
JsonArray? jsonArray = _parent as JsonArray;

if (jsonArray is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray));
}

return jsonArray.GetElementIndex(this);
}

/// <summary>
/// Compares the values of two nodes, including the values of all descendant nodes.
/// </summary>
/// <param name="node1">The <see cref="JsonNode"/> to compare.</param>
/// <param name="node2">The <see cref="JsonNode"/> to compare.</param>
/// <returns><c>true</c> if the tokens are equal; otherwise <c>false</c>.</returns>
public static bool DeepEquals(JsonNode? node1, JsonNode? node2)
{
if (node1 is null)
{
return node2 is null;
}

return node1.DeepEquals(node2);
}

internal abstract bool DeepEquals(JsonNode? node);

/// <summary>
/// Replaces this node with a new value.
/// </summary>
/// <typeparam name="T">The type of value to be replaced.</typeparam>
/// <param name="value">Value that replaces this node.</param>
[RequiresUnreferencedCode(JsonValue.CreateUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonValue.CreateDynamicCodeMessage)]
public void ReplaceWith<T>(T value)
{
switch (_parent)
{
case null:
return;
case JsonObject jsonObject:
JsonValue? jsonValue = JsonValue.Create(value);
jsonObject.SetItem(GetPropertyName(), jsonValue);
return;
case JsonArray jsonArray:
JsonValue? jValue = JsonValue.Create(value);
jsonArray.SetItem(GetElementIndex(), jValue);
return;
}
}

internal void AssignParent(JsonNode parent)
{
if (Parent != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,46 @@ internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : this
_jsonElement = element;
}

internal override JsonNode InternalDeepClone()
{
if (_jsonElement.HasValue)
{
return new JsonObject(_jsonElement!.Value.Clone(), Options);
}

if (_dictionary is not null)
{
bool caseInsensitive = Options.HasValue ? Options.Value.PropertyNameCaseInsensitive : false;
var jObject = new JsonObject(Options)
{
_dictionary = new JsonPropertyDictionary<JsonNode?>(caseInsensitive, _dictionary.Count)
};

foreach (KeyValuePair<string, JsonNode?> item in _dictionary)
{
if (item.Value is not null)
{
jObject.Add(item.Key, item.Value.DeepClone());
}
else
{
jObject.Add(item.Key, null);
}
}
return jObject;
}

return new JsonObject(Options);
}

internal string GetPropertyName(JsonNode? node)
{
InitializeIfRequired();
Debug.Assert(_dictionary != null);
KeyValuePair<string, JsonNode?>? item = _dictionary.FindValue(node);
return item.HasValue ? item.Value.Key : string.Empty;
}

/// <summary>
/// Returns the value of a property with the specified name.
/// </summary>
Expand Down Expand Up @@ -110,6 +150,43 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
}
}

internal override bool DeepEquals(JsonNode? node)
{
switch (node)
{
case null or JsonArray:
return false;
case JsonValue value:
// JsonValueTrimmable/NonTrimmable can hold the object type so calling this method to continue the deep comparision.
return value.DeepEquals(this);
case JsonObject jsonObject:
InitializeIfRequired();
jsonObject.InitializeIfRequired();
Debug.Assert(_dictionary is not null);
Debug.Assert(jsonObject._dictionary is not null);

if (_dictionary.Count != jsonObject._dictionary.Count)
{
return false;
}

foreach (KeyValuePair<string, JsonNode?> item in this)
{
JsonNode? jsonNode = jsonObject._dictionary[item.Key];

if (!DeepEquals(item.Value, jsonNode))
{
return false;
}
}

return true;
default:
Debug.Fail("Impossible case");
return false;
}
}

internal JsonNode? GetItem(string propertyName)
{
if (TryGetPropertyValue(propertyName, out JsonNode? value))
Expand Down
Loading

0 comments on commit dd079f5

Please sign in to comment.