pax_global_header00006660000000000000000000000064124525777720014534gustar00rootroot0000000000000052 comment=b3af30a6aec7f6467c773cd49b2d974a70f7c0d4 json-patch-php-0.1.0/000077500000000000000000000000001245257777200143655ustar00rootroot00000000000000json-patch-php-0.1.0/.gitignore000066400000000000000000000000331245257777200163510ustar00rootroot00000000000000*~ \#* .idea .fuse* vendor/json-patch-php-0.1.0/.gitmodules000066400000000000000000000001621245257777200165410ustar00rootroot00000000000000[submodule "json-patch-tests"] path = json-patch-tests url = https://github.com/json-patch/json-patch-tests.git json-patch-php-0.1.0/.travis.yml000066400000000000000000000002141245257777200164730ustar00rootroot00000000000000language: php php: - 5.3 - 5.4 - 5.5 - 5.6 - hhvm before_script: - composer install --prefer-source script: php run_tests.php json-patch-php-0.1.0/README.md000066400000000000000000000046611245257777200156530ustar00rootroot00000000000000json-patch-php ================ Produce and apply json-patch objects. Implements IETF JSON-patch (RFC 6902) and JSON-pointer (RFC 6901): http://tools.ietf.org/html/rfc6902 http://tools.ietf.org/html/rfc6901 Using with Composer ------------------- To use this library as a Composer dependency in your project, include the following sections in your project's `composer.json` file: ``` "repositories": [ { "type": "vcs", "url": "https://github.com/mikemccabe/json-patch-php" } ], "require": { "mikemccabe/json-patch-php": "dev-master" } ``` Then, in your project's code, use the `JsonPatch` class definition from the `mikemccabe\JsonPatch` namespace like so: ```php use mikemccabe\JsonPatch\JsonPatch; ``` Entry points ------------ - JsonPatch::get($doc, $pointer) - get a value from a json document - JsonPatch::patch($doc, $patches) - apply patches to $doc and return result - JsonPatch::diff($src, $dst) - return patches to create $dst from $src Arguments are PHP arrays, i.e. the output of json_decode($json_string, 1) (Note that you MUST pass 1 as the second argument to json_decode to get an array. This library does not work with stdClass objects.) All structures are implemented directly as PHP arrays. An array is considered to be 'associative' (e.g. like a JSON 'object') if it contains at least one non-numeric key. Because of this, empty arrays ([]) and empty objects ({}) compare the same, and (for instance) an 'add' of a string key to an empty array will succeed in this implementation where it might fail in others. $simplexml_mode is provided to help with working with arrays produced from XML in the style of simplexml - e.g. repeated XML elements are expressed as arrays. When $simplexml_mode is enabled, leaves with scalar values are implicitly treated as length-1 arrays, so this test will succeed: { "comment": "basic simplexml array promotion", "doc": { "foo":1 }, "patch": [ { "op":"add", "path":"/foo/1", "value":2 } ], "expected": { "foo":[1, 2] } }, Also, when $simplexml_mode is true, 1-length arrays are converted to scalars on return from patch(). Tests ----- Some tests are in a submodule (https://github.com/json-patch/json-patch-tests). Do 'git submodule init' to pull these, then 'php runtests.php' to run them. [![Build Status](https://secure.travis-ci.org/mikemccabe/json-patch-php.png)](http://travis-ci.org/mikemccabe/json-patch-php) json-patch-php-0.1.0/composer.json000066400000000000000000000003521245257777200171070ustar00rootroot00000000000000{ "name": "mikemccabe/json-patch-php", "description": "Produce and apply json-patch objects", "type": "library", "license": "LGPL-3.0", "autoload": { "psr-4": { "mikemccabe\\JsonPatch\\": "src" } } } json-patch-php-0.1.0/json-patch-tests/000077500000000000000000000000001245257777200175735ustar00rootroot00000000000000json-patch-php-0.1.0/local_tests.json000066400000000000000000000175201245257777200176010ustar00rootroot00000000000000[ { "comment": "blur arrays and objects", "doc": { "foo": 1 }, "patch": [ { "op": "add", "path": "/1", "value": 2 } ], "expected": { "foo": 1, "1": 2 } }, { "comment": "Adding to \"/-\" adds to the end of the array", "doc": [ 1, 2 ], "patch": [ { "op": "add", "path": "/-", "value": 3 } ], "expected": [ 1, 2, 3 ] }, { "comment": "value in array append not flattened", "doc": [1, 2], "patch": [{"op": "add", "path": "/-", "value": [3]}], "expected": [1, 2, [3]] }, { "comment": "move target can use '-'", "doc": {"to":[ 1, 2 ], "from": 3}, "patch": [{"op": "move", "from":"/from", "path": "/to/-"}], "expected": {"to":[ 1, 2, 3 ]}}, { "comment": "copy target can use '-'", "doc": {"to":[1, 2], "from": 3}, "patch": [{"op": "copy", "from": "/from", "path": "/to/-"}], "expected": { "to":[ 1, 2, 3 ], "from": 3 } }, { "comment": "replace target must exist", "doc": {"foo": "bar"}, "patch": [{"op": "replace", "path": "/baz", "value": "sil"}], "error": "replace target '/baz' not set" }, { "comment": "- as remove target not allowed", "doc": [1, 2], "patch": [{"op": "remove", "path": "/-"}], "error": "Non-array key '-' used on array" }, { "comment": "remove of numeric index from obj doesn't convert to array", "doc": {"foo": 1, "0":2, "bar":3}, "patch": [{"op": "remove", "path":"/0"}], "expected": {"foo":1, "bar":3} }, { "comment": "- as remove target for obj isn't special", "doc": {"-": 1, "foo": 2}, "patch": [{"op": "remove", "path": "/-"}], "expected": {"foo": 2} }, { "comment": "toplevel as remove target", "doc": [1], "patch": [{"op": "remove", "path": ""}], "error": "Can't remove whole document" }, { "comment": "Ok to have doc as toplevel string?", "doc": 1, "patch": [{"op": "replace", "path": "", "value": "bar"}], "expected": "bar" }, { "comment": "Ok to have doc as toplevel number?", "doc": 1, "patch": [{"op": "replace", "path": "", "value": 1}], "expected": 1 }, { "comment": "Ok to have result doc as toplevel string?", "doc": [ 1 ], "patch": [{"op": "replace", "path": "", "value": "bar"}], "expected": "bar" }, { "comment": "'add' should replace existing member if it already exists", "doc": { "foo": 1 }, "patch": [{"op": "add", "path": "/foo", "value": 2}], "expected": { "foo": 2 } }, { "comment": "test op with string at toplevel", "doc": "foo", "patch": [{"op": "test", "path":"", "value": "foo"}] }, { "comment": "test op with number at toplevel", "doc": 1, "patch": [{"op": "test", "path":"", "value": 1}] }, { "comment": "test op with false at toplevel", "doc": false, "patch": [{"op": "test", "path":"", "value": false}] }, { "comment": "test op with true at toplevel", "doc": true, "patch": [{"op": "test", "path":"", "value": true}] }, { "comment": "test op with null at toplevel", "doc": null, "patch": [{"op": "test", "path":"", "value": null}] }, { "comment": "test null != false", "doc": null, "patch": [{"op": "test", "path":"", "value": false}], "error": "expected false value not found" }, { "comment": "test false != null", "doc": false, "patch": [{"op": "test", "path":"", "value": null}], "error": "test target value different - expected null, found false" }, { "comment": "test null != false", "doc": null, "patch": [{"op": "test", "path":"", "value": false}], "error": "test target value different - expected false, found null" }, { "comment": "test emptystr != false", "doc": "", "patch": [{"op": "test", "path":"", "value": false}], "error": "test target value different - expected false, found \"\"" }, { "comment": "test false != emptystr", "doc": false, "patch": [{"op": "test", "path":"", "value": ""}], "error": "test target value different - expected \"\", found false" }, { "comment": "null within string", "doc": [ "foo\u0000foo" ], "patch": [{"op":"test", "path":"/0", "value":"foo\u0000foo"}] }, { "comment": "null string", "doc": [ "\u0000" ], "patch": [{"op":"test", "path":"/0", "value":"\u0000"}] }, { "comment": "null in key", "doc": { "foo\u0000foo": 1 }, "patch": [{"op":"replace", "path":"/foo\u0000foo", "value":2}], "expected": { "foo\u0000foo": 2 } }, { "comment": "null in key - test against prefix", "doc": { "foo": 1, "foo\u0000foo": 2 }, "patch": [{"op":"test", "path":"/foo\u0000foo", "value":2}] }, { "comment": "null in key - trailing", "doc": { "foo": 1, "foo\u0000": 2 }, "patch": [{"op":"test", "path":"/foo\u0000", "value":2}] }, { "comment": "null as key", "doc": { "\u0000": 1 }, "patch": [{"op":"replace", "path":"/\u0000", "value":2}], "expected": { "\u0000": 2 } }, { "comment": "null as key prefix", "doc": { "\u0000foo": 1 }, "patch": [{"op":"replace", "path":"/\u0000foo", "value":2}], "expected": { "\u0000foo": 2 } }, { "comment": "copy doc onto child", "doc": { "foo": 1 }, "patch": [{"op":"copy", "from":"", "path":"/bar"}], "expected": { "foo": 1, "bar": { "foo": 1 }} }, { "comment": "move doc onto child ('from' must not be proper prefix)", "doc": { "foo": { "bar": 1 } }, "patch": [{"op":"move", "from":"/foo", "path":"/foo/bar"}], "error": "path '/foo/bar' not found (already removed)"}, { "comment": "need bounds check on intermediate path", "doc": [1, [2]], "patch": [{"op": "test", "path":"/2/0", "value": 2}], "error": "path '/2/0' not in target doc" }, { "comment": "'-' should be legit member for object", "doc": {"foo": 1}, "patch": [{"op": "add", "path":"/-", "value": 2}], "expected": {"foo": 1, "-": 2} }, { "comment": "remove of array-looking element of object", "doc": {"foo":1, "0":2}, "patch": [{"op":"remove", "path":"/0"}], "expected": {"foo": 1} }, { "comment": "replace of array-looking element of object", "doc": {"foo":1, "0":2}, "patch": [{"op":"replace", "path":"/0", "value":3}], "expected": {"foo": 1, "0":3} }, { "comment": "replace string with null (elicits diff error)", "doc": [""], "patch": [{"op": "replace", "path": "/0", "value": null}], "expected": [null] }, { "comment": "test object sorting for equality if numeric indices exist", "doc": {"foo":1,"bar":3,"0":2}, "patch": [{"op": "test", "path":"", "value": {"foo":1,"0":2,"bar":3}}] }, { "comment": "test php-style array element delete - disabled as reverse diff (gappy array from pure array) is impossible in json-patch without borrowing php array semantics", "doc": {"0":"a", "2":"c"}, "patch": {"op":"add", "path":"/1", "value":"b"}, "expected": {"0":"a", "1":"b", "2":"c"}, "disabled": true }, { "comment": "test php-style array element delete - assoc-ish indexes", "doc": {"0a":"a", "2c":"c"}, "patch": {"op":"add", "path":"/1b", "value":"b"}, "expected": {"0a":"a", "1b":"b", "2c": "c"} }, { "comment": "Numerically equal must test equal", "doc": [1.00], "patch": [{"op": "test", "path":"/0", "value":1}]}, { "comment": "Numerically equal must test equal", "doc": [1], "patch": [{"op": "test", "path":"/0", "value":1.00}]}, { "comment": "Numerically equal must test equal", "doc": [1e0], "patch": [{"op": "test", "path":"/0", "value":1.00}]}, { "comment": "append", "doc": [1, 2, 3, 4], "patch": [{"op": "append", "path": "/-", "value":[5, 6, 7, 8]}], "expected": [1, 2, 3, 4, 5, 6, 7, 8], "disabled": true }, { "comment": "last" } ] json-patch-php-0.1.0/run_tests.php000066400000000000000000000112631245257777200171270ustar00rootroot00000000000000getMessage() . "\n"); print_test($test); print("\n"); return false; } else { if ($verbose) { if (array_key_exists('comment', $test)) { print "OK: " . $test['comment'] . "\n"; } print("caught: " . $ex->getMessage() . "\n"); print("expected: " . $test['error'] . "\n\n"); } return true; } } } // Piggyback on patch tests to test diff as well - use 'doc' and // 'expected' from testcases. Generate a diff, apply it, and check // that it matches the target - in both directions. function diff_test($test) { // Skip comment-only or test op tests if (!(isset($test['doc']) && isset($test['expected']))) { return true; } $result = true; try { $doc1 = $test['doc']; // copy, in case sort/patch alters $doc2 = $test['expected']; $patch = JsonPatch::diff($doc1, $doc2); $patched = JsonPatch::patch($doc1, $patch); if (!JsonPatch::considered_equal($patched, $doc2)) { print("diff test failed:\n"); print_test($test); print("from: " . json_encode($doc1) . "\n"); print("diff: " . json_encode($patch) . "\n"); print("found: " . json_encode($patched) . "\n"); print("expected: " . json_encode($doc2) . "\n\n"); $result = false; } // reverse order $doc1 = $test['expected']; // copy, in case sort/patch alters $doc2 = $test['doc']; $patch = JsonPatch::diff($doc1, $doc2); $patched = JsonPatch::patch($doc1, $patch); if (!JsonPatch::considered_equal($patched, $doc2)) { print("reverse diff test failed:\n"); print_test($test); print("from: " . json_encode($doc1) . "\n"); print("diff: " . json_encode($patch) . "\n"); print("found: " . json_encode($patched) . "\n"); print("expected: " . json_encode($doc2) . "\n\n"); $result = false; } } catch (Exception $ex) { print("caught exception ".$ex->getMessage()."\n"); return false; } return $result; } function test_file($filename, $simplexml_mode=false) { $testfile = file_get_contents($filename); if (!$testfile) { throw new Exception("Couldn't find test file $filename"); return false; } $tests = json_decode($testfile, 1); if (is_null($tests)) { throw new Exception("Error json-decoding test file $filename"); } $success = true; foreach ($tests as $test) { if (isset($test['disabled']) && $test['disabled']) { continue; } if (!do_test($test, $simplexml_mode)) { $success = false; } if (!$simplexml_mode && !diff_test($test)) { $success = false; } } return $success; } function main() { $result = true; $testfiles = array( 'local_tests.json', 'json-patch-tests/tests.json', 'json-patch-tests/spec_tests.json' ); foreach ($testfiles as $testfile) { if (!test_file($testfile)) { $result = false; } } if (!test_file('simplexml_tests.json', true)) { $result = false; } return $result; } if (!main()) { exit(1); } else { exit(0); }json-patch-php-0.1.0/simplexml_tests.json000066400000000000000000000044031245257777200205150ustar00rootroot00000000000000[ { "comment": "simplexml promotion - add after scalar", "doc": { "foo":1 }, "patch": [ { "op":"add", "path":"/foo/1", "value":2 } ], "expected": { "foo":[1, 2] } }, { "comment": "simplexml promotion - add before scalar", "doc": { "foo":1 }, "patch": [ { "op":"add", "path":"/foo/0", "value":2 } ], "expected": { "foo":[2, 1] } }, { "comment": "simplexml promotion - append", "doc": { "foo":1 }, "patch": [ { "op":"add", "path":"/foo/-", "value":2 } ], "expected": { "foo":[1, 2] } }, { "comment": "append to array", "doc": { "foo":1 }, "patch": [ { "op":"add", "path":"/foo/-", "value":2 } ], "expected": { "foo":[1, 2] } }, { "comment": "mid-path 0-index with tail 0-index - append", "doc": { "foo": { "bar": 1 } }, "patch": [ { "op":"add", "path":"/foo/0/bar/-", "value":2 }], "expected": { "foo": { "bar": [1,2] }} }, { "comment": "Add 1-length array is equivalent to scalar add", "doc": { }, "patch": [ { "op":"add", "path":"/foo/0", "value":1 } ], "expected": { "foo":1 }, "disabled": true }, { "comment": "simple 0-index of scalar ok", "doc": { "foo": 1 }, "patch": [ { "op":"test", "path":"/foo/0", "value":1 }] }, { "comment": "nested 0-index of scalar ok", "doc": { "foo": { "bar": 1 } }, "patch": [ { "op":"test", "path":"/foo/bar/0", "value":1 }] }, { "comment": "0-index after actual 0-index ok", "doc": { "foo": [{ "bar": 1 }, 1] }, "patch": [ { "op":"test", "path":"/foo/0/bar/0", "value":1 }] }, { "comment": "mid-path 0-index", "doc": { "foo": { "bar": [1, 2] } }, "patch": [ { "op":"test", "path":"/foo/0/bar/0", "value":1 }] }, { "comment": "mid-path 0-index with tail 0-index", "doc": { "foo": { "bar": 1 } }, "patch": [ { "op":"test", "path":"/foo/0/bar/0", "value":1 }] }, { "comment": "replace as array", "doc": { "foo":1 }, "patch": [ { "op":"replace", "path":"/foo/0", "value":2 } ], "expected": { "foo":2 } }, { "comment": "remove last demotes to singleton", "doc": { "foo":[1, 2] }, "patch": [ { "op":"remove", "path":"/foo/1"} ], "expected": { "foo":1 } }, { "comment": "tests complete" } ] json-patch-php-0.1.0/src/000077500000000000000000000000001245257777200151545ustar00rootroot00000000000000json-patch-php-0.1.0/src/JsonPatch.php000066400000000000000000000405411245257777200175620ustar00rootroot00000000000000 "replace", "path" => "$path", "value" => $other)); } } return array(); } // Walk associative arrays $src and $dst, returning a list of patches private static function diff_assoc($path, $src, $dst) { $result = array(); if (count($src) == 0 && count($dst) != 0) { $result[] = array("op" => "replace", "path" => "$path", "value" => $dst); } else { foreach (array_keys($src) as $key) { $ekey = self::escape_pointer_part($key); if (!array_key_exists($key, $dst)) { $result[] = array("op" => "remove", "path" => "$path/$ekey"); } else { $result = array_merge($result, self::diff_values("$path/$ekey", $src[$key], $dst[$key])); } } foreach (array_keys($dst) as $key) { if (!array_key_exists($key, $src)) { $ekey = self::escape_pointer_part($key); $result[] = array("op" => "add", "path" => "$path/$ekey", "value" => $dst[$key]); } } } return $result; } // Walk simple arrays $src and $dst, returning a list of patches private static function diff_array($path, $src, $dst) { $result = array(); $lsrc = count($src); $ldst = count($dst); $max = ($lsrc > $ldst) ? $lsrc : $ldst; // Walk backwards through arrays, starting with longest $i = $max - 1; while ($i >= 0) // equivalent for loop didn't work? { if ($i < $lsrc && $i < $ldst && array_key_exists($i, $src) && array_key_exists($i, $dst)) { $result = array_merge($result, self::diff_values("$path/$i", $src[$i], $dst[$i])); } else if ($i < $ldst && array_key_exists($i, $dst)) { $result[] = array("op" => "add", "path" => "$path/$i", "value" => $dst[$i]); } else if ($i < $lsrc && !array_key_exists($i, $dst)) { $result[] = array("op" => "remove", "path" => "$path/$i"); } $i--; } return $result; } // patch support functions // Implements the 'test' op private static function test($doc, $path, $parts, $value, $simplexml_mode) { $found = self::get_helper($doc, $path, $parts, $simplexml_mode); if (!self::considered_equal($found, $value)) { throw new JsonPatchException("test target value different - expected " . json_encode($value) . ", found " . json_encode($found)); } } // Helper for get() and 'copy', 'move', 'test' ops - get a value from a doc. private static function get_helper($doc, $path, $parts, $simplexml_mode) { if (count($parts) == 0) { return $doc; } $part = array_shift($parts); if (!is_array($doc) || !array_key_exists($part, $doc)) { throw new JsonPatchException("Path '$path' not found"); } if ($simplexml_mode && count($parts) > 0 && $parts[0] == '0' && self::is_associative($doc) && !(is_array($doc[$part]) && !self::is_associative($doc[$part]))) { return self::get_helper(array($doc[$part]), $path, $parts, $simplexml_mode); } else { return self::get_helper($doc[$part], $path, $parts, $simplexml_mode); } } // Test whether a php array looks 'associative' - does it have // any non-numeric keys? // // note: is_associative(array()) === false private static function is_associative($a) { if (!is_array($a)) { return false; } foreach (array_keys($a) as $key) { if (is_string($key)) { return true; } } // Also treat php gappy arrays as associative. // (e.g. {"0":"a", "2":"c"}) $len = count($a); for ($i = 0; $i < $len; $i++) { if (!array_key_exists($i, $a)) { return true; } } return false; } // Recursively sort array keys private static function rksort($a) { if (!is_array($a)) { return $a; } foreach (array_keys($a) as $key) { $a[$key] = self::rksort($a[$key]); } // SORT_STRING seems required, as otherwise numeric indices // (e.g. "4") aren't sorted. ksort($a, SORT_STRING); return $a; } // Per http://tools.ietf.org/html/rfc6902#section-4.6 public static function considered_equal($a1, $a2) { return json_encode(self::rksort($a1)) === json_encode(self::rksort($a2)); } // Apply a single op to modify the given document. // // As php arrays are not passed by reference, this function works // recursively, rebuilding complete subarrays that need changing; // the revised subarray is changed in the parent array before // returning it. private static function do_op($doc, $op, $path, $parts, $value, $simplexml_mode) { // Special-case toplevel if (count($parts) == 0) { if ($op == 'add' || $op == 'replace') { return $value; } else if ($op == 'remove') { throw new JsonPatchException("Can't remove whole document"); } else { throw new JsonPatchException("'$op' can't operate on whole document"); } } $part = array_shift($parts); // recur until we get to the target if (count($parts) > 0) { if (!array_key_exists($part, $doc)) { throw new JsonPatchException("Path '$path' not found"); } // recur, adding resulting sub-doc into doc returned to caller // special case for simplexml-style behavior - make singleton // scalar leaves look like 1-length arrays if ($simplexml_mode && count($parts) > 0 && ($parts[0] == '0' || $parts[0] == '1' || $parts[0] == '-') && self::is_associative($doc) && !(is_array($doc[$part]) && !self::is_associative($doc[$part]))) { $doc[$part] = self::do_op(array($doc[$part]), $op, $path, $parts, $value, $simplexml_mode); } else { $doc[$part] = self::do_op($doc[$part], $op, $path, $parts, $value, $simplexml_mode); } return $doc; } // at target if (!is_array($doc)) { throw new JsonPatchException('Target must be array or associative array'); } if (!self::is_associative($doc)) // N.B. returns false for empty arrays { if (count($doc) && !self::is_index($part) && !($part == '-' && ($op == 'add' || $op == 'append'))) { throw new JsonPatchException("Non-array key '$part' used on array"); } else { // check range, if numeric if (self::is_index($part) && ($part < 0 || (($op == 'remove' && $part >= count($doc)) || ($op != 'remove' && $part > count($doc))))) { throw new JsonPatchException("Can't operate outside of array bounds"); } } } if ($op == 'add' || $op == 'append') { if (!self::is_associative($doc) && (self::is_index($part) || $part == '-')) { // If index is '-', use array length $index = ($part == '-') ? count($doc) : $part; if ($op == 'append') { array_splice($doc, $index, 0, $value); } else { array_splice($doc, $index, 0, Array($value)); } } else { $doc[$part] = $value; } } else if ($op == 'replace') { if (!self::is_associative($doc) && self::is_index($part)) { array_splice($doc, $part, 1, Array($value)); } else { if (!array_key_exists($part, $doc)) { throw new JsonPatchException("replace target '$path' not set"); } $doc[$part] = $value; } } else if ($op == 'remove') { if (!self::is_associative($doc) && self::is_index($part)) { array_splice($doc, $part, 1); } else { if (!array_key_exists($part, $doc)) { throw new JsonPatchException("remove target '$path' not set"); } unset($doc[$part]); } } return $doc; } }