Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial implementation

  • Loading branch information...
commit 639442ba0d2de696e727f508ea81e7914fb99b16 2 parents a6ff900 + 12f9ea3
@aogriffiths authored
View
1  .gitignore
@@ -0,0 +1 @@
+*.sw?
View
8 .npmignore
@@ -0,0 +1,8 @@
+.git
+.gitignore
+qunit.css
+qunit.js
+index.html
+test.js
+jslitmus.js
+jsonpatch.coffee
View
11 .project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>jsondiff-js</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ </buildSpec>
+ <natures>
+ </natures>
+</projectDescription>
View
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Adam Griffiths & Byron Ruth
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Django nor the names of its contributors may be used
+ to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
27 README.md
@@ -1,4 +1,25 @@
-jsondiff-js
-===========
+# jsondiff.js
+
+Library to generate JSON Patches in JavaScript, from two differing json obejcts.
+
+see also:
+* http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08
+* https://github.com/bruth/jsonpatch-js
+
+jsondiff.js works as in the browser as a script, as a Node module and as an
+AMD module.
+
+## Install
+
+**Bower**
+
+```
+bower install json-diff
+```
+
+**NPM**
+
+```
+npm install json-diff
+```
-A JavaScript implementation to create json patches of the JSON Media Type for partial modifications: http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08
View
5 component.json
@@ -0,0 +1,5 @@
+{
+ "name": "json-diff",
+ "version": "0.0.1",
+ "main": "./jsondiff.js"
+}
View
20 index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>QUnit Test Suite</title>
+ <link rel="stylesheet" href="qunit.css" type="text/css" media="screen">
+ <script type="text/javascript" src="qunit.js"></script>
+ <script type="text/javascript" src="jsondiff.js"></script>
+ <script type="text/javascript" src="jslitmus.js"></script>
+ <script type="text/javascript" src="test.js"></script>
+</head>
+<body>
+ <h1 id="qunit-header">QUnit Test Suite</h1>
+ <h2 id="qunit-banner"></h2>
+ <div id="qunit-testrunner-toolbar"></div>
+ <h2 id="qunit-userAgent"></h2>
+ <ol id="qunit-tests"></ol>
+ <div id="qunit-fixture"></div>
+</body>
+</html>
View
649 jslitmus.js
@@ -0,0 +1,649 @@
+// JSLitmus.js
+//
+// Copyright (c) 2010, Robert Kieffer, http://broofa.com
+// Available under MIT license (http://en.wikipedia.org/wiki/MIT_License)
+
+(function() {
+ // Private methods and state
+
+ // Get platform info but don't go crazy trying to recognize everything
+ // that's out there. This is just for the major platforms and OSes.
+ var platform = 'unknown platform', ua = navigator.userAgent;
+
+ // Detect OS
+ var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|');
+ var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null;
+ if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null;
+
+ // Detect browser
+ var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null;
+
+ // Detect version
+ var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)');
+ var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null;
+ var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform';
+
+ /**
+ * A smattering of methods that are needed to implement the JSLitmus testbed.
+ */
+ var jsl = {
+ /**
+ * Enhanced version of escape()
+ */
+ escape: function(s) {
+ s = s.replace(/,/g, '\\,');
+ s = escape(s);
+ s = s.replace(/\+/g, '%2b');
+ s = s.replace(/ /g, '+');
+ return s;
+ },
+
+ /**
+ * Get an element by ID.
+ */
+ $: function(id) {
+ return document.getElementById(id);
+ },
+
+ /**
+ * Null function
+ */
+ F: function() {},
+
+ /**
+ * Set the status shown in the UI
+ */
+ status: function(msg) {
+ var el = jsl.$('jsl_status');
+ if (el) el.innerHTML = msg || '';
+ },
+
+ /**
+ * Convert a number to an abbreviated string like, "15K" or "10M"
+ */
+ toLabel: function(n) {
+ if (n == Infinity) {
+ return 'Infinity';
+ } else if (n > 1e9) {
+ n = Math.round(n/1e8);
+ return n/10 + 'B';
+ } else if (n > 1e6) {
+ n = Math.round(n/1e5);
+ return n/10 + 'M';
+ } else if (n > 1e3) {
+ n = Math.round(n/1e2);
+ return n/10 + 'K';
+ }
+ return n;
+ },
+
+ /**
+ * Copy properties from src to dst
+ */
+ extend: function(dst, src) {
+ for (var k in src) dst[k] = src[k]; return dst;
+ },
+
+ /**
+ * Like Array.join(), but for the key-value pairs in an object
+ */
+ join: function(o, delimit1, delimit2) {
+ if (o.join) return o.join(delimit1); // If it's an array
+ var pairs = [];
+ for (var k in o) pairs.push(k + delimit1 + o[k]);
+ return pairs.join(delimit2);
+ },
+
+ /**
+ * Array#indexOf isn't supported in IE, so we use this as a cross-browser solution
+ */
+ indexOf: function(arr, o) {
+ if (arr.indexOf) return arr.indexOf(o);
+ for (var i = 0; i < this.length; i++) if (arr[i] === o) return i;
+ return -1;
+ }
+ };
+
+ /**
+ * Test manages a single test (created with
+ * JSLitmus.test())
+ *
+ * @private
+ */
+ var Test = function (name, f) {
+ if (!f) throw new Error('Undefined test function');
+ if (!/function[^\(]*\(([^,\)]*)/.test(f.toString())) {
+ throw new Error('"' + name + '" test: Test is not a valid Function object');
+ }
+ this.loopArg = RegExp.$1;
+ this.name = name;
+ this.f = f;
+ };
+
+ jsl.extend(Test, /** @lends Test */ {
+ /** Calibration tests for establishing iteration loop overhead */
+ CALIBRATIONS: [
+ new Test('calibrating loop', function(count) {while (count--);}),
+ new Test('calibrating function', jsl.F)
+ ],
+
+ /**
+ * Run calibration tests. Returns true if calibrations are not yet
+ * complete (in which case calling code should run the tests yet again).
+ * onCalibrated - Callback to invoke when calibrations have finished
+ */
+ calibrate: function(onCalibrated) {
+ for (var i = 0; i < Test.CALIBRATIONS.length; i++) {
+ var cal = Test.CALIBRATIONS[i];
+ if (cal.running) return true;
+ if (!cal.count) {
+ cal.isCalibration = true;
+ cal.onStop = onCalibrated;
+ //cal.MIN_TIME = .1; // Do calibrations quickly
+ cal.run(2e4);
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ jsl.extend(Test.prototype, {/** @lends Test.prototype */
+ /** Initial number of iterations */
+ INIT_COUNT: 10,
+ /** Max iterations allowed (i.e. used to detect bad looping functions) */
+ MAX_COUNT: 1e9,
+ /** Minimum time a test should take to get valid results (secs) */
+ MIN_TIME: .5,
+
+ /** Callback invoked when test state changes */
+ onChange: jsl.F,
+
+ /** Callback invoked when test is finished */
+ onStop: jsl.F,
+
+ /**
+ * Reset test state
+ */
+ reset: function() {
+ delete this.count;
+ delete this.time;
+ delete this.running;
+ delete this.error;
+ },
+
+ /**
+ * Run the test (in a timeout). We use a timeout to make sure the browser
+ * has a chance to finish rendering any UI changes we've made, like
+ * updating the status message.
+ */
+ run: function(count) {
+ count = count || this.INIT_COUNT;
+ jsl.status(this.name + ' x ' + count);
+ this.running = true;
+ var me = this;
+ setTimeout(function() {me._run(count);}, 200);
+ },
+
+ /**
+ * The nuts and bolts code that actually runs a test
+ */
+ _run: function(count) {
+ var me = this;
+
+ // Make sure calibration tests have run
+ if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return;
+ this.error = null;
+
+ try {
+ var start, f = this.f, now, i = count;
+
+ // Start the timer
+ start = new Date();
+
+ // Now for the money shot. If this is a looping function ...
+ if (this.loopArg) {
+ // ... let it do the iteration itself
+ f(count);
+ } else {
+ // ... otherwise do the iteration for it
+ while (i--) f();
+ }
+
+ // Get time test took (in secs)
+ this.time = Math.max(1,new Date() - start)/1000;
+
+ // Store iteration count and per-operation time taken
+ this.count = count;
+ this.period = this.time/count;
+
+ // Do we need to do another run?
+ this.running = this.time <= this.MIN_TIME;
+
+ // ... if so, compute how many times we should iterate
+ if (this.running) {
+ // Bump the count to the nearest power of 2
+ var x = this.MIN_TIME/this.time;
+ var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2))));
+ count *= pow;
+ if (count > this.MAX_COUNT) {
+ throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.');
+ }
+ }
+ } catch (e) {
+ // Exceptions are caught and displayed in the test UI
+ this.reset();
+ this.error = e;
+ }
+
+ // Figure out what to do next
+ if (this.running) {
+ me.run(count);
+ } else {
+ jsl.status('');
+ me.onStop(me);
+ }
+
+ // Finish up
+ this.onChange(this);
+ },
+
+ /**
+ * Get the number of operations per second for this test.
+ *
+ * @param normalize if true, iteration loop overhead taken into account
+ */
+ getHz: function(/**Boolean*/ normalize) {
+ var p = this.period;
+
+ // Adjust period based on the calibration test time
+ if (normalize && !this.isCalibration) {
+ var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1];
+
+ // If the period is within 20% of the calibration time, then zero the
+ // it out
+ p = p < cal.period*1.2 ? 0 : p - cal.period;
+ }
+
+ return Math.round(1/p);
+ },
+
+ /**
+ * Get a friendly string describing the test
+ */
+ toString: function() {
+ return this.name + ' - ' + this.time/this.count + ' secs';
+ }
+ });
+
+ // CSS we need for the UI
+ var STYLESHEET = '<style> \
+ #jslitmus {font-family:sans-serif; font-size: 12px;} \
+ #jslitmus a {text-decoration: none;} \
+ #jslitmus a:hover {text-decoration: underline;} \
+ #jsl_status { \
+ margin-top: 10px; \
+ font-size: 10px; \
+ color: #888; \
+ } \
+ A IMG {border:none} \
+ #test_results { \
+ margin-top: 10px; \
+ font-size: 12px; \
+ font-family: sans-serif; \
+ border-collapse: collapse; \
+ border-spacing: 0px; \
+ } \
+ #test_results th, #test_results td { \
+ border: solid 1px #ccc; \
+ vertical-align: top; \
+ padding: 3px; \
+ } \
+ #test_results th { \
+ vertical-align: bottom; \
+ background-color: #ccc; \
+ padding: 1px; \
+ font-size: 10px; \
+ } \
+ #test_results #test_platform { \
+ color: #444; \
+ text-align:center; \
+ } \
+ #test_results .test_row { \
+ color: #006; \
+ cursor: pointer; \
+ } \
+ #test_results .test_nonlooping { \
+ border-left-style: dotted; \
+ border-left-width: 2px; \
+ } \
+ #test_results .test_looping { \
+ border-left-style: solid; \
+ border-left-width: 2px; \
+ } \
+ #test_results .test_name {white-space: nowrap;} \
+ #test_results .test_pending { \
+ } \
+ #test_results .test_running { \
+ font-style: italic; \
+ } \
+ #test_results .test_done {} \
+ #test_results .test_done { \
+ text-align: right; \
+ font-family: monospace; \
+ } \
+ #test_results .test_error {color: #600;} \
+ #test_results .test_error .error_head {font-weight:bold;} \
+ #test_results .test_error .error_body {font-size:85%;} \
+ #test_results .test_row:hover td { \
+ background-color: #ffc; \
+ text-decoration: underline; \
+ } \
+ #chart { \
+ margin: 10px 0px; \
+ width: 250px; \
+ } \
+ #chart img { \
+ border: solid 1px #ccc; \
+ margin-bottom: 5px; \
+ } \
+ #chart #tiny_url { \
+ height: 40px; \
+ width: 250px; \
+ } \
+ #jslitmus_credit { \
+ font-size: 10px; \
+ color: #888; \
+ margin-top: 8px; \
+ } \
+ </style>';
+
+ // HTML markup for the UI
+ var MARKUP = '<div id="jslitmus"> \
+ <button onclick="JSLitmus.runAll(event)">Run Tests</button> \
+ <button id="stop_button" disabled="disabled" onclick="JSLitmus.stop()">Stop Tests</button> \
+ <br \> \
+ <br \> \
+ <input type="checkbox" style="vertical-align: middle" id="test_normalize" checked="checked" onchange="JSLitmus.renderAll()""> Normalize results \
+ <table id="test_results"> \
+ <colgroup> \
+ <col /> \
+ <col width="100" /> \
+ </colgroup> \
+ <tr><th id="test_platform" colspan="2">' + platform + '</th></tr> \
+ <tr><th>Test</th><th>Ops/sec</th></tr> \
+ <tr id="test_row_template" class="test_row" style="display:none"> \
+ <td class="test_name"></td> \
+ <td class="test_result">Ready</td> \
+ </tr> \
+ </table> \
+ <div id="jsl_status"></div> \
+ <div id="chart" style="display:none"> \
+ <a id="chart_link" target="_blank"><img id="chart_image"></a> \
+ TinyURL (for chart): \
+ <iframe id="tiny_url" frameBorder="0" scrolling="no" src=""></iframe> \
+ </div> \
+ <a id="jslitmus_credit" title="JSLitmus home page" href="http://code.google.com/p/jslitmus" target="_blank">Powered by JSLitmus</a> \
+ </div>';
+
+ /**
+ * The public API for creating and running tests
+ */
+ window.JSLitmus = {
+ /** The list of all tests that have been registered with JSLitmus.test */
+ _tests: [],
+ /** The queue of tests that need to be run */
+ _queue: [],
+
+ /**
+ * The parsed query parameters the current page URL. This is provided as a
+ * convenience for test functions - it's not used by JSLitmus proper
+ */
+ params: {},
+
+ /**
+ * Initialize
+ */
+ _init: function() {
+ // Parse query params into JSLitmus.params[] hash
+ var match = (location + '').match(/([^?#]*)(#.*)?$/);
+ if (match) {
+ var pairs = match[1].split('&');
+ for (var i = 0; i < pairs.length; i++) {
+ var pair = pairs[i].split('=');
+ if (pair.length > 1) {
+ var key = pair.shift();
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ this.params[key] = value;
+ }
+ }
+ }
+
+ // Write out the stylesheet. We have to do this here because IE
+ // doesn't honor sheets written after the document has loaded.
+ document.write(STYLESHEET);
+
+ // Setup the rest of the UI once the document is loaded
+ if (window.addEventListener) {
+ window.addEventListener('load', this._setup, false);
+ } else if (document.addEventListener) {
+ document.addEventListener('load', this._setup, false);
+ } else if (window.attachEvent) {
+ window.attachEvent('onload', this._setup);
+ }
+
+ return this;
+ },
+
+ /**
+ * Set up the UI
+ */
+ _setup: function() {
+ var el = jsl.$('jslitmus_container');
+ if (!el) document.body.appendChild(el = document.createElement('div'));
+
+ el.innerHTML = MARKUP;
+
+ // Render the UI for all our tests
+ for (var i=0; i < JSLitmus._tests.length; i++)
+ JSLitmus.renderTest(JSLitmus._tests[i]);
+ },
+
+ /**
+ * (Re)render all the test results
+ */
+ renderAll: function() {
+ for (var i = 0; i < JSLitmus._tests.length; i++)
+ JSLitmus.renderTest(JSLitmus._tests[i]);
+ JSLitmus.renderChart();
+ },
+
+ /**
+ * (Re)render the chart graphics
+ */
+ renderChart: function() {
+ var url = JSLitmus.chartUrl();
+ jsl.$('chart_link').href = url;
+ jsl.$('chart_image').src = url;
+ jsl.$('chart').style.display = '';
+
+ // Update the tiny URL
+ jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url);
+ },
+
+ /**
+ * (Re)render the results for a specific test
+ */
+ renderTest: function(test) {
+ // Make a new row if needed
+ if (!test._row) {
+ var trow = jsl.$('test_row_template');
+ if (!trow) return;
+
+ test._row = trow.cloneNode(true);
+ test._row.style.display = '';
+ test._row.id = '';
+ test._row.onclick = function() {JSLitmus._queueTest(test);};
+ test._row.title = 'Run ' + test.name + ' test';
+ trow.parentNode.appendChild(test._row);
+ test._row.cells[0].innerHTML = test.name;
+ }
+
+ var cell = test._row.cells[1];
+ var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping'];
+
+ if (test.error) {
+ cns.push('test_error');
+ cell.innerHTML =
+ '<div class="error_head">' + test.error + '</div>' +
+ '<ul class="error_body"><li>' +
+ jsl.join(test.error, ': ', '</li><li>') +
+ '</li></ul>';
+ } else {
+ if (test.running) {
+ cns.push('test_running');
+ cell.innerHTML = 'running';
+ } else if (jsl.indexOf(JSLitmus._queue, test) >= 0) {
+ cns.push('test_pending');
+ cell.innerHTML = 'pending';
+ } else if (test.count) {
+ cns.push('test_done');
+ var hz = test.getHz(jsl.$('test_normalize').checked);
+ cell.innerHTML = hz != Infinity ? hz : '&infin;';
+ cell.title = 'Looped ' + test.count + ' times in ' + test.time + ' seconds';
+ } else {
+ cell.innerHTML = 'ready';
+ }
+ }
+ cell.className = cns.join(' ');
+ },
+
+ /**
+ * Create a new test
+ */
+ test: function(name, f) {
+ // Create the Test object
+ var test = new Test(name, f);
+ JSLitmus._tests.push(test);
+
+ // Re-render if the test state changes
+ test.onChange = JSLitmus.renderTest;
+
+ // Run the next test if this one finished
+ test.onStop = function(test) {
+ if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test);
+ JSLitmus.currentTest = null;
+ JSLitmus._nextTest();
+ };
+
+ // Render the new test
+ this.renderTest(test);
+ },
+
+ /**
+ * Add all tests to the run queue
+ */
+ runAll: function(e) {
+ e = e || window.event;
+ var reverse = e && e.shiftKey, len = JSLitmus._tests.length;
+ for (var i = 0; i < len; i++) {
+ JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]);
+ }
+ },
+
+ /**
+ * Remove all tests from the run queue. The current test has to finish on
+ * it's own though
+ */
+ stop: function() {
+ while (JSLitmus._queue.length) {
+ var test = JSLitmus._queue.shift();
+ JSLitmus.renderTest(test);
+ }
+ },
+
+ /**
+ * Run the next test in the run queue
+ */
+ _nextTest: function() {
+ if (!JSLitmus.currentTest) {
+ var test = JSLitmus._queue.shift();
+ if (test) {
+ jsl.$('stop_button').disabled = false;
+ JSLitmus.currentTest = test;
+ test.run();
+ JSLitmus.renderTest(test);
+ if (JSLitmus.onTestStart) JSLitmus.onTestStart(test);
+ } else {
+ jsl.$('stop_button').disabled = true;
+ JSLitmus.renderChart();
+ }
+ }
+ },
+
+ /**
+ * Add a test to the run queue
+ */
+ _queueTest: function(test) {
+ if (jsl.indexOf(JSLitmus._queue, test) >= 0) return;
+ JSLitmus._queue.push(test);
+ JSLitmus.renderTest(test);
+ JSLitmus._nextTest();
+ },
+
+ /**
+ * Generate a Google Chart URL that shows the data for all tests
+ */
+ chartUrl: function() {
+ var n = JSLitmus._tests.length, markers = [], data = [];
+ var d, min = 0, max = -1e10;
+ var normalize = jsl.$('test_normalize').checked;
+
+ // Gather test data
+ for (var i=0; i < JSLitmus._tests.length; i++) {
+ var test = JSLitmus._tests[i];
+ if (test.count) {
+ var hz = test.getHz(normalize);
+ var v = hz != Infinity ? hz : 0;
+ data.push(v);
+ markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' +
+ markers.length + ',10');
+ max = Math.max(v, max);
+ }
+ }
+ if (markers.length <= 0) return null;
+
+ // Build chart title
+ var title = document.getElementsByTagName('title');
+ title = (title && title.length) ? title[0].innerHTML : null;
+ var chart_title = [];
+ if (title) chart_title.push(title);
+ chart_title.push('Ops/sec (' + platform + ')');
+
+ // Build labels
+ var labels = [jsl.toLabel(min), jsl.toLabel(max)];
+
+ var w = 250, bw = 15;
+ var bs = 5;
+ var h = markers.length*(bw + bs) + 30 + chart_title.length*20;
+
+ var params = {
+ chtt: escape(chart_title.join('|')),
+ chts: '000000,10',
+ cht: 'bhg', // chart type
+ chd: 't:' + data.join(','), // data set
+ chds: min + ',' + max, // max/min of data
+ chxt: 'x', // label axes
+ chxl: '0:|' + labels.join('|'), // labels
+ chsp: '0,1',
+ chm: markers.join('|'), // test names
+ chbh: [bw, 0, bs].join(','), // bar widths
+ // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient
+ chs: w + 'x' + h
+ };
+ return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&');
+ }
+ };
+
+ JSLitmus._init();
+})();
View
256 jsondiff.coffee
@@ -0,0 +1,256 @@
+# jsonpatch.js 0.3.2
+# (c) 2011-2013 Adam Griffiths
+# (c) 2011-2013 Byron Ruth
+# Original code started from https://gist.github.com/bruth/4715999
+# jsondiff may be freely distributed under the BSD license
+
+((root, factory) ->
+ if typeof exports isnt 'undefined'
+ # Node/CommonJS
+ factory(root, exports)
+ else if typeof define is 'function' and define.amd
+ # AMD
+ define ['exports'], (exports) ->
+ root.jsondiff = factory(root, exports)
+ else
+ # Browser globals
+ root.jsondiff = factory(root, {})
+) @, (root) ->
+
+ # Utilities
+ toString = Object.prototype.toString
+ hasOwnProperty = Object.prototype.hasOwnProperty
+
+ # Define a few helper functions taken from the awesome underscore library
+ isArray = (obj) -> toString.call(obj) is '[object Array]'
+ isObject = (obj) -> toString.call(obj) is '[object Object]'
+ isString = (obj) -> toString.call(obj) is '[object String]'
+ isFunction = (obj) -> toString.call(obj) is '[object Function]'
+ has = (obj, key) -> hasOwnProperty.call(obj, key)
+
+ isEqual = (a, b) -> eq a, b, [], []
+
+ # Internal recursive comparison function for `isEqual`.
+ eq = (a, b, aStack, bStack) ->
+
+ # Identical objects are equal. `0 === -0`, but they aren't identical.
+ # See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
+ return a isnt 0 or 1 / a is 1 / b if a is b
+
+ # A strict comparison is necessary because `null == undefined`.
+ return a is b if not a? or not b?
+
+ # Unwrap any wrapped objects.
+ # commenting out next too lines form the underscore implemenation
+ # a = a._wrapped if a instanceof _
+ # b = b._wrapped if b instanceof _
+
+ # Compare `[[Class]]` names.
+ className = toString.call(a)
+ return false unless className is toString.call(b)
+ switch className
+
+ # Strings, numbers, dates, and booleans are compared by value.
+ when "[object String]"
+
+ # Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+ # equivalent to `new String("5")`.
+ return a is String(b)
+ when "[object Number]"
+
+ # `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+ # other numeric values.
+ return (if a isnt +a then b isnt +b else ((if a is 0 then 1 / a is 1 / b else a is +b)))
+ when "[object Date]", "[object Boolean]"
+
+ # Coerce dates and booleans to numeric primitive values. Dates are compared by their
+ # millisecond representations. Note that invalid dates with millisecond representations
+ # of `NaN` are not equivalent.
+ return +a is +b
+
+ # RegExps are compared by their source patterns and flags.
+ when "[object RegExp]"
+ return a.source is b.source and a.global is b.global and a.multiline is b.multiline and a.ignoreCase is b.ignoreCase
+ return false if typeof a isnt "object" or typeof b isnt "object"
+
+ # Assume equality for cyclic structures. The algorithm for detecting cyclic
+ # structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+ length = aStack.length
+
+ # Linear search. Performance is inversely proportional to the number of
+ # unique nested structures.
+ return bStack[length] is b if aStack[length] is a while length--
+
+ # Add the first object to the stack of traversed objects.
+ aStack.push a
+ bStack.push b
+ size = 0
+ result = true
+
+
+ # Recursively compare objects and arrays.
+ if className is "[object Array]"
+
+ # Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length
+ result = size is b.length
+
+ # Deep compare the contents, ignoring non-numeric properties.
+ if result
+ while size--
+ unless result = eq(a[size], b[size], aStack, bStack)
+ break
+ else
+
+ # Objects with different constructors are not equivalent, but `Object`s
+ # from different frames are.
+ aCtor = a.constructor
+ bCtor = b.constructor
+ return false if aCtor isnt bCtor and not (isFunction(aCtor) and (aCtor instanceof aCtor) and isFunction(bCtor) and (bCtor instanceof bCtor))
+
+ # Deep compare objects.
+ for key of a
+ if has(a, key)
+
+ # Count the expected number of properties.
+ size++
+
+ # Deep compare each member.
+ break unless result = has(b, key) and eq(a[key], b[key], aStack, bStack)
+
+ # Ensure that both objects contain the same number of properties.
+ if result
+ for key of b
+ break if has(b, key) and not (size--)
+ result = not size
+
+ # Remove the first object from the stack of traversed objects.
+ aStack.pop()
+ bStack.pop()
+ result
+
+
+ #Patch helper functions
+ getParent = (paths, path) ->
+ paths[path.substr(0, path.match(/\//g).length)]
+
+ #Checks if `obj` is an array or object
+ isContainer = (obj) ->
+ isArray(obj) || isObject(obj)
+
+ #Checks if the two objects are of the same container type
+ #returns false if they are different contianers or non-containers
+ isSameContainer = (obj1, obj2) ->
+ (isArray(obj1) && isArray(obj2)) || (isObject(obj1) && isObject(obj2))
+
+ #Flattens an object to a hash of paths and values.
+ flattenObject = (obj, prefix = "/", paths = {}) ->
+ paths[prefix] =
+ path: prefix
+ value: obj
+
+ if prefix != '/'
+ prefix = prefix + '/'
+
+ #Recurse for container types
+ if isArray(obj)
+ flattenObject o, prefix + i, paths for o, i in obj
+ else if isObject(obj)
+ flattenObject o, prefix + key, paths for key, o of obj
+
+ return paths;
+
+ #Constructs a patch that when applied to `obj2`, it will be equivalent
+ #to `obj1`. The patch format conforms to IETF JSON Patch proposal
+ #http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-01
+ diff = (obj1, obj2) ->
+ #Patches are only applicable to two of the same container types.
+ if !isSameContainer obj1, obj2
+ throw new Error('Patches can only be derived from objects or arrays');
+
+ paths1 = flattenObject obj1
+ paths2 = flattenObject obj2
+ add = {}
+ remove = {}
+ replace = {}
+ move = {}
+
+ #Iterate over the first object's paths and compare them to the second
+ #set of paths.
+ for key of paths1
+ doc1 = paths1[key]
+ doc2 = paths2[key]
+
+ # If the parent of `doc2` doesn't exist, skip it since neither a
+ # remove or replace can occur.
+ if !getParent paths2, key
+ continue
+
+ # Else, if doc2 does not exist then key must have been removed from the
+ # second object, so is be marked for removal.
+ else if !doc2
+ remove[key] = doc1
+
+ # Else, if doc1 and doc2 are the same
+ # container type, values will be replaced downstream.
+ else if isSameContainer doc1.value, doc2.value
+ continue
+
+ # Else, if doc1 and doc2 are not the same
+ # then doc2 must have replaced doc1
+ else if !isEqual doc1.value, doc2.value
+ replace[key] = doc2
+
+ # Iterate over the second object's paths and compare them to the first
+ # set of paths.
+ for key of paths2
+ doc1 = paths1[key]
+ doc2 = paths2[key]
+
+ # Missing in first object, thus we mark it to be added.
+ # If the parent path is not present in the first obj, then this
+ # means the whole array/object is new.
+ if !doc1 and isSameContainer getParent(paths1, key), getParent(paths2, key)
+ add[key] = doc2;
+
+ # Attempt to promote add/remove operations to a move operation.
+ # The first occurence of the same value, we can promote to a move.
+ for key1, doc1 of remove
+ for key2, doc2 of add
+ if isEqual(doc2.value, doc1.value)
+ # conver the add+remove to an move
+ delete remove[key1]
+ delete add[key2]
+ move[key2] = key1
+ break
+
+ # Populate the patch
+ patch = []
+ for key, doc of add
+ patch.push
+ op: 'add'
+ path: key
+ value: doc.value
+
+ for key of remove
+ patch.push
+ op: 'remove'
+ path: key
+
+ for key, doc of replace
+ patch.push
+ op: 'replace'
+ path: key
+ value: doc.value
+
+ for keyto, keyfrom of move
+ patch.push
+ op: 'move'
+ from: keyfrom
+ path: keyto
+ patch
+
+
+ # Export to root
+ root.diff = diff
+ return root
View
231 jsondiff.js
@@ -0,0 +1,231 @@
+// Generated by CoffeeScript 1.4.0
+(function() {
+
+ (function(root, factory) {
+ if (typeof exports !== 'undefined') {
+ return factory(root, exports);
+ } else if (typeof define === 'function' && define.amd) {
+ return define(['exports'], function(exports) {
+ return root.jsondiff = factory(root, exports);
+ });
+ } else {
+ return root.jsondiff = factory(root, {});
+ }
+ })(this, function(root) {
+ var diff, eq, flattenObject, getParent, has, hasOwnProperty, isArray, isContainer, isEqual, isFunction, isObject, isSameContainer, isString, toString;
+ toString = Object.prototype.toString;
+ hasOwnProperty = Object.prototype.hasOwnProperty;
+ isArray = function(obj) {
+ return toString.call(obj) === '[object Array]';
+ };
+ isObject = function(obj) {
+ return toString.call(obj) === '[object Object]';
+ };
+ isString = function(obj) {
+ return toString.call(obj) === '[object String]';
+ };
+ isFunction = function(obj) {
+ return toString.call(obj) === '[object Function]';
+ };
+ has = function(obj, key) {
+ return hasOwnProperty.call(obj, key);
+ };
+ isEqual = function(a, b) {
+ return eq(a, b, [], []);
+ };
+ eq = function(a, b, aStack, bStack) {
+ var aCtor, bCtor, className, key, length, result, size;
+ if (a === b) {
+ return a !== 0 || 1 / a === 1 / b;
+ }
+ if (!(a != null) || !(b != null)) {
+ return a === b;
+ }
+ className = toString.call(a);
+ if (className !== toString.call(b)) {
+ return false;
+ }
+ switch (className) {
+ case "[object String]":
+ return a === String(b);
+ case "[object Number]":
+ return (a !== +a ? b !== +b : (a === 0 ? 1 / a === 1 / b : a === +b));
+ case "[object Date]":
+ case "[object Boolean]":
+ return +a === +b;
+ case "[object RegExp]":
+ return a.source === b.source && a.global === b.global && a.multiline === b.multiline && a.ignoreCase === b.ignoreCase;
+ }
+ if (typeof a !== "object" || typeof b !== "object") {
+ return false;
+ }
+ length = aStack.length;
+ if ((function() {
+ var _results;
+ _results = [];
+ while (length--) {
+ _results.push(aStack[length] === a);
+ }
+ return _results;
+ })()) {
+ return bStack[length] === b;
+ }
+ aStack.push(a);
+ bStack.push(b);
+ size = 0;
+ result = true;
+ if (className === "[object Array]") {
+ size = a.length;
+ result = size === b.length;
+ if (result) {
+ while (size--) {
+ if (!(result = eq(a[size], b[size], aStack, bStack))) {
+ break;
+ }
+ }
+ }
+ } else {
+ aCtor = a.constructor;
+ bCtor = b.constructor;
+ if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) && isFunction(bCtor) && (bCtor instanceof bCtor))) {
+ return false;
+ }
+ for (key in a) {
+ if (has(a, key)) {
+ size++;
+ if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack))) {
+ break;
+ }
+ }
+ }
+ if (result) {
+ for (key in b) {
+ if (has(b, key) && !(size--)) {
+ break;
+ }
+ }
+ result = !size;
+ }
+ }
+ aStack.pop();
+ bStack.pop();
+ return result;
+ };
+ getParent = function(paths, path) {
+ return paths[path.substr(0, path.match(/\//g).length)];
+ };
+ isContainer = function(obj) {
+ return isArray(obj) || isObject(obj);
+ };
+ isSameContainer = function(obj1, obj2) {
+ return (isArray(obj1) && isArray(obj2)) || (isObject(obj1) && isObject(obj2));
+ };
+ flattenObject = function(obj, prefix, paths) {
+ var i, key, o, _i, _len;
+ if (prefix == null) {
+ prefix = "/";
+ }
+ if (paths == null) {
+ paths = {};
+ }
+ paths[prefix] = {
+ path: prefix,
+ value: obj
+ };
+ if (prefix !== '/') {
+ prefix = prefix + '/';
+ }
+ if (isArray(obj)) {
+ for (i = _i = 0, _len = obj.length; _i < _len; i = ++_i) {
+ o = obj[i];
+ flattenObject(o, prefix + i, paths);
+ }
+ } else if (isObject(obj)) {
+ for (key in obj) {
+ o = obj[key];
+ flattenObject(o, prefix + key, paths);
+ }
+ }
+ return paths;
+ };
+ diff = function(obj1, obj2) {
+ var add, doc, doc1, doc2, key, key1, key2, keyfrom, keyto, move, patch, paths1, paths2, remove, replace;
+ if (!isSameContainer(obj1, obj2)) {
+ throw new Error('Patches can only be derived from objects or arrays');
+ }
+ paths1 = flattenObject(obj1);
+ paths2 = flattenObject(obj2);
+ add = {};
+ remove = {};
+ replace = {};
+ move = {};
+ for (key in paths1) {
+ doc1 = paths1[key];
+ doc2 = paths2[key];
+ if (!getParent(paths2, key)) {
+ continue;
+ } else if (!doc2) {
+ remove[key] = doc1;
+ } else if (isSameContainer(doc1.value, doc2.value)) {
+ continue;
+ } else if (!isEqual(doc1.value, doc2.value)) {
+ replace[key] = doc2;
+ }
+ }
+ for (key in paths2) {
+ doc1 = paths1[key];
+ doc2 = paths2[key];
+ if (!doc1 && isSameContainer(getParent(paths1, key), getParent(paths2, key))) {
+ add[key] = doc2;
+ }
+ }
+ for (key1 in remove) {
+ doc1 = remove[key1];
+ for (key2 in add) {
+ doc2 = add[key2];
+ if (isEqual(doc2.value, doc1.value)) {
+ delete remove[key1];
+ delete add[key2];
+ move[key2] = key1;
+ break;
+ }
+ }
+ }
+ patch = [];
+ for (key in add) {
+ doc = add[key];
+ patch.push({
+ op: 'add',
+ path: key,
+ value: doc.value
+ });
+ }
+ for (key in remove) {
+ patch.push({
+ op: 'remove',
+ path: key
+ });
+ }
+ for (key in replace) {
+ doc = replace[key];
+ patch.push({
+ op: 'replace',
+ path: key,
+ value: doc.value
+ });
+ }
+ for (keyto in move) {
+ keyfrom = move[keyto];
+ patch.push({
+ op: 'move',
+ from: keyfrom,
+ path: keyto
+ });
+ }
+ return patch;
+ };
+ root.diff = diff;
+ return root;
+ });
+
+}).call(this);
View
376 jsonpatch.coffee
@@ -0,0 +1,376 @@
+# jsonpatch.js 0.3.2
+# (c) 2011-2012 Byron Ruth
+# jsonpatch may be freely distributed under the BSD license
+
+((root, factory) ->
+ if typeof exports isnt 'undefined'
+ # Node/CommonJS
+ factory(root, exports)
+ else if typeof define is 'function' and define.amd
+ # AMD
+ define ['exports'], (exports) ->
+ root.jsonpatch = factory(root, exports)
+ else
+ # Browser globals
+ root.jsonpatch = factory(root, {})
+) @, (root) ->
+
+ # Utilities
+ toString = Object.prototype.toString
+ hasOwnProperty = Object.prototype.hasOwnProperty
+
+ # Define a few helper functions taken from the awesome underscore library
+ isArray = (obj) -> toString.call(obj) is '[object Array]'
+ isObject = (obj) -> toString.call(obj) is '[object Object]'
+ isString = (obj) -> toString.call(obj) is '[object String]'
+
+ # Limited Underscore.js implementation, internal recursive comparison function.
+ _isEqual = (a, b, stack) ->
+ # Identical objects are equal. `0 === -0`, but they aren't identical.
+ # See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
+ if a is b then return a isnt 0 or 1 / a == 1 / b
+ # A strict comparison is necessary because `null == undefined`.
+ if a == null or b == null then return a is b
+ # Compare `[[Class]]` names.
+ className = toString.call(a)
+ if className isnt toString.call(b) then return false
+ switch className
+ # Strings, numbers, and booleans are compared by value.
+ when '[object String]'
+ # Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+ # equivalent to `new String("5")`.
+ String(a) is String(b)
+ when '[object Number]'
+ a = +a
+ b = +b
+ # `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+ # other numeric values.
+ if a isnt a
+ b isnt b
+ else
+ if a is 0
+ 1 / a is 1 / b
+ else
+ a is b
+ when '[object Boolean]'
+ # Coerce dates and booleans to numeric primitive values. Dates are compared by their
+ # millisecond representations. Note that invalid dates with millisecond representations
+ # of `NaN` are not equivalent.
+ +a is +b
+
+ if typeof a isnt 'object' or typeof b isnt 'object' then return false
+ # Assume equality for cyclic structures. The algorithm for detecting cyclic
+ # structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+ length = stack.length
+ while length--
+ # Linear search. Performance is inversely proportional to the number of
+ # unique nested structures.
+ if stack[length] is a then return true
+
+ # Add the first object to the stack of traversed objects.
+ stack.push(a)
+ size = 0
+ result = true
+ # Recursively compare objects and arrays.
+ if className is '[object Array]'
+ # Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length
+ result = size is b.length
+ if result
+ # Deep compare the contents, ignoring non-numeric properties.
+ while size--
+ # Ensure commutative equality for sparse arrays.
+ if not (result = size in a is size in b and _isEqual(a[size], b[size], stack)) then break
+ else
+ # Objects with different constructors are not equivalent.
+ if "constructor" in a isnt "constructor" in b or a.constructor isnt b.constructor then return false
+ # Deep compare objects.
+ for key of a
+ if hasOwnProperty.call(a, key)
+ # Count the expected number of properties.
+ size++
+ # Deep compare each member.
+ if not (result = hasOwnProperty.call(b, key) and _isEqual(a[key], b[key], stack)) then break
+
+ # Ensure that both objects contain the same number of properties.
+ if result
+ for key of b
+ if hasOwnProperty.call(b, key) and not size-- then break
+ result = not size
+ # Remove the first object from the stack of traversed objects.
+ stack.pop()
+ return result
+
+ # Perform a deep comparison to check if two objects are equal.
+ isEqual = (a, b) -> _isEqual(a, b, [])
+
+
+ # Various error constructors
+ class JSONPatchError extends Error
+ constructor: (@message='JSON patch error') ->
+ @name = 'JSONPatchError'
+
+ class InvalidPointerError extends Error
+ constructor: (@message='Invalid pointer') ->
+ @name = 'InvalidPointer'
+
+ class InvalidPatchError extends JSONPatchError
+ constructor: (@message='Invalid patch') ->
+ @name = 'InvalidPatch'
+
+ class PatchConflictError extends JSONPatchError
+ constructor: (@message='Patch conflict') ->
+ @name = 'PatchConflictError'
+
+
+ # Spec: http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-05
+ class JSONPointer
+ constructor: (path) ->
+ steps = []
+ # If a path is specified, it must start with a /
+ if path and (steps = decodeURIComponent(path).split '/').shift() isnt ''
+ throw new InvalidPointerError()
+
+ # Decode each component, decode JSON Pointer specific syntax ~0 and ~1
+ for step, i in steps
+ steps[i] = step.replace('~1', '/').replace('~0', '~')
+
+ # The final segment is the accessor (property/index) of the object
+ # the pointer ultimately references
+ @accessor = steps.pop()
+ @steps = steps
+ @path = path
+
+ # Returns an object with the object reference and the accessor
+ getReference: (parent) ->
+ for step in @steps
+ if isArray parent then step = parseInt(step, 10)
+ if step not of parent
+ throw new PatchConflictError('Array location out of '
+ 'bounds or not an instance property')
+ parent = parent[step]
+ return parent
+
+
+ # Interface for patch operation classes
+ class JSONPatch
+ constructor: (patch) ->
+ # All patches required a 'path' member
+ if 'path' not of patch
+ throw new InvalidPatchError()
+
+ # Validates the patch based on the requirements of this operation
+ @validate(patch)
+ @patch = patch
+ # Create the primary pointer for this operation
+ @path = new JSONPointer(patch.path)
+ # Call for operation-specific setup
+ @initialize(patch)
+
+ initialize: ->
+
+ validate: (patch) ->
+
+ apply: (document) -> throw new Error('Method not implemented')
+
+
+ class AddPatch extends JSONPatch
+ validate: (patch) ->
+ if 'value' not of patch then throw new InvalidPatchError()
+
+ apply: (document) ->
+ reference = @path.getReference(document)
+ accessor = @path.accessor
+ value = @patch.value
+
+ if isArray(reference)
+ if accessor is '-'
+ reference.push(value)
+ else
+ accessor = parseInt(accessor, 10)
+ if accessor < 0 or accessor > reference.length
+ throw new PatchConflictError("Index #{accessor} out of bounds")
+ reference.splice(accessor, 0, value)
+ else
+ if accessor of reference
+ throw new PatchConflictError("Value at #{accessor} exists")
+ reference[accessor] = value
+ return
+
+
+ class RemovePatch extends JSONPatch
+ apply: (document) ->
+ reference = @path.getReference(document)
+ accessor = @path.accessor
+
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ reference.splice(accessor, 1)
+ else
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ delete reference[accessor]
+ return
+
+
+ class ReplacePatch extends JSONPatch
+ validate: (patch) ->
+ if 'value' not of patch then throw new InvalidPatchError()
+
+ apply: (document) ->
+ reference = @path.getReference(document)
+ accessor = @path.accessor
+ value = @patch.value
+
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ reference.splice(accessor, 1, value)
+ else
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ reference[accessor] = value
+ return
+
+
+ class TestPatch extends JSONPatch
+ validate: (patch) ->
+ if 'value' not of patch then throw new InvalidPatchError()
+
+ apply: (document) ->
+ reference = @path.getReference(document)
+ accessor = @path.accessor
+ value = @patch.value
+
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ return isEqual(reference[accessor], value)
+
+
+ class MovePatch extends JSONPatch
+ initialize: (patch) ->
+ @from = new JSONPointer(patch.from)
+ len = @from.steps.length
+
+ within = true
+ for i in [0..len]
+ if @from.steps[i] isnt @path.steps[i]
+ within = false
+ break
+
+ if within
+ if @path.steps.length isnt len
+ throw new InvalidPatchError("'to' member cannot be a descendent of 'path'")
+ if @from.accessor is @path.accessor
+ # The path and to pointers reference the same location,
+ # therefore apply can be a no-op
+ @apply = ->
+
+ validate: (patch) ->
+ if 'from' not of patch then throw new InvalidPatchError()
+
+ apply: (document) ->
+ reference = @from.getReference(document)
+ accessor = @from.accessor
+
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ value = reference.splice(accessor, 1)[0]
+ else
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ value = reference[accessor]
+ delete reference[accessor]
+
+ reference = @path.getReference(document)
+ accessor = @path.accessor
+
+ # Add to object
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ if accessor < 0 or accessor > reference.length
+ throw new PatchConflictError("Index #{accessor} out of bounds")
+ reference.splice(accessor, 0, value)
+ else
+ if accessor of reference
+ throw new PatchConflictError("Value at #{accessor} exists")
+ reference[accessor] = value
+ return
+
+
+ class CopyPatch extends MovePatch
+ apply: (document) ->
+ reference = @from.getReference(document)
+ accessor = @from.accessor
+
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ value = reference.slice(accessor, accessor + 1)[0]
+ else
+ if accessor not of reference
+ throw new PatchConflictError("Value at #{accessor} does not exist")
+ value = reference[accessor]
+
+ reference = @path.getReference(document)
+ accessor = @path.accessor
+
+ # Add to object
+ if isArray(reference)
+ accessor = parseInt(accessor, 10)
+ if accessor < 0 or accessor > reference.length
+ throw new PatchConflictError("Index #{accessor} out of bounds")
+ reference.splice(accessor, 0, value)
+ else
+ if accessor of reference
+ throw new PatchConflictError("Value at #{accessor} exists")
+ reference[accessor] = value
+ return
+
+
+ # Map of operation classes
+ operationMap =
+ add: AddPatch
+ remove: RemovePatch
+ replace: ReplacePatch
+ move: MovePatch
+ copy: CopyPatch
+ test: TestPatch
+
+
+ # Validates and compiles a patch document and returns a function to apply
+ # to multiple documents
+ compile = (patch) ->
+ ops = []
+
+ for p in patch
+ # Not a valid operation
+ if not (klass = operationMap[p.op])
+ throw new InvalidPatchError()
+ ops.push new klass(p)
+
+ return (document) ->
+ for op in ops
+ result = op.apply(document)
+ return result
+
+
+ # Applies a patch to a document
+ apply = (document, patch) ->
+ compile(patch)(document)
+
+
+ # Export to root
+ root.apply = apply
+ root.compile = compile
+ root.JSONPatchError = JSONPatchError
+ root.InvalidPointerError = InvalidPointerError
+ root.InvalidPatchError = InvalidPatchError
+ root.PatchConflictError = PatchConflictError
+ return root
View
504 jsonpatch.js
@@ -0,0 +1,504 @@
+// Generated by CoffeeScript 1.4.0
+(function() {
+ var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+ (function(root, factory) {
+ if (typeof exports !== 'undefined') {
+ return factory(root, exports);
+ } else if (typeof define === 'function' && define.amd) {
+ return define(['exports'], function(exports) {
+ return root.jsonpatch = factory(root, exports);
+ });
+ } else {
+ return root.jsonpatch = factory(root, {});
+ }
+ })(this, function(root) {
+ var AddPatch, CopyPatch, InvalidPatchError, InvalidPointerError, JSONPatch, JSONPatchError, JSONPointer, MovePatch, PatchConflictError, RemovePatch, ReplacePatch, TestPatch, apply, compile, hasOwnProperty, isArray, isEqual, isObject, isString, operationMap, toString, _isEqual;
+ toString = Object.prototype.toString;
+ hasOwnProperty = Object.prototype.hasOwnProperty;
+ isArray = function(obj) {
+ return toString.call(obj) === '[object Array]';
+ };
+ isObject = function(obj) {
+ return toString.call(obj) === '[object Object]';
+ };
+ isString = function(obj) {
+ return toString.call(obj) === '[object String]';
+ };
+ _isEqual = function(a, b, stack) {
+ var className, key, length, result, size;
+ if (a === b) {
+ return a !== 0 || 1 / a === 1 / b;
+ }
+ if (a === null || b === null) {
+ return a === b;
+ }
+ className = toString.call(a);
+ if (className !== toString.call(b)) {
+ return false;
+ }
+ switch (className) {
+ case '[object String]':
+ String(a) === String(b);
+ break;
+ case '[object Number]':
+ a = +a;
+ b = +b;
+ if (a !== a) {
+ b !== b;
+ } else {
+ if (a === 0) {
+ 1 / a === 1 / b;
+ } else {
+ a === b;
+ }
+ }
+ break;
+ case '[object Boolean]':
+ +a === +b;
+ }
+ if (typeof a !== 'object' || typeof b !== 'object') {
+ return false;
+ }
+ length = stack.length;
+ while (length--) {
+ if (stack[length] === a) {
+ return true;
+ }
+ }
+ stack.push(a);
+ size = 0;
+ result = true;
+ if (className === '[object Array]') {
+ size = a.length;
+ result = size === b.length;
+ if (result) {
+ while (size--) {
+ if (!(result = __indexOf.call(a, size) >= 0 === __indexOf.call(b, size) >= 0 && _isEqual(a[size], b[size], stack))) {
+ break;
+ }
+ }
+ }
+ } else {
+ if (__indexOf.call(a, "constructor") >= 0 !== __indexOf.call(b, "constructor") >= 0 || a.constructor !== b.constructor) {
+ return false;
+ }
+ for (key in a) {
+ if (hasOwnProperty.call(a, key)) {
+ size++;
+ if (!(result = hasOwnProperty.call(b, key) && _isEqual(a[key], b[key], stack))) {
+ break;
+ }
+ }
+ }
+ if (result) {
+ for (key in b) {
+ if (hasOwnProperty.call(b, key) && !size--) {
+ break;
+ }
+ }
+ result = !size;
+ }
+ }
+ stack.pop();
+ return result;
+ };
+ isEqual = function(a, b) {
+ return _isEqual(a, b, []);
+ };
+ JSONPatchError = (function(_super) {
+
+ __extends(JSONPatchError, _super);
+
+ function JSONPatchError(message) {
+ this.message = message != null ? message : 'JSON patch error';
+ this.name = 'JSONPatchError';
+ }
+
+ return JSONPatchError;
+
+ })(Error);
+ InvalidPointerError = (function(_super) {
+
+ __extends(InvalidPointerError, _super);
+
+ function InvalidPointerError(message) {
+ this.message = message != null ? message : 'Invalid pointer';
+ this.name = 'InvalidPointer';
+ }
+
+ return InvalidPointerError;
+
+ })(Error);
+ InvalidPatchError = (function(_super) {
+
+ __extends(InvalidPatchError, _super);
+
+ function InvalidPatchError(message) {
+ this.message = message != null ? message : 'Invalid patch';
+ this.name = 'InvalidPatch';
+ }
+
+ return InvalidPatchError;
+
+ })(JSONPatchError);
+ PatchConflictError = (function(_super) {
+
+ __extends(PatchConflictError, _super);
+
+ function PatchConflictError(message) {
+ this.message = message != null ? message : 'Patch conflict';
+ this.name = 'PatchConflictError';
+ }
+
+ return PatchConflictError;
+
+ })(JSONPatchError);
+ JSONPointer = (function() {
+
+ function JSONPointer(path) {
+ var i, step, steps, _i, _len;
+ steps = [];
+ if (path && (steps = decodeURIComponent(path).split('/')).shift() !== '') {
+ throw new InvalidPointerError();
+ }
+ for (i = _i = 0, _len = steps.length; _i < _len; i = ++_i) {
+ step = steps[i];
+ steps[i] = step.replace('~1', '/').replace('~0', '~');
+ }
+ this.accessor = steps.pop();
+ this.steps = steps;
+ this.path = path;
+ }
+
+ JSONPointer.prototype.getReference = function(parent) {
+ var step, _i, _len, _ref;
+ _ref = this.steps;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ step = _ref[_i];
+ if (isArray(parent)) {
+ step = parseInt(step, 10);
+ }
+ if (!(step in parent)) {
+ throw new PatchConflictError('Array location out of ', 'bounds or not an instance property');
+ }
+ parent = parent[step];
+ }
+ return parent;
+ };
+
+ return JSONPointer;
+
+ })();
+ JSONPatch = (function() {
+
+ function JSONPatch(patch) {
+ if (!('path' in patch)) {
+ throw new InvalidPatchError();
+ }
+ this.validate(patch);
+ this.patch = patch;
+ this.path = new JSONPointer(patch.path);
+ this.initialize(patch);
+ }
+
+ JSONPatch.prototype.initialize = function() {};
+
+ JSONPatch.prototype.validate = function(patch) {};
+
+ JSONPatch.prototype.apply = function(document) {
+ throw new Error('Method not implemented');
+ };
+
+ return JSONPatch;
+
+ })();
+ AddPatch = (function(_super) {
+
+ __extends(AddPatch, _super);
+
+ function AddPatch() {
+ return AddPatch.__super__.constructor.apply(this, arguments);
+ }
+
+ AddPatch.prototype.validate = function(patch) {
+ if (!('value' in patch)) {
+ throw new InvalidPatchError();
+ }
+ };
+
+ AddPatch.prototype.apply = function(document) {
+ var accessor, reference, value;
+ reference = this.path.getReference(document);
+ accessor = this.path.accessor;
+ value = this.patch.value;
+ if (isArray(reference)) {
+ if (accessor === '-') {
+ reference.push(value);
+ } else {
+ accessor = parseInt(accessor, 10);
+ if (accessor < 0 || accessor > reference.length) {
+ throw new PatchConflictError("Index " + accessor + " out of bounds");
+ }
+ reference.splice(accessor, 0, value);
+ }
+ } else {
+ if (accessor in reference) {
+ throw new PatchConflictError("Value at " + accessor + " exists");
+ }
+ reference[accessor] = value;
+ }
+ };
+
+ return AddPatch;
+
+ })(JSONPatch);
+ RemovePatch = (function(_super) {
+
+ __extends(RemovePatch, _super);
+
+ function RemovePatch() {
+ return RemovePatch.__super__.constructor.apply(this, arguments);
+ }
+
+ RemovePatch.prototype.apply = function(document) {
+ var accessor, reference;
+ reference = this.path.getReference(document);
+ accessor = this.path.accessor;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ reference.splice(accessor, 1);
+ } else {
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ delete reference[accessor];
+ }
+ };
+
+ return RemovePatch;
+
+ })(JSONPatch);
+ ReplacePatch = (function(_super) {
+
+ __extends(ReplacePatch, _super);
+
+ function ReplacePatch() {
+ return ReplacePatch.__super__.constructor.apply(this, arguments);
+ }
+
+ ReplacePatch.prototype.validate = function(patch) {
+ if (!('value' in patch)) {
+ throw new InvalidPatchError();
+ }
+ };
+
+ ReplacePatch.prototype.apply = function(document) {
+ var accessor, reference, value;
+ reference = this.path.getReference(document);
+ accessor = this.path.accessor;
+ value = this.patch.value;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ reference.splice(accessor, 1, value);
+ } else {
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ reference[accessor] = value;
+ }
+ };
+
+ return ReplacePatch;
+
+ })(JSONPatch);
+ TestPatch = (function(_super) {
+
+ __extends(TestPatch, _super);
+
+ function TestPatch() {
+ return TestPatch.__super__.constructor.apply(this, arguments);
+ }
+
+ TestPatch.prototype.validate = function(patch) {
+ if (!('value' in patch)) {
+ throw new InvalidPatchError();
+ }
+ };
+
+ TestPatch.prototype.apply = function(document) {
+ var accessor, reference, value;
+ reference = this.path.getReference(document);
+ accessor = this.path.accessor;
+ value = this.patch.value;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ }
+ return isEqual(reference[accessor], value);
+ };
+
+ return TestPatch;
+
+ })(JSONPatch);
+ MovePatch = (function(_super) {
+
+ __extends(MovePatch, _super);
+
+ function MovePatch() {
+ return MovePatch.__super__.constructor.apply(this, arguments);
+ }
+
+ MovePatch.prototype.initialize = function(patch) {
+ var i, len, within, _i;
+ this.from = new JSONPointer(patch.from);
+ len = this.from.steps.length;
+ within = true;
+ for (i = _i = 0; 0 <= len ? _i <= len : _i >= len; i = 0 <= len ? ++_i : --_i) {
+ if (this.from.steps[i] !== this.path.steps[i]) {
+ within = false;
+ break;
+ }
+ }
+ if (within) {
+ if (this.path.steps.length !== len) {
+ throw new InvalidPatchError("'to' member cannot be a descendent of 'path'");
+ }
+ if (this.from.accessor === this.path.accessor) {
+ return this.apply = function() {};
+ }
+ }
+ };
+
+ MovePatch.prototype.validate = function(patch) {
+ if (!('from' in patch)) {
+ throw new InvalidPatchError();
+ }
+ };
+
+ MovePatch.prototype.apply = function(document) {
+ var accessor, reference, value;
+ reference = this.from.getReference(document);
+ accessor = this.from.accessor;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ value = reference.splice(accessor, 1)[0];
+ } else {
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ value = reference[accessor];
+ delete reference[accessor];
+ }
+ reference = this.path.getReference(document);
+ accessor = this.path.accessor;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ if (accessor < 0 || accessor > reference.length) {
+ throw new PatchConflictError("Index " + accessor + " out of bounds");
+ }
+ reference.splice(accessor, 0, value);
+ } else {
+ if (accessor in reference) {
+ throw new PatchConflictError("Value at " + accessor + " exists");
+ }
+ reference[accessor] = value;
+ }
+ };
+
+ return MovePatch;
+
+ })(JSONPatch);
+ CopyPatch = (function(_super) {
+
+ __extends(CopyPatch, _super);
+
+ function CopyPatch() {
+ return CopyPatch.__super__.constructor.apply(this, arguments);
+ }
+
+ CopyPatch.prototype.apply = function(document) {
+ var accessor, reference, value;
+ reference = this.from.getReference(document);
+ accessor = this.from.accessor;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ value = reference.slice(accessor, accessor + 1)[0];
+ } else {
+ if (!(accessor in reference)) {
+ throw new PatchConflictError("Value at " + accessor + " does not exist");
+ }
+ value = reference[accessor];
+ }
+ reference = this.path.getReference(document);
+ accessor = this.path.accessor;
+ if (isArray(reference)) {
+ accessor = parseInt(accessor, 10);
+ if (accessor < 0 || accessor > reference.length) {
+ throw new PatchConflictError("Index " + accessor + " out of bounds");
+ }
+ reference.splice(accessor, 0, value);
+ } else {
+ if (accessor in reference) {
+ throw new PatchConflictError("Value at " + accessor + " exists");
+ }
+ reference[accessor] = value;
+ }
+ };
+
+ return CopyPatch;
+
+ })(MovePatch);
+ operationMap = {
+ add: AddPatch,
+ remove: RemovePatch,
+ replace: ReplacePatch,
+ move: MovePatch,
+ copy: CopyPatch,
+ test: TestPatch
+ };
+ compile = function(patch) {
+ var klass, ops, p, _i, _len;
+ ops = [];
+ for (_i = 0, _len = patch.length; _i < _len; _i++) {
+ p = patch[_i];
+ if (!(klass = operationMap[p.op])) {
+ throw new InvalidPatchError();
+ }
+ ops.push(new klass(p));
+ }
+ return function(document) {
+ var op, result, _j, _len1;
+ for (_j = 0, _len1 = ops.length; _j < _len1; _j++) {
+ op = ops[_j];
+ result = op.apply(document);
+ }
+ return result;
+ };
+ };
+ apply = function(document, patch) {
+ return compile(patch)(document);
+ };
+ root.apply = apply;
+ root.compile = compile;
+ root.JSONPatchError = JSONPatchError;
+ root.InvalidPointerError = InvalidPointerError;
+ root.InvalidPatchError = InvalidPatchError;
+ root.PatchConflictError = PatchConflictError;
+ return root;
+ });
+
+}).call(this);
View
22 package.json
@@ -0,0 +1,22 @@
+{
+ "name": "json-diff",
+ "author": "Adam Griffiths & Byron Ruth",
+ "version": "0.0.1",
+ "description": "A JavaScript implementation of the JSON Media Type for partial modifications: http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-06",
+ "license": "BSD",
+ "keywords": [
+ "diff",
+ "patch",
+ "json",
+ "jsonpatch",
+ "jsonpointer"
+ ],
+ "main": "jsondiff.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/aogriffiths/jsondiff-js.git"
+ }
+}
View
235 qunit.css
@@ -0,0 +1,235 @@
+/**
+ * QUnit v1.10.0 - A JavaScript Unit Testing Framework
+ *
+ * http://qunitjs.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+ margin: 0;
+ padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+ padding: 0.5em 0 0.5em 1em;
+
+ color: #8699a4;
+ background-color: #0d3349;
+
+ font-size: 1.5em;
+ line-height: 1em;
+ font-weight: normal;
+
+ border-radius: 5px 5px 0 0;
+ -moz-border-radius: 5px 5px 0 0;
+ -webkit-border-top-right-radius: 5px;
+ -webkit-border-top-left-radius: 5px;
+}
+
+#qunit-header a {
+ text-decoration: none;
+ color: #c2ccd1;
+}
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+ color: #fff;
+}
+
+#qunit-testrunner-toolbar label {
+ display: inline-block;
+ padding: 0 .5em 0 .1em;
+}
+
+#qunit-banner {
+ height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+ padding: 0.5em 0 0.5em 2em;
+ color: #5E740B;
+ background-color: #eee;
+ overflow: hidden;
+}
+
+#qunit-userAgent {
+ padding: 0.5em 0 0.5em 2.5em;
+ background-color: #2b81af;
+ color: #fff;
+ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+#qunit-modulefilter-container {
+ float: right;
+}
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+ list-style-position: inside;
+}
+
+#qunit-tests li {
+ padding: 0.4em 0.5em 0.4em 2.5em;
+ border-bottom: 1px solid #fff;
+ list-style-position: inside;
+}
+
+#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
+ display: none;
+}
+
+#qunit-tests li strong {
+ cursor: pointer;
+}
+
+#qunit-tests li a {
+ padding: 0.5em;
+ color: #c2ccd1;
+ text-decoration: none;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+ color: #000;
+}
+
+#qunit-tests ol {
+ margin-top: 0.5em;
+ padding: 0.5em;
+
+ background-color: #fff;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+}
+
+#qunit-tests table {
+ border-collapse: collapse;
+ margin-top: .2em;
+}
+
+#qunit-tests th {
+ text-align: right;
+ vertical-align: top;
+ padding: 0 .5em 0 0;
+}
+
+#qunit-tests td {
+ vertical-align: top;
+}
+
+#qunit-tests pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+#qunit-tests del {
+ background-color: #e0f2be;
+ color: #374e0c;
+ text-decoration: none;
+}
+
+#qunit-tests ins {
+ background-color: #ffcaca;
+ color: #500;
+ text-decoration: none;
+}
+
+/*** Test Counts */
+
+#qunit-tests b.counts { color: black; }
+#qunit-tests b.passed { color: #5E740B; }
+#qunit-tests b.failed { color: #710909; }
+
+#qunit-tests li li {
+ padding: 5px;
+ background-color: #fff;
+ border-bottom: none;
+ list-style-position: inside;
+}
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+ color: #3c510c;
+ background-color: #fff;
+ border-left: 10px solid #C6E746;
+}
+
+#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected { color: #999999; }
+
+#qunit-banner.qunit-pass { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+ color: #710909;
+ background-color: #fff;
+ border-left: 10px solid #EE5757;
+ white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+ border-radius: 0 0 5px 5px;
+ -moz-border-radius: 0 0 5px 5px;
+ -webkit-border-bottom-right-radius: 5px;
+ -webkit-border-bottom-left-radius: 5px;
+}
+
+#qunit-tests .fail { color: #000000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name { color: #000000; }
+
+#qunit-tests .fail .test-actual { color: #EE5757; }
+#qunit-tests .fail .test-expected { color: green; }
+
+#qunit-banner.qunit-fail { background-color: #EE5757; }
+
+
+/** Result */
+
+#qunit-testresult {