Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions JsonDiffPatch/CompareOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace JsonDiffPatch
{
public class CompareOptions
{
public static readonly CompareOptions Default = new CompareOptions();

public bool EnableIdentifierCheck { get; }
public string IdentifierProperty { get; }

public CompareOptions(bool enableIdentifierCheck = false, string identifierProperty = "id")
{
EnableIdentifierCheck = enableIdentifierCheck;
IdentifierProperty = identifierProperty;
}
}
}
101 changes: 59 additions & 42 deletions JsonDiffPatch/JsonDiffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ internal static Operation Replace(string path, string key, JToken value)
return Build("replace", path, key, value);
}

internal static IEnumerable<Operation> CalculatePatch(JToken left, JToken right, bool useIdToDetermineEquality,
string path = "")
internal static IEnumerable<Operation> CalculatePatch(JToken left, JToken right, CompareOptions compareOptions, string path = "")
{
if (left.Type != right.Type)
{
Expand All @@ -57,7 +56,7 @@ internal static IEnumerable<Operation> CalculatePatch(JToken left, JToken right,
if (left.Type == JTokenType.Array)
{
Operation prev = null;
foreach (var operation in ProcessArray(left, right, path, useIdToDetermineEquality))
foreach (var operation in ProcessArray(left, right, path, compareOptions))
{
var prevRemove = prev as RemoveOperation;
var add = operation as AddOperation;
Expand Down Expand Up @@ -98,7 +97,7 @@ internal static IEnumerable<Operation> CalculatePatch(JToken left, JToken right,
foreach (var match in zipped)
{
string newPath = path + "/" + match.key;
foreach (var patch in CalculatePatch(match.left, match.right, useIdToDetermineEquality, newPath))
foreach (var patch in CalculatePatch(match.left, match.right, compareOptions, newPath))
yield return patch;
}
yield break;
Expand All @@ -114,13 +113,9 @@ internal static IEnumerable<Operation> CalculatePatch(JToken left, JToken right,
}
}

private static IEnumerable<Operation> ProcessArray(JToken left, JToken right, string path,
bool useIdPropertyToDetermineEquality)
private static IEnumerable<Operation> ProcessArray(JToken left, JToken right, string path, CompareOptions compareOptions)
{
var comparer = new CustomCheckEqualityComparer(useIdPropertyToDetermineEquality, new JTokenEqualityComparer());



var comparer = new CustomCheckEqualityComparer(compareOptions, new JTokenEqualityComparer());

int commonHead = 0;
int commonTail = 0;
Expand All @@ -134,7 +129,7 @@ private static IEnumerable<Operation> ProcessArray(JToken left, JToken right, st
if (comparer.Equals(array1[commonHead], array2[commonHead]) == false) break;

//diff and yield objects here
foreach (var operation in CalculatePatch(array1[commonHead], array2[commonHead], useIdPropertyToDetermineEquality, path + "/" + commonHead))
foreach (var operation in CalculatePatch(array1[commonHead], array2[commonHead], compareOptions, path + "/" + commonHead))
{
yield return operation;
}
Expand All @@ -149,7 +144,7 @@ private static IEnumerable<Operation> ProcessArray(JToken left, JToken right, st

var index1 = len1 - 1 - commonTail;
var index2 = len2 - 1 - commonTail;
foreach (var operation in CalculatePatch(array1[index1], array2[index2], useIdPropertyToDetermineEquality, path + "/" + index1))
foreach (var operation in CalculatePatch(array1[index1], array2[index2], compareOptions, path + "/" + index1))
{
yield return operation;
}
Expand All @@ -175,7 +170,7 @@ private static IEnumerable<Operation> ProcessArray(JToken left, JToken right, st
{
for (int i = 0; i < leftMiddle.Length; i++)
{
foreach (var operation in CalculatePatch(leftMiddle[i], rightMiddle[i], useIdPropertyToDetermineEquality, path + "/" + commonHead))
foreach (var operation in CalculatePatch(leftMiddle[i], rightMiddle[i], compareOptions, path + "/" + commonHead))
{
yield return operation;
}
Expand Down Expand Up @@ -332,67 +327,89 @@ public int GetHashCode(KeyValuePair<string, JToken> obj)
/// <summary>
///
/// </summary>
/// <param name="@from"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns></returns>
public PatchDocument Diff(JToken @from, JToken to)
{
return new PatchDocument(CalculatePatch(@from, to, CompareOptions.Default).ToArray());
}

/// <summary>
///
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="compareOptions">Use id comparison on array members to determine equality</param>
/// <returns></returns>
public PatchDocument Diff(JToken @from, JToken to, CompareOptions compareOptions)
{
if (compareOptions == null)
throw new ArgumentNullException(nameof(compareOptions));

return new PatchDocument(CalculatePatch(@from, to, compareOptions).ToArray());
}

/// <summary>
///
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="useIdPropertyToDetermineEquality">Use id propety on array members to determine equality</param>
/// <param name="useIdPropertyToDetermineEquality">Use id property on array members to determine equality</param>
/// <returns></returns>
[Obsolete("Use Diff(@from, to, compareOptions) instead. Kept for backwards compatibility only.")]
public PatchDocument Diff(JToken @from, JToken to, bool useIdPropertyToDetermineEquality)
{
return new PatchDocument(CalculatePatch(@from, to, useIdPropertyToDetermineEquality).ToArray());
CompareOptions compareOptions = new CompareOptions(useIdPropertyToDetermineEquality);

return new PatchDocument(CalculatePatch(@from, to, compareOptions).ToArray());
}
}

internal class CustomCheckEqualityComparer : IEqualityComparer<JToken>
{
private readonly bool _enableIdCheck;
private readonly CompareOptions _compareOptions;
private readonly IEqualityComparer<JToken> _inner;

public CustomCheckEqualityComparer(bool enableIdCheck, IEqualityComparer<JToken> inner)
public CustomCheckEqualityComparer(CompareOptions compareOptions, IEqualityComparer<JToken> inner)
{
_enableIdCheck = enableIdCheck;
_compareOptions = compareOptions;
_inner = inner;
}

public bool Equals(JToken x, JToken y)
{
if (_enableIdCheck && x.Type == JTokenType.Object && y.Type == JTokenType.Object)
if (_compareOptions.EnableIdentifierCheck && x.Type == JTokenType.Object && y.Type == JTokenType.Object)
{
var xIdToken = x["id"];
var yIdToken = y["id"];
var xIdToken = x[_compareOptions.IdentifierProperty];
var yIdToken = y[_compareOptions.IdentifierProperty];

var xId = xIdToken != null ? xIdToken.Value<string>() : null;
var yId = yIdToken != null ? yIdToken.Value<string>() : null;
if (xId != null && xId == yId)
if (xIdToken != null && yIdToken != null)
{
return true;
if (xIdToken.HasValues && y.HasValues)
return _inner.Equals(xIdToken, yIdToken);

if (xIdToken.HasValues != yIdToken.HasValues)
return false;
}

var xId = xIdToken?.Value<string>();
var yId = yIdToken?.Value<string>();

return (xId != null && xId == yId);
}
return _inner.Equals(x, y);
}

public int GetHashCode(JToken obj)
{
if (_enableIdCheck && obj.Type == JTokenType.Object)
if (_compareOptions.EnableIdentifierCheck && obj.Type == JTokenType.Object)
{
var xIdToken = obj["id"];
var xIdToken = obj[_compareOptions.IdentifierProperty];
var xId = xIdToken != null && xIdToken.HasValues ? xIdToken.Value<string>() : null;
if (xId != null) return xId.GetHashCode() + _inner.GetHashCode(obj);
}
return _inner.GetHashCode(obj);
}

public static bool HaveEqualIds(JToken x, JToken y)
{
if (x.Type == JTokenType.Object && y.Type == JTokenType.Object)
{
var xIdToken = x["id"];
var yIdToken = y["id"];

var xId = xIdToken != null ? xIdToken.Value<string>() : null;
var yId = yIdToken != null ? yIdToken.Value<string>() : null;
return xId != null && xId == yId;
}
return false;
}
}
}
67 changes: 62 additions & 5 deletions JsonDiffPatchTests/DiffTests2.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using System.Reflection;
using JsonDiffPatch;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
Expand All @@ -10,25 +12,80 @@ public class DiffTests2 {
[Test]
public void ComplexExampleWithDeepArrayChange()
{

var leftPath = @".\samples\scene{0}a.json";
var rightPath = @".\samples\scene{0}b.json";
var currentDir = Path.GetDirectoryName(Assembly.GetCallingAssembly().CodeBase);
var leftPath = Path.Combine(currentDir, @"Samples\scene{0}a.json").Replace("file://", "").Replace("file:\\", "");
var rightPath = Path.Combine(currentDir, @"Samples\scene{0}b.json").Replace("file://", "").Replace("file:\\", "");
var i = 1;
var filesFound = false;
while(File.Exists(string.Format(leftPath, i)))
{
filesFound = true;

var scene1Text = File.ReadAllText(string.Format(leftPath, i));
var scene1 = JToken.Parse(scene1Text);
var scene2Text = File.ReadAllText(string.Format(rightPath, i));
var scene2 = JToken.Parse(scene2Text);
var patchDoc = new JsonDiffer().Diff(scene1, scene2, true);
var patchDoc = new JsonDiffer().Diff(scene1, scene2, new CompareOptions(true));
//Assert.AreEqual("[{\"op\":\"remove\",\"path\":\"/items/0/entities/1\"}]",
var patcher = new JsonPatcher();
patcher.Patch(ref scene1, patchDoc);
Assert.True(JToken.DeepEquals(scene1, scene2));
i++;
}

Assert.IsTrue(filesFound);
}

[Test]
public void ComplexExampleWithDeepArrayChangeOtherIdProperty()
{
var currentDir = Path.GetDirectoryName(Assembly.GetCallingAssembly().CodeBase);
var leftPath = Path.Combine(currentDir, @"Samples\scene{0}a_otherid.json").Replace("file://", "").Replace("file:\\", "");
var rightPath = Path.Combine(currentDir, @"Samples\scene{0}b_otherid.json").Replace("file://", "").Replace("file:\\", "");
var i = 1;
var filesFound = false;
while (File.Exists(Path.Combine(currentDir, string.Format(leftPath, i))))
{
filesFound = true;

var scene1Text = File.ReadAllText(Path.Combine(currentDir, string.Format(leftPath, i)));
var scene1 = JToken.Parse(scene1Text);
var scene2Text = File.ReadAllText(Path.Combine(currentDir, string.Format(rightPath, i)));
var scene2 = JToken.Parse(scene2Text);
var patchDoc = new JsonDiffer().Diff(scene1, scene2, new CompareOptions(true, "otherId"));
var patcher = new JsonPatcher();
patcher.Patch(ref scene1, patchDoc);
Assert.True(JToken.DeepEquals(scene1, scene2));
i++;
}

Assert.IsTrue(filesFound);
}

[Test]
public void ComplexExampleWithDeepArrayChangeComplexIdProperty()
{
var currentDir = Path.GetDirectoryName(Assembly.GetCallingAssembly().CodeBase);
var leftPath = Path.Combine(currentDir, @"Samples\scene{0}a_complex_id.json").Replace("file://", "").Replace("file:\\", "");
var rightPath = Path.Combine(currentDir, @"Samples\scene{0}b_complex_id.json").Replace("file://", "").Replace("file:\\", "");
var i = 1;
var filesFound = false;
while (File.Exists(string.Format(leftPath, i)))
{
filesFound = true;

var scene1Text = File.ReadAllText(string.Format(leftPath, i));
var scene1 = JToken.Parse(scene1Text);
var scene2Text = File.ReadAllText(string.Format(rightPath, i));
var scene2 = JToken.Parse(scene2Text);
var patchDoc = new JsonDiffer().Diff(scene1, scene2, new CompareOptions(true));
var patcher = new JsonPatcher();
patcher.Patch(ref scene1, patchDoc);
Assert.True(JToken.DeepEquals(scene1, scene2));
i++;
}

Assert.IsTrue(filesFound);
}
}
}
76 changes: 69 additions & 7 deletions JsonDiffPatchTests/JsonDiffPatch.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,83 @@
<ItemGroup>
<None Remove="Samples\LoadTest1.json" />
<None Remove="Samples\scene1a.json" />
<None Remove="Samples\scene1a_complex_id.json" />
<None Remove="Samples\scene1a_otherid.json" />
<None Remove="Samples\scene1b.json" />
<None Remove="Samples\scene1b_complex_id.json" />
<None Remove="Samples\scene1b_otherid.json" />
<None Remove="Samples\scene2a.json" />
<None Remove="Samples\scene2a_complex_id.json" />
<None Remove="Samples\scene2a_otherid.json" />
<None Remove="Samples\scene2b.json" />
<None Remove="Samples\scene2b_complex_id.json" />
<None Remove="Samples\scene2b_otherid.json" />
<None Remove="Samples\scene3a.json" />
<None Remove="Samples\scene3a_complex_id.json" />
<None Remove="Samples\scene3a_otherid.json" />
<None Remove="Samples\scene3b.json" />
<None Remove="Samples\scene3b_complex_id.json" />
<None Remove="Samples\scene3b_otherid.json" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Samples\LoadTest1.json" />
<EmbeddedResource Include="Samples\scene1a.json" />
<EmbeddedResource Include="Samples\scene1b.json" />
<EmbeddedResource Include="Samples\scene2a.json" />
<EmbeddedResource Include="Samples\scene2b.json" />
<EmbeddedResource Include="Samples\scene3a.json" />
<EmbeddedResource Include="Samples\scene3b.json" />
<EmbeddedResource Include="Samples\LoadTest1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene1a_complex_id.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene1a_otherid.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene1a.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene1b_complex_id.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene1b_otherid.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene1b.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene2a_complex_id.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene2a_otherid.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene2a.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene2b_complex_id.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene2b_otherid.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene2b.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene3a_complex_id.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene3a_otherid.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene3a.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene3b_complex_id.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene3b_otherid.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Samples\scene3b.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
Expand Down
Loading