-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce json patch rewriting to convert patches into specific changes.
Rewrite changes to Sense.PartOfSpeechId into SetPartOfSpeechChange.
- Loading branch information
Showing
5 changed files
with
211 additions
and
3 deletions.
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,39 @@ | ||
using LcmCrdt.Changes; | ||
using LcmCrdt.Objects; | ||
using SystemTextJsonPatch; | ||
|
||
namespace LcmCrdt.Tests; | ||
|
||
public class JsonPatchRewriteTests | ||
{ | ||
[Fact] | ||
public void RewritePartOfSpeechChangesIntoSetPartOfSpeechChange() | ||
{ | ||
var newPartOfSpeechId = Guid.NewGuid(); | ||
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>(); | ||
patchDocument.Replace(s => s.PartOfSpeechId, newPartOfSpeechId); | ||
patchDocument.Replace(s => s.Gloss["en"], "new gloss"); | ||
|
||
var changes = Sense.ChangesFromJsonPatch(Guid.NewGuid(), patchDocument).ToArray(); | ||
|
||
var setPartOfSpeechChange = changes.OfType<SetPartOfSpeechChange>().Should().ContainSingle().Subject; | ||
setPartOfSpeechChange.PartOfSpeechId.Should().Be(newPartOfSpeechId); | ||
|
||
var patchChange = changes.OfType<JsonPatchChange<Sense>>().Should().ContainSingle().Subject; | ||
patchChange.PatchDocument.Operations.Should().ContainSingle().Subject.Value.Should().Be("new gloss"); | ||
} | ||
|
||
[Fact] | ||
public void JsonPatchChangeRewriteDoesNotReturnEmptyPatchChanges() | ||
{ | ||
var newPartOfSpeechId = Guid.NewGuid(); | ||
var patchDocument = new JsonPatchDocument<MiniLcm.Sense>(); | ||
patchDocument.Replace(s => s.PartOfSpeechId, newPartOfSpeechId); | ||
|
||
var changes = Sense.ChangesFromJsonPatch(Guid.NewGuid(), patchDocument).ToArray(); | ||
|
||
var setPartOfSpeechChange = changes.Should().ContainSingle() | ||
.Subject.Should().BeOfType<SetPartOfSpeechChange>().Subject; | ||
setPartOfSpeechChange.PartOfSpeechId.Should().Be(newPartOfSpeechId); | ||
} | ||
} |
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,19 @@ | ||
using Crdt.Changes; | ||
using Crdt.Entities; | ||
|
||
namespace LcmCrdt.Changes; | ||
|
||
public class SetPartOfSpeechChange(Guid entityId, Guid? partOfSpeechId) : EditChange<Sense>(entityId), ISelfNamedType<SetPartOfSpeechChange> | ||
{ | ||
public Guid? PartOfSpeechId { get; } = partOfSpeechId; | ||
|
||
public override async ValueTask ApplyChange(Sense entity, ChangeContext context) | ||
{ | ||
entity.PartOfSpeechId = PartOfSpeechId switch | ||
{ | ||
null => null, | ||
var id when await context.IsObjectDeleted(id.Value) => null, | ||
_ => PartOfSpeechId | ||
}; | ||
} | ||
} |
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
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,124 @@ | ||
using System.Globalization; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
using Crdt.Changes; | ||
using SystemTextJsonPatch; | ||
using SystemTextJsonPatch.Operations; | ||
|
||
namespace LcmCrdt.Utils; | ||
|
||
public static class JsonPatchRewriter | ||
{ | ||
|
||
public static IEnumerable<IChange> RewriteChanges<T, TProp>(this JsonPatchDocument<T> patchDocument, | ||
Expression<Func<T, TProp>> expr, Func<TProp?, OperationType, IChange> changeFactory) where T : class | ||
{ | ||
var path = GetPath(expr, null, patchDocument.Options); | ||
foreach (var operation in patchDocument.Operations.ToArray()) | ||
{ | ||
if (operation.Path == path) | ||
{ | ||
patchDocument.Operations.Remove(operation); | ||
yield return changeFactory((TProp?)operation.Value, operation.OperationType); | ||
} | ||
} | ||
} | ||
|
||
//won't work until dotnet 9 per https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute?view=net-8.0#remarks | ||
//this is due to generics, for now we will use the version copied below | ||
private static class JsonPatchAccessors<T> where T : class | ||
{ | ||
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPath")] | ||
public static extern string ExpressionToJsonPointer<TProp>( | ||
JsonPatchDocument<T> patchDocument, | ||
Expression<Func<T, TProp>> expr, | ||
string? position); | ||
} | ||
|
||
|
||
public static string GetPath<TProp, TModel>(Expression<Func<TModel, TProp>> expr, string? position, JsonSerializerOptions options) | ||
{ | ||
var segments = GetPathSegments(expr.Body, options); | ||
var path = string.Join("/", segments); | ||
if (position != null) | ||
{ | ||
path += "/" + position; | ||
if (segments.Count == 0) | ||
{ | ||
return path; | ||
} | ||
} | ||
|
||
return "/" + path; | ||
} | ||
|
||
private static List<string> GetPathSegments(Expression? expr, JsonSerializerOptions options) | ||
{ | ||
if (expr == null || expr.NodeType == ExpressionType.Parameter) return []; | ||
var listOfSegments = new List<string>(); | ||
switch (expr.NodeType) | ||
{ | ||
case ExpressionType.ArrayIndex: | ||
var binaryExpression = (BinaryExpression)expr; | ||
listOfSegments.AddRange(GetPathSegments(binaryExpression.Left, options)); | ||
listOfSegments.Add(binaryExpression.Right.ToString()); | ||
return listOfSegments; | ||
|
||
case ExpressionType.Call: | ||
var methodCallExpression = (MethodCallExpression)expr; | ||
listOfSegments.AddRange(GetPathSegments(methodCallExpression.Object, options)); | ||
listOfSegments.Add(EvaluateExpression(methodCallExpression.Arguments[0])); | ||
return listOfSegments; | ||
|
||
case ExpressionType.Convert: | ||
listOfSegments.AddRange(GetPathSegments(((UnaryExpression)expr).Operand, options)); | ||
return listOfSegments; | ||
|
||
case ExpressionType.MemberAccess: | ||
var memberExpression = (MemberExpression)expr; | ||
listOfSegments.AddRange(GetPathSegments(memberExpression.Expression, options)); | ||
// Get property name, respecting JsonProperty attribute | ||
listOfSegments.Add(GetPropertyNameFromMemberExpression(memberExpression, options)); | ||
return listOfSegments; | ||
|
||
default: | ||
throw new InvalidOperationException($"type of expression not supported {expr}"); | ||
} | ||
} | ||
|
||
private static string GetPropertyNameFromMemberExpression(MemberExpression memberExpression, JsonSerializerOptions options) | ||
{ | ||
var jsonPropertyNameAttr = memberExpression.Member.GetCustomAttribute<JsonPropertyNameAttribute>(); | ||
|
||
if (jsonPropertyNameAttr != null && !string.IsNullOrEmpty(jsonPropertyNameAttr.Name)) | ||
{ | ||
return jsonPropertyNameAttr.Name; | ||
} | ||
|
||
var memberName = memberExpression.Member.Name; | ||
|
||
if (options.PropertyNamingPolicy != null) | ||
{ | ||
return options.PropertyNamingPolicy.ConvertName(memberName); | ||
} | ||
|
||
return memberName; | ||
} | ||
|
||
|
||
// Evaluates the value of the key or index which may be an int or a string, | ||
// or some other expression type. | ||
// The expression is converted to a delegate and the result of executing the delegate is returned as a string. | ||
private static string EvaluateExpression(Expression expression) | ||
{ | ||
var converted = Expression.Convert(expression, typeof(object)); | ||
var fakeParameter = Expression.Parameter(typeof(object), null); | ||
var lambda = Expression.Lambda<Func<object?, object>>(converted, fakeParameter); | ||
var func = lambda.Compile(); | ||
|
||
return Convert.ToString(func(null), CultureInfo.InvariantCulture) ?? ""; | ||
} | ||
} |