Added JsonDeltaFormatter
parent
ca0e88aa56
commit
14ed050e05
|
@ -0,0 +1,112 @@
|
|||
using JsonDiffPatchDotNet.Formatters.JsonPatch;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace JsonDiffPatchDotNet.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class JsonDeltaFormatterUnitTests
|
||||
{
|
||||
[Test]
|
||||
public void Format_SupportsRemove_Success()
|
||||
{
|
||||
var jdp = new JsonDiffPatch();
|
||||
var formatter = new JsonDeltaFormatter();
|
||||
var left = JObject.Parse(@"{ ""p"" : true }");
|
||||
var right = JObject.Parse(@"{ }");
|
||||
var patch = jdp.Diff(left, right);
|
||||
var operations = formatter.Format(patch);
|
||||
|
||||
Assert.AreEqual(1, operations.Count);
|
||||
AssertOperation(operations[0], OperationTypes.Remove, "/p");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_SupportsAdd_Success()
|
||||
{
|
||||
var jdp = new JsonDiffPatch();
|
||||
var formatter = new JsonDeltaFormatter();
|
||||
var left = JObject.Parse(@"{ }");
|
||||
var right = JObject.Parse(@"{ ""p"" : true }");
|
||||
var patch = jdp.Diff(left, right);
|
||||
var operations = formatter.Format(patch);
|
||||
|
||||
Assert.AreEqual(1, operations.Count);
|
||||
AssertOperation(operations[0], OperationTypes.Add, "/p", new JValue(true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_SupportsReplace_Success()
|
||||
{
|
||||
var jdp = new JsonDiffPatch();
|
||||
var formatter = new JsonDeltaFormatter();
|
||||
var left = JObject.Parse(@"{ ""p"" : false }");
|
||||
var right = JObject.Parse(@"{ ""p"" : true }");
|
||||
var patch = jdp.Diff(left, right);
|
||||
var operations = formatter.Format(patch);
|
||||
|
||||
Assert.AreEqual(1, operations.Count);
|
||||
AssertOperation(operations[0], OperationTypes.Replace, "/p", new JValue(true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_SupportsArrayAdd_Success()
|
||||
{
|
||||
var jdp = new JsonDiffPatch();
|
||||
var formatter = new JsonDeltaFormatter();
|
||||
var left = JObject.Parse(@"{ ""items"" : [""car"", ""bus""] }");
|
||||
var right = JObject.Parse(@"{ ""items"" : [""bike"", ""car"", ""bus""] }");
|
||||
var patch = jdp.Diff(left, right);
|
||||
var operations = formatter.Format(patch);
|
||||
|
||||
Assert.AreEqual(1, operations.Count);
|
||||
AssertOperation(operations[0], OperationTypes.Add, "/items/0", JValue.CreateString("bike"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_SupportsArrayRemove_Success()
|
||||
{
|
||||
var jdp = new JsonDiffPatch();
|
||||
var formatter = new JsonDeltaFormatter();
|
||||
var left = JObject.Parse(@"{ ""items"" : [""bike"", ""car"", ""bus""] }");
|
||||
var right = JObject.Parse(@"{ ""items"" : [""car"", ""bus""] }");
|
||||
var patch = jdp.Diff(left, right);
|
||||
var operations = formatter.Format(patch);
|
||||
|
||||
Assert.AreEqual(1, operations.Count);
|
||||
AssertOperation(operations[0], OperationTypes.Remove, "/items/0");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_SupportsArrayMove_Success()
|
||||
{
|
||||
var jdp = new JsonDiffPatch();
|
||||
var formatter = new JsonDeltaFormatter();
|
||||
var left = JObject.Parse(@"{ ""items"" : [""bike"", ""car"", ""bus""] }");
|
||||
var right = JObject.Parse(@"{ ""items"" : [""bike"", ""bus"", ""car""] }");
|
||||
var patch = jdp.Diff(left, right);
|
||||
var operations = formatter.Format(patch);
|
||||
|
||||
Assert.AreEqual(2, operations.Count);
|
||||
AssertOperation(operations[0], OperationTypes.Remove, "/items/2");
|
||||
AssertOperation(operations[1], OperationTypes.Add, "/items/1", JValue.CreateString("bus"));
|
||||
}
|
||||
|
||||
private void AssertOperation(Operation operation, string expectedOp, string expectedPath, JValue expectedValue = null)
|
||||
{
|
||||
Assert.AreEqual(expectedOp, operation.Op);
|
||||
Assert.AreEqual(expectedPath, operation.Path);
|
||||
|
||||
if (expectedValue != null)
|
||||
{
|
||||
var value = operation.Value as JValue;
|
||||
Assert.IsNotNull(value, "Operation value was expected to be a JValue");
|
||||
Assert.AreEqual(expectedValue, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(operation.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace JsonDiffPatchDotNet.Formatters
|
||||
{
|
||||
internal class ArrayKeyComparer : IComparer<string>
|
||||
{
|
||||
public int Compare(string x, string y)
|
||||
{
|
||||
// This purposefully REVERSED from benjamine/jsondiffpatch,
|
||||
// In order to match logic found in JsonDiffPatch.ArrayPatch,
|
||||
// which applies operations in reverse order to avoid shifting floor
|
||||
return ArrayKeyToSortNumber(y) - ArrayKeyToSortNumber(x);
|
||||
}
|
||||
|
||||
private static int ArrayKeyToSortNumber(string key)
|
||||
{
|
||||
if (key == "_t")
|
||||
return -1;
|
||||
|
||||
if (key.Length > 1 && key[0] == '_')
|
||||
return int.Parse(key.Substring(1)) * 10;
|
||||
|
||||
return (int.Parse(key) * 10) + 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace JsonDiffPatchDotNet.Formatters
|
||||
{
|
||||
public abstract class BaseDeltaFormatter<TContext, TResult>
|
||||
where TContext : IFormatContext<TResult>, new()
|
||||
{
|
||||
public delegate void DeltaKeyIterator(string key, string leftKey, MoveDestination movedFrom, bool isLast);
|
||||
|
||||
private static readonly IComparer<string> s_arrayKeyComparer = new ArrayKeyComparer();
|
||||
|
||||
public TResult Format(JToken delta)
|
||||
{
|
||||
var context = new TContext();
|
||||
Recurse(context, delta, left: null, key: null, leftKey: null, movedFrom: null, isLast: false);
|
||||
return context.Result();
|
||||
}
|
||||
|
||||
protected abstract bool IncludeMoveDestinations { get; }
|
||||
|
||||
protected abstract void NodeBegin(TContext context, string key, string leftKey, DeltaType type, NodeType nodeType, bool isLast);
|
||||
|
||||
protected abstract void NodeEnd(TContext context, string key, string leftKey, DeltaType type, NodeType nodeType, bool isLast);
|
||||
|
||||
protected abstract void RootBegin(TContext context, DeltaType type, NodeType nodeType);
|
||||
|
||||
protected abstract void RootEnd(TContext context, DeltaType type, NodeType nodeType);
|
||||
|
||||
protected abstract void Format(DeltaType type, TContext context, JToken delta, JToken leftValue, string key, string leftKey, MoveDestination movedFrom);
|
||||
|
||||
protected void Recurse(TContext context, JToken delta, JToken left, string key, string leftKey, MoveDestination movedFrom, bool isLast)
|
||||
{
|
||||
var useMoveOriginHere = delta != null && movedFrom != null;
|
||||
var leftValue = useMoveOriginHere ? movedFrom.Value : left;
|
||||
|
||||
if (delta == null && string.IsNullOrEmpty(key))
|
||||
return;
|
||||
|
||||
var type = GetDeltaType(delta, movedFrom);
|
||||
var nodeType = type == DeltaType.Node ? (delta["_t"]?.Value<string>() == "a" ? NodeType.Array : NodeType.Object) : NodeType.Unknown;
|
||||
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
NodeBegin(context, key, leftKey, type, nodeType, isLast);
|
||||
else
|
||||
RootBegin(context, type, nodeType);
|
||||
|
||||
Format(type, context, delta, leftValue, key, leftKey, movedFrom);
|
||||
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
NodeEnd(context, key, leftKey, type, nodeType, isLast);
|
||||
else
|
||||
RootEnd(context, type, nodeType);
|
||||
}
|
||||
|
||||
protected void FormatDeltaChildren(TContext context, JToken delta, JToken left)
|
||||
{
|
||||
ForEachDeltaKey(delta, left, Iterator);
|
||||
|
||||
void Iterator(string key, string leftKey, MoveDestination movedFrom, bool isLast)
|
||||
{
|
||||
Recurse(context, delta[key], left?[leftKey], key, leftKey, movedFrom, isLast);
|
||||
}
|
||||
}
|
||||
|
||||
protected void ForEachDeltaKey(JToken delta, JToken left, DeltaKeyIterator iterator)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
var arrayKeys = false;
|
||||
var movedDestinations = new Dictionary<string, MoveDestination>();
|
||||
|
||||
if (delta is JObject jObject)
|
||||
{
|
||||
keys = jObject.Properties().Select(p => p.Name).ToList();
|
||||
arrayKeys = jObject["_t"]?.Value<string>() == "a";
|
||||
}
|
||||
|
||||
if (left != null && left is JObject leftObject)
|
||||
{
|
||||
foreach (var kvp in leftObject)
|
||||
{
|
||||
if (delta[kvp.Key] == null && (!arrayKeys || delta["_"+ kvp.Key] == null))
|
||||
{
|
||||
keys.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delta is JObject deltaObject)
|
||||
{
|
||||
foreach (var kvp in deltaObject)
|
||||
{
|
||||
var value = kvp.Value;
|
||||
if (value is JArray valueArray && valueArray.Count == 3)
|
||||
{
|
||||
var diffOp = valueArray[2].Value<int>();
|
||||
if (diffOp == (int)DiffOperation.ArrayMove)
|
||||
{
|
||||
var moveKey = valueArray[1].ToString();
|
||||
movedDestinations[moveKey] = new MoveDestination(kvp.Key, left?[kvp.Key.Substring(1)]);
|
||||
|
||||
if (IncludeMoveDestinations && left == null && deltaObject.Property(moveKey) == null)
|
||||
keys.Add(moveKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (arrayKeys)
|
||||
keys.Sort(s_arrayKeyComparer);
|
||||
else
|
||||
keys.Sort();
|
||||
|
||||
for (var index = 0; index < keys.Count; index++)
|
||||
{
|
||||
var key = keys[index];
|
||||
if (arrayKeys && key == "_t")
|
||||
continue;
|
||||
|
||||
var leftKey = arrayKeys
|
||||
? key.TrimStart('_')
|
||||
: key;
|
||||
|
||||
var isLast = index == keys.Count - 1;
|
||||
var movedFrom = movedDestinations.ContainsKey(leftKey) ? movedDestinations[leftKey] : null;
|
||||
iterator(key, leftKey, movedFrom, isLast);
|
||||
}
|
||||
}
|
||||
|
||||
protected static DeltaType GetDeltaType(JToken delta = null, MoveDestination movedFrom = null)
|
||||
{
|
||||
if (delta == null)
|
||||
return movedFrom != null ? DeltaType.MoveDestination : DeltaType.Unchanged;
|
||||
|
||||
switch (delta.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
{
|
||||
var deltaArray = (JArray)delta;
|
||||
switch (deltaArray.Count)
|
||||
{
|
||||
case 1: return DeltaType.Added;
|
||||
case 2: return DeltaType.Modified;
|
||||
case 3:
|
||||
{
|
||||
switch ((DiffOperation)deltaArray[2].Value<int>())
|
||||
{
|
||||
case DiffOperation.Deleted: return DeltaType.Deleted;
|
||||
case DiffOperation.TextDiff: return DeltaType.TextDiff;
|
||||
case DiffOperation.ArrayMove: return DeltaType.Moved;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case JTokenType.Object:
|
||||
return DeltaType.Node;
|
||||
}
|
||||
|
||||
return DeltaType.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace JsonDiffPatchDotNet.Formatters
|
||||
{
|
||||
public enum DeltaType
|
||||
{
|
||||
Unknown,
|
||||
Unchanged,
|
||||
Added,
|
||||
Moved,
|
||||
Deleted,
|
||||
MoveDestination,
|
||||
Modified,
|
||||
Node,
|
||||
TextDiff
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace JsonDiffPatchDotNet.Formatters
|
||||
{
|
||||
public interface IFormatContext<out TResult>
|
||||
{
|
||||
TResult Result();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace JsonDiffPatchDotNet.Formatters.JsonPatch
|
||||
{
|
||||
public class JsonDeltaFormatter : BaseDeltaFormatter<JsonFormatContext, IList<Operation>>
|
||||
{
|
||||
protected override bool IncludeMoveDestinations => true;
|
||||
|
||||
protected override void Format(DeltaType type, JsonFormatContext context, JToken delta, JToken leftValue, string key, string leftKey, MoveDestination movedFrom)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case DeltaType.Added:
|
||||
FormatAdded(context, delta);
|
||||
break;
|
||||
|
||||
case DeltaType.Node:
|
||||
FormatNode(context, delta, leftValue);
|
||||
break;
|
||||
|
||||
case DeltaType.Modified:
|
||||
FormatModified(context, delta);
|
||||
break;
|
||||
|
||||
case DeltaType.Deleted:
|
||||
FormatDeleted(context);
|
||||
break;
|
||||
|
||||
case DeltaType.Moved:
|
||||
FormatMoved(context, delta);
|
||||
break;
|
||||
|
||||
case DeltaType.Unknown:
|
||||
case DeltaType.Unchanged:
|
||||
case DeltaType.MoveDestination:
|
||||
break;
|
||||
|
||||
case DeltaType.TextDiff:
|
||||
throw new InvalidOperationException("JSON RFC 6902 does not support TextDiff.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void NodeBegin(JsonFormatContext context, string key, string leftKey, DeltaType type, NodeType nodeType, bool isLast)
|
||||
{
|
||||
context.Path.Add(leftKey);
|
||||
}
|
||||
|
||||
protected override void NodeEnd(JsonFormatContext context, string key, string leftKey, DeltaType type, NodeType nodeType, bool isLast)
|
||||
{
|
||||
if (context.Path.Count > 0)
|
||||
context.Path.RemoveAt(context.Path.Count - 1);
|
||||
}
|
||||
|
||||
protected override void RootBegin(JsonFormatContext context, DeltaType type, NodeType nodeType) { }
|
||||
|
||||
protected override void RootEnd(JsonFormatContext context, DeltaType type, NodeType nodeType) { }
|
||||
|
||||
private void FormatNode(JsonFormatContext context, JToken delta, JToken left)
|
||||
{
|
||||
FormatDeltaChildren(context, delta, left);
|
||||
}
|
||||
|
||||
private void FormatAdded(JsonFormatContext context, JToken delta)
|
||||
{
|
||||
context.PushCurrentOp(OperationTypes.Add, delta[0]);
|
||||
}
|
||||
|
||||
private void FormatModified(JsonFormatContext context, JToken delta)
|
||||
{
|
||||
context.PushCurrentOp(OperationTypes.Replace, delta[1]);
|
||||
}
|
||||
|
||||
private void FormatDeleted(JsonFormatContext context)
|
||||
{
|
||||
context.PushCurrentOp(OperationTypes.Remove);
|
||||
}
|
||||
|
||||
private void FormatMoved(JsonFormatContext context, JToken delta)
|
||||
{
|
||||
context.PushMoveOp(delta[1].ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace JsonDiffPatchDotNet.Formatters.JsonPatch
|
||||
{
|
||||
public class JsonFormatContext : IFormatContext<IList<Operation>>
|
||||
{
|
||||
public JsonFormatContext()
|
||||
{
|
||||
Operations = new List<Operation>();
|
||||
Path = new List<string>();
|
||||
}
|
||||
|
||||
public IList<Operation> Operations { get; }
|
||||
|
||||
public IList<string> Path { get; }
|
||||
|
||||
public IList<Operation> Result()
|
||||
{
|
||||
return Operations;
|
||||
}
|
||||
|
||||
public void PushCurrentOp(string op)
|
||||
{
|
||||
Operations.Add(new Operation(op, CurrentPath(), null));
|
||||
}
|
||||
|
||||
public void PushCurrentOp(string op, object value)
|
||||
{
|
||||
Operations.Add(new Operation(op, CurrentPath(), null, value));
|
||||
}
|
||||
|
||||
public void PushMoveOp(string to)
|
||||
{
|
||||
Operations.Add(new Operation(OperationTypes.Move, ToPath(to), CurrentPath()));
|
||||
}
|
||||
|
||||
private string CurrentPath()
|
||||
{
|
||||
return $"/{string.Join("/", Path)}";
|
||||
}
|
||||
|
||||
private string ToPath(string toPath)
|
||||
{
|
||||
var to = Path.ToList();
|
||||
to[to.Count - 1] = toPath;
|
||||
return $"/{string.Join("/", to)}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace JsonDiffPatchDotNet.Formatters.JsonPatch
|
||||
{
|
||||
public class Operation
|
||||
{
|
||||
public Operation() { }
|
||||
|
||||
public Operation(string op, string path, string from)
|
||||
{
|
||||
Op = op;
|
||||
Path = path;
|
||||
From = from;
|
||||
}
|
||||
|
||||
public Operation(string op, string path, string from, object value)
|
||||
{
|
||||
Op = op;
|
||||
Path = path;
|
||||
From = from;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[JsonProperty("path")]
|
||||
public string Path { get; set; }
|
||||
|
||||
[JsonProperty("op")]
|
||||
public string Op { get; set; }
|
||||
|
||||
[JsonProperty("from")]
|
||||
public string From { get; set; }
|
||||
|
||||
[JsonProperty("value")]
|
||||
public object Value { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
namespace JsonDiffPatchDotNet.Formatters.JsonPatch
|
||||
{
|
||||
public static class OperationTypes
|
||||
{
|
||||
public const string Add = "add";
|
||||
|
||||
public const string Replace = "replace";
|
||||
|
||||
public const string Move = "move";
|
||||
|
||||
public const string Remove = "remove";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace JsonDiffPatchDotNet.Formatters
|
||||
{
|
||||
public class MoveDestination
|
||||
{
|
||||
public MoveDestination(string key, JToken value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
|
||||
public JToken Value { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace JsonDiffPatchDotNet.Formatters
|
||||
{
|
||||
public enum NodeType
|
||||
{
|
||||
Unknown,
|
||||
Object,
|
||||
Array
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue