├── .gitattributes ├── .gitignore ├── JsonDiffPatch.sln ├── JsonDiffPatch ├── AbstractPatcher.cs ├── AddOperation.cs ├── ArrayLcs.cs ├── CopyOperation.cs ├── JsonDiffPatch.csproj ├── JsonDiffer.cs ├── JsonPatcher.cs ├── MoveOperation.cs ├── Operation.cs ├── PatchDocument.cs ├── RemoveOperation.cs ├── ReplaceOperation.cs ├── Tavis.JsonPointer │ └── JsonPointer.cs ├── TestOperation.cs └── packages.config ├── JsonDiffPatchTests ├── AddTests.cs ├── CopyTests.cs ├── DiffTests.cs ├── DiffTests2.cs ├── JsonDiffPatch.Tests.csproj ├── MoveTests.cs ├── PatchTests.cs ├── PatchTests2.cs ├── RemoveTests.cs ├── ReplaceTests.cs ├── Samples │ ├── LoadTest1.json │ ├── scene1a.json │ ├── scene1b.json │ ├── scene2a.json │ ├── scene2b.json │ ├── scene3a.json │ └── scene3b.json ├── TestTests.cs ├── app.config └── packages.config ├── License.txt └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | packages/ 134 | *.nupkg 135 | 136 | # Windows Azure Build Output 137 | csx 138 | *.build.csdef 139 | 140 | # Windows Store app package directory 141 | AppPackages/ 142 | 143 | # Others 144 | sql/ 145 | *.Cache 146 | ClientBin/ 147 | [Ss]tyle[Cc]op.* 148 | ~$* 149 | *~ 150 | *.dbmdl 151 | *.[Pp]ublish.xml 152 | *.pfx 153 | *.publishsettings 154 | 155 | # RIA/Silverlight projects 156 | Generated_Code/ 157 | 158 | # Backup & report files from converting an old project file to a newer 159 | # Visual Studio version. Backup files are not needed, because we have git ;-) 160 | _UpgradeReport_Files/ 161 | Backup*/ 162 | UpgradeLog*.XML 163 | UpgradeLog*.htm 164 | 165 | # SQL Server files 166 | App_Data/*.mdf 167 | App_Data/*.ldf 168 | 169 | ############# 170 | ## Windows detritus 171 | ############# 172 | 173 | # Windows image file caches 174 | Thumbs.db 175 | ehthumbs.db 176 | 177 | # Folder config file 178 | Desktop.ini 179 | 180 | # Recycle Bin used on file shares 181 | $RECYCLE.BIN/ 182 | 183 | # Mac crap 184 | .DS_Store 185 | 186 | 187 | ############# 188 | ## Python 189 | ############# 190 | 191 | *.py[co] 192 | 193 | # Packages 194 | *.egg 195 | *.egg-info 196 | dist/ 197 | 198 | eggs/ 199 | parts/ 200 | var/ 201 | sdist/ 202 | develop-eggs/ 203 | .installed.cfg 204 | 205 | # Installer logs 206 | pip-log.txt 207 | 208 | # Unit test / coverage reports 209 | .coverage 210 | .tox 211 | 212 | #Translations 213 | *.mo 214 | 215 | #Mr Developer 216 | .mr.developer.cfg 217 | /.vs 218 | -------------------------------------------------------------------------------- /JsonDiffPatch.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonDiffPatch", "JsonDiffPatch\JsonDiffPatch.csproj", "{F28B8328-85BE-4048-AA16-0C069095FB1F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonDiffPatch.Tests", "JsonDiffPatchTests\JsonDiffPatch.Tests.csproj", "{1FCBC3F6-73A3-4686-9A8B-A7709CF1C198}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {F28B8328-85BE-4048-AA16-0C069095FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {F28B8328-85BE-4048-AA16-0C069095FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {F28B8328-85BE-4048-AA16-0C069095FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {F28B8328-85BE-4048-AA16-0C069095FB1F}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {1FCBC3F6-73A3-4686-9A8B-A7709CF1C198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {1FCBC3F6-73A3-4686-9A8B-A7709CF1C198}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {1FCBC3F6-73A3-4686-9A8B-A7709CF1C198}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {1FCBC3F6-73A3-4686-9A8B-A7709CF1C198}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /JsonDiffPatch/AbstractPatcher.cs: -------------------------------------------------------------------------------- 1 | namespace JsonDiffPatch 2 | { 3 | public abstract class AbstractPatcher where TDoc : class 4 | { 5 | 6 | /// 7 | /// Apply the patch document to the target 8 | /// 9 | /// has to be a ref, as the object may be replaced with something quite different for remove operations that apply to the root 10 | public virtual void Patch(ref TDoc target, PatchDocument document) 11 | { 12 | foreach (var operation in document.Operations) 13 | { 14 | target = ApplyOperation(operation, target); 15 | } 16 | } 17 | 18 | /// 19 | /// return value indicates that a new 20 | /// 21 | /// 22 | /// 23 | /// a new root document, or null if one is not needed 24 | public virtual TDoc ApplyOperation(Operation operation, TDoc target) 25 | { 26 | if (operation is AddOperation) Add((AddOperation)operation, target); 27 | else if (operation is CopyOperation) Copy((CopyOperation)operation, target); 28 | else if (operation is MoveOperation) Move((MoveOperation)operation, target); 29 | else if (operation is RemoveOperation) Remove((RemoveOperation)operation, target); 30 | else if (operation is ReplaceOperation) target = Replace((ReplaceOperation)operation, target) ?? target; 31 | else if (operation is TestOperation) Test((TestOperation)operation, target); 32 | return target; 33 | } 34 | 35 | protected abstract void Add(AddOperation operation, TDoc target); 36 | protected abstract void Copy(CopyOperation operation, TDoc target); 37 | protected abstract void Move(MoveOperation operation, TDoc target); 38 | protected abstract void Remove(RemoveOperation operation, TDoc target); 39 | protected abstract TDoc Replace(ReplaceOperation operation, TDoc target); 40 | protected abstract void Test(TestOperation operation, TDoc target); 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /JsonDiffPatch/AddOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Tavis; 4 | 5 | namespace JsonDiffPatch 6 | { 7 | public class AddOperation : Operation 8 | { 9 | public JToken Value { get; private set; } 10 | 11 | public AddOperation() 12 | { 13 | 14 | } 15 | 16 | public AddOperation(JsonPointer path, JToken value) : base(path) 17 | { 18 | Value = value; 19 | } 20 | 21 | public override void Write(JsonWriter writer) 22 | { 23 | writer.WriteStartObject(); 24 | 25 | WriteOp(writer, "add"); 26 | WritePath(writer,Path); 27 | WriteValue(writer,Value); 28 | 29 | writer.WriteEndObject(); 30 | } 31 | 32 | public override void Read(JObject jOperation) 33 | { 34 | 35 | Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); 36 | Value = jOperation.GetValue("value"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /JsonDiffPatch/ArrayLcs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace JsonDiffPatch 7 | { 8 | class ArrayLcs 9 | { 10 | private readonly Func _matchObject; 11 | private Func _match; 12 | 13 | public ArrayLcs(Func matchObject) 14 | { 15 | _matchObject = matchObject; 16 | _match = (array1, array2, index1, index2, context) => _matchObject(array1[index1], array2[index2]); 17 | } 18 | 19 | public class BackTrackResult 20 | { 21 | public List sequence { get; set; } = new List(); 22 | public List indices1 { get; set; } = new List(); 23 | public List indices2 { get; set; } = new List(); 24 | } 25 | 26 | 27 | private int[][] LengthMatrix(object[] array1, object[] array2, IDictionary context) 28 | { 29 | var len1 = array1.Length; 30 | var len2 = array2.Length; 31 | 32 | // initialize empty matrix of len1+1 x len2+1 33 | var matrix = new int[len1 + 1][]; 34 | for (var x = 0; x < matrix.Length; x++) 35 | { 36 | matrix[x] = Enumerable.Repeat(0, len2 + 1).ToArray(); 37 | } 38 | // save sequence lengths for each coordinate 39 | for (var x = 1; x < len1 + 1; x++) 40 | { 41 | for (var y = 1; y < len2 + 1; y++) 42 | { 43 | if (_match(array1, array2, x - 1, y - 1, context)) 44 | { 45 | matrix[x][y] = 1 + (int)matrix[x - 1][y - 1]; 46 | } 47 | else 48 | { 49 | matrix[x][y] = Math.Max(matrix[x - 1][y], matrix[x][y - 1]); 50 | } 51 | } 52 | } 53 | return matrix; 54 | } 55 | 56 | private BackTrackResult backtrack(int[][] matrix, object[] array1, object[] array2, int index1, int index2, IDictionary context) 57 | { 58 | if (index1 == 0 || index2 == 0) 59 | { 60 | return new BackTrackResult(); 61 | } 62 | 63 | if (_match(array1, array2, index1 - 1, index2 - 1, context)) 64 | { 65 | var subsequence = backtrack(matrix, array1, array2, index1 - 1, index2 - 1, context); 66 | subsequence.sequence.Add(array1[index1 - 1]); 67 | subsequence.indices1.Add(index1 - 1); 68 | subsequence.indices2.Add(index2 - 1); 69 | return subsequence; 70 | } 71 | 72 | if (matrix[index1][index2 - 1] > matrix[index1 - 1][index2]) 73 | { 74 | return backtrack(matrix, array1, array2, index1, index2 - 1, context); 75 | } 76 | else 77 | { 78 | return backtrack(matrix, array1, array2, index1 - 1, index2, context); 79 | } 80 | } 81 | 82 | 83 | 84 | public BackTrackResult Get(Object[] array1, object[] array2, IDictionary context) 85 | { 86 | context = context ?? new Dictionary(); 87 | var matrix = LengthMatrix(array1, array2, context); 88 | var result = backtrack(matrix, array1, array2, array1.Length, array2.Length, context); 89 | //if (typeof array1 == = 'string' && typeof array2 == = 'string') 90 | //{ 91 | // result.sequence = result.sequence.join(''); 92 | //} 93 | return result; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /JsonDiffPatch/CopyOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Tavis; 4 | 5 | namespace JsonDiffPatch 6 | { 7 | public class CopyOperation : Operation 8 | { 9 | public JsonPointer FromPath { get; private set; } 10 | 11 | public CopyOperation() 12 | { 13 | 14 | } 15 | 16 | public CopyOperation(JsonPointer path, JsonPointer fromPath) : base(path) 17 | { 18 | FromPath = fromPath; 19 | } 20 | 21 | public override void Write(JsonWriter writer) 22 | { 23 | writer.WriteStartObject(); 24 | 25 | WriteOp(writer, "copy"); 26 | WritePath(writer, Path); 27 | WriteFromPath(writer, FromPath); 28 | 29 | writer.WriteEndObject(); 30 | } 31 | 32 | public override void Read(JObject jOperation) 33 | { 34 | Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); 35 | FromPath = new JsonPointer(SplitPath((string)jOperation.GetValue("from"))); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JsonDiffPatch/JsonDiffPatch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.4;net452;netcoreapp1.1 5 | Library for diffing and RFC 6902 patching json.net json objects - forked from Tavis.JsonPatch, with an addition of json diff code by Ian Mercer, with additional partial array LCS diff by JC Dickinson 6 | https://github.com/mcintyre321/JsonDiffPatch/blob/master/License.txt 7 | 8 | https://github.com/mcintyre321/JsonDiffPatch 9 | JSON, Diff, Patch, RFC, 6902 10 | True 11 | 1.0.0 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /JsonDiffPatch/JsonDiffer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using Tavis; 6 | 7 | namespace JsonDiffPatch 8 | { 9 | /// 10 | /// Parts adapted from https://github.com/benjamine/jsondiffpatch/blob/42ce1b6ca30c4d7a19688a020fce021a756b43cc/src/filters/arrays.js 11 | /// 12 | public class JsonDiffer 13 | { 14 | internal static string Extend(string path, string extension) 15 | { 16 | // TODO: JSON property name needs escaping for path ?? 17 | return path + "/" + EncodeKey(extension); 18 | } 19 | 20 | private static string EncodeKey(string key) => key.Replace("~", "~0").Replace("/", "~1"); 21 | 22 | private static Operation Build(string op, string path, string key, JToken value) 23 | { 24 | if (string.IsNullOrEmpty(key)) 25 | return 26 | Operation.Parse("{ 'op' : '" + op + "' , path: '" + path + "', value: " + 27 | (value == null ? "null" : value.ToString(Formatting.None)) + "}"); 28 | else 29 | return 30 | Operation.Parse("{ op : '" + op + "' , path : '" + Extend(path, key) + "' , value : " + 31 | (value == null ? "null" : value.ToString(Formatting.None)) + "}"); 32 | } 33 | 34 | internal static Operation Add(string path, string key, JToken value) 35 | { 36 | return Build("add", path, key, value); 37 | } 38 | 39 | internal static Operation Remove(string path, string key) 40 | { 41 | return Build("remove", path, key, null); 42 | } 43 | 44 | internal static Operation Replace(string path, string key, JToken value) 45 | { 46 | return Build("replace", path, key, value); 47 | } 48 | 49 | internal static IEnumerable CalculatePatch(JToken left, JToken right, bool useIdToDetermineEquality, 50 | string path = "") 51 | { 52 | if (left.Type != right.Type) 53 | { 54 | yield return JsonDiffer.Replace(path, "", right); 55 | yield break; 56 | } 57 | 58 | if (left.Type == JTokenType.Array) 59 | { 60 | Operation prev = null; 61 | foreach (var operation in ProcessArray(left, right, path, useIdToDetermineEquality)) 62 | { 63 | var prevRemove = prev as RemoveOperation; 64 | var add = operation as AddOperation; 65 | if (prevRemove != null && add != null && add.Path.ToString() == prevRemove.Path.ToString()) 66 | { 67 | yield return Replace(add.Path.ToString(), "", add.Value); 68 | prev = null; 69 | } 70 | else 71 | { 72 | if (prev != null) yield return prev; 73 | prev = operation; 74 | } 75 | } 76 | 77 | if (prev != null) 78 | { 79 | yield return prev; 80 | } 81 | } 82 | else if (left.Type == JTokenType.Object) 83 | { 84 | var lprops = ((IDictionary) left).OrderBy(p => p.Key); 85 | var rprops = ((IDictionary) right).OrderBy(p => p.Key); 86 | 87 | foreach (var removed in lprops.Except(rprops, MatchesKey.Instance)) 88 | { 89 | yield return JsonDiffer.Remove(path, removed.Key); 90 | } 91 | 92 | foreach (var added in rprops.Except(lprops, MatchesKey.Instance)) 93 | { 94 | yield return JsonDiffer.Add(path, added.Key, added.Value); 95 | } 96 | 97 | var matchedKeys = lprops.Select(x => x.Key).Intersect(rprops.Select(y => y.Key)); 98 | var zipped = matchedKeys.Select(k => new {key = k, left = left[k], right = right[k]}); 99 | 100 | foreach (var match in zipped) 101 | { 102 | string newPath = path + "/" + EncodeKey(match.key); 103 | foreach (var patch in CalculatePatch(match.left, match.right, useIdToDetermineEquality, newPath)) 104 | yield return patch; 105 | } 106 | 107 | yield break; 108 | } 109 | else 110 | { 111 | // Two values, same type, not JObject so no properties 112 | 113 | if (left.ToString() == right.ToString()) 114 | yield break; 115 | else 116 | yield return JsonDiffer.Replace(path, "", right); 117 | } 118 | } 119 | 120 | private static IEnumerable ProcessArray(JToken left, JToken right, string path, 121 | bool useIdPropertyToDetermineEquality) 122 | { 123 | var comparer = 124 | new CustomCheckEqualityComparer(useIdPropertyToDetermineEquality, new JTokenEqualityComparer()); 125 | 126 | int commonHead = 0; 127 | int commonTail = 0; 128 | var array1 = left.ToArray(); 129 | var len1 = array1.Length; 130 | var array2 = right.ToArray(); 131 | var len2 = array2.Length; 132 | // if (len1 == 0 && len2 ==0 ) yield break; 133 | while (commonHead < len1 && commonHead < len2) 134 | { 135 | if (comparer.Equals(array1[commonHead], array2[commonHead]) == false) break; 136 | 137 | //diff and yield objects here 138 | foreach (var operation in CalculatePatch(array1[commonHead], array2[commonHead], 139 | useIdPropertyToDetermineEquality, path + "/" + commonHead)) 140 | { 141 | yield return operation; 142 | } 143 | 144 | commonHead++; 145 | } 146 | 147 | // separate common tail 148 | while (commonTail + commonHead < len1 && commonTail + commonHead < len2) 149 | { 150 | if (comparer.Equals(array1[len1 - 1 - commonTail], array2[len2 - 1 - commonTail]) == false) break; 151 | 152 | var index1 = len1 - 1 - commonTail; 153 | var index2 = len2 - 1 - commonTail; 154 | foreach (var operation in CalculatePatch(array1[index1], array2[index2], 155 | useIdPropertyToDetermineEquality, path + "/" + index1)) 156 | { 157 | yield return operation; 158 | } 159 | 160 | commonTail++; 161 | } 162 | 163 | if (commonHead == 0 && commonTail == 0 && len2 > 0 && len1 > 0) 164 | { 165 | yield return new ReplaceOperation(new JsonPointer(path), new JArray(array2)); 166 | yield break; 167 | } 168 | 169 | var leftMiddle = array1.Skip(commonHead).Take(array1.Length - commonTail - commonHead).ToArray(); 170 | var rightMiddle = array2.Skip(commonHead).Take(array2.Length - commonTail - commonHead).ToArray(); 171 | 172 | // Just a replace of values! 173 | if (leftMiddle.Length == rightMiddle.Length) 174 | { 175 | for (int i = 0; i < leftMiddle.Length; i++) 176 | { 177 | foreach (var operation in CalculatePatch(leftMiddle[i], rightMiddle[i], 178 | useIdPropertyToDetermineEquality, $"{path}/{commonHead + i}")) 179 | { 180 | yield return operation; 181 | } 182 | } 183 | 184 | yield break; 185 | } 186 | 187 | foreach (var jToken in leftMiddle) 188 | { 189 | yield return new RemoveOperation(new JsonPointer($"{path}/{commonHead}")); 190 | } 191 | 192 | for (int i = 0; i < rightMiddle.Length; i++) 193 | { 194 | yield return new AddOperation(new JsonPointer($"{path}/{commonHead + i}"), rightMiddle[i]); 195 | } 196 | 197 | //if (commonHead + commonTail == len1) 198 | //{ 199 | // if (len1 == len2) 200 | // { 201 | // // arrays are identical 202 | // yield break; 203 | // } 204 | // // trivial case, a block (1 or more consecutive items) was added 205 | 206 | // for (index = commonHead; index < len2 - commonTail; index++) 207 | // { 208 | // yield return new AddOperation() 209 | // { 210 | // Value = array2[index], 211 | // Path = new JsonPointer(path + "/" + index) 212 | // }; 213 | // } 214 | //} 215 | //if (commonHead + commonTail == len2) 216 | //{ 217 | // // trivial case, a block (1 or more consecutive items) was removed 218 | // for (index = commonHead; index < len1 - commonTail; index++) 219 | // { 220 | // yield return new RemoveOperation() 221 | // { 222 | // Path = new JsonPointer(path + "/" + index) 223 | // }; 224 | // } 225 | //} 226 | 227 | //var context = new Dictionary(); 228 | 229 | //var lcs = new ArrayLcs((a, b) => comparer.Equals((JToken)a, (JToken)b)); 230 | //var trimmed1 = array1.Skip(commonHead).Take(len1 - commonTail).ToArray(); 231 | //var trimmed2 = array2.Skip(commonHead).Take(len2 - commonTail).ToArray(); 232 | //var seq = lcs.Get(trimmed1, trimmed2, context); 233 | //for (index = commonHead; index < len1 - commonTail; index++) 234 | //{ 235 | // if ((seq.indices1).IndexOf(index - commonHead) < 0) 236 | // { 237 | // // removed 238 | // yield return new RemoveOperation() 239 | // { 240 | // Path = new JsonPointer(path + "/" + commonHead) 241 | // }; 242 | // //removedItems.push(index); 243 | // } 244 | //} 245 | 246 | //var detectMove = true; 247 | //if (context.options && context.options.arrays && context.options.arrays.detectMove === false) 248 | //{ 249 | // detectMove = false; 250 | //} 251 | //var includeValueOnMove = false; 252 | //if (context.options && context.options.arrays && context.options.arrays.includeValueOnMove) 253 | //{ 254 | // includeValueOnMove = true; 255 | //} 256 | 257 | //var removedItemsLength = removedItems.length; 258 | //for (index = commonHead; index < len2 - commonTail; index++) 259 | //{ 260 | // var indexOnArray2 = arrayIndexOf(seq.indices2, index - commonHead); 261 | // if (indexOnArray2 < 0) 262 | // { 263 | // // added, try to match with a removed item and register as position move 264 | // var isMove = false; 265 | // if (detectMove && removedItemsLength > 0) 266 | // { 267 | // for (var removeItemIndex1 = 0; removeItemIndex1 < removedItemsLength; removeItemIndex1++) 268 | // { 269 | // index1 = removedItems[removeItemIndex1]; 270 | // if (matchItems(trimmed1, trimmed2, index1 - commonHead, 271 | // index - commonHead, matchContext)) 272 | // { 273 | // // store position move as: [originalValue, newPosition, ARRAY_MOVE] 274 | // result['_' + index1].splice(1, 2, index, ARRAY_MOVE); 275 | // if (!includeValueOnMove) 276 | // { 277 | // // don't include moved value on diff, to save bytes 278 | // result['_' + index1][0] = ''; 279 | // } 280 | 281 | // index2 = index; 282 | // child = new DiffContext(context.left[index1], context.right[index2]); 283 | // context.push(child, index2); 284 | // removedItems.splice(removeItemIndex1, 1); 285 | // isMove = true; 286 | // break; 287 | // } 288 | // } 289 | // } 290 | // if (!isMove) 291 | // { 292 | // // added 293 | // result[index] = [array2[index]]; 294 | // } 295 | // } 296 | // else 297 | // { 298 | // // match, do inner diff 299 | // index1 = seq.indices1[indexOnArray2] + commonHead; 300 | // index2 = seq.indices2[indexOnArray2] + commonHead; 301 | // child = new DiffContext(context.left[index1], context.right[index2]); 302 | // context.push(child, index2); 303 | // } 304 | //} 305 | } 306 | 307 | private class MatchesKey : IEqualityComparer> 308 | { 309 | public static MatchesKey Instance = new MatchesKey(); 310 | 311 | public bool Equals(KeyValuePair x, KeyValuePair y) 312 | { 313 | return x.Key.Equals(y.Key); 314 | } 315 | 316 | public int GetHashCode(KeyValuePair obj) 317 | { 318 | return obj.Key.GetHashCode(); 319 | } 320 | } 321 | 322 | /// 323 | /// 324 | /// 325 | /// 326 | /// 327 | /// Use id propety on array members to determine equality 328 | /// 329 | public PatchDocument Diff(JToken @from, JToken to, bool useIdPropertyToDetermineEquality) 330 | { 331 | return new PatchDocument(CalculatePatch(@from, to, useIdPropertyToDetermineEquality).ToArray()); 332 | } 333 | } 334 | 335 | internal class CustomCheckEqualityComparer : IEqualityComparer 336 | { 337 | private readonly bool _enableIdCheck; 338 | private readonly IEqualityComparer _inner; 339 | 340 | public CustomCheckEqualityComparer(bool enableIdCheck, IEqualityComparer inner) 341 | { 342 | _enableIdCheck = enableIdCheck; 343 | _inner = inner; 344 | } 345 | 346 | public bool Equals(JToken x, JToken y) 347 | { 348 | if (_enableIdCheck && x.Type == JTokenType.Object && y.Type == JTokenType.Object) 349 | { 350 | var xIdToken = x["id"]; 351 | var yIdToken = y["id"]; 352 | 353 | var xId = xIdToken != null ? xIdToken.Value() : null; 354 | var yId = yIdToken != null ? yIdToken.Value() : null; 355 | if (xId != null && xId == yId) 356 | { 357 | return true; 358 | } 359 | } 360 | 361 | return _inner.Equals(x, y); 362 | } 363 | 364 | public int GetHashCode(JToken obj) 365 | { 366 | if (_enableIdCheck && obj.Type == JTokenType.Object) 367 | { 368 | var xIdToken = obj["id"]; 369 | var xId = xIdToken != null && xIdToken.HasValues ? xIdToken.Value() : null; 370 | if (xId != null) return xId.GetHashCode() + _inner.GetHashCode(obj); 371 | } 372 | 373 | return _inner.GetHashCode(obj); 374 | } 375 | 376 | public static bool HaveEqualIds(JToken x, JToken y) 377 | { 378 | if (x.Type == JTokenType.Object && y.Type == JTokenType.Object) 379 | { 380 | var xIdToken = x["id"]; 381 | var yIdToken = y["id"]; 382 | 383 | var xId = xIdToken != null ? xIdToken.Value() : null; 384 | var yId = yIdToken != null ? yIdToken.Value() : null; 385 | return xId != null && xId == yId; 386 | } 387 | 388 | return false; 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /JsonDiffPatch/JsonPatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace JsonDiffPatch 6 | { 7 | public class JsonPatcher : AbstractPatcher 8 | { 9 | 10 | protected override JToken Replace(ReplaceOperation operation, JToken target) 11 | { 12 | operation.Path.ToString().Replace("~1", "/").Replace("~0", "~"); 13 | var token = operation.Path.Find(target); 14 | if (token.Parent == null) 15 | { 16 | return operation.Value; 17 | } 18 | else 19 | { 20 | token.Replace(operation.Value); 21 | return null; 22 | } 23 | } 24 | 25 | protected override void Add(AddOperation operation, JToken target) 26 | { 27 | JToken token = null; 28 | JObject parenttoken = null; 29 | var parentPath = operation.Path.ParentPointer.ToString(); 30 | int index = parentPath == "/" ? parentPath.Length : parentPath.Length + 1; 31 | var propertyName = operation.Path.ToString().Substring(index).Replace("~1", "/").Replace("~0", "~"); 32 | try 33 | { 34 | var parentArray = operation.Path.ParentPointer.Find(target) as JArray; 35 | bool isNewPointer = operation.Path.IsNewPointer(); 36 | if (parentArray == null || isNewPointer) 37 | { 38 | if (isNewPointer) 39 | { 40 | var parentPointer = operation.Path.ParentPointer; 41 | token = parentPointer.Find(target) as JArray; 42 | } 43 | else 44 | { 45 | token = operation.Path.Find(target); 46 | } 47 | } 48 | else 49 | { 50 | parentArray.Insert(int.Parse(propertyName), operation.Value); 51 | return; 52 | } 53 | } 54 | catch (ArgumentException) 55 | { 56 | var parentPointer = operation.Path.ParentPointer; 57 | parenttoken = parentPointer.Find(target) as JObject; 58 | } 59 | 60 | if (token == null && parenttoken != null) 61 | { 62 | parenttoken.Add(propertyName, operation.Value); 63 | } 64 | else if (token is JArray) 65 | { 66 | var array = token as JArray; 67 | 68 | array.Add(operation.Value); 69 | } 70 | else if (token.Parent is JProperty) 71 | { 72 | var prop = token.Parent as JProperty; 73 | prop.Value = operation.Value; 74 | } 75 | } 76 | 77 | 78 | protected override void Remove(RemoveOperation operation, JToken target) 79 | { 80 | var token = operation.Path.Find(target); 81 | if (token.Parent is JProperty) 82 | { 83 | token.Parent.Remove(); 84 | } 85 | else 86 | { 87 | token.Remove(); 88 | } 89 | } 90 | 91 | protected override void Move(MoveOperation operation, JToken target) 92 | { 93 | if (operation.Path.ToString().StartsWith(operation.FromPath.ToString())) throw new ArgumentException("To path cannot be below from path"); 94 | 95 | var token = operation.FromPath.Find(target); 96 | Remove(new RemoveOperation(operation.FromPath), target); 97 | Add(new AddOperation(operation.Path, token), target); 98 | } 99 | 100 | protected override void Test(TestOperation operation, JToken target) 101 | { 102 | var existingValue = operation.Path.Find(target); 103 | if (!existingValue.Equals(target)) 104 | { 105 | throw new InvalidOperationException("Value at " + operation.Path.ToString() + " does not match."); 106 | } 107 | } 108 | 109 | protected override void Copy(CopyOperation operation, JToken target) 110 | { 111 | var token = operation.FromPath.Find(target); // Do I need to clone this? 112 | Add(new AddOperation(operation.Path, token), target); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /JsonDiffPatch/MoveOperation.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using Tavis; 6 | 7 | namespace JsonDiffPatch 8 | { 9 | public class MoveOperation : Operation 10 | { 11 | public JsonPointer FromPath { get; private set; } 12 | 13 | public MoveOperation() 14 | { 15 | 16 | } 17 | 18 | public MoveOperation(JsonPointer path, JsonPointer fromPath) : base(path) 19 | { 20 | FromPath = fromPath; 21 | } 22 | 23 | public override void Write(JsonWriter writer) 24 | { 25 | writer.WriteStartObject(); 26 | 27 | WriteOp(writer, "move"); 28 | WritePath(writer, Path); 29 | WriteFromPath(writer, FromPath); 30 | 31 | writer.WriteEndObject(); 32 | } 33 | 34 | public override void Read(JObject jOperation) 35 | { 36 | Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); 37 | FromPath = new JsonPointer(SplitPath((string)jOperation.GetValue("from"))); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /JsonDiffPatch/Operation.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Tavis; 5 | 6 | namespace JsonDiffPatch 7 | { 8 | public abstract class Operation 9 | { 10 | public JsonPointer Path { get; protected set; } 11 | 12 | public Operation() 13 | { 14 | 15 | } 16 | 17 | public Operation(JsonPointer path) 18 | { 19 | Path = path; 20 | } 21 | 22 | public abstract void Write(JsonWriter writer); 23 | 24 | protected static void WriteOp(JsonWriter writer, string op) 25 | { 26 | writer.WritePropertyName("op"); 27 | writer.WriteValue(op); 28 | } 29 | 30 | protected static void WritePath(JsonWriter writer, JsonPointer pointer) 31 | { 32 | writer.WritePropertyName("path"); 33 | writer.WriteValue(pointer.ToString()); 34 | } 35 | 36 | protected static void WriteFromPath(JsonWriter writer, JsonPointer pointer) 37 | { 38 | writer.WritePropertyName("from"); 39 | writer.WriteValue(pointer.ToString()); 40 | } 41 | protected static void WriteValue(JsonWriter writer, JToken value) 42 | { 43 | writer.WritePropertyName("value"); 44 | value.WriteTo(writer); 45 | } 46 | 47 | protected static string[] SplitPath(string path) => path.Split('/').Skip(1).ToArray(); 48 | 49 | public abstract void Read(JObject jOperation); 50 | 51 | public static Operation Parse(string json) 52 | { 53 | return Build(JObject.Parse(json)); 54 | } 55 | 56 | public static Operation Build(JObject jOperation) 57 | { 58 | var op = PatchDocument.CreateOperation((string)jOperation["op"]); 59 | op.Read(jOperation); 60 | return op; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /JsonDiffPatch/PatchDocument.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace JsonDiffPatch 9 | { 10 | public class PatchDocument 11 | { 12 | private readonly List _Operations = new List(); 13 | 14 | public PatchDocument(params Operation[] operations) 15 | { 16 | _Operations.AddRange(operations); 17 | } 18 | 19 | public IReadOnlyCollection Operations 20 | { 21 | get { return _Operations; } 22 | } 23 | 24 | public void AddOperation(Operation operation) 25 | { 26 | _Operations.Add(operation); 27 | } 28 | 29 | public static PatchDocument Load(Stream document) 30 | { 31 | var reader = new StreamReader(document); 32 | 33 | return Parse(reader.ReadToEnd()); 34 | } 35 | 36 | public static PatchDocument Load(JArray document) 37 | { 38 | var root = new PatchDocument(); 39 | 40 | if (document != null) 41 | { 42 | foreach (var jOperation in document.Children().Cast()) 43 | { 44 | var op = Operation.Build(jOperation); 45 | root.AddOperation(op); 46 | } 47 | } 48 | 49 | return root; 50 | } 51 | 52 | public static PatchDocument Parse(string jsondocument) 53 | { 54 | var root = JToken.Parse(jsondocument) as JArray; 55 | 56 | return Load(root); 57 | } 58 | 59 | public static Operation CreateOperation(string op) 60 | { 61 | switch (op) 62 | { 63 | case "add": return new AddOperation(); 64 | case "copy": return new CopyOperation(); 65 | case "move": return new MoveOperation(); 66 | case "remove": return new RemoveOperation(); 67 | case "replace": return new ReplaceOperation(); 68 | case "test" : return new TestOperation(); 69 | } 70 | return null; 71 | } 72 | 73 | /// 74 | /// Create memory stream with serialized version of PatchDocument 75 | /// 76 | /// 77 | public MemoryStream ToStream() 78 | { 79 | var stream = new MemoryStream(); 80 | CopyToStream(stream, Formatting.Indented); 81 | stream.Flush(); 82 | stream.Position = 0; 83 | return stream; 84 | } 85 | 86 | /// 87 | /// Copy serialized version of Patch document to provided stream 88 | /// 89 | /// 90 | /// 91 | public void CopyToStream(Stream stream, Formatting formatting = Formatting.Indented) 92 | { 93 | var sw = new JsonTextWriter(new StreamWriter(stream)); 94 | sw.Formatting = formatting; 95 | 96 | sw.WriteStartArray(); 97 | 98 | foreach (var operation in Operations) 99 | { 100 | operation.Write(sw); 101 | } 102 | 103 | sw.WriteEndArray(); 104 | 105 | sw.Flush(); 106 | } 107 | 108 | public override string ToString() 109 | { 110 | return ToString(Formatting.Indented); 111 | } 112 | 113 | public string ToString(Formatting formatting) 114 | { 115 | using (var ms = new MemoryStream()) 116 | { 117 | CopyToStream(ms, formatting); 118 | ms.Position = 0; 119 | using (StreamReader reader = new StreamReader(ms, Encoding.UTF8)) 120 | { 121 | return reader.ReadToEnd(); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /JsonDiffPatch/RemoveOperation.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using Tavis; 6 | 7 | namespace JsonDiffPatch 8 | { 9 | public class RemoveOperation : Operation 10 | { 11 | public RemoveOperation() 12 | { 13 | 14 | } 15 | 16 | public RemoveOperation(JsonPointer path) : base(path) 17 | { 18 | } 19 | 20 | public override void Write(JsonWriter writer) 21 | { 22 | writer.WriteStartObject(); 23 | 24 | WriteOp(writer, "remove"); 25 | WritePath(writer, Path); 26 | 27 | writer.WriteEndObject(); 28 | } 29 | 30 | public override void Read(JObject jOperation) 31 | { 32 | Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /JsonDiffPatch/ReplaceOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Tavis; 4 | 5 | namespace JsonDiffPatch 6 | { 7 | public class ReplaceOperation : Operation 8 | { 9 | public JToken Value { get; private set; } 10 | 11 | public ReplaceOperation() 12 | { 13 | 14 | } 15 | 16 | public ReplaceOperation(JsonPointer path, JToken value) : base(path) 17 | { 18 | Value = value; 19 | } 20 | 21 | public override void Write(JsonWriter writer) 22 | { 23 | writer.WriteStartObject(); 24 | 25 | WriteOp(writer, "replace"); 26 | WritePath(writer, Path); 27 | WriteValue(writer, Value); 28 | 29 | writer.WriteEndObject(); 30 | } 31 | 32 | public override void Read(JObject jOperation) 33 | { 34 | Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); 35 | Value = jOperation.GetValue("value"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JsonDiffPatch/Tavis.JsonPointer/JsonPointer.cs: -------------------------------------------------------------------------------- 1 | //see https://github.com/tavis-software/Tavis.JsonPointer 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace Tavis 9 | { 10 | public class JsonPointer 11 | { 12 | private readonly IReadOnlyList _Tokens; 13 | 14 | public JsonPointer(string pointer) 15 | { 16 | _Tokens = pointer.Split('/').Skip(1).Select(Decode).ToArray(); 17 | } 18 | 19 | internal JsonPointer(IReadOnlyList tokens) 20 | { 21 | _Tokens = tokens; 22 | } 23 | private string Decode(string token) 24 | { 25 | return Uri.UnescapeDataString(token).Replace("~1", "/").Replace("~0", "~"); 26 | } 27 | 28 | public bool IsNewPointer() 29 | { 30 | return _Tokens[_Tokens.Count - 1] == "-"; 31 | } 32 | 33 | public JsonPointer ParentPointer 34 | { 35 | get 36 | { 37 | if (_Tokens.Count == 0) return null; 38 | 39 | var tokens = new string[_Tokens.Count - 1]; 40 | for (int i = 0; i < _Tokens.Count - 1; i++) 41 | { 42 | tokens[i] = _Tokens[i]; 43 | } 44 | 45 | return new JsonPointer(tokens); 46 | } 47 | } 48 | 49 | public JToken Find(JToken sample) 50 | { 51 | if (_Tokens.Count == 0) 52 | { 53 | return sample; 54 | } 55 | try 56 | { 57 | var pointer = sample; 58 | foreach (var token in _Tokens.Select(t => t.Replace("~1", "/").Replace("~0", "~"))) 59 | { 60 | if (pointer is JArray) 61 | { 62 | pointer = pointer[Convert.ToInt32(token)]; 63 | } 64 | else 65 | { 66 | pointer = pointer[token]; 67 | if (pointer == null) 68 | { 69 | throw new ArgumentException("Cannot find " + token); 70 | } 71 | 72 | } 73 | } 74 | return pointer; 75 | } 76 | catch (Exception ex) 77 | { 78 | throw new ArgumentException("Failed to dereference pointer",ex); 79 | } 80 | } 81 | 82 | public override string ToString() 83 | { 84 | return "/" + String.Join("/", _Tokens); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /JsonDiffPatch/TestOperation.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using Tavis; 6 | 7 | namespace JsonDiffPatch 8 | { 9 | public class TestOperation : Operation 10 | { 11 | public JToken Value { get; private set; } 12 | 13 | public TestOperation() 14 | { 15 | } 16 | 17 | public TestOperation(JsonPointer path, JToken value) : base(path) 18 | { 19 | Value = value; 20 | } 21 | 22 | public override void Write(JsonWriter writer) 23 | { 24 | writer.WriteStartObject(); 25 | 26 | WriteOp(writer, "test"); 27 | WritePath(writer, Path); 28 | WriteValue(writer, Value); 29 | 30 | writer.WriteEndObject(); 31 | } 32 | 33 | public override void Read(JObject jOperation) 34 | { 35 | Path = new JsonPointer(SplitPath((string)jOperation.GetValue("path"))); 36 | Value = jOperation.GetValue("value"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /JsonDiffPatch/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/AddTests.cs: -------------------------------------------------------------------------------- 1 | using JsonDiffPatch; 2 | using Newtonsoft.Json.Linq; 3 | using NUnit.Framework; 4 | 5 | namespace Tavis.JsonPatch.Tests 6 | { 7 | public class AddTests 8 | { 9 | 10 | [Test] 11 | public void Add_an_array_element() 12 | { 13 | 14 | var sample = PatchTests.GetSample2(); 15 | 16 | var patchDocument = new PatchDocument(); 17 | var pointer = new JsonPointer("/books"); 18 | 19 | patchDocument.AddOperation(new AddOperation(pointer, new JObject(new[] { new JProperty("author", "James Brown") }))); 20 | 21 | var patcher = new JsonPatcher(); 22 | patcher.Patch(ref sample, patchDocument); 23 | 24 | var list = sample["books"] as JArray; 25 | 26 | Assert.AreEqual(3, list.Count); 27 | 28 | } 29 | 30 | [Test] 31 | public void Add_an_existing_member_property() // Why isn't this replace? 32 | { 33 | 34 | var sample = PatchTests.GetSample2(); 35 | 36 | var patchDocument = new PatchDocument(); 37 | var pointer = new JsonPointer("/books/0/title"); 38 | 39 | patchDocument.AddOperation(new AddOperation(pointer, new JValue("Little Red Riding Hood"))); 40 | 41 | var patcher = new JsonPatcher(); 42 | patcher.Patch(ref sample, patchDocument); 43 | 44 | 45 | var result = (string)pointer.Find(sample); 46 | Assert.AreEqual("Little Red Riding Hood", result); 47 | 48 | } 49 | 50 | [Test] 51 | public void Add_a_non_existing_member_property() 52 | { 53 | 54 | var sample = PatchTests.GetSample2(); 55 | 56 | var patchDocument = new PatchDocument(); 57 | var pointer = new JsonPointer("/books/0/ISBN"); 58 | 59 | patchDocument.AddOperation(new AddOperation(pointer, new JValue("213324234343"))); 60 | 61 | var patcher = new JsonPatcher(); 62 | patcher.Patch(ref sample, patchDocument); 63 | 64 | 65 | var result = (string)pointer.Find(sample); 66 | Assert.AreEqual("213324234343", result); 67 | 68 | } 69 | 70 | [Test] 71 | public void Add_a_non_existing_member_property_with_slash_character() 72 | { 73 | 74 | var sample = PatchTests.GetSample2(); 75 | 76 | var patchDocument = new PatchDocument(); 77 | var pointer = new JsonPointer("/books/0/b~1c"); 78 | 79 | patchDocument.AddOperation(new AddOperation(pointer, new JValue("42"))); 80 | 81 | var patcher = new JsonPatcher(); 82 | patcher.Patch(ref sample, patchDocument); 83 | 84 | 85 | var result = (string)pointer.Find(sample); 86 | Assert.AreEqual("42", result); 87 | 88 | } 89 | 90 | [Test] 91 | public void Add_a_non_existing_member_property_with_tilda_character() 92 | { 93 | 94 | var sample = PatchTests.GetSample2(); 95 | 96 | var patchDocument = new PatchDocument(); 97 | var pointer = new JsonPointer("/books/0/b~0c"); 98 | 99 | patchDocument.AddOperation(new AddOperation(pointer, new JValue("42"))); 100 | 101 | var patcher = new JsonPatcher(); 102 | patcher.Patch(ref sample, patchDocument); 103 | 104 | 105 | var result = (string)pointer.Find(sample); 106 | Assert.AreEqual("42", result); 107 | 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/CopyTests.cs: -------------------------------------------------------------------------------- 1 | using JsonDiffPatch; 2 | using Newtonsoft.Json.Linq; 3 | using NUnit.Framework; 4 | 5 | namespace Tavis.JsonPatch.Tests 6 | { 7 | public class CopyTests 8 | { 9 | [Test] 10 | public void Copy_array_element() 11 | { 12 | var sample = PatchTests.GetSample2(); 13 | 14 | var patchDocument = new PatchDocument(); 15 | var frompointer = new JsonPointer("/books/0"); 16 | var topointer = new JsonPointer("/books/-"); 17 | 18 | patchDocument.AddOperation(new CopyOperation(topointer, frompointer)); 19 | 20 | var patcher = new JsonPatcher(); 21 | patcher.Patch(ref sample, patchDocument); 22 | 23 | var result = new JsonPointer("/books/2").Find(sample); 24 | Assert.IsInstanceOf(typeof(JObject), result); 25 | 26 | } 27 | 28 | [Test] 29 | public void Copy_property() 30 | { 31 | var sample = PatchTests.GetSample2(); 32 | 33 | var patchDocument = new PatchDocument(); 34 | var frompointer = new JsonPointer("/books/0/ISBN"); 35 | var topointer = new JsonPointer("/books/1/ISBN"); 36 | 37 | patchDocument.AddOperation(new AddOperation(frompointer, new JValue("21123123"))); 38 | patchDocument.AddOperation(new CopyOperation(topointer, frompointer)); 39 | 40 | var patcher = new JsonPatcher(); 41 | patcher.Patch(ref sample, patchDocument); 42 | 43 | var result = new JsonPointer("/books/1/ISBN").Find(sample); 44 | Assert.AreEqual("21123123", result.Value()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/DiffTests.cs: -------------------------------------------------------------------------------- 1 | using JsonDiffPatch; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Tavis.JsonPatch.Tests 7 | { 8 | [TestFixture] 9 | public class DiffTests 10 | { 11 | [TestCase("{a:1, b:2, c:3}", 12 | "{a:1, b:2}", 13 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/c\"}]", 14 | TestName = "JsonPatch remove works for a simple value")] 15 | 16 | [TestCase("{a:1, b:2, c:{d:1,e:2}}", 17 | "{a:1, b:2}", 18 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/c\"}]", 19 | TestName = "JsonPatch remove works for a complex value")] 20 | 21 | [TestCase("{a:1, b:2}", 22 | "{a:1, b:2, c:3}", 23 | ExpectedResult = "[{\"op\":\"add\",\"path\":\"/c\",\"value\":3}]", 24 | TestName = "JsonPatch add works for a simple value")] 25 | 26 | [TestCase("{a:1, b:2}", 27 | "{a:1, b:2, c:{d:1,e:2}}", 28 | ExpectedResult = "[{\"op\":\"add\",\"path\":\"/c\",\"value\":{\"d\":1,\"e\":2}}]", 29 | TestName = "JsonPatch add works for a complex value")] 30 | 31 | [TestCase("{a:1, b:2, c:3}", 32 | "{a:1, b:2, c:4}", 33 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/c\",\"value\":4}]", 34 | TestName = "JsonPatch replace works for int")] 35 | 36 | [TestCase("{a:1, b:2, c:\"foo\"}", 37 | "{a:1, b:2, c:\"bar\"}", 38 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/c\",\"value\":\"bar\"}]", 39 | TestName = "JsonPatch replace works for string")] 40 | 41 | [TestCase("{a:1, b:2, c:{foo:1}}", 42 | "{a:1, b:2, c:{bar:2}}", 43 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/c/foo\"},{\"op\":\"add\",\"path\":\"/c/bar\",\"value\":2}]", 44 | TestName = "JsonPatch replace works for object")] 45 | 46 | [TestCase("{a:1, b:2, c:3}", 47 | "{c:3, b:2, a:1}", 48 | ExpectedResult = "[]", 49 | TestName = "JsonPatch order does not matter")] 50 | 51 | [TestCase("{a:{b:{c:{d:1}}}}", 52 | "{a:{b:{d:{c:1}}}}", 53 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/a/b/c\"},{\"op\":\"add\",\"path\":\"/a/b/d\",\"value\":{\"c\":1}}]", 54 | TestName = "JsonPatch handles deep nesting")] 55 | 56 | [TestCase("[1,2,3,4]", 57 | "[5,6,7]", 58 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/\",\"value\":[5,6,7]}]", 59 | TestName = "JsonPatch handles a simple array and replaces it")] 60 | 61 | [TestCase("{a:[1,2,3,4]}", 62 | "{a:[5,6,7]}", 63 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a\",\"value\":[5,6,7]}]", 64 | TestName = "JsonPatch handles a simple array under a property and replaces it")] 65 | 66 | [TestCase("{a:[1,2,3,4]}", 67 | "{a:[1,2,3,4]}", 68 | ExpectedResult = "[]", 69 | TestName = "JsonPatch handles same array")] 70 | 71 | [TestCase("{a:[1,2,3,{name:'a'}]}", 72 | "{a:[1,2,3,{name:'a'}]}", 73 | ExpectedResult = "[]", 74 | TestName = "JsonPatch handles same array containing objects")] 75 | 76 | [TestCase("{a:[1,2,3,{name:'a'},4,5]}", 77 | "{a:[1,2,3,{name:'b'},4,5]}", 78 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a/3/name\",\"value\":\"b\"}]", 79 | TestName = "Replaces array items")] 80 | 81 | [TestCase("{a:[]}", 82 | "{a:[]}", 83 | ExpectedResult = "[]", 84 | TestName = "Empty array gives no operations")] 85 | 86 | [TestCase("['a', 'b', 'c']", 87 | "['a', 'd', 'c']", 88 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/1\",\"value\":\"d\"}]" 89 | , TestName = "Inserts item in centre of array correctly")] 90 | 91 | [TestCase( 92 | "[1,4,5,6,2]", 93 | "[1,3,4,5,2]", 94 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/1\",\"value\":3},{\"op\":\"replace\",\"path\":\"/2\",\"value\":4},{\"op\":\"replace\",\"path\":\"/3\",\"value\":5}]" 95 | , TestName = "Replaces items in middle of int array")] 96 | 97 | [TestCase( 98 | "[\"1\",\"4\",\"5\",\"6\",\"2\"]", 99 | "[\"1\",\"3\",\"4\",\"5\",\"2\"]", 100 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/1\",\"value\":\"3\"},{\"op\":\"replace\",\"path\":\"/2\",\"value\":\"4\"},{\"op\":\"replace\",\"path\":\"/3\",\"value\":\"5\"}]" 101 | , TestName = "Replaces items in middle of string array")] 102 | 103 | [TestCase( 104 | "[{\"prop\":\"1\"},{\"prop\":\"4\"},{\"prop\":\"5\"},{\"prop\":\"6\"},{\"prop\":\"2\"}]", 105 | "[{\"prop\":\"1\"},{\"prop\":\"3\"},{\"prop\":\"4\"},{\"prop\":\"5\"},{\"prop\":\"2\"}]", 106 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/1/prop\",\"value\":\"3\"},{\"op\":\"replace\",\"path\":\"/2/prop\",\"value\":\"4\"},{\"op\":\"replace\",\"path\":\"/3/prop\",\"value\":\"5\"}]" 107 | , TestName = "Replaces items in middle of complex objects array")] 108 | 109 | [TestCase( 110 | "[1,4,5,6,2]", 111 | "[1,3,4,5,7,2]", 112 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/1\"},{\"op\":\"remove\",\"path\":\"/1\"},{\"op\":\"replace\",\"path\":\"/1\",\"value\":3},{\"op\":\"add\",\"path\":\"/2\",\"value\":4},{\"op\":\"add\",\"path\":\"/3\",\"value\":5},{\"op\":\"add\",\"path\":\"/4\",\"value\":7}]" 113 | , TestName = "Manipulates items in middle of int array with different length")] 114 | 115 | [TestCase( 116 | "{a:{}}", 117 | "{a:{'foo/bar':1337}}", 118 | ExpectedResult = "[{\"op\":\"add\",\"path\":\"/a/foo~1bar\",\"value\":1337}]" 119 | , TestName = "Adds a key containing a slash")] 120 | 121 | [TestCase( 122 | "{a:{}}", 123 | "{a:{'foo~1bar':1337}}", 124 | ExpectedResult = "[{\"op\":\"add\",\"path\":\"/a/foo~01bar\",\"value\":1337}]" 125 | , TestName = "Adds a key containing a tilde")] 126 | 127 | [TestCase( 128 | "{a:{}}", 129 | "{a:{'foo0~/1bar':1337}}", 130 | ExpectedResult = "[{\"op\":\"add\",\"path\":\"/a/foo0~0~11bar\",\"value\":1337}]" 131 | , TestName = "Adds a key containing a tilde and a slash")] 132 | 133 | [TestCase( 134 | "{a:{'foo/bar':42}}", 135 | "{a:{'foo/bar':1337}}", 136 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a/foo~1bar\",\"value\":1337}]" 137 | , TestName = "Replaces a key containing a slash")] 138 | 139 | [TestCase( 140 | "{a:{'foo/bar':42}}", 141 | "{a:{}}", 142 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/a/foo~1bar\"}]" 143 | , TestName = "Remove a key containing a slash")] 144 | 145 | [TestCase( 146 | "{a:[]}", 147 | "{a:[{'foo/bar':1337}]}", 148 | ExpectedResult = "[{\"op\":\"add\",\"path\":\"/a/0\",\"value\":{\"foo/bar\":1337}}]" 149 | , TestName = "Adds an object to an array that contains a slash in some property")] 150 | 151 | [TestCase( 152 | "{a:[{'foo/bar':1337}]}", 153 | "{a:[]}", 154 | ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/a/0\"}]" 155 | , TestName = "Remove an object to an array that contains a slash in some property")] 156 | 157 | [TestCase( 158 | "{a:[{},{'foo/bar':42}]}", 159 | "{a:[{},{'foo/bar':1337}]}", 160 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a/1/foo~1bar\",\"value\":1337}]" 161 | , TestName = "Replace a value in an object in an array that contains a slash in some property")] 162 | 163 | [TestCase( 164 | "{a:[{'foo/bar':42}]}", 165 | "{a:[{'foo/bar':1337}]}", 166 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a\",\"value\":[{\"foo/bar\":1337}]}]" 167 | , TestName = "Replace whole array that contains a slash in some property")] 168 | 169 | [TestCase( 170 | "{a:[{'foo~bar':42}]}", 171 | "{a:[{'foo~bar':1337}]}", 172 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a\",\"value\":[{\"foo~bar\":1337}]}]" 173 | , TestName = "Replace whole array that contains a tilde in some property")] 174 | 175 | [TestCase( 176 | "{a:[{},{'foo~bar':42}]}", 177 | "{a:[{},{'foo~bar':1337}]}", 178 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a/1/foo~0bar\",\"value\":1337}]" 179 | , TestName = "Replace a value in an object in an array that contains a tilde in some property")] 180 | 181 | [TestCase("{a:[1,2,3,{name:'a'}]}", 182 | "{a:[1,2,3,{name:'b'}]}", 183 | ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a/3/name\",\"value\":\"b\"}]", 184 | TestName = "JsonPatch handles same array containing different objects")] 185 | public string JsonPatchesWorks(string leftString, string rightString) 186 | { 187 | var left = JToken.Parse(leftString); 188 | var right = JToken.Parse(rightString); 189 | 190 | var patchDoc = new JsonDiffer().Diff(left, right, false); 191 | var patcher = new JsonPatcher(); 192 | patcher.Patch(ref left, patchDoc); 193 | 194 | 195 | Assert.True(JToken.DeepEquals(left, right)); 196 | //Assert.AreEqual(expected, patchedLeft); 197 | 198 | var patchJson = patchDoc.ToString(Formatting.None); 199 | return patchJson; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/DiffTests2.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using JsonDiffPatch; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Tavis.JsonPatch.Tests 7 | { 8 | public class DiffTests2 { 9 | 10 | [Test] 11 | public void ComplexExampleWithDeepArrayChange() 12 | { 13 | 14 | var leftPath = @".\samples\scene{0}a.json"; 15 | var rightPath = @".\samples\scene{0}b.json"; 16 | var i = 1; 17 | while(File.Exists(string.Format(leftPath, i))) 18 | { 19 | var scene1Text = File.ReadAllText(string.Format(leftPath, i)); 20 | var scene1 = JToken.Parse(scene1Text); 21 | var scene2Text = File.ReadAllText(string.Format(rightPath, i)); 22 | var scene2 = JToken.Parse(scene2Text); 23 | var patchDoc = new JsonDiffer().Diff(scene1, scene2, true); 24 | //Assert.AreEqual("[{\"op\":\"remove\",\"path\":\"/items/0/entities/1\"}]", 25 | var patcher = new JsonPatcher(); 26 | patcher.Patch(ref scene1, patchDoc); 27 | Assert.True(JToken.DeepEquals(scene1, scene2)); 28 | i++; 29 | } 30 | } 31 | 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/JsonDiffPatch.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/MoveTests.cs: -------------------------------------------------------------------------------- 1 | using JsonDiffPatch; 2 | using Newtonsoft.Json.Linq; 3 | using NUnit.Framework; 4 | 5 | namespace Tavis.JsonPatch.Tests 6 | { 7 | public class MoveTests 8 | { 9 | 10 | [Test] 11 | public void Move_property() 12 | { 13 | var sample = PatchTests.GetSample2(); 14 | 15 | var patchDocument = new PatchDocument(); 16 | var frompointer = new JsonPointer("/books/0/author"); 17 | var topointer = new JsonPointer("/books/1/author"); 18 | 19 | patchDocument.AddOperation(new MoveOperation(topointer, frompointer)); 20 | 21 | var patcher = new JsonPatcher(); 22 | patcher.Patch(ref sample, patchDocument); 23 | 24 | 25 | var result = (string)topointer.Find(sample); 26 | Assert.AreEqual("F. Scott Fitzgerald", result); 27 | } 28 | 29 | [Test] 30 | public void Move_array_element() 31 | { 32 | var sample = PatchTests.GetSample2(); 33 | 34 | var patchDocument = new PatchDocument(); 35 | var frompointer = new JsonPointer("/books/1"); 36 | var topointer = new JsonPointer("/books/0/child"); 37 | 38 | patchDocument.AddOperation(new MoveOperation(topointer, frompointer)); 39 | 40 | var patcher = new JsonPatcher(); 41 | patcher.Patch(ref sample, patchDocument); 42 | 43 | 44 | var result = topointer.Find(sample); 45 | Assert.IsInstanceOf(typeof(JObject), result); 46 | } 47 | 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/PatchTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using JsonDiffPatch; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Tavis.JsonPatch.Tests 7 | { 8 | public class PatchTests 9 | { 10 | [Test] 11 | public void CreateEmptyPatch() 12 | { 13 | 14 | var sample = GetSample2(); 15 | var sampletext = sample.ToString(); 16 | 17 | var patchDocument = new PatchDocument(); 18 | new JsonPatcher().Patch(ref sample, patchDocument); 19 | 20 | Assert.AreEqual(sampletext,sample.ToString()); 21 | } 22 | 23 | 24 | [Test] 25 | public void LoadPatch1() 26 | { 27 | var names = this.GetType() 28 | .Assembly.GetManifestResourceNames(); 29 | var patchDoc = 30 | PatchDocument.Load(this.GetType() 31 | .Assembly.GetManifestResourceStream("JsonDiffPatch.Tests.Samples.LoadTest1.json")); 32 | 33 | Assert.NotNull(patchDoc); 34 | Assert.AreEqual(6,patchDoc.Operations.Count); 35 | } 36 | 37 | 38 | [Test] 39 | public void TestExample1() 40 | { 41 | var targetDoc = JToken.Parse("{ 'foo': 'bar'}"); 42 | var patchDoc = PatchDocument.Parse(@"[ 43 | { 'op': 'add', 'path': '/baz', 'value': 'qux' } 44 | ]"); 45 | new JsonPatcher().Patch(ref targetDoc, patchDoc); 46 | 47 | 48 | Assert.True(JToken.DeepEquals(JToken.Parse(@"{ 49 | 'foo': 'bar', 50 | 'baz': 'qux' 51 | }"), targetDoc)); 52 | } 53 | 54 | 55 | 56 | 57 | [Test] 58 | public void SerializePatchDocument() 59 | { 60 | var patchDoc = new PatchDocument(new Operation[] 61 | { 62 | new TestOperation(new JsonPointer("/a/b/c"), new JValue("foo")), 63 | new RemoveOperation(new JsonPointer("/a/b/c")), 64 | new AddOperation(new JsonPointer("/a/b/c"), new JArray(new JValue("foo"), new JValue("bar"))), 65 | new ReplaceOperation(new JsonPointer("/a/b/c"), new JValue(42)), 66 | new MoveOperation(new JsonPointer("/a/b/d"), new JsonPointer("/a/b/c")), 67 | new CopyOperation(new JsonPointer("/a/b/e"), new JsonPointer("/a/b/d")), 68 | }); 69 | 70 | var outputstream = patchDoc.ToStream(); 71 | var output = new StreamReader(outputstream).ReadToEnd(); 72 | 73 | var jOutput = JToken.Parse(output); 74 | 75 | var jExpected = JToken.Parse(new StreamReader(this.GetType() 76 | .Assembly.GetManifestResourceStream("JsonDiffPatch.Tests.Samples.LoadTest1.json")).ReadToEnd()); 77 | Assert.True(JToken.DeepEquals(jExpected,jOutput)); 78 | } 79 | 80 | 81 | 82 | public static JToken GetSample2() 83 | { 84 | return JToken.Parse(@"{ 85 | 'books': [ 86 | { 87 | 'title' : 'The Great Gatsby', 88 | 'author' : 'F. Scott Fitzgerald' 89 | }, 90 | { 91 | 'title' : 'The Grapes of Wrath', 92 | 'author' : 'John Steinbeck' 93 | } 94 | ] 95 | }"); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/PatchTests2.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JsonDiffPatch; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using NUnit.Framework; 6 | 7 | namespace Tavis.JsonPatch.Tests 8 | { 9 | [TestFixture] 10 | public class PatchTests2 11 | { 12 | [TestCase("{a:1, b:2, c:3}", 13 | "[{\"op\":\"remove\",\"path\":\"/c\"}]", 14 | ExpectedResult = "{\"a\":1,\"b\":2}", 15 | TestName = "Patch can remove works for a simple value")] 16 | [TestCase("{a:1, b:2, c:3}", 17 | "[{\"op\":\"replace\",\"path\":\"\",value:{\"x\":0}}]", 18 | ExpectedResult = "{\"x\":0}", 19 | TestName = "Can patch the root object")] 20 | [TestCase("{\"\":1 }", 21 | "[{\"op\":\"replace\",\"path\":\"/\",value:{\"x\":0}}]", 22 | ExpectedResult = "{\"\":{\"x\":0}}", 23 | TestName = "Can patch a space named property")] 24 | 25 | public string JsonPatchesWorks(string leftString, string patchString) 26 | { 27 | var left = JToken.Parse(leftString); 28 | var patchDoc = PatchDocument.Parse(patchString); 29 | var patcher = new JsonPatcher(); 30 | patcher.Patch(ref left, patchDoc); 31 | 32 | 33 | var patchJson = left.ToString(Formatting.None); 34 | return patchJson; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/RemoveTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JsonDiffPatch; 3 | using NUnit.Framework; 4 | 5 | namespace Tavis.JsonPatch.Tests 6 | { 7 | public class RemoveTests 8 | { 9 | [Test] 10 | public void Remove_a_property() 11 | { 12 | 13 | var sample = PatchTests.GetSample2(); 14 | 15 | var patchDocument = new PatchDocument(); 16 | var pointer = new JsonPointer("/books/0/author"); 17 | 18 | patchDocument.AddOperation(new RemoveOperation(pointer)); 19 | 20 | new JsonPatcher().Patch(ref sample, patchDocument); 21 | 22 | Assert.Throws(typeof(ArgumentException), () => { pointer.Find(sample); }); 23 | } 24 | 25 | [Test] 26 | public void Remove_an_array_element() 27 | { 28 | 29 | var sample = PatchTests.GetSample2(); 30 | 31 | var patchDocument = new PatchDocument(); 32 | var pointer = new JsonPointer("/books/0"); 33 | 34 | patchDocument.AddOperation(new RemoveOperation(pointer)); 35 | 36 | var patcher = new JsonPatcher(); 37 | patcher.Patch(ref sample, patchDocument); 38 | 39 | Assert.Throws(typeof(ArgumentException), () => 40 | { 41 | var x = pointer.Find("/books/1"); 42 | }); 43 | 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/ReplaceTests.cs: -------------------------------------------------------------------------------- 1 | using JsonDiffPatch; 2 | using Newtonsoft.Json.Linq; 3 | using NUnit.Framework; 4 | 5 | namespace Tavis.JsonPatch.Tests 6 | { 7 | public class ReplaceTests 8 | { 9 | [Test] 10 | public void Replace_a_property_value_with_a_new_value() 11 | { 12 | 13 | var sample = PatchTests.GetSample2(); 14 | 15 | var patchDocument = new PatchDocument(); 16 | var pointer = new JsonPointer("/books/0/author"); 17 | 18 | patchDocument.AddOperation(new ReplaceOperation(pointer, new JValue("Bob Brown"))); 19 | 20 | new JsonPatcher().Patch(ref sample, patchDocument); 21 | 22 | Assert.AreEqual("Bob Brown", (string)pointer.Find(sample)); 23 | } 24 | 25 | [Test] 26 | public void Replace_a_property_value_with_an_object() 27 | { 28 | 29 | var sample = PatchTests.GetSample2(); 30 | 31 | var patchDocument = new PatchDocument(); 32 | var pointer = new JsonPointer("/books/0/author"); 33 | 34 | patchDocument.AddOperation(new ReplaceOperation(pointer, new JObject(new[] { new JProperty("hello", "world") }))); 35 | 36 | new JsonPatcher().Patch(ref sample, patchDocument); 37 | 38 | var newPointer = new JsonPointer("/books/0/author/hello"); 39 | Assert.AreEqual("world", (string)newPointer.Find(sample)); 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/LoadTest1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "op": "test", "path": "/a/b/c", "value": "foo" }, 3 | { "op": "remove", "path": "/a/b/c" }, 4 | { "op": "add", "path": "/a/b/c", "value": ["foo", "bar"] }, 5 | { "op": "replace", "path": "/a/b/c", "value": 42 }, 6 | { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }, 7 | { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" } 8 | ] -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/scene1a.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "id": "viewpoint", 5 | "entities": [ 6 | { 7 | "name": "Donn Crimmins (sdf)" 8 | }, 9 | { 10 | "name": "Chester Westberg" 11 | } 12 | ], 13 | "type": "Viewpoint" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/scene1b.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "id": "viewpoint", 5 | "entities": [ 6 | { 7 | "name": "Donn Crimmins (sdf)" 8 | } 9 | ], 10 | "type": "Viewpoint" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/scene2a.json: -------------------------------------------------------------------------------- 1 | { 2 | "incidents": [ 3 | { 4 | "when": "2015-09-08T12:15:25+01:00", 5 | "id": "7f468da7-3788-40c0-90fd-b06bc61c86dd", 6 | "where": "Fey St", 7 | "what": [ 8 | "Franklyn Sanfilippo (aaaa) says \"1\"" 9 | ], 10 | "type": "Incident" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/scene2b.json: -------------------------------------------------------------------------------- 1 | { 2 | "incidents": [ 3 | { 4 | "when": "2015-09-08T12:15:46+01:00", 5 | "id": "dab4a570-2dde-458e-92b1-f71e42605f90", 6 | "where": "Fey St", 7 | "what": [ 8 | "Franklyn Sanfilippo (aaaa) says \"2\"" 9 | ], 10 | "type": "Incident" 11 | }, 12 | { 13 | "when": "2015-09-08T12:15:25+01:00", 14 | "id": "7f468da7-3788-40c0-90fd-b06bc61c86dd", 15 | "where": "Fey St", 16 | "what": [ 17 | "Franklyn Sanfilippo (aaaa) says \"1\"" 18 | ], 19 | "type": "Incident" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/scene3a.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": [ 3 | "cash: 400", 4 | "product: 100" 5 | ] 6 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/Samples/scene3b.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": [ 3 | "cash: 700", 4 | "product: 000" 5 | ] 6 | } -------------------------------------------------------------------------------- /JsonDiffPatchTests/TestTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JsonDiffPatch; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace Tavis.JsonPatch.Tests 7 | { 8 | public class TestTests 9 | { 10 | [Test] 11 | public void Test_a_value() 12 | { 13 | 14 | var sample = PatchTests.GetSample2(); 15 | 16 | var patchDocument = new PatchDocument(); 17 | var pointer = new JsonPointer("/books/0/author"); 18 | 19 | patchDocument.AddOperation(new TestOperation(pointer, new JValue("Billy Burton"))); 20 | 21 | Assert.Throws(typeof(InvalidOperationException), () => 22 | { 23 | var patcher = new JsonPatcher(); 24 | patcher.Patch(ref sample, patchDocument); 25 | }); 26 | 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /JsonDiffPatchTests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | 180 | Copyright 2012 Tavis Software Inc. 181 | 182 | Licensed under the Apache License, Version 2.0 (the "License"); 183 | you may not use this file except in compliance with the License. 184 | You may obtain a copy of the License at 185 | 186 | http://www.apache.org/licenses/LICENSE-2.0 187 | 188 | Unless required by applicable law or agreed to in writing, software 189 | distributed under the License is distributed on an "AS IS" BASIS, 190 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 191 | See the License for the specific language governing permissions and 192 | limitations under the License. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # JsonDiffPatch 2 | 3 | 4 | 5 | This library is an implementation of a Json Patch [RFC 6902](http://tools.ietf.org/html/rfc6902). 6 | * forked from https://github.com/tavis-software/Tavis.JsonPatch 7 | * plus a modified diff generator by Ian Mercer (http://blog.abodit.com/2014/05/json-patch-c-implementation/) 8 | 9 | The default example from the specification looks like this, 10 | 11 | [ 12 | { "op": "test", "path": "/a/b/c", "value": "foo" }, 13 | { "op": "remove", "path": "/a/b/c" }, 14 | { "op": "add", "path": "/a/b/c", "value": ["foo", "bar"] }, 15 | { "op": "replace", "path": "/a/b/c", "value": 42 }, 16 | { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }, 17 | { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" } 18 | ] 19 | 20 | This library allows you to create this document by doing, 21 | 22 | var patchDoc = new PatchDocument( new Operation[] 23 | { 24 | new TestOperation() {Path = new JsonPointer("/a/b/c"), Value = new JValue("foo")}, 25 | new RemoveOperation() {Path = new JsonPointer("/a/b/c") }, 26 | new AddOperation() {Path = new JsonPointer("/a/b/c"), Value = new JArray(new JValue("foo"), new JValue("bar"))}, 27 | new ReplaceOperation() {Path = new JsonPointer("/a/b/c"), Value = new JValue(42)}, 28 | new MoveOperation() {FromPath = new JsonPointer("/a/b/c"), Path = new JsonPointer("/a/b/d") }, 29 | new CopyOperation() {FromPath = new JsonPointer("/a/b/d"), Path = new JsonPointer("/a/b/e") }, 30 | }); 31 | 32 | This document can be serialized to the wire format like this, 33 | 34 | var outputstream = patchDoc.ToStream(); 35 | 36 | You can also read patch documents from the wire representation and apply them to a JSON document. 37 | 38 | var targetDoc = JToken.Parse("{ 'foo': 'bar'}"); 39 | var patchDoc = PatchDocument.Parse(@"[ { 'op': 'add', 'path': '/baz', 'value': 'qux' } ]"); 40 | 41 | patchDoc.ApplyTo(targetDoc); 42 | 43 | You can also generate a patchdocument by diffing two json objects 44 | 45 | var left = JToken.Parse(leftString); 46 | var right = JToken.Parse(rightString); 47 | var patchDoc = new JsonDiffer().Diff(left, right); 48 | 49 | You can apply a patch document to a json object too. 50 | 51 | var left = JToken.Parse(leftString); 52 | var patcher = new JsonPatcher(); 53 | patcher.Patch(ref left, patchDoc); //left is now updated by the patchDoc 54 | 55 | 56 | 57 | The unit tests provide examples of other usages. 58 | 59 | This library is a PCL based library and so will work on Windows 8, Windows Phone 7.5, .Net 4. 60 | 61 | A nuget package is available [here](http://www.nuget.org/packages/Tavis.JsonPatch). 62 | --------------------------------------------------------------------------------