diff --git a/JsonDiffPatch/AddOperation.cs b/JsonDiffPatch/AddOperation.cs index 8fb6a7c..ebf793c 100644 --- a/JsonDiffPatch/AddOperation.cs +++ b/JsonDiffPatch/AddOperation.cs @@ -18,21 +18,17 @@ public AddOperation(JsonPointer path, JToken value) : base(path) Value = value; } - public override void Write(JsonWriter writer) + public override void Write(IJsonObjectWriter writer) { - writer.WriteStartObject(); - - WriteOp(writer, "add"); - WritePath(writer,Path); - WriteValue(writer,Value); - - writer.WriteEndObject(); + writer.WriteOp("add"). + WritePath(Path). + WriteValue(Value); } public override void Read(JObject jOperation) { - Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); + Path = new JsonPointer(jOperation.GetValue("path")); Value = jOperation.GetValue("value"); } } diff --git a/JsonDiffPatch/CopyOperation.cs b/JsonDiffPatch/CopyOperation.cs index fed21a5..d5a21ff 100644 --- a/JsonDiffPatch/CopyOperation.cs +++ b/JsonDiffPatch/CopyOperation.cs @@ -18,21 +18,17 @@ public CopyOperation(JsonPointer path, JsonPointer fromPath) : base(path) FromPath = fromPath; } - public override void Write(JsonWriter writer) + public override void Write(IJsonObjectWriter writer) { - writer.WriteStartObject(); - - WriteOp(writer, "copy"); - WritePath(writer, Path); - WriteFromPath(writer, FromPath); - - writer.WriteEndObject(); + writer.WriteOp("copy"). + WritePath(Path). + WriteFromPath(FromPath); } public override void Read(JObject jOperation) { - Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); - FromPath = new JsonPointer(SplitPath((string)jOperation.GetValue("from"))); + Path = new JsonPointer(jOperation.GetValue("path")); + FromPath = new JsonPointer(jOperation.GetValue("from")); } } } diff --git a/JsonDiffPatch/IJsonObjectWriter.cs b/JsonDiffPatch/IJsonObjectWriter.cs new file mode 100644 index 0000000..a6c3fd7 --- /dev/null +++ b/JsonDiffPatch/IJsonObjectWriter.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Newtonsoft.Json.Linq; + +namespace JsonDiffPatch +{ + public interface IJsonObjectWriter + { + void WriteProperty(string name, string value); + + void WriteProperty(string name, JToken value); + } +} diff --git a/JsonDiffPatch/JsonDiffer.cs b/JsonDiffPatch/JsonDiffer.cs index 3bd531a..3c7a590 100644 --- a/JsonDiffPatch/JsonDiffer.cs +++ b/JsonDiffPatch/JsonDiffer.cs @@ -9,9 +9,9 @@ namespace JsonDiffPatch /// /// Parts adapted from https://github.com/benjamine/jsondiffpatch/blob/42ce1b6ca30c4d7a19688a020fce021a756b43cc/src/filters/arrays.js /// - public class JsonDiffer + public static class JsonDiffer { - internal static string Extend(string path, string extension) + private static string Extend(string path, string extension) { // TODO: JSON property name needs escaping for path ?? return path + "/" + EncodeKey(extension); @@ -31,22 +31,22 @@ private static Operation Build(string op, string path, string key, JToken value) (value == null ? "null" : value.ToString(Formatting.None)) + "}"); } - internal static Operation Add(string path, string key, JToken value) + private static Operation Add(string path, string key, JToken value) { return Build("add", path, key, value); } - internal static Operation Remove(string path, string key) + private static Operation Remove(string path, string key) { return Build("remove", path, key, null); } - internal static Operation Replace(string path, string key, JToken value) + private static Operation Replace(string path, string key, JToken value) { return Build("replace", path, key, value); } - internal static IEnumerable CalculatePatch(JToken left, JToken right, bool useIdToDetermineEquality, + private static IEnumerable CalculatePatch(JToken left, JToken right, bool useIdToDetermineEquality, string path = "") { if (left.Type != right.Type) @@ -306,7 +306,12 @@ private static IEnumerable ProcessArray(JToken left, JToken right, st private class MatchesKey : IEqualityComparer> { - public static MatchesKey Instance = new MatchesKey(); + public readonly static MatchesKey Instance = new MatchesKey(); + + private MatchesKey() + { + + } public bool Equals(KeyValuePair x, KeyValuePair y) { @@ -326,9 +331,9 @@ public int GetHashCode(KeyValuePair obj) /// /// Use id propety on array members to determine equality /// - public PatchDocument Diff(JToken @from, JToken to, bool useIdPropertyToDetermineEquality) + public static PatchDocument Diff(JToken @from, JToken to, bool useIdPropertyToDetermineEquality) { - return new PatchDocument(CalculatePatch(@from, to, useIdPropertyToDetermineEquality).ToArray()); + return new PatchDocument(CalculatePatch(@from, to, useIdPropertyToDetermineEquality)); } } diff --git a/JsonDiffPatch/JsonObjectWriterExtensions.cs b/JsonDiffPatch/JsonObjectWriterExtensions.cs new file mode 100644 index 0000000..fae7c27 --- /dev/null +++ b/JsonDiffPatch/JsonObjectWriterExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Newtonsoft.Json.Linq; +using Tavis; + +namespace JsonDiffPatch +{ + internal static class JsonObjectWriterExtensions + { + public static IJsonObjectWriter WriteOp(this IJsonObjectWriter writer, string op) + { + writer.WriteProperty("op", op); + return writer; + } + + public static IJsonObjectWriter WritePath(this IJsonObjectWriter writer, JsonPointer pointer) + { + writer.WriteProperty("path", pointer.ToString()); + return writer; + } + + public static IJsonObjectWriter WriteFromPath(this IJsonObjectWriter writer, JsonPointer pointer) + { + writer.WriteProperty("from", pointer.ToString()); + return writer; + } + public static IJsonObjectWriter WriteValue(this IJsonObjectWriter writer, JToken value) + { + writer.WriteProperty("value", value); + return writer; + } + } +} diff --git a/JsonDiffPatch/JsonPointerTokensSource.cs b/JsonDiffPatch/JsonPointerTokensSource.cs new file mode 100644 index 0000000..67c7716 --- /dev/null +++ b/JsonDiffPatch/JsonPointerTokensSource.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Newtonsoft.Json.Linq; + +namespace JsonDiffPatch +{ + internal class JsonPointerTokensSource : IEnumerable + { + private readonly IReadOnlyList _list; + private readonly int startIndex; + private readonly int lastIndex; + + public bool IsEmpty => lastIndex < startIndex; + + public JsonPointerTokensSource(string pointer) + { + _list = pointer.Split('/').ToList(); + startIndex = 1; + lastIndex = _list.Count - 1; + } + + private JsonPointerTokensSource(IReadOnlyList list, int startIndex, int lastIndex) + { + _list = list; + this.startIndex = startIndex; + this.lastIndex = lastIndex; + } + + public string Last() + { + return lastIndex >= startIndex ? _list[lastIndex] : string.Empty; + } + + internal JsonPointerTokensSource ForParent() + { + return new JsonPointerTokensSource(_list, startIndex, lastIndex - 1); + } + + public IEnumerator GetEnumerator() + { + for (int i = startIndex; i <= lastIndex; i++) + { + yield return _list[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/JsonDiffPatch/MoveOperation.cs b/JsonDiffPatch/MoveOperation.cs index 65c5fcd..ce2c4ef 100644 --- a/JsonDiffPatch/MoveOperation.cs +++ b/JsonDiffPatch/MoveOperation.cs @@ -20,21 +20,17 @@ public MoveOperation(JsonPointer path, JsonPointer fromPath) : base(path) FromPath = fromPath; } - public override void Write(JsonWriter writer) + public override void Write(IJsonObjectWriter writer) { - writer.WriteStartObject(); - - WriteOp(writer, "move"); - WritePath(writer, Path); - WriteFromPath(writer, FromPath); - - writer.WriteEndObject(); + writer.WriteOp("move"). + WritePath(Path). + WriteFromPath(FromPath); } public override void Read(JObject jOperation) { - Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); - FromPath = new JsonPointer(SplitPath((string)jOperation.GetValue("from"))); + Path = new JsonPointer(jOperation.GetValue("path")); + FromPath = new JsonPointer(jOperation.GetValue("from")); } } } diff --git a/JsonDiffPatch/Operation.cs b/JsonDiffPatch/Operation.cs index e4a3bee..fad995a 100644 --- a/JsonDiffPatch/Operation.cs +++ b/JsonDiffPatch/Operation.cs @@ -19,32 +19,7 @@ public Operation(JsonPointer path) Path = path; } - public abstract void Write(JsonWriter writer); - - protected static void WriteOp(JsonWriter writer, string op) - { - writer.WritePropertyName("op"); - writer.WriteValue(op); - } - - protected static void WritePath(JsonWriter writer, JsonPointer pointer) - { - writer.WritePropertyName("path"); - writer.WriteValue(pointer.ToString()); - } - - protected static void WriteFromPath(JsonWriter writer, JsonPointer pointer) - { - writer.WritePropertyName("from"); - writer.WriteValue(pointer.ToString()); - } - protected static void WriteValue(JsonWriter writer, JToken value) - { - writer.WritePropertyName("value"); - value.WriteTo(writer); - } - - protected static string[] SplitPath(string path) => path.Split('/').Skip(1).ToArray(); + public abstract void Write(IJsonObjectWriter writer); public abstract void Read(JObject jOperation); @@ -55,7 +30,7 @@ public static Operation Parse(string json) public static Operation Build(JObject jOperation) { - var op = PatchDocument.CreateOperation((string)jOperation["op"]); + var op = OperationCreator.Create((string)jOperation["op"]); op.Read(jOperation); return op; } diff --git a/JsonDiffPatch/OperationCollection.cs b/JsonDiffPatch/OperationCollection.cs new file mode 100644 index 0000000..630e7d2 --- /dev/null +++ b/JsonDiffPatch/OperationCollection.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace JsonDiffPatch +{ + internal class OperationCollection : IReadOnlyCollection + { + private readonly List _operations = new List(); + + public int Count => _operations.Count; + + public void Add(Operation operation) + { + _operations.Add(operation); + } + + internal void AddRange(IEnumerable operations) + { + _operations.AddRange(operations); + } + + public IEnumerator GetEnumerator() + { + return _operations.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _operations.GetEnumerator(); + } + } +} diff --git a/JsonDiffPatch/OperationCreator.cs b/JsonDiffPatch/OperationCreator.cs new file mode 100644 index 0000000..3cac98e --- /dev/null +++ b/JsonDiffPatch/OperationCreator.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonDiffPatch +{ + internal static class OperationCreator + { + public static Operation Create(string op) + { + switch (op) + { + case "add": return new AddOperation(); + case "copy": return new CopyOperation(); + case "move": return new MoveOperation(); + case "remove": return new RemoveOperation(); + case "replace": return new ReplaceOperation(); + case "test": return new TestOperation(); + } + return null; + } + } +} diff --git a/JsonDiffPatch/PatchDocument.cs b/JsonDiffPatch/PatchDocument.cs index 6c8556b..1356e9e 100644 --- a/JsonDiffPatch/PatchDocument.cs +++ b/JsonDiffPatch/PatchDocument.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -9,9 +9,24 @@ namespace JsonDiffPatch { public class PatchDocument { - private readonly List _Operations = new List(); + private readonly static PatchDocumentSerializer documentSerializer = new PatchDocumentSerializer(); + + private readonly OperationCollection _Operations = new OperationCollection(); + + public PatchDocument() + { + + } public PatchDocument(params Operation[] operations) + { + foreach (var operation in operations) + { + AddOperation(operation); + } + } + + public PatchDocument(IEnumerable operations) { _Operations.AddRange(operations); } @@ -56,71 +71,19 @@ public static PatchDocument Parse(string jsondocument) return Load(root); } - public static Operation CreateOperation(string op) - { - switch (op) - { - case "add": return new AddOperation(); - case "copy": return new CopyOperation(); - case "move": return new MoveOperation(); - case "remove": return new RemoveOperation(); - case "replace": return new ReplaceOperation(); - case "test" : return new TestOperation(); - } - return null; - } - - /// - /// Create memory stream with serialized version of PatchDocument - /// - /// public MemoryStream ToStream() { - var stream = new MemoryStream(); - CopyToStream(stream, Formatting.Indented); - stream.Flush(); - stream.Position = 0; - return stream; - } - - /// - /// Copy serialized version of Patch document to provided stream - /// - /// - /// - public void CopyToStream(Stream stream, Formatting formatting = Formatting.Indented) - { - var sw = new JsonTextWriter(new StreamWriter(stream)); - sw.Formatting = formatting; - - sw.WriteStartArray(); - - foreach (var operation in Operations) - { - operation.Write(sw); - } - - sw.WriteEndArray(); - - sw.Flush(); + return documentSerializer.ToStream(this); } public override string ToString() { - return ToString(Formatting.Indented); + return documentSerializer.Serialize(this, Formatting.Indented); //ToString(Formatting.Indented); } public string ToString(Formatting formatting) { - using (var ms = new MemoryStream()) - { - CopyToStream(ms, formatting); - ms.Position = 0; - using (StreamReader reader = new StreamReader(ms, Encoding.UTF8)) - { - return reader.ReadToEnd(); - } - } + return documentSerializer.Serialize(this, formatting); //ToString(Formatting.Indented); } } -} +} \ No newline at end of file diff --git a/JsonDiffPatch/PatchDocumentSerializer.cs b/JsonDiffPatch/PatchDocumentSerializer.cs new file mode 100644 index 0000000..b771c11 --- /dev/null +++ b/JsonDiffPatch/PatchDocumentSerializer.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonDiffPatch +{ + internal class PatchDocumentSerializer + { + public string Serialize(PatchDocument patchDocument, Formatting formatting) + { + using (var ms = new MemoryStream()) + { + using (var obj = CopyToStream(patchDocument, ms, formatting)) + { + ms.Position = 0; + using (StreamReader reader = new StreamReader(ms, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } + } + + /// + /// Create memory stream with serialized version of PatchDocument + /// + /// + public MemoryStream ToStream(PatchDocument patchDocument) + { + var stream = new MemoryStream(); + CopyToStream(patchDocument, stream, Formatting.Indented); + stream.Flush(); + stream.Position = 0; + return stream; + } + + /// + /// Copy serialized version of Patch document to provided stream + /// + /// + /// + private IDisposable CopyToStream(PatchDocument patchDocument, Stream stream, Formatting formatting = Formatting.Indented) + { + var sw = new JsonArrayWriter(stream, formatting); + sw.WriteStartArray(); + foreach (var operation in patchDocument.Operations) + { + sw.WriteStartObject(); + operation.Write(sw); + sw.WriteEndObject(); + } + sw.WriteEndArray(); + return sw; + } + + private class JsonArrayWriter : IJsonObjectWriter, IDisposable + { + private readonly JsonTextWriter jsonTextWriter; + + public JsonArrayWriter(Stream stream, Formatting formatting) + { + jsonTextWriter = new JsonTextWriter(new StreamWriter(stream)); + jsonTextWriter.Formatting = formatting; + } + + public void Dispose() + { + ((IDisposable)jsonTextWriter).Dispose(); + } + + public void WriteProperty(string name, string value) + { + jsonTextWriter.WritePropertyName(name); + jsonTextWriter.WriteValue(value); + } + + public void WriteProperty(string name, JToken value) + { + jsonTextWriter.WritePropertyName(name); + value.WriteTo(jsonTextWriter); + } + + internal void WriteStartArray() + { + jsonTextWriter.WriteStartArray(); + } + + internal void WriteEndArray() + { + jsonTextWriter.WriteEndArray(); + jsonTextWriter.Flush(); + } + + internal void WriteStartObject() + { + jsonTextWriter.WriteStartObject(); + } + + internal void WriteEndObject() + { + jsonTextWriter.WriteEndObject(); + } + } + } +} diff --git a/JsonDiffPatch/RemoveOperation.cs b/JsonDiffPatch/RemoveOperation.cs index cb6bb9e..d82b1ff 100644 --- a/JsonDiffPatch/RemoveOperation.cs +++ b/JsonDiffPatch/RemoveOperation.cs @@ -17,19 +17,14 @@ public RemoveOperation(JsonPointer path) : base(path) { } - public override void Write(JsonWriter writer) + public override void Write(IJsonObjectWriter writer) { - writer.WriteStartObject(); - - WriteOp(writer, "remove"); - WritePath(writer, Path); - - writer.WriteEndObject(); + writer.WriteOp("remove").WritePath(Path); } public override void Read(JObject jOperation) { - Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); + Path = new JsonPointer(jOperation.GetValue("path")); } } } diff --git a/JsonDiffPatch/ReplaceOperation.cs b/JsonDiffPatch/ReplaceOperation.cs index 4a205f1..3eba874 100644 --- a/JsonDiffPatch/ReplaceOperation.cs +++ b/JsonDiffPatch/ReplaceOperation.cs @@ -18,20 +18,16 @@ public ReplaceOperation(JsonPointer path, JToken value) : base(path) Value = value; } - public override void Write(JsonWriter writer) + public override void Write(IJsonObjectWriter writer) { - writer.WriteStartObject(); - - WriteOp(writer, "replace"); - WritePath(writer, Path); - WriteValue(writer, Value); - - writer.WriteEndObject(); + writer.WriteOp("replace"). + WritePath(Path). + WriteValue(Value); } public override void Read(JObject jOperation) { - Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); + Path = new JsonPointer(jOperation.GetValue("path")); Value = jOperation.GetValue("value"); } } diff --git a/JsonDiffPatch/Tavis.JsonPointer/JsonPointer.cs b/JsonDiffPatch/Tavis.JsonPointer/JsonPointer.cs index c6713a2..c8e7304 100644 --- a/JsonDiffPatch/Tavis.JsonPointer/JsonPointer.cs +++ b/JsonDiffPatch/Tavis.JsonPointer/JsonPointer.cs @@ -2,53 +2,51 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; + +using JsonDiffPatch; + using Newtonsoft.Json.Linq; namespace Tavis { public class JsonPointer { - private readonly IReadOnlyList _Tokens; + private readonly JsonPointerTokensSource _Tokens; public JsonPointer(string pointer) { - _Tokens = pointer.Split('/').Skip(1).Select(Decode).ToArray(); + _Tokens = new JsonPointerTokensSource(Uri.UnescapeDataString(pointer)); } - internal JsonPointer(IReadOnlyList tokens) + public JsonPointer(JToken jToken) { - _Tokens = tokens; + _Tokens = new JsonPointerTokensSource(jToken.ToString()); } - private string Decode(string token) + + private JsonPointer(JsonPointerTokensSource tokens) { - return Uri.UnescapeDataString(token).Replace("~1", "/").Replace("~0", "~"); + _Tokens = tokens; } public bool IsNewPointer() { - return _Tokens[_Tokens.Count - 1] == "-"; + return _Tokens.Last() == "-"; } public JsonPointer ParentPointer { get { - if (_Tokens.Count == 0) return null; - - var tokens = new string[_Tokens.Count - 1]; - for (int i = 0; i < _Tokens.Count - 1; i++) - { - tokens[i] = _Tokens[i]; - } - - return new JsonPointer(tokens); + if (_Tokens.IsEmpty) return null; + return new JsonPointer(_Tokens.ForParent()); } } public JToken Find(JToken sample) { - if (_Tokens.Count == 0) + if (_Tokens.IsEmpty) { return sample; } @@ -75,7 +73,7 @@ public JToken Find(JToken sample) } catch (Exception ex) { - throw new ArgumentException("Failed to dereference pointer",ex); + throw new ArgumentException("Failed to dereference pointer", ex); } } diff --git a/JsonDiffPatch/TestOperation.cs b/JsonDiffPatch/TestOperation.cs index 3dfff92..db98d68 100644 --- a/JsonDiffPatch/TestOperation.cs +++ b/JsonDiffPatch/TestOperation.cs @@ -19,20 +19,16 @@ public TestOperation(JsonPointer path, JToken value) : base(path) Value = value; } - public override void Write(JsonWriter writer) + public override void Write(IJsonObjectWriter writer) { - writer.WriteStartObject(); - - WriteOp(writer, "test"); - WritePath(writer, Path); - WriteValue(writer, Value); - - writer.WriteEndObject(); + writer.WriteOp("test"). + WritePath(Path). + WriteValue(Value); } public override void Read(JObject jOperation) { - Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); + Path = new JsonPointer(jOperation.GetValue("path")); Value = jOperation.GetValue("value"); } } diff --git a/JsonDiffPatchTests/DiffTests.cs b/JsonDiffPatchTests/DiffTests.cs index e7b1ad1..5a38079 100644 --- a/JsonDiffPatchTests/DiffTests.cs +++ b/JsonDiffPatchTests/DiffTests.cs @@ -187,7 +187,7 @@ public string JsonPatchesWorks(string leftString, string rightString) var left = JToken.Parse(leftString); var right = JToken.Parse(rightString); - var patchDoc = new JsonDiffer().Diff(left, right, false); + var patchDoc = JsonDiffer.Diff(left, right, false); var patcher = new JsonPatcher(); patcher.Patch(ref left, patchDoc); diff --git a/JsonDiffPatchTests/DiffTests2.cs b/JsonDiffPatchTests/DiffTests2.cs index 8291074..bd3aae7 100644 --- a/JsonDiffPatchTests/DiffTests2.cs +++ b/JsonDiffPatchTests/DiffTests2.cs @@ -20,7 +20,7 @@ public void ComplexExampleWithDeepArrayChange() var scene1 = JToken.Parse(scene1Text); var scene2Text = File.ReadAllText(string.Format(rightPath, i)); var scene2 = JToken.Parse(scene2Text); - var patchDoc = new JsonDiffer().Diff(scene1, scene2, true); + var patchDoc = JsonDiffer.Diff(scene1, scene2, true); //Assert.AreEqual("[{\"op\":\"remove\",\"path\":\"/items/0/entities/1\"}]", var patcher = new JsonPatcher(); patcher.Patch(ref scene1, patchDoc);