├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── basic.png ├── examples.js └── mapper.png ├── index.js ├── package.json └── test ├── .eslintrc.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [package.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "@satazor/eslint-config/es5", 5 | "@satazor/eslint-config/addons/node" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.* 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /screenshots 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | - "5" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 IndigoUnited 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff-json-structure 2 | 3 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] [![Greenkeeper badge][greenkeeper-image]][greenkeeper-url] 4 | 5 | 6 | [npm-url]:https://npmjs.org/package/diff-json-structure 7 | [downloads-image]:http://img.shields.io/npm/dm/diff-json-structure.svg 8 | [npm-image]:http://img.shields.io/npm/v/diff-json-structure.svg 9 | [travis-url]:https://travis-ci.org/IndigoUnited/node-diff-json-structure 10 | [travis-image]:http://img.shields.io/travis/IndigoUnited/node-diff-json-structure/master.svg 11 | [david-dm-url]:https://david-dm.org/IndigoUnited/node-diff-json-structure 12 | [david-dm-image]:https://img.shields.io/david/IndigoUnited/node-diff-json-structure.svg 13 | [david-dm-dev-url]:https://david-dm.org/IndigoUnited/node-diff-json-structure?type=dev 14 | [david-dm-dev-image]:https://img.shields.io/david/dev/IndigoUnited/node-diff-json-structure.svg 15 | [greenkeeper-image]:https://badges.greenkeeper.io/IndigoUnited/node-diff-json-structure.svg 16 | [greenkeeper-url]:https://greenkeeper.io/ 17 | 18 | Get the structural diff of two JSON objects, using [diff](https://www.npmjs.com/package/diff)'s internally which is a module used by several test frameworks. 19 | 20 | 21 | It is considered a structural difference whenever: 22 | 23 | - items are added or removed to objects and arrays 24 | - the type of the item changes 25 | 26 | 27 | ## Installation 28 | 29 | `$ npm install diff-json-structure` 30 | 31 | 32 | ## Usage 33 | 34 | `diff(oldObj, newObj, [options])` 35 | 36 | Calculates the structural diff between `oldObj` and `newObj`, returning an array of parts. 37 | 38 | Available options: 39 | 40 | - typeMapper - A function that lets you override types for specific paths 41 | - .. and any option that [diff](https://www.npmjs.com/package/diff)'s `.diffJson()` method supports 42 | 43 | 44 | ### Examples 45 | 46 | Simple usage: 47 | 48 | ```js 49 | var diff = require('diff-json-structure'); 50 | var chalk = require('chalk'); 51 | 52 | // Utility function to visually print the diff 53 | // Tweak it at your own taste 54 | function printDiff(parts) { 55 | parts.forEach(function (part) { 56 | part.value 57 | .split('\n') 58 | .filter(function (line) { return !!line; }) 59 | .forEach(function (line) { 60 | if (part.added) { 61 | process.stdout.write(chalk.green('+ ' + line) + '\n'); 62 | } else if (part.removed) { 63 | process.stdout.write(chalk.red('- ' + line) + '\n'); 64 | } else { 65 | process.stdout.write(chalk.dim(' ' + line) + '\n'); 66 | } 67 | }); 68 | }); 69 | 70 | process.stdout.write('\n'); 71 | } 72 | 73 | var oldObject = { 74 | environment: 'dev', 75 | googleAppId: 'UA-3234432-22', 76 | socialProviders: ['facebook'], 77 | libraries: { 78 | jquery: './node_modules/jquery', 79 | }, 80 | }; 81 | 82 | var newObj = { 83 | environment: 'prod', 84 | dbHost: '127.0.0.1:9000', 85 | socialProviders: ['facebook', 'twitter'], 86 | libraries: { 87 | jquery: './node_modules/jquery/jquery', 88 | moment: './node_modules/moment/moment', 89 | }, 90 | }; 91 | 92 | printDiff(diff(oldObj, newObj)); 93 | ``` 94 | 95 | 96 | 97 | 98 | Usage with `options.typeMapper` to ignore differences of socialProvider items of the previous example: 99 | 100 | ```js 101 | printDiff(diff(oldObj, newObj, { 102 | typeMapper: function (path, value, prop, subject) { 103 | // path is a string that contains the full path to this value 104 | // e.g.: 'libraries.jquery' and 'socialProviders[0]' 105 | 106 | // You may return custom types here.. if nothing is returned, the normal 107 | // flow of identifying the structure recursively will continue 108 | if (path === 'socialProviders') { 109 | return 'array'; 110 | } 111 | }, 112 | })); 113 | ``` 114 | 115 | 116 | 117 | 118 | ## Tests 119 | 120 | `$ npm test` 121 | 122 | 123 | ## License 124 | 125 | Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php). 126 | -------------------------------------------------------------------------------- /doc/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoUnited/node-diff-json-structure/71c4d10b4fb236fc09eb534d4dfe5042ea0fed44/doc/basic.png -------------------------------------------------------------------------------- /doc/examples.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var diff = require('../'); 4 | var chalk = require('chalk'); 5 | 6 | var oldObj; 7 | var newObj; 8 | 9 | function printDiff(parts) { 10 | parts.forEach(function (part) { 11 | part.value 12 | .split('\n') 13 | .filter(function (line) { return !!line; }) 14 | .forEach(function (line) { 15 | if (part.added) { 16 | process.stdout.write(chalk.green('+ ' + line) + '\n'); 17 | } else if (part.removed) { 18 | process.stdout.write(chalk.red('- ' + line) + '\n'); 19 | } else { 20 | process.stdout.write(chalk.dim(' ' + line) + '\n'); 21 | } 22 | }); 23 | }); 24 | 25 | process.stdout.write('\n'); 26 | } 27 | 28 | oldObj = { 29 | environment: 'dev', 30 | googleAppId: 'UA-3234432-22', 31 | socialProviders: ['facebook'], 32 | libraries: { 33 | jquery: './node_modules/jquery', 34 | }, 35 | }; 36 | 37 | newObj = { 38 | environment: 'prod', 39 | dbHost: '127.0.0.1:9000', 40 | socialProviders: ['facebook', 'twitter'], 41 | libraries: { 42 | jquery: './node_modules/jquery/jquery', 43 | moment: './node_modules/moment/moment', 44 | }, 45 | }; 46 | 47 | printDiff(diff(oldObj, newObj)); 48 | printDiff(diff(oldObj, newObj, { 49 | typeMapper: function (path) { 50 | if (path === 'socialProviders') { 51 | return 'array'; 52 | } 53 | }, 54 | })); 55 | -------------------------------------------------------------------------------- /doc/mapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndigoUnited/node-diff-json-structure/71c4d10b4fb236fc09eb534d4dfe5042ea0fed44/doc/mapper.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var typeOf = require('typeof'); 4 | var cloneDeep = require('lodash.clonedeep'); 5 | var assign = require('lodash.assign'); 6 | var isPlainObject = require('is-plain-object'); 7 | var forEach = require('deep-for-each'); 8 | var jsdiff = require('diff'); 9 | 10 | function mapToType(value, options) { 11 | options = assign({ 12 | typeMapper: null, 13 | }, options); 14 | 15 | value = cloneDeep(value); 16 | 17 | forEach(value, function (value, prop, subject, path) { 18 | var type = options.typeMapper && options.typeMapper(path, value, prop, subject); 19 | 20 | if (type) { 21 | subject[prop] = '<' + type + '>'; 22 | } else if (!Array.isArray(value) && !isPlainObject(value)) { 23 | subject[prop] = '<' + typeOf(value) + '>'; 24 | } 25 | }); 26 | 27 | return value; 28 | } 29 | 30 | function diff(oldObj, newObj, options) { 31 | oldObj = mapToType(oldObj, options); 32 | newObj = mapToType(newObj, options); 33 | 34 | return jsdiff.diffJson(oldObj, newObj, options); 35 | } 36 | 37 | module.exports = diff; 38 | module.exports.mapToType = mapToType; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff-json-structure", 3 | "version": "1.0.8", 4 | "description": "Get the structural diff of two JSON objects", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint '{*.js,test/**/*.js}'", 8 | "test": "mocha --bail" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/IndigoUnited/node-diff-json-structure/issues/" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/IndigoUnited/node-diff-json-structure.git" 16 | }, 17 | "keywords": [ 18 | "diff", 19 | "json", 20 | "structure", 21 | "structural" 22 | ], 23 | "author": "IndigoUnited (http://indigounited.com)", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@satazor/eslint-config": "^3.0.0", 27 | "chalk": "^2.0.0", 28 | "deep-filter": "^1.0.0", 29 | "eslint": "^3.0.0", 30 | "expect.js": "^0.3.1", 31 | "mocha": "^3.0.2" 32 | }, 33 | "dependencies": { 34 | "deep-for-each": "^1.0.0", 35 | "diff": "^3.0.0", 36 | "is-plain-object": "^2.0.1", 37 | "lodash.assign": "^4.0.0", 38 | "lodash.clonedeep": "^4.0.1", 39 | "typeof": "^1.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect.js'); 4 | var diff = require('../'); 5 | 6 | describe('diff-json-structure', function () { 7 | describe('map-to-type', function () { 8 | it('should deep clone stuff during the process', function () { 9 | var obj = { 10 | some: 'prop', 11 | array: [1, 2, { other: 'prop' }], 12 | nested: { 13 | foo: 'bar', 14 | }, 15 | regexp: /foo/, 16 | bool: true, 17 | null: null, 18 | undefined: undefined, 19 | }; 20 | 21 | diff.mapToType(obj); 22 | 23 | expect(obj.some).to.be('prop'); 24 | expect(obj.array).to.eql([1, 2, { other: 'prop' }]); 25 | }); 26 | 27 | it('should correctly map values to types', function () { 28 | var obj = { 29 | some: 'prop', 30 | array: [1, 2, { other: 'prop' }], 31 | nested: { 32 | foo: 'bar', 33 | }, 34 | regexp: /foo/, 35 | bool: true, 36 | null: null, 37 | undefined: undefined, 38 | }; 39 | 40 | expect(diff.mapToType(obj)).to.eql({ 41 | some: '', 42 | array: ['', '', { other: '' }], 43 | nested: { 44 | foo: '', 45 | }, 46 | regexp: '', 47 | bool: '', 48 | null: '', 49 | undefined: '', 50 | }); 51 | }); 52 | 53 | it('should respect typeMapper option', function () { 54 | var obj = { 55 | some: 'prop', 56 | array: [1, 2, { other: 'prop' }], 57 | nested: { 58 | foo: 'bar', 59 | }, 60 | regexp: /foo/, 61 | bool: true, 62 | null: null, 63 | undefined: undefined, 64 | }; 65 | 66 | expect(diff.mapToType(obj, { 67 | typeMapper: function (path) { 68 | switch (path) { 69 | case 'array[2]': 70 | return 'object'; 71 | case 'nested.foo': 72 | return 'any'; 73 | default: 74 | } 75 | }, 76 | })).to.eql({ 77 | some: '', 78 | array: ['', '', ''], 79 | nested: { 80 | foo: '', 81 | }, 82 | regexp: '', 83 | bool: '', 84 | null: '', 85 | undefined: '', 86 | }); 87 | }); 88 | }); 89 | 90 | it('should return the correct parts array', function () { 91 | var oldObj = { 92 | environment: 'dev', 93 | googleAppId: 'UA-3234432-22', 94 | socialProviders: ['facebook'], 95 | libraries: { 96 | jquery: './node_modules/jquery', 97 | }, 98 | }; 99 | 100 | var newObj = { 101 | environment: 'dev', 102 | dbHost: '127.0.0.1:9000', 103 | socialProviders: ['facebook', 'twitter'], 104 | libraries: { 105 | jquery: './node_modules/jquery/jquery', 106 | moment: './node_modules/moment/moment', 107 | }, 108 | }; 109 | 110 | var parts = diff(oldObj, newObj); 111 | 112 | // Cleanup undefined values from the objects before we compare 113 | parts = JSON.parse(JSON.stringify(parts)); 114 | 115 | expect(parts).to.eql([ 116 | { 117 | count: 1, 118 | value: '{\n', 119 | }, 120 | { 121 | count: 1, 122 | added: true, 123 | value: ' "dbHost": "",\n', 124 | }, 125 | { 126 | count: 1, 127 | value: ' "environment": "",\n', 128 | }, 129 | { 130 | count: 1, 131 | removed: true, 132 | value: ' "googleAppId": "",\n', 133 | }, 134 | { 135 | count: 2, 136 | value: ' "libraries": {\n "jquery": "",\n', 137 | }, 138 | { 139 | count: 1, 140 | added: true, 141 | value: ' "moment": ""\n', 142 | }, 143 | { 144 | count: 3, 145 | value: ' },\n "socialProviders": [\n "",\n', 146 | }, 147 | { 148 | count: 1, 149 | added: true, 150 | value: ' ""\n', 151 | }, 152 | { 153 | count: 2, 154 | value: ' ]\n}', 155 | }, 156 | ]); 157 | }); 158 | 159 | it('should work with a custom typeMapper', function () { 160 | var oldObj = { 161 | environment: 'dev', 162 | googleAppId: 'UA-3234432-22', 163 | socialProviders: ['facebook'], 164 | libraries: { 165 | jquery: './node_modules/jquery', 166 | }, 167 | }; 168 | 169 | var newObj = { 170 | environment: 'dev', 171 | dbHost: '127.0.0.1:9000', 172 | socialProviders: ['facebook', 'twitter'], 173 | libraries: { 174 | jquery: './node_modules/jquery/jquery', 175 | moment: './node_modules/moment/moment', 176 | }, 177 | }; 178 | 179 | var parts = diff(oldObj, newObj, { 180 | typeMapper: function (path) { 181 | if (path === 'socialProviders') { 182 | return 'array'; 183 | } 184 | }, 185 | }); 186 | 187 | // Cleanup undefined values from the objects before we compare 188 | parts = JSON.parse(JSON.stringify(parts)); 189 | 190 | expect(parts).to.eql([ 191 | { 192 | count: 1, 193 | value: '{\n', 194 | }, 195 | { 196 | count: 1, 197 | added: true, 198 | value: ' "dbHost": "",\n', 199 | }, 200 | { 201 | count: 1, 202 | value: ' "environment": "",\n', 203 | }, 204 | { 205 | count: 1, 206 | removed: true, 207 | value: ' "googleAppId": "",\n', 208 | }, 209 | { 210 | count: 2, 211 | value: ' "libraries": {\n "jquery": "",\n', 212 | }, 213 | { 214 | count: 1, 215 | added: true, 216 | value: ' "moment": ""\n', 217 | }, 218 | { 219 | count: 3, 220 | value: ' },\n \"socialProviders\": \"\"\n}', 221 | }, 222 | ]); 223 | }); 224 | }); 225 | --------------------------------------------------------------------------------