diff --git a/.gitattributes b/.gitattributes index e52196c..ce5e37a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ /phpunit.xml export-ignore /changelog.md export-ignore /Makefile export-ignore +/.gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..35b70e3 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,39 @@ +before_script: +- apt-get update -yqq +- apt-get install git unzip -yqq +- curl https://composer.github.io/installer.sig | tr -d '\n' > installer.sig +- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" +- php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" +- php composer-setup.php +- php -r "unlink('composer-setup.php'); unlink('installer.sig');" +- php composer.phar install --prefer-dist --no-ansi --no-interaction --no-progress + +test:5.6: + image: php:5.6 + script: + - pecl install xdebug-2.5.5 + - docker-php-ext-enable xdebug + - vendor/bin/phpunit --configuration phpunit.xml -v --coverage-text --colors=never --stderr + +test:7.0: + image: php:7.0 + script: + - pecl install xdebug + - docker-php-ext-enable xdebug + - vendor/bin/phpunit --configuration phpunit.xml -v --coverage-text --colors=never --stderr + +test:7.1: + image: php:7.1 + script: + - pecl install xdebug + - docker-php-ext-enable xdebug + - vendor/bin/phpunit --configuration phpunit.xml -v --coverage-text --colors=never --stderr + +test:7.2: + image: php:7.2 + script: + - pecl install xdebug + - docker-php-ext-enable xdebug + - vendor/bin/phpunit --configuration phpunit.xml -v --coverage-text --colors=never --stderr + - curl https://github.com/phpstan/phpstan/releases/download/0.9.2/phpstan.phar -sLo ./phpstan.phar + - php phpstan.phar analyze -l 7 ./src diff --git a/.travis.yml b/.travis.yml index d0f7fa9..b6004a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ cache: # execute any number of scripts before the test run, custom env's are available as variables before_script: - composer install --dev --no-interaction --prefer-dist - - if [[ $(phpenv version-name) =~ 7.2 ]] ; then test -f $HOME/.composer/cache/phpstan.phar || wget https://github.com/phpstan/phpstan/releases/download/0.9.1/phpstan.phar -O $HOME/.composer/cache/phpstan.phar; fi + - if [[ $(phpenv version-name) =~ 7.2 ]] ; then test -f $HOME/.composer/cache/phpstan.phar || wget https://github.com/phpstan/phpstan/releases/download/0.9.2/phpstan.phar -O $HOME/.composer/cache/phpstan.phar; fi - if [[ $(phpenv version-name) =~ 7.2 ]] ; then test -f $HOME/.composer/cache/ocular.phar || wget https://scrutinizer-ci.com/ocular.phar -O $HOME/.composer/cache/ocular.phar; fi - if [[ $(phpenv version-name) =~ 7.2 ]] ; then test -f $HOME/.composer/cache/cctr || wget https://codeclimate.com/downloads/test-reporter/test-reporter-0.1.4-linux-amd64 -O $HOME/.composer/cache/cctr && chmod +x $HOME/.composer/cache/cctr; fi - if [[ $(phpenv version-name) =~ 7.2 ]] ; then $HOME/.composer/cache/cctr before-build; fi diff --git a/README.md b/README.md index 1046462..52153d1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A PHP implementation for finding unordered diff between two `JSON` documents. * To detect breaking changes by analyzing removals and changes from original `JSON`. * To keep original order of object sets (for example `swagger.json` [parameters](https://swagger.io/docs/specification/describing-parameters/) list). * To make and apply JSON Patches, specified in [RFC 6902](http://tools.ietf.org/html/rfc6902) from the IETF. + * To make and apply JSON Merge Patches, specified in [RFC 7386](https://tools.ietf.org/html/rfc7386) from the IETF. * To retrieve and modify data by [JSON Pointer](http://tools.ietf.org/html/rfc6901). * To recursively replace by JSON value. @@ -55,11 +56,23 @@ $r = new JsonDiff( ); ``` +Available options: + * `REARRANGE_ARRAYS` is an option to enable arrays rearrangement to minimize the difference. + * `STOP_ON_DIFF` is an option to improve performance by stopping comparison when a difference is found. + * `JSON_URI_FRAGMENT_ID` is an option to use URI Fragment Identifier Representation (example: "#/c%25d"). If not set default JSON String Representation (example: "/c%d"). + * `SKIP_JSON_PATCH` is an option to improve performance by not building JsonPatch for this diff. + * `SKIP_JSON_MERGE_PATCH` is an option to improve performance by not building JSON Merge Patch value for this diff. + +Options can be combined, e.g. `JsonDiff::REARRANGE_ARRAYS + JsonDiff::STOP_ON_DIFF`. + On created object you have several handy methods. #### `getPatch` Returns [`JsonPatch`](#jsonpatch) of difference +#### `getMergePatch` +Returns [JSON Merge Patch](https://tools.ietf.org/html/rfc7386) value of difference + #### `getRearranged` Returns new value, rearranged with original order. @@ -137,6 +150,11 @@ Gets value from data at path specified `JSON Pointer` string. #### `remove` Removes value from data at path specified by segments. +### `JsonMergePatch` + +#### `apply` +Applies patch to `JSON`-decoded data. + ### `JsonValueReplace` #### `process` diff --git a/composer.json b/composer.json index 5f30193..9ffd7e9 100644 --- a/composer.json +++ b/composer.json @@ -1,32 +1,35 @@ { - "name": "swaggest/json-diff", - "description": "JSON diff/rearrange/patch/pointer library for PHP", - "type": "library", - "license": "MIT", - "authors": [ - { - "name": "Viacheslav Poturaev", - "email": "vearutop@gmail.com" - } - ], - "require-dev": { - "phpunit/phpunit": "^4.8.23", - "phpunit/php-code-coverage": "2.2.4", - "codeclimate/php-test-reporter": "^0.4.0" - }, - "autoload": { - "psr-4": { - "Swaggest\\JsonDiff\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Swaggest\\JsonDiff\\Tests\\": "tests/src" - } - }, - "config": { - "platform": { - "php": "5.4.45" - } + "name": "swaggest/json-diff", + "description": "JSON diff/rearrange/patch/pointer library for PHP", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" } + ], + "require": { + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.23", + "phpunit/php-code-coverage": "2.2.4", + "codeclimate/php-test-reporter": "^0.4.0" + }, + "autoload": { + "psr-4": { + "Swaggest\\JsonDiff\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Swaggest\\JsonDiff\\Tests\\": "tests/src" + } + }, + "config": { + "platform": { + "php": "5.4.45" + } + } } diff --git a/composer.lock b/composer.lock index 04705b4..2d67e17 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aaaf9789dd963d059456f3f5891a10d9", + "content-hash": "4d460047db5cbb49e07eb51f920b5da0", "packages": [], "packages-dev": [ { @@ -66,6 +66,62 @@ ], "time": "2017-02-15T22:25:47+00:00" }, + { + "name": "composer/ca-bundle", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0", + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2018-08-08T08:57:40+00:00" + }, { "name": "doctrine/instantiator", "version": "1.0.5", @@ -218,36 +274,44 @@ }, { "name": "padraic/humbug_get_contents", - "version": "1.0.4", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/humbug/file_get_contents.git", - "reference": "66797199019d0cb4529cb8d29c6f0b4c5085b53a" + "reference": "dcb086060c9dd6b2f51d8f7a895500307110b7a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/file_get_contents/zipball/66797199019d0cb4529cb8d29c6f0b4c5085b53a", - "reference": "66797199019d0cb4529cb8d29c6f0b4c5085b53a", + "url": "https://api.github.com/repos/humbug/file_get_contents/zipball/dcb086060c9dd6b2f51d8f7a895500307110b7a7", + "reference": "dcb086060c9dd6b2f51d8f7a895500307110b7a7", "shasum": "" }, "require": { - "php": ">=5.3" + "composer/ca-bundle": "^1.0", + "ext-openssl": "*", + "php": "^5.3 || ^7.0 || ^7.1 || ^7.2" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "bamarni/composer-bin-plugin": "^1.1", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5" }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": false + }, "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { "psr-4": { - "Humbug\\": "src/Humbug/" + "Humbug\\": "src/" }, "files": [ - "src/function.php" + "src/function.php", + "src/functions.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -259,6 +323,10 @@ "name": "Pádraic Brady", "email": "padraic.brady@gmail.com", "homepage": "http://blog.astrumfutura.com" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" } ], "description": "Secure wrapper for accessing HTTPS resources with file_get_contents for PHP 5.3+", @@ -271,24 +339,24 @@ "ssl", "tls" ], - "time": "2015-04-22T18:45:00+00:00" + "time": "2018-02-12T18:47:17+00:00" }, { "name": "padraic/phar-updater", - "version": "1.0.4", + "version": "v1.0.6", "source": { "type": "git", "url": "https://github.com/humbug/phar-updater.git", - "reference": "ac8802df2d1d03b7092b6f044a914f8d21592aae" + "reference": "d01d3b8f26e541ac9b9eeba1e18d005d852f7ff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/phar-updater/zipball/ac8802df2d1d03b7092b6f044a914f8d21592aae", - "reference": "ac8802df2d1d03b7092b6f044a914f8d21592aae", + "url": "https://api.github.com/repos/humbug/phar-updater/zipball/d01d3b8f26e541ac9b9eeba1e18d005d852f7ff1", + "reference": "d01d3b8f26e541ac9b9eeba1e18d005d852f7ff1", "shasum": "" }, "require": { - "padraic/humbug_get_contents": "1.0.4", + "padraic/humbug_get_contents": "^1.0", "php": ">=5.3.3" }, "require-dev": { @@ -311,7 +379,7 @@ ], "authors": [ { - "name": "Padraic Brady", + "name": "Pádraic Brady", "email": "padraic.brady@gmail.com", "homepage": "http://blog.astrumfutura.com" } @@ -323,7 +391,7 @@ "self-update", "update" ], - "time": "2017-07-12T22:42:45+00:00" + "time": "2018-03-30T12:52:15+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -376,33 +444,33 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.4", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "9f901e29c93dae4aa77c0bb161df4276f9c9a1be" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/9f901e29c93dae4aa77c0bb161df4276f9c9a1be", - "reference": "9f901e29c93dae4aa77c0bb161df4276f9c9a1be", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -435,7 +503,7 @@ "spy", "stub" ], - "time": "2018-02-11T18:49:29+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -919,6 +987,7 @@ "github", "test" ], + "abandoned": "php-coveralls/php-coveralls", "time": "2017-12-06T23:17:56+00:00" }, { @@ -1295,21 +1364,22 @@ }, { "name": "symfony/config", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "17605ff58313d9fe94e507620a399721fc347b6d" + "reference": "06c0be4cdd8363f3ec8d592c9a4d1b981d5052af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/17605ff58313d9fe94e507620a399721fc347b6d", - "reference": "17605ff58313d9fe94e507620a399721fc347b6d", + "url": "https://api.github.com/repos/symfony/config/zipball/06c0be4cdd8363f3ec8d592c9a4d1b981d5052af", + "reference": "06c0be4cdd8363f3ec8d592c9a4d1b981d5052af", "shasum": "" }, "require": { "php": ">=5.3.9", - "symfony/filesystem": "~2.3|~3.0.0" + "symfony/filesystem": "~2.3|~3.0.0", + "symfony/polyfill-ctype": "~1.8" }, "require-dev": { "symfony/yaml": "~2.7|~3.0.0" @@ -1347,20 +1417,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-01-21T19:03:25+00:00" + "time": "2018-07-26T11:13:39+00:00" }, { "name": "symfony/console", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "162ca7d0ea597599967aa63b23418e747da0896b" + "reference": "0c1fcbb9afb5cff992c982ff99c0434f0146dcfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/162ca7d0ea597599967aa63b23418e747da0896b", - "reference": "162ca7d0ea597599967aa63b23418e747da0896b", + "url": "https://api.github.com/repos/symfony/console/zipball/0c1fcbb9afb5cff992c982ff99c0434f0146dcfc", + "reference": "0c1fcbb9afb5cff992c982ff99c0434f0146dcfc", "shasum": "" }, "require": { @@ -1374,7 +1444,7 @@ "symfony/process": "~2.1|~3.0.0" }, "suggest": { - "psr/log": "For using the console logger", + "psr/log-implementation": "For using the console logger", "symfony/event-dispatcher": "", "symfony/process": "" }, @@ -1408,20 +1478,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-01-29T08:54:45+00:00" + "time": "2018-07-26T11:13:39+00:00" }, { "name": "symfony/debug", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "35e36287fc0fdc8a08f70efcd4865ae6d8a6ee55" + "reference": "cbb8a5f212148964efbc414838c527229f9951b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/35e36287fc0fdc8a08f70efcd4865ae6d8a6ee55", - "reference": "35e36287fc0fdc8a08f70efcd4865ae6d8a6ee55", + "url": "https://api.github.com/repos/symfony/debug/zipball/cbb8a5f212148964efbc414838c527229f9951b7", + "reference": "cbb8a5f212148964efbc414838c527229f9951b7", "shasum": "" }, "require": { @@ -1465,20 +1535,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-01-18T22:12:33+00:00" + "time": "2018-08-03T09:45:57+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d64be24fc1eba62f9daace8a8918f797fc8e87cc" + "reference": "84ae343f39947aa084426ed1138bb96bf94d1f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d64be24fc1eba62f9daace8a8918f797fc8e87cc", - "reference": "d64be24fc1eba62f9daace8a8918f797fc8e87cc", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/84ae343f39947aa084426ed1138bb96bf94d1f12", + "reference": "84ae343f39947aa084426ed1138bb96bf94d1f12", "shasum": "" }, "require": { @@ -1525,24 +1595,25 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" + "time": "2018-07-26T09:03:18+00:00" }, { "name": "symfony/filesystem", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "1f4e8351e0196562f5e8ec584baeceeb8e2e92f6" + "reference": "0b252f4e25b7da17abb5a98eb60755b71d082c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/1f4e8351e0196562f5e8ec584baeceeb8e2e92f6", - "reference": "1f4e8351e0196562f5e8ec584baeceeb8e2e92f6", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/0b252f4e25b7da17abb5a98eb60755b71d082c9c", + "reference": "0b252f4e25b7da17abb5a98eb60755b71d082c9c", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { @@ -1574,20 +1645,78 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" + "time": "2018-08-07T09:12:42+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.7.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b" + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b", - "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", "shasum": "" }, "require": { @@ -1599,7 +1728,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -1633,20 +1762,20 @@ "portable", "shim" ], - "time": "2018-01-30T19:27:44+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/stopwatch", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e" + "reference": "12a4b0c2a1788adf16a5548ab18ab9e8801d71d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/57021208ad9830f8f8390c1a9d7bb390f32be89e", - "reference": "57021208ad9830f8f8390c1a9d7bb390f32be89e", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/12a4b0c2a1788adf16a5548ab18ab9e8801d71d8", + "reference": "12a4b0c2a1788adf16a5548ab18ab9e8801d71d8", "shasum": "" }, "require": { @@ -1682,24 +1811,25 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" + "time": "2018-07-24T10:05:38+00:00" }, { "name": "symfony/yaml", - "version": "v2.8.34", + "version": "v2.8.45", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "be720fcfae4614df204190d57795351059946a77" + "reference": "fbf876678e29dc634430dcf0096e216eb0004467" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/be720fcfae4614df204190d57795351059946a77", - "reference": "be720fcfae4614df204190d57795351059946a77", + "url": "https://api.github.com/repos/symfony/yaml/zipball/fbf876678e29dc634430dcf0096e216eb0004467", + "reference": "fbf876678e29dc634430dcf0096e216eb0004467", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { @@ -1731,7 +1861,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:36:31+00:00" + "time": "2018-07-26T09:03:18+00:00" } ], "aliases": [], @@ -1739,7 +1869,9 @@ "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-json": "*" + }, "platform-dev": [], "platform-overrides": { "php": "5.4.45" diff --git a/src/JsonDiff.php b/src/JsonDiff.php index 5295f60..8599cd0 100644 --- a/src/JsonDiff.php +++ b/src/JsonDiff.php @@ -9,19 +9,41 @@ class JsonDiff { + /** + * REARRANGE_ARRAYS is an option to enable arrays rearrangement to minimize the difference. + */ const REARRANGE_ARRAYS = 1; + + /** + * STOP_ON_DIFF is an option to improve performance by stopping comparison when a difference is found. + */ const STOP_ON_DIFF = 2; /** - * Use URI Fragment Identifier Representation will be used (example: "#/c%25d"). + * JSON_URI_FRAGMENT_ID is an option to use URI Fragment Identifier Representation (example: "#/c%25d"). * If not set default JSON String Representation (example: "/c%d"). */ const JSON_URI_FRAGMENT_ID = 4; + /** + * SKIP_JSON_PATCH is an option to improve performance by not building JsonPatch for this diff. + */ + const SKIP_JSON_PATCH = 8; + + /** + * SKIP_JSON_MERGE_PATCH is an option to improve performance by not building JSON Merge Patch value for this diff. + */ + const SKIP_JSON_MERGE_PATCH = 16; + private $options = 0; private $original; private $new; + /** + * @var mixed Merge patch container + */ + private $merge; + private $added; private $addedCnt = 0; private $addedPaths = array(); @@ -44,7 +66,6 @@ class JsonDiff private $jsonPatch; /** - * Processor constructor. * @param mixed $original * @param mixed $new * @param int $options @@ -52,7 +73,9 @@ class JsonDiff */ public function __construct($original, $new, $options = 0) { - $this->jsonPatch = new JsonPatch(); + if (!($options & self::SKIP_JSON_PATCH)) { + $this->jsonPatch = new JsonPatch(); + } $this->original = $original; $this->new = $new; @@ -63,6 +86,9 @@ public function __construct($original, $new, $options = 0) } $this->rearranged = $this->rearrange(); + if (($new !== null) && $this->merge === null) { + $this->merge = new \stdClass(); + } } /** @@ -182,6 +208,15 @@ public function getPatch() return $this->jsonPatch; } + /** + * Returns JSON Merge Patch value of difference + */ + public function getMergePatch() + { + return $this->merge; + + } + /** * @return array|null|object|\stdClass * @throws Exception @@ -199,6 +234,8 @@ private function rearrange() */ private function process($original, $new) { + $merge = !($this->options & self::SKIP_JSON_MERGE_PATCH); + if ( (!$original instanceof \stdClass && !is_array($original)) || (!$new instanceof \stdClass && !is_array($new)) @@ -210,12 +247,17 @@ private function process($original, $new) } $this->modifiedPaths [] = $this->path; - $this->jsonPatch->op(new Test($this->path, $original)); - $this->jsonPatch->op(new Replace($this->path, $new)); + if ($this->jsonPatch !== null) { + $this->jsonPatch->op(new Test($this->path, $original)); + $this->jsonPatch->op(new Replace($this->path, $new)); + } JsonPointer::add($this->modifiedOriginal, $this->pathItems, $original); JsonPointer::add($this->modifiedNew, $this->pathItems, $new); + if ($merge) { + JsonPointer::add($this->merge, $this->pathItems, $new, JsonPointer::RECURSIVE_KEY_CREATION); + } } return $new; } @@ -234,6 +276,15 @@ private function process($original, $new) $isArray = is_array($original); $removedOffset = 0; + if ($merge && is_array($new) && !is_array($original)) { + $merge = false; + JsonPointer::add($this->merge, $this->pathItems, $new); + } elseif ($merge && $new instanceof \stdClass && !$original instanceof \stdClass) { + $merge = false; + JsonPointer::add($this->merge, $this->pathItems, $new); + } + + $isUriFragment = (bool)($this->options & self::JSON_URI_FRAGMENT_ID); foreach ($originalKeys as $key => $originalValue) { if ($this->options & self::STOP_ON_DIFF) { if ($this->modifiedCnt || $this->addedCnt || $this->removedCnt) { @@ -247,7 +298,7 @@ private function process($original, $new) if ($isArray) { $actualKey -= $removedOffset; } - $this->path .= '/' . JsonPointer::escapeSegment($actualKey, $this->options & self::JSON_URI_FRAGMENT_ID); + $this->path .= '/' . JsonPointer::escapeSegment((string)$actualKey, $isUriFragment); $this->pathItems[] = $actualKey; if (array_key_exists($key, $newArray)) { @@ -263,9 +314,15 @@ private function process($original, $new) $removedOffset++; } - $this->jsonPatch->op(new Remove($this->path)); + if ($this->jsonPatch !== null) { + $this->jsonPatch->op(new Remove($this->path)); + } JsonPointer::add($this->removed, $this->pathItems, $originalValue); + if ($merge) { + JsonPointer::add($this->merge, $this->pathItems, null); + } + } $this->path = $path; $this->pathItems = $pathItems; @@ -278,13 +335,19 @@ private function process($original, $new) return null; } $newOrdered[$key] = $value; - $path = $this->path . '/' . JsonPointer::escapeSegment($key, $this->options & self::JSON_URI_FRAGMENT_ID); + $path = $this->path . '/' . JsonPointer::escapeSegment($key, $isUriFragment); $pathItems = $this->pathItems; $pathItems[] = $key; JsonPointer::add($this->added, $pathItems, $value); + if ($merge) { + JsonPointer::add($this->merge, $pathItems, $value); + } + $this->addedPaths [] = $path; - $this->jsonPatch->op(new Add($path, $value)); + if ($this->jsonPatch !== null) { + $this->jsonPatch->op(new Add($path, $value)); + } } @@ -302,6 +365,7 @@ private function rearrangeArray(array $original, array $new) $uniqueIdx = array(); // find unique key for all items + /** @var mixed[string] $f */ $f = get_object_vars($first); foreach ($f as $key => $value) { if (is_array($value) || $value instanceof \stdClass) { diff --git a/src/JsonMergePatch.php b/src/JsonMergePatch.php new file mode 100644 index 0000000..9b49543 --- /dev/null +++ b/src/JsonMergePatch.php @@ -0,0 +1,31 @@ + $val) { + if ($val === null) { + unset($original->$key); + } else { + if (!is_object($original)) { + $original = new \stdClass(); + } + $branch = &$original->$key; + if (null === $branch) { + $branch = new \stdClass(); + } + self::apply($branch, $val); + } + } + } else { + $original = $patch; + } + } +} \ No newline at end of file diff --git a/src/JsonValueReplace.php b/src/JsonValueReplace.php index 2088c68..63c4414 100644 --- a/src/JsonValueReplace.php +++ b/src/JsonValueReplace.php @@ -30,6 +30,7 @@ public function __construct($search, $replace, $pathFilter = null) * Recursively replaces all nodes equal to `search` value with `replace` value. * @param mixed $data * @return mixed + * @throws Exception */ public function process($data) { diff --git a/tests/assets/merge-patch.json b/tests/assets/merge-patch.json new file mode 100644 index 0000000..5adc003 --- /dev/null +++ b/tests/assets/merge-patch.json @@ -0,0 +1,190 @@ +[ + { + "doc": { + "a": "b" + }, + "patch": { + "a": "c" + }, + "expected": { + "a": "c" + } + }, + { + "doc": { + "a": "b" + }, + "patch": { + "b": "c" + }, + "expected": { + "a": "b", + "b": "c" + } + }, + { + "doc": { + "a": "b" + }, + "patch": { + "a": null + }, + "expected": {} + }, + { + "doc": { + "a": "b", + "b": "c" + }, + "patch": { + "a": null + }, + "expected": { + "b": "c" + } + }, + { + "doc": { + "a": [ + "b" + ] + }, + "patch": { + "a": "c" + }, + "expected": { + "a": "c" + } + }, + { + "doc": { + "a": "c" + }, + "patch": { + "a": [ + "b" + ] + }, + "expected": { + "a": [ + "b" + ] + } + }, + { + "doc": { + "a": { + "b": "c" + } + }, + "patch": { + "a": { + "b": "d", + "c": null + } + }, + "expected": { + "a": { + "b": "d" + } + } + }, + { + "doc": { + "a": [ + { + "b": "c" + } + ] + }, + "patch": { + "a": [ + 1 + ] + }, + "expected": { + "a": [ + 1 + ] + } + }, + { + "doc": [ + "a", + "b" + ], + "patch": [ + "c", + "d" + ], + "expected": [ + "c", + "d" + ] + }, + { + "doc": { + "a": "b" + }, + "patch": [ + "c" + ], + "expected": [ + "c" + ] + }, + { + "doc": { + "a": "foo" + }, + "patch": null, + "expected": null + }, + { + "doc": { + "a": "foo" + }, + "patch": "bar", + "expected": "bar" + }, + { + "doc": { + "e": null + }, + "patch": { + "a": 1 + }, + "expected": { + "e": null, + "a": 1 + } + }, + { + "doc": [ + 1, + 2 + ], + "patch": { + "a": "b", + "c": null + }, + "expected": { + "a": "b" + } + }, + { + "doc": {}, + "patch": { + "a": { + "bb": { + "ccc": null + } + } + }, + "expected": { + "a": { + "bb": {} + } + } + } +] \ No newline at end of file diff --git a/tests/src/DiffTest.php b/tests/src/DiffTest.php index 7958bf0..3d0c8e8 100644 --- a/tests/src/DiffTest.php +++ b/tests/src/DiffTest.php @@ -18,4 +18,29 @@ public function testStopOnDiff() $this->assertSame(1, $diff->getDiffCnt()); } + /** + * @throws \Swaggest\JsonDiff\Exception + */ + public function testSkipPatch() + { + $original = (object)(array("root" => (object)array("a" => 1, "b" => 2))); + $new = (object)(array("root" => (object)array("b" => 3, "c" => 4))); + + $diff = new JsonDiff($original, $new, JsonDiff::SKIP_JSON_PATCH); + $this->assertSame(null, $diff->getPatch()); + $this->assertSame(3, $diff->getDiffCnt()); + $this->assertSame(1, $diff->getAddedCnt()); + $this->assertSame(1, $diff->getRemovedCnt()); + $this->assertSame(1, $diff->getModifiedCnt()); + + $diff = new JsonDiff($original, $new, JsonDiff::REARRANGE_ARRAYS); + $this->assertEquals('[{"op":"remove","path":"/root/a"},{"value":2,"op":"test","path":"/root/b"},{"value":3,"op":"replace","path":"/root/b"},{"value":4,"op":"add","path":"/root/c"}]', + json_encode($diff->getPatch(), JSON_UNESCAPED_SLASHES)); + $this->assertSame(3, $diff->getDiffCnt()); + $this->assertSame(1, $diff->getAddedCnt()); + $this->assertSame(1, $diff->getRemovedCnt()); + $this->assertSame(1, $diff->getModifiedCnt()); + + } + } \ No newline at end of file diff --git a/tests/src/MergePatchTest.php b/tests/src/MergePatchTest.php new file mode 100644 index 0000000..eb02c21 --- /dev/null +++ b/tests/src/MergePatchTest.php @@ -0,0 +1,293 @@ +doc; + $patch = $case->patch; + $expected = $case->expected; + + JsonMergePatch::apply($doc, $patch); + + $this->assertEquals($expected, $doc); + } + + public function test13() + { + $case = json_decode(<<<'JSON' +{ + "doc": [ + 1, + 2 + ], + "patch": { + "a": "b", + "c": null + }, + "expected": { + "a": "b" + } +} +JSON + ); + $doc = $case->doc; + $patch = $case->patch; + $expected = $case->expected; + + JsonMergePatch::apply($doc, $patch); + + $this->assertEquals($expected, $doc); + + } + + public function testGetMergePatch() + { + $case = json_decode(<<<'JSON' +{ + "doc": { + "a": { + "b": "c" + } + }, + "patch": { + "a": { + "b": "d", + "c": null + } + }, + "expected": { + "a": { + "b": "d" + } + } +} +JSON + ); + $doc = $case->doc; + $expected = $case->expected; + $diff = new JsonDiff($doc, $expected); + $mergePatch = $diff->getMergePatch(); + JsonMergePatch::apply($doc, $mergePatch); + + $this->assertEquals($expected, $doc, 'Apply created failed: ' . json_encode( + [ + "test" => $case, + "patch" => $mergePatch, + "result" => $doc, + ], JSON_UNESCAPED_SLASHES + JSON_PRETTY_PRINT)); + + } + + + public function testGetMergePatchOfArray() + { + $case = json_decode(<<<'JSON' +{ + "doc": { + "a": "b" + }, + "patch": [ + "c" + ], + "expected": [ + "c" + ] +} +JSON + ); + $doc = $case->doc; + $expected = $case->expected; + $diff = new JsonDiff($doc, $expected); + $mergePatch = $diff->getMergePatch(); + JsonMergePatch::apply($doc, $mergePatch); + + $this->assertEquals($expected, $doc, 'Apply created failed: ' . json_encode( + [ + "test" => $case, + "patch" => $mergePatch, + "result" => $doc, + ], JSON_UNESCAPED_SLASHES + JSON_PRETTY_PRINT)); + + } + + + public function testSame() + { + $case = json_decode(<<<'JSON' +{ + "doc": { + "a": { + "b": "d" + } + }, + "patch": { + "a": { + "b": "d" + } + }, + "expected": { + "a": { + "b": "d" + } + } +} +JSON + ); + $doc = $case->doc; + $expected = $case->expected; + $diff = new JsonDiff($doc, $expected); + //print_r($diff->getPatch()); + $mergePatch = $diff->getMergePatch(); + JsonMergePatch::apply($doc, $mergePatch); + + $this->assertEquals($expected, $doc, 'Apply created failed: ' . json_encode( + [ + "test" => $case, + "patch" => $mergePatch, + "result" => $doc, + ], JSON_UNESCAPED_SLASHES + JSON_PRETTY_PRINT)); + + } + + public function testArraySwap() + { + $case = json_decode(<<<'JSON' +{ + "doc": [ + "a", + "b" + ], + "patch": [ + "c", + "d" + ], + "expected": [ + "c", + "d" + ] +} +JSON + ); + $doc = $case->doc; + $expected = $case->expected; + $diff = new JsonDiff($doc, $expected); + //print_r($diff->getPatch()); + $mergePatch = $diff->getMergePatch(); + JsonMergePatch::apply($doc, $mergePatch); + + $this->assertEquals($expected, $doc, 'Apply created failed: ' . json_encode( + [ + "test" => $case, + "patch" => $mergePatch, + "result" => $doc, + ], JSON_UNESCAPED_SLASHES + JSON_PRETTY_PRINT)); + + + } + + + /** + * @dataProvider specTestsProvider + */ + public function testSpec($case) + { + $this->doTest($case); + } + + public function specTestsProvider() + { + return $this->provider(__DIR__ . '/../assets/merge-patch.json'); + } + + protected function provider($path) + { + $cases = json_decode(file_get_contents($path)); + + $testCases = array(); + foreach ($cases as $i => $case) { + if (!isset($case->comment)) { + $comment = 'unknown' . $i; + } else { + $comment = $case->comment; + } + + $testCases[$comment] = array( + 'case' => $case, + ); + } + return $testCases; + } + + protected function doTest($case) + { + $case = clone $case; + + if (!is_object($case->doc)) { + $doc = $case->doc; + } else { + $doc = clone $case->doc; + } + $patch = $case->patch; + $expected = isset($case->expected) ? $case->expected : null; + $jsonOptions = JSON_UNESCAPED_SLASHES + JSON_PRETTY_PRINT; + + + JsonMergePatch::apply($doc, $patch); + $this->assertEquals($expected, $doc, 'Apply failed: ' . json_encode( + [ + "test" => $case, + "result" => $doc, + ], $jsonOptions)); + + if (!is_object($case->doc)) { + $doc = $case->doc; + } else { + $doc = clone $case->doc; + } + try { + $diff = new JsonDiff($doc, $expected); + $mergePatch = $diff->getMergePatch(); + } catch (Exception $exception) { + $mergePatch = $exception->getMessage(); + } + JsonMergePatch::apply($doc, $mergePatch); + + $this->assertEquals($expected, $doc, 'Apply created failed: ' . json_encode( + [ + "test" => $case, + "patch" => $mergePatch, + "result" => $doc, + ], $jsonOptions)); + + + } + + +} \ No newline at end of file diff --git a/tests/src/RearrangeTest.php b/tests/src/RearrangeTest.php index 2446c60..660982f 100644 --- a/tests/src/RearrangeTest.php +++ b/tests/src/RearrangeTest.php @@ -8,6 +8,9 @@ class RearrangeTest extends \PHPUnit_Framework_TestCase { + /** + * @throws \Swaggest\JsonDiff\Exception + */ public function testKeepOrder() { $originalJson = <<<'JSON' diff --git a/tests/src/SpecTest.php b/tests/src/SpecTest.php index baf0434..e02fdb5 100644 --- a/tests/src/SpecTest.php +++ b/tests/src/SpecTest.php @@ -56,7 +56,8 @@ protected function provider($path) return $testCases; } - protected function doTest($case) { + protected function doTest($case) + { $case = clone $case; if (isset($case->disabled) && $case->disabled) {