Skip to content

Commit

Permalink
Handle creation of missing nested objects for JSON MergePatch (#276)
Browse files Browse the repository at this point in the history
This also adds a `create` argument to make creation of nodes optional.
  • Loading branch information
mburumaxwell authored Jun 26, 2024
1 parent e34ba5f commit c5104a6
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 28 deletions.
6 changes: 6 additions & 0 deletions src/Tingle.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
10 changes: 6 additions & 4 deletions src/Tingle.AspNetCore.JsonPatch/JsonMergePatchDocumentOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,23 @@ public JsonMergePatchDocument(List<Operation<TModel>> operations, JsonSerializer
/// Apply this JsonMergePatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonMergePatchDocument to</param>
public void ApplyTo(TModel objectToApplyTo)
/// <param name="create">Whether to create nested objects if they do not exist</param>
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));
}

/// <summary>
/// Apply this JsonMergePatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonMergePatchDocument to</param>
/// <param name="logErrorAction">Action to log errors</param>
public void ApplyTo(TModel objectToApplyTo, Action<JsonPatchError> logErrorAction)
/// <param name="create">Whether to create nested objects if they do not exist</param>
public void ApplyTo(TModel objectToApplyTo, Action<JsonPatchError> 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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonMergePatchDocument<Video>>(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<JsonMergePatchDocument<Video>>(node.ToJsonString(), serializerOptions)!;
var doc = JsonSerializer.Deserialize<JsonMergePatchDocument<Vehicle>>(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<JsonMergePatchDocument<Vehicle>>(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
Expand All @@ -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; }
}
}

0 comments on commit c5104a6

Please sign in to comment.