From c5104a61d886cfa2e4623c7dbf6af57f54501234 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Wed, 26 Jun 2024 16:05:33 +0300 Subject: [PATCH] Handle creation of missing nested objects for JSON MergePatch (#276) This also adds a `create` argument to make creation of nodes optional. --- .../Internal/ObjectVisitor.cs | 6 ++ .../JsonMergePatchDocumentOfT.cs | 10 ++- ...nMergePatchDocumentConverterHelperTests.cs | 85 +++++++++++++------ 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/Tingle.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs b/src/Tingle.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs index a8f4e63..4b45b71 100644 --- a/src/Tingle.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs +++ b/src/Tingle.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs @@ -49,6 +49,12 @@ public bool TryVisit(ref object target, [NotNullWhen(true)] out IAdapter? adapte } } + // If we hit a null on an interior segment but we can create, then we should try to create. + if (next == null && create) + { + adapter.TryCreate(target, path.Segments[i], serializerOptions, out next, out errorMessage); + } + // If we hit a null on an interior segment then we need to stop traversing. if (next == null) { diff --git a/src/Tingle.AspNetCore.JsonPatch/JsonMergePatchDocumentOfT.cs b/src/Tingle.AspNetCore.JsonPatch/JsonMergePatchDocumentOfT.cs index 7ac49b3..25cdaac 100644 --- a/src/Tingle.AspNetCore.JsonPatch/JsonMergePatchDocumentOfT.cs +++ b/src/Tingle.AspNetCore.JsonPatch/JsonMergePatchDocumentOfT.cs @@ -30,11 +30,12 @@ public JsonMergePatchDocument(List> operations, JsonSerializer /// Apply this JsonMergePatchDocument /// /// Object to apply the JsonMergePatchDocument to - public void ApplyTo(TModel objectToApplyTo) + /// Whether to create nested objects if they do not exist + public void ApplyTo(TModel objectToApplyTo, bool create = true) { ArgumentNullException.ThrowIfNull(objectToApplyTo); - ApplyTo(objectToApplyTo, new ObjectAdapter(SerializerOptions, null, AdapterFactory.Default, create: true)); + ApplyTo(objectToApplyTo, new ObjectAdapter(SerializerOptions, null, AdapterFactory.Default, create)); } /// @@ -42,9 +43,10 @@ public void ApplyTo(TModel objectToApplyTo) /// /// Object to apply the JsonMergePatchDocument to /// Action to log errors - public void ApplyTo(TModel objectToApplyTo, Action logErrorAction) + /// Whether to create nested objects if they do not exist + public void ApplyTo(TModel objectToApplyTo, Action logErrorAction, bool create = true) { - ApplyTo(objectToApplyTo, new ObjectAdapter(SerializerOptions, logErrorAction, AdapterFactory.Default, create: true), logErrorAction); + ApplyTo(objectToApplyTo, new ObjectAdapter(SerializerOptions, logErrorAction, AdapterFactory.Default, create), logErrorAction); } /// diff --git a/tests/Tingle.AspNetCore.JsonPatch.Tests/JsonMergePatchDocumentConverterHelperTests.cs b/tests/Tingle.AspNetCore.JsonPatch.Tests/JsonMergePatchDocumentConverterHelperTests.cs index b4da1a7..62eb66e 100644 --- a/tests/Tingle.AspNetCore.JsonPatch.Tests/JsonMergePatchDocumentConverterHelperTests.cs +++ b/tests/Tingle.AspNetCore.JsonPatch.Tests/JsonMergePatchDocumentConverterHelperTests.cs @@ -159,47 +159,66 @@ public void Apply_Works() Assert.Equal("immigration", video.Description); Assert.Null(video.Name); Assert.Equal("123", video.Id); + + video = new Video { Metadata = new() { ["primary"] = "cake", } }; + doc = JsonSerializer.Deserialize>(node.ToJsonString(), serializerOptions)!; + var modelState = new ModelStateDictionary(); + doc.ApplyToSafely(video, modelState); + Assert.True(modelState.IsValid); + Assert.Empty(modelState); + + Assert.Equal("rudi shule", Assert.Contains("swa", video.Translations).Body); + Assert.Equal("google", Assert.Contains("swa", video.Translations).Provider); + Assert.Equal("hapa tu", Assert.Contains("primary", video.Metadata)); + Assert.Equal("pale tu", Assert.Contains("secondary", video.Metadata)); + Assert.Equal(["prod", "ken"], video.Tags); + Assert.Equal("immigration", video.Description); + Assert.Null(video.Name); + Assert.Equal("123", video.Id); } [Fact] - public void ApplyToSafely_Works() + public void Apply_Works_CreatesNodes() { var node = new JsonObject { - ["translations"] = new JsonObject + ["make"] = "Tesla", + ["model"] = "Model 3", + ["owner"] = new JsonObject { - ["swa"] = new JsonObject + ["name"] = "Elon Musk", + ["address"] = new JsonObject { - ["body"] = "rudi shule", - ["provider"] = "google", + ["country"] = "USA", }, }, - ["metadata"] = new JsonObject - { - ["primary"] = "hapa tu", - ["secondary"] = "pale tu", - }, - ["tags"] = new JsonArray { "prod", "ken", }, - ["description"] = "immigration", - ["name"] = null, }; - var video = new Video { Metadata = new() { ["primary"] = "cake", } }; + var vehicle = new Vehicle { Make = "Toyota", Model = "Corolla" }; var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - var doc = JsonSerializer.Deserialize>(node.ToJsonString(), serializerOptions)!; + var doc = JsonSerializer.Deserialize>(node.ToJsonString(), serializerOptions)!; + doc.ApplyTo(vehicle); + + Assert.Equal("Tesla", vehicle.Make); + Assert.Equal("Model 3", vehicle.Model); + Assert.NotNull(vehicle.Owner); + Assert.Equal("Elon Musk", vehicle.Owner.Name); + Assert.NotNull(vehicle.Owner.Address); + Assert.Equal("USA", vehicle.Owner.Address.Country); + + vehicle = new Vehicle { Make = "Toyota", Model = "Corolla" }; + doc = JsonSerializer.Deserialize>(node.ToJsonString(), serializerOptions)!; var modelState = new ModelStateDictionary(); - doc.ApplyToSafely(video, modelState); + doc.ApplyToSafely(vehicle, modelState); Assert.True(modelState.IsValid); Assert.Empty(modelState); - Assert.Equal("rudi shule", Assert.Contains("swa", video.Translations).Body); - Assert.Equal("google", Assert.Contains("swa", video.Translations).Provider); - Assert.Equal("hapa tu", Assert.Contains("primary", video.Metadata)); - Assert.Equal("pale tu", Assert.Contains("secondary", video.Metadata)); - Assert.Equal(["prod", "ken"], video.Tags); - Assert.Equal("immigration", video.Description); - Assert.Null(video.Name); - Assert.Equal("123", video.Id); + Assert.Equal("Tesla", vehicle.Make); + Assert.Equal("Model 3", vehicle.Model); + Assert.NotNull(vehicle.Owner); + Assert.Equal("Elon Musk", vehicle.Owner.Name); + Assert.NotNull(vehicle.Owner.Address); + Assert.Equal("USA", vehicle.Owner.Address.Country); } class Video @@ -217,4 +236,22 @@ class VideoTranslation public string? Body { get; set; } public string? Provider { get; set; } } + + class Vehicle + { + public string? Make { get; set; } + public string? Model { get; set; } + public VehicleOwner? Owner { get; set; } + } + + class VehicleOwner + { + public string? Name { get; set; } + public VehicleOwnerAddress? Address { get; set; } + } + + class VehicleOwnerAddress + { + public string? Country { get; set; } + } }