Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to write JSON Merge Patch JSON from MutableJsonDocument change list #38058

Merged
merged 32 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
241b333
Initial checkin of JSON Merge Patch functionality
annelo-msft Aug 2, 2023
ecfdcd0
remove auxiliary APIs for this PR
annelo-msft Aug 2, 2023
d22c2a7
arrays - work in progress
annelo-msft Aug 2, 2023
5f3ae6b
Handle array root element as special case.
annelo-msft Aug 2, 2023
4ac2678
Refactor iteration over change loop
annelo-msft Aug 2, 2023
54df729
Add debug info, change handling of currentPath
annelo-msft Aug 2, 2023
900d728
arrays
annelo-msft Aug 3, 2023
c3d834e
nit: loop consistency
annelo-msft Aug 3, 2023
4c15719
tidy up
annelo-msft Aug 4, 2023
199d582
Add array tests
annelo-msft Aug 4, 2023
a661c4d
Merge remote-tracking branch 'upstream/main' into core-merge-patch
annelo-msft Aug 4, 2023
a680a08
Indicental deletes
annelo-msft Aug 4, 2023
6fbdb10
refactor
annelo-msft Aug 4, 2023
bd8bb6e
add tests, remove debug printf's
annelo-msft Aug 4, 2023
5b55230
Save for a different PR
annelo-msft Aug 4, 2023
586bc99
Standard format and test case
annelo-msft Aug 4, 2023
3ede3a7
Add failing test and steps toward fix
annelo-msft Aug 4, 2023
1aa9a58
Merge remote-tracking branch 'upstream/main' into core-merge-patch
annelo-msft Aug 7, 2023
c5094ce
refactor tests
annelo-msft Aug 7, 2023
2d706bf
refactor
annelo-msft Aug 7, 2023
13e3882
refactor
annelo-msft Aug 7, 2023
2f67cb2
refactor and add debug
annelo-msft Aug 7, 2023
ca29f77
refactor
annelo-msft Aug 7, 2023
31c4d3b
refactor
annelo-msft Aug 7, 2023
a9b9101
Merge remote-tracking branch 'upstream/main' into core-merge-patch
annelo-msft Aug 8, 2023
fb49e4d
pr fb
annelo-msft Aug 8, 2023
f6c5dba
array pool
annelo-msft Aug 8, 2023
d3802b9
pr fb
annelo-msft Aug 8, 2023
4a38a15
pr fb
annelo-msft Aug 8, 2023
f730dad
wip
annelo-msft Aug 8, 2023
8643e8d
refactor: Split()
annelo-msft Aug 8, 2023
e682ffc
Update OpenAncestorObjects
annelo-msft Aug 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions sdk/core/Azure.Core/src/DynamicData/MutableJsonChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,27 @@ internal JsonElement GetSerializedValue()

internal bool IsDescendant(string path)
{
if (path.Length > 0)
return IsDescendant(path.AsSpan());
}

internal bool IsDescendant(ReadOnlySpan<char> ancestorPath)
{
return IsDescendant(ancestorPath, Path.AsSpan());
}

internal static bool IsDescendant(ReadOnlySpan<char> ancestorPath, ReadOnlySpan<char> descendantPath)
{
if (ancestorPath.Length == 0)
{
// Restrict matches (e.g. so we don't think 'a' is a parent of 'abc').
path += MutableJsonDocument.ChangeTracker.Delimiter;
return descendantPath.Length > 0;
}

return Path.StartsWith(path, StringComparison.Ordinal);
// Restrict matches (e.g. so we don't think 'a' is a parent of 'abc').
Span<char> pathWithDelimiter = stackalloc char[ancestorPath.Length + 1];
ancestorPath.CopyTo(pathWithDelimiter);
pathWithDelimiter[ancestorPath.Length] = MutableJsonDocument.ChangeTracker.Delimiter;

return descendantPath.StartsWith(pathWithDelimiter, StringComparison.Ordinal);
}

internal bool IsDirectDescendant(string path)
Expand All @@ -85,6 +99,16 @@ internal bool IsDirectDescendant(string path)
return ancestorPathLength == (descendantPathLength - 1);
}

internal bool IsLessThan(ReadOnlySpan<char> otherPath)
{
return Path.AsSpan().SequenceCompareTo(otherPath) < 0;
}

internal bool IsGreaterThan(ReadOnlySpan<char> otherPath)
{
return Path.AsSpan().SequenceCompareTo(otherPath) > 0;
}

internal string AsString()
{
return GetSerializedValue().ToString() ?? "null";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Azure.Core.Json
{
internal enum MutableJsonChangeKind
{
PropertyValue,
PropertyUpdate,
PropertyAddition,
PropertyRemoval,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;

namespace Azure.Core.Json
Expand Down Expand Up @@ -64,6 +65,11 @@ internal bool DescendantChanged(string path, int highWaterMark)
}

internal bool TryGetChange(string path, in int lastAppliedChange, out MutableJsonChange change)
{
return TryGetChange(path.AsSpan(), lastAppliedChange, out change);
}

internal bool TryGetChange(ReadOnlySpan<char> path, in int lastAppliedChange, out MutableJsonChange change)
{
if (_changes == null)
{
Expand All @@ -74,7 +80,7 @@ internal bool TryGetChange(string path, in int lastAppliedChange, out MutableJso
for (int i = _changes!.Count - 1; i > lastAppliedChange; i--)
{
MutableJsonChange c = _changes[i];
if (c.Path == path)
if (c.Path.AsSpan().SequenceEqual(path))
{
change = c;
return true;
Expand All @@ -85,7 +91,7 @@ internal bool TryGetChange(string path, in int lastAppliedChange, out MutableJso
return false;
}

internal int AddChange(string path, object? value, MutableJsonChangeKind changeKind = MutableJsonChangeKind.PropertyValue, string? addedPropertyName = null)
internal int AddChange(string path, object? value, MutableJsonChangeKind changeKind = MutableJsonChangeKind.PropertyUpdate, string? addedPropertyName = null)
{
if (_changes == null)
{
Expand Down Expand Up @@ -135,6 +141,70 @@ internal IEnumerable<MutableJsonChange> GetRemovedProperties(string path, int hi
}
}

internal MutableJsonChange? GetFirstMergePatchChange(ReadOnlySpan<char> rootPath, out int maxPathLength)
{
// This method gets the first change from the list in sorted order by path
// It also returns the max path length of changes on the list.

maxPathLength = -1;

if (_changes == null)
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
return null;
}

MutableJsonChange? min = null;

for (int i = _changes!.Count - 1; i >= 0; i--)
{
MutableJsonChange c = _changes[i];

if (c.Path.AsSpan().StartsWith(rootPath) &&
(min == null || c.IsLessThan(min.Value.Path.AsSpan())))
{
min = c;
}

if (c.Path.Length > maxPathLength)
{
maxPathLength = c.Path.Length;
}
}

return min;
}

internal MutableJsonChange? GetNextMergePatchChange(ReadOnlySpan<char> rootPath, ReadOnlySpan<char> lastChangePath)
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
// This method gets changes from the list in sorted order by path.

if (_changes == null)
{
return null;
}

MutableJsonChange? min = null;

// This implementation is based on the assumption that iterating through
// list elements is fast.
// Iterating backwards means we get the latest change for a given path.
for (int i = _changes!.Count - 1; i >= 0; i--)
{
MutableJsonChange c = _changes[i];

if (c.Path.AsSpan().StartsWith(rootPath) &&
c.IsGreaterThan(lastChangePath) &&
(min == null || c.IsLessThan(min.Value.Path.AsSpan())) &&
// Ignore descendant if its ancestor changed
!c.IsDescendant(lastChangePath))
{
min = c;
}
}

return min;
}

internal bool WasRemoved(string path, int highWaterMark)
{
if (_changes == null)
Expand Down Expand Up @@ -175,16 +245,21 @@ internal static string PushProperty(string path, string value)
return string.Concat(path, Delimiter, value);
}

internal static string PushProperty(string path, ReadOnlySpan<byte> value)
internal static void PushProperty(Span<char> path, ref int pathLength, ReadOnlySpan<char> value, int valueLength)
{
string propertyName = BinaryData.FromBytes(value.ToArray()).ToString();
// Validate that path is large enough to write value into
Debug.Assert(path.Length - pathLength >= valueLength);

if (path.Length == 0)
if (pathLength == 0)
{
return propertyName;
value.Slice(0, valueLength).CopyTo(path);
pathLength = valueLength;
return;
}

return string.Concat(path, Delimiter, propertyName);
path[pathLength] = Delimiter;
value.Slice(0, valueLength).CopyTo(path.Slice(pathLength + 1));
pathLength += valueLength + 1;
}

internal static string PopProperty(string path)
Expand All @@ -198,6 +273,12 @@ internal static string PopProperty(string path)

return path.Substring(0, lastDelimiter);
}

internal static void PopProperty(Span<char> path, ref int pathLength)
{
int lastDelimiter = path.Slice(0, pathLength).LastIndexOf(Delimiter);
pathLength = lastDelimiter == -1 ? 0 : lastDelimiter;
}
}
}
}
41 changes: 35 additions & 6 deletions sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,34 @@ public MutableJsonElement RootElement
/// <param name="format">A format string indicating the format to use when writing the document.</param>
/// <exception cref="ArgumentNullException">The <paramref name="stream"/> parameter is <see langword="null"/>.</exception>
/// <exception cref="FormatException">Thrown if an unsupported value is passed for format.</exception>
/// <remarks>The value of <paramref name="format"/> can be default or 'J' to write the document as JSON.</remarks>
public void WriteTo(Stream stream, StandardFormat format = default)
/// <remarks>The value of <paramref name="format"/> can be default or 'J' to write the document as JSON, or 'P' to write the changes as JSON Merge Patch.</remarks>
public void WriteTo(Stream stream, string? format = default)
{
Argument.AssertNotNull(stream, nameof(stream));
ValidateFormat(format);

if (format != default && format.Symbol != 'J')
switch (format)
{
throw new FormatException($"Unsupported format {format.Symbol}. Supported formats are: 'J' - JSON.");
case "P":
WritePatch(stream);
break;
case "J":
default:
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
WriteJson(stream);
break;
}
}

internal void ValidateFormat(string? format)
{
if (format != default && format != "J" && format != "P")
{
throw new FormatException($"Unsupported format {format}. Supported formats are: \"J\" - JSON, \"P\" - JSON Merge Patch.");
}
}

private void WriteJson(Stream stream)
{
if (!Changes.HasChanges)
{
Write(stream, _original.Span);
Expand All @@ -67,6 +85,17 @@ public void WriteTo(Stream stream, StandardFormat format = default)
RootElement.WriteTo(writer);
}

private void WritePatch(Stream stream)
{
if (!Changes.HasChanges)
{
return;
}

using Utf8JsonWriter writer = new(stream);
RootElement.WritePatch(writer);
}

/// <summary>
/// Writes the document to the provided stream as a JSON value.
/// </summary>
Expand Down Expand Up @@ -110,7 +139,7 @@ private static void Write(Stream stream, ReadOnlySpan<byte> buffer)
/// <exception cref="JsonException"><paramref name="utf8Json"/> does not represent a valid single JSON value.</exception>
public static MutableJsonDocument Parse(ReadOnlyMemory<byte> utf8Json, JsonSerializerOptions? serializerOptions = default)
{
var doc = JsonDocument.Parse(utf8Json);
JsonDocument doc = JsonDocument.Parse(utf8Json);
return new MutableJsonDocument(doc, utf8Json, serializerOptions);
}

Expand All @@ -123,7 +152,7 @@ public static MutableJsonDocument Parse(ReadOnlyMemory<byte> utf8Json, JsonSeria
/// <exception cref="JsonException"><paramref name="utf8Json"/> does not represent a valid single JSON value.</exception>
public static MutableJsonDocument Parse(BinaryData utf8Json, JsonSerializerOptions? serializerOptions = default)
{
var doc = JsonDocument.Parse(utf8Json);
JsonDocument doc = JsonDocument.Parse(utf8Json);
return new MutableJsonDocument(doc, utf8Json.ToMemory(), serializerOptions);
}

Expand Down
Loading