-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add unit test support to system text json version (#64)
- Loading branch information
Showing
3 changed files
with
256 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |