Skip to content

Commit

Permalink
Add ability to write JSON Merge Patch JSON from MutableJsonDocument c…
Browse files Browse the repository at this point in the history
…hange list (#38058)

* Initial checkin of JSON Merge Patch functionality

* remove auxiliary APIs for this PR

* arrays - work in progress

* Handle array root element as special case.

* Refactor iteration over change loop

* Add debug info, change handling of currentPath

* arrays

* nit: loop consistency

* tidy up

* Add array tests

* Indicental deletes

* refactor

* add tests, remove debug printf's

* Save for a different PR

* Standard format and test case

* Add failing test and steps toward fix

* refactor tests

* refactor

* refactor

* refactor and add debug

* refactor

* refactor

* pr fb

* array pool

* pr fb

* pr fb

* wip

* refactor: Split()

* Update OpenAncestorObjects
  • Loading branch information
annelo-msft authored Aug 9, 2023
1 parent 3651f38 commit df67b2d
Show file tree
Hide file tree
Showing 9 changed files with 1,498 additions and 38 deletions.
31 changes: 27 additions & 4 deletions sdk/core/Azure.Core/src/DynamicData/MutableJsonChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Buffers;
using System.Text.Json;

namespace Azure.Core.Json
Expand Down Expand Up @@ -62,13 +63,25 @@ 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);
return descendantPath.Length > ancestorPath.Length &&
descendantPath.StartsWith(ancestorPath) &&
// Restrict matches (e.g. so we don't think 'a' is a parent of 'abc').
descendantPath[ancestorPath.Length] == MutableJsonDocument.ChangeTracker.Delimiter;
}

internal bool IsDirectDescendant(string path)
Expand All @@ -85,6 +98,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)
{
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)
{
// 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 All @@ -155,6 +225,45 @@ internal bool WasRemoved(string path, int highWaterMark)
return false;
}

internal static SegmentEnumerator Split(ReadOnlySpan<char> path) => new(path);

internal ref struct SegmentEnumerator
{
private readonly ReadOnlySpan<char> _path;

private int _start = 0;
private int _segmentLength;
private ReadOnlySpan<char> _current;

public SegmentEnumerator(ReadOnlySpan<char> path)
{
_path = path;
}

public readonly SegmentEnumerator GetEnumerator() => this;

public bool MoveNext()
{
if (_start > _path.Length)
{
return false;
}

_segmentLength = _path.Slice(_start).IndexOf(Delimiter);
if (_segmentLength == -1)
{
_segmentLength = _path.Length - _start;
}

_current = _path.Slice(_start, _segmentLength);
_start += _segmentLength + 1;

return true;
}

public readonly ReadOnlySpan<char> Current => _current;
}

internal static string PushIndex(string path, int index)
{
return PushProperty(path, $"{index}");
Expand All @@ -175,16 +284,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)
{
string propertyName = BinaryData.FromBytes(value.ToArray()).ToString();
// Validate that path is large enough to write value into
Debug.Assert(path.Length - pathLength >= value.Length);

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

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

internal static string PopProperty(string path)
Expand All @@ -198,6 +312,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;
}
}
}
}
40 changes: 34 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,33 @@ 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));

if (format != default && format.Symbol != 'J')
switch (format)
{
throw new FormatException($"Unsupported format {format.Symbol}. Supported formats are: 'J' - JSON.");
case "J":
case null:
WriteJson(stream);
break;
case "P":
WritePatch(stream);
break;
default:
AssertInvalidFormat(format);
break;
}
}

internal void AssertInvalidFormat(string? format)
{
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 +84,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 +138,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 +151,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

0 comments on commit df67b2d

Please sign in to comment.