Skip to content

Commit

Permalink
add unit test support to system text json version (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
caleidon authored Aug 19, 2024
1 parent 11381cf commit fba28d8
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 1 deletion.
99 changes: 99 additions & 0 deletions src/6.0-JsonMergePatch.SystemText/Builders/DiffBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace Morcatko.AspNetCore.JsonMergePatch.SystemText.Builders
{
public static class DiffBuilder
{
public static JsonDocument Build<TModel>(TModel original, TModel patched) where TModel : class
{
var originalJson = JsonSerializer.SerializeToElement(original);
var patchedJson = JsonSerializer.SerializeToElement(patched);
return BuildDiff(originalJson, patchedJson);
}

private static JsonDocument BuildDiff(JsonElement original, JsonElement patched)
{
if (original.ValueKind == JsonValueKind.Null && patched.ValueKind == JsonValueKind.Null)
return JsonDocument.Parse("{}");

if (original.ValueKind == JsonValueKind.Null)
return JsonDocument.Parse(patched.GetRawText());

if (patched.ValueKind == JsonValueKind.Null)
return JsonDocument.Parse("null");

if (original.ValueKind == JsonValueKind.Array || patched.ValueKind == JsonValueKind.Array)
return BuildArrayDiff(original, patched);

return original.ValueKind == JsonValueKind.Object ?
BuildObjectDiff(original, patched) : BuildValueDiff(original, patched);
}

private static JsonDocument BuildObjectDiff(JsonElement original, JsonElement patched)
{
var result = new Dictionary<string, JsonElement>();

var propertyNames = original.EnumerateObject()
.Select(p => p.Name)
.Union(patched.EnumerateObject().Select(p => p.Name))
.Distinct();

foreach (var propertyName in propertyNames)
{
var originalPropExists = original.TryGetProperty(propertyName, out var originalValue);
var patchedPropExists = patched.TryGetProperty(propertyName, out var patchedValue);

// If the property exists in both and is unchanged, skip it
if (originalPropExists &&
patchedPropExists &&
originalValue.ValueKind == patchedValue.ValueKind &&
originalValue.ToString() == patchedValue.ToString())
{
continue;
}

var patchToken = BuildDiff(
originalPropExists ? originalValue : default,
patchedPropExists ? patchedValue : default
);

if (patchToken != null && patchToken.RootElement.ValueKind != JsonValueKind.Undefined)
result[propertyName] = patchToken.RootElement.Clone();
}

if (!result.Any()) {
return JsonDocument.Parse("{}");
}

var serializedResult = JsonSerializer.Serialize(result);
return JsonDocument.Parse(serializedResult);

}

private static JsonDocument BuildValueDiff(JsonElement original, JsonElement patched)
{
return JsonDocument.Parse(!original.Equals(patched) ? patched.GetRawText() : "{}");
}

private static JsonDocument BuildArrayDiff(JsonElement original, JsonElement patched)
{
return JsonDocument.Parse(JsonArrayEquals(original, patched) ? "{}" : patched.GetRawText());

bool JsonArrayEquals(JsonElement left, JsonElement right)
{
if (left.GetArrayLength() != right.GetArrayLength())
return false;

for (int i = 0; i < left.GetArrayLength(); i++)
{
var diff = BuildDiff(left[i], right[i]);
if (diff.RootElement.ValueKind != JsonValueKind.Undefined)
return false;
}
return true;
}
}
}
}
41 changes: 40 additions & 1 deletion src/6.0-JsonMergePatch.SystemText/Builders/PatchBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@

namespace Morcatko.AspNetCore.JsonMergePatch.SystemText.Builders
{
public static class PatchBuilder<TModel> where TModel : class
{
public static JsonMergePatchDocument<TModel> Build(TModel original, TModel patched, JsonMergePatchOptions options = null)
{
var diff = DiffBuilder.Build(original, patched);
var jsonElement = diff != null ? diff.RootElement.Clone() : JsonDocument.Parse("{}").RootElement;
return PatchBuilder.CreatePatchDocument<TModel>(jsonElement, options);
}

public static JsonMergePatchDocument<TModel> Build(string jsonObjectPatch, JsonSerializerOptions jsonOptions = null, JsonMergePatchOptions options = null)
{
var jsonElement = JsonDocument.Parse(jsonObjectPatch).RootElement;
return PatchBuilder.CreatePatchDocument<TModel>(jsonElement, jsonOptions ?? new JsonSerializerOptions(), options ?? new JsonMergePatchOptions());
}

public static JsonMergePatchDocument<TModel> Build(object jsonObjectPatch, JsonMergePatchOptions options = null)
{
var json = JsonSerializer.Serialize(jsonObjectPatch);
var jsonElement = JsonDocument.Parse(json).RootElement;
return PatchBuilder.CreatePatchDocument<TModel>(jsonElement, options);
}

public static JsonMergePatchDocument<TModel> Build(JsonElement jsonObjectPatch, JsonMergePatchOptions options = null)
{
return PatchBuilder.CreatePatchDocument<TModel>(jsonObjectPatch, options);
}
}

public static class PatchBuilder
{
private static object ToObject(this JsonElement jsonElement)
Expand Down Expand Up @@ -67,6 +95,17 @@ private static void AddOperation(IInternalJsonMergePatchDocument jsonMergePatchD
}

static readonly Type internalJsonMergePatchDocumentType = typeof(InternalJsonMergePatchDocument<>);

internal static JsonMergePatchDocument<TModel> CreatePatchDocument<TModel>(JsonElement patchObject, JsonMergePatchOptions options = null) where TModel : class
{
return CreatePatchDocument(typeof(TModel), patchObject, new JsonSerializerOptions(), options ?? new JsonMergePatchOptions()) as JsonMergePatchDocument<TModel>;
}

internal static JsonMergePatchDocument<TModel> CreatePatchDocument<TModel>(JsonElement patchObject, JsonSerializerOptions jsonOptions, JsonMergePatchOptions mergePatchOptions) where TModel : class
{
return CreatePatchDocument(typeof(TModel), patchObject, jsonOptions, mergePatchOptions) as JsonMergePatchDocument<TModel>;
}

internal static IInternalJsonMergePatchDocument CreatePatchDocument(Type modelType, JsonElement jsonElement, JsonSerializerOptions jsonOptions, JsonMergePatchOptions mergePatchOptions)
{
var jsonMergePatchType = internalJsonMergePatchDocumentType.MakeGenericType(modelType);
Expand All @@ -78,4 +117,4 @@ internal static IInternalJsonMergePatchDocument CreatePatchDocument(Type modelTy
return jsonMergePatchDocument;
}
}
}
}
117 changes: 117 additions & 0 deletions src/6.0-JsonMergePatch.Tests/PatchBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Linq;
using System.Text.Json;
using Morcatko.AspNetCore.JsonMergePatch.SystemText.Builders;
using Xunit;

namespace Morcatko.AspNetCore.JsonMergePatch.Tests;

public class PatchBuilderTests
{
[Fact]
public void Build_FromOriginalAndPatched_ShouldCreatePatchDocument()
{
var original = new TestModel { Name = "John", Age = 30 };
var patched = new TestModel { Name = "John", Age = 31 };

var patchDocument = PatchBuilder<TestModel>.Build(original, patched);

Assert.NotNull(patchDocument);
Assert.Single(patchDocument.Operations);
Assert.Equal("/Age", patchDocument.Operations[0].path);
Assert.Equal(31, patchDocument.Operations[0].value);
}

[Fact]
public void Build_FromJsonString_ShouldCreatePatchDocument()
{
var jsonPatch = "{\"Age\":31}";
var patchDocument = PatchBuilder<TestModel>.Build(jsonPatch);

Assert.NotNull(patchDocument);
Assert.Single(patchDocument.Operations);
Assert.Equal("/Age", patchDocument.Operations[0].path);
Assert.Equal(31, patchDocument.Operations[0].value);
}

[Fact]
public void Build_FromObject_ShouldCreatePatchDocument()
{
var patchObject = new { Age = 31 };
var patchDocument = PatchBuilder<TestModel>.Build(patchObject);

Assert.NotNull(patchDocument);
Assert.Single(patchDocument.Operations);
Assert.Equal("/Age", patchDocument.Operations[0].path);
Assert.Equal(31, patchDocument.Operations[0].value);
}

[Fact]
public void Build_FromJsonElement_ShouldCreatePatchDocument()
{
var jsonElement = JsonDocument.Parse("{\"Age\":31}").RootElement;
var patchDocument = PatchBuilder<TestModel>.Build(jsonElement);

Assert.NotNull(patchDocument);
Assert.Single(patchDocument.Operations);
Assert.Equal("/Age", patchDocument.Operations[0].path);
Assert.Equal(31, patchDocument.Operations[0].value);
}

[Fact]
public void PatchNewsletter_ShouldApplyChangesCorrectly()
{
var originalModel = new TestModel
{
Name = "John",
Surname = "Appleseed",
Age = 30
};

var patchDto = new TestPatchDto
{
Name = "John", // Unchanged
Surname = null, // Set to null
Age = 31 // Changed
};

var patchDocument = PatchBuilder<TestPatchDto>.Build(new TestPatchDto
{
Name = originalModel.Name,
Surname = originalModel.Surname,
Age = originalModel.Age
}, patchDto);

Assert.NotNull(patchDocument);
Assert.Equal(2, patchDocument.Operations.Count); // Only 'TemplateName' and 'Age' should change

var templateNameOperation = patchDocument.Operations.FirstOrDefault(op => op.path == "/Surname");
var ageOperation = patchDocument.Operations.FirstOrDefault(op => op.path == "/Age");

Assert.NotNull(templateNameOperation);
Assert.Null(templateNameOperation.value); // Set to null

Assert.NotNull(ageOperation);
Assert.Equal(31, ageOperation.value); // Updated to 31

patchDocument.ApplyToT(originalModel);

Assert.Equal("John", originalModel.Name); // Unchanged
Assert.Equal(31, originalModel.Age); // Updated
Assert.Null(originalModel.Surname); // Set to null
}
}

public class TestPatchDto
{
public string? Name { get; set; }
public string? Surname { get; set; }
public int? Age { get; set; }
}


public class TestModel
{
public string Name { get; init; }
public string Surname { get; init; }
public int Age { get; init; }
}

0 comments on commit fba28d8

Please sign in to comment.