Quantcast
Channel: Ian Mercer » My News
Viewing all articles
Browse latest Browse all 15

JSON Patch – a C# implementation

$
0
0

I’m in the process of trying to implement JSON Patch as a way to cut the chatter down between my various home automation systems. In particular, since I’ve implemented a SignalR-all-the-things web interface I want to push updates across to the browser as efficiently as possible. I also want to make communication with mobile devices as efficient as possible.

To this end my home automation server now maintains a state for each mobile device or browser session for all the objects that browser or mobile connection is currently displaying. When things change it filters the changes according to what’s on the display and sends down the full or partial objects accordingly. But now I plan to calculate a set of JSON Patch differences and send them down as things change rather than sending the whole object. This should dramatically reduce how much data is being sent.

To that end I started writing an implementation of JSON Patch tonight. The first part is concerned with calculating a patch as a difference of two objects. Tomorrow night I’ll implement the apply method that applies a patch to an existing object.

Anyway, here’s tonight’s code in case anyone else wants to do this. If you make enough noise I might even put it on Github.

First the test cases:-

using System;
using NUnit.Framework;
using Abodit;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using FluentAssertions;
using System.Linq;

namespace TestSync
{
    [TestFixture]
    public class TestJsonPatches
    {
        [TestCase("{a:1, b:2, c:3}",
            "{a:1, b:2}",
            ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/c\",\"value\":null}]",
            TestName = "JsonPatch remove works for a simple value")]

        [TestCase("{a:1, b:2, c:{d:1,e:2}}",
            "{a:1, b:2}",
            ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/c\",\"value\":null}]",
            TestName = "JsonPatch remove works for a complex value")]

        [TestCase("{a:1, b:2}",
            "{a:1, b:2, c:3}",
            ExpectedResult = "[{\"op\":\"add\",\"path\":\"/c\",\"value\":3}]",
            TestName = "JsonPatch add works for a simple value")]

        [TestCase("{a:1, b:2}",
            "{a:1, b:2, c:{d:1,e:2}}",
            ExpectedResult = "[{\"op\":\"add\",\"path\":\"/c\",\"value\":\"{d:1,e:2}\"}]",
            TestName = "JsonPatch add works for a complex value")]

        [TestCase("{a:1, b:2, c:3}",
            "{a:1, b:2, c:4}", 
            ExpectedResult="[{\"op\":\"replace\",\"path\":\"/c\",\"value\":4}]", 
            TestName="JsonPatch replace works for int")]

        [TestCase("{a:1, b:2, c:\"foo\"}",
            "{a:1, b:2, c:\"bar\"}",
            ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/c\",\"value\":\"bar\"}]",
            TestName = "JsonPatch replace works for string")]

        [TestCase("{a:1, b:2, c:{foo:1}}",
            "{a:1, b:2, c:{bar:2}}",
            ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/c/foo\",\"value\":null},{\"op\":\"add\",\"path\":\"/c/bar\",\"value\":2}]",
            TestName = "JsonPatch replace works for object")]

        [TestCase("{a:1, b:2, c:3}",
            "{c:3, b:2, a:1}",
            ExpectedResult = "[]",
            TestName = "JsonPatch order does not matter")]

        [TestCase("{a:{b:{c:{d:1}}}}",
            "{a:{b:{d:{c:1}}}}",
            ExpectedResult = "[{\"op\":\"remove\",\"path\":\"/a/b/c\",\"value\":null},{\"op\":\"add\",\"path\":\"/a/b/d\",\"value\":{\"c\":1}}]",
            TestName = "JsonPatch handles deep nesting")]

        [TestCase("[1,2,3,4]",
            "[5,6,7]",
            ExpectedResult = "[{\"op\":\"replace\",\"path\":\"\",\"value\":[5,6,7]}]",
            TestName = "JsonPatch handles a simple array and replaces it")]

        [TestCase("{a:[1,2,3,4]}",
            "{a:[5,6,7]}",
            ExpectedResult = "[{\"op\":\"replace\",\"path\":\"/a\",\"value\":[5,6,7]}]",
            TestName = "JsonPatch handles a simple array under a property and replaces it")]

        [TestCase("{a:[1,2,3,4]}",
            "{a:[1,2,3,4]}",
            ExpectedResult = "[]",
            TestName = "JsonPatch handles same array")]

        public string JsonPatchesWorks(string leftString, string rightString)
        {
            var left = JToken.Parse(leftString);
            var right = JToken.Parse(rightString);

            var patches = JPatch.CalculatePatch(left, right).ToList();
            var pts = JsonConvert.SerializeObject(patches);
            System.Diagnostics.Trace.WriteLine(pts);
            return pts;
        }
    }
}

And now the code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Abodit
{
    public class JPatch
    {
        public string op { get; set; }        // add, remove, replace
        public string path { get; set; }
        public JToken value { get; set; }

        private JPatch() { }

        public static string Extend(string path, string extension)
        {
            // TODO: JSON property name needs escaping for path ??
            return path + "/" + extension;
        }

        private static JPatch Build(string op, string path, string key, JToken value)
        {
            if (string.IsNullOrEmpty(key))
                return new JPatch { op = op, path = path, value = value };
            else
                return new JPatch { op = op, path = Extend(path, key), value = value };
        }

        public static JPatch Add(string path, string key, JToken value)
        {
            return Build("add", path, key, value);
        }

        public static JPatch Remove(string path, string key)
        {
            return Build("remove", path, key, null);
        }

        public static JPatch Replace(string path, string key, JToken value)
        {
            return Build("replace", path, key, value);
        }

        public static IEnumerable<JPatch> CalculatePatch(JToken left, JToken right, string path = "")
        {
            if (left.Type != right.Type)
            {
                yield return JPatch.Replace(path, "", right);
                yield break;
            }

            if (left.Type == JTokenType.Array)
            {
                if (left.Children().SequenceEqual(right.Children()))        // TODO: Need a DEEP EQUALS HERE
                    yield break;

                // No array insert or delete operators in jpatch (yet?)
                yield return JPatch.Replace(path, "", right);
                yield break;
            }

            if (left.Type == JTokenType.Object)
            {
                var lprops = ((IDictionary<string, JToken>)left).OrderBy(p => p.Key);
                var rprops = ((IDictionary<string, JToken>)right).OrderBy(p => p.Key);

                foreach (var removed in lprops.Except(rprops, MatchesKey.Instance))
                {
                    yield return JPatch.Remove(path, removed.Key);
                }

                foreach (var added in rprops.Except(lprops, MatchesKey.Instance))
                {
                    yield return JPatch.Add(path, added.Key, added.Value);
                }

                var matchedKeys = lprops.Select(x => x.Key).Intersect(rprops.Select(y => y.Key));
                var zipped = matchedKeys.Select(k => new { key = k, left = left[k], right = right[k] });

                foreach (var match in zipped)
                {
                    string newPath = path + "/" + match.key;
                    foreach (var patch in CalculatePatch(match.left, match.right, newPath))
                        yield return patch;
                }
                yield break;
            }
            else
            {
                // Two values, same type, not JObject so no properties

                if (left.ToString() == right.ToString())
                    yield break;
                else
                    yield return JPatch.Replace(path, "", right);
            }
        }

        private class MatchesKey : IEqualityComparer<KeyValuePair<string, JToken>>
        {
            public static MatchesKey Instance = new MatchesKey();
            public bool Equals(KeyValuePair<string, JToken> x, KeyValuePair<string, JToken> y)
            {
                return x.Key.Equals(y.Key);
            }

            public int GetHashCode(KeyValuePair<string, JToken> obj)
            {
                return obj.Key.GetHashCode();
            }
        }
    }
}

Next up I will implement the ability to apply a patch to an existing object. This code will need to be in both C# and Javascript so it can
run in the browser or on Android/iOS.

The post JSON Patch – a C# implementation appeared first on Ian Mercer.


Viewing all articles
Browse latest Browse all 15