diff --git a/packages/map2tree/.babelrc b/packages/map2tree/.babelrc new file mode 100755 index 00000000..9d06ebbb --- /dev/null +++ b/packages/map2tree/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015-loose", "stage-0"] +} diff --git a/packages/map2tree/.eslintignore b/packages/map2tree/.eslintignore new file mode 100755 index 00000000..0d38857e --- /dev/null +++ b/packages/map2tree/.eslintignore @@ -0,0 +1,4 @@ +lib +**/node_modules +**/webpack.config.js +examples/**/server.js \ No newline at end of file diff --git a/packages/map2tree/.eslintrc b/packages/map2tree/.eslintrc new file mode 100755 index 00000000..90cecc39 --- /dev/null +++ b/packages/map2tree/.eslintrc @@ -0,0 +1,10 @@ +{ + "extends": "eslint:recommended", + "env": { + "browser": true, + "node": true, + "es6": true + }, + + "parser": "babel-eslint" +} diff --git a/packages/map2tree/.npmignore b/packages/map2tree/.npmignore new file mode 100755 index 00000000..2929ccfd --- /dev/null +++ b/packages/map2tree/.npmignore @@ -0,0 +1,4 @@ +.DS_Store +*.log +test +.idea diff --git a/packages/map2tree/LICENSE.md b/packages/map2tree/LICENSE.md new file mode 100755 index 00000000..bff65e7d --- /dev/null +++ b/packages/map2tree/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Romain Séguy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/map2tree/README.md b/packages/map2tree/README.md new file mode 100755 index 00000000..e36c21b9 --- /dev/null +++ b/packages/map2tree/README.md @@ -0,0 +1,88 @@ +A pure function to convert a map into a tree structure. + +The following opinions must be taken into account since the primary use case of this library is [redux-devtools-chart-monitor](https://github.com/romseguy/redux-devtools-chart-monitor): + +- Objects and arrays deeply nested within collections are not converted into a tree structure. See `someNestedObject` and `someNestedArray` in the [output](https://github.com/romseguy/map2tree#output) below, or the [corresponding test](https://github.com/romseguy/map2tree/blob/master/test/map2tree.js#L140). +- Provides support for [Immutable.js](https://github.com/facebook/immutable-js) data structures (only List and Map though). + + +# Usage + + +```javascript +map2tree(someMap, options = { + key: 'state', // the name you want for as the root node of the output tree + pushMethod: 'push' // use 'unshift' to change the order children nodes are added +}) +``` + +# Input + +```javascript +const someMap = { + someReducer: { + todos: [ + {title: 'map', someNestedObject: {foo: 'bar'}}, + {title: 'to', someNestedArray: ['foo', 'bar']}, + {title: 'tree'}, + {title: 'map2tree'} + ], + completedCount: 1 + }, + otherReducer: { + foo: 0, + bar:{key: 'value'} + } +}; +``` + +# Output + +```javascript +{ + name: `${options.key}`, + children: [ + { + name: 'someReducer', + children: [ + { + name: 'todos', + children: [ + { + name: 'todo[0]', + object: { + title: 'map', + someNestedObject: {foo: 'bar'} + } + }, + { + name: 'todo[1]', + object: { + title: 'to', + someNestedArray: ['foo', 'bar'] + } + }, + // ... + ] + }, + // ... + ] + }, + { + name: 'otherReducer', + children: [ + { + name: 'foo', + value: 0 + }, + { + name: 'bar', + object: { + key: 'value' + } + } + ] + } + ] +} +``` diff --git a/packages/map2tree/package.json b/packages/map2tree/package.json new file mode 100755 index 00000000..28edb23e --- /dev/null +++ b/packages/map2tree/package.json @@ -0,0 +1,60 @@ +{ + "name": "map2tree", + "version": "1.4.0", + "description": "Utility for mapping maps to trees", + "main": "lib/index.js", + "scripts": { + "clean": "rimraf lib dist", + "build": "babel src --out-dir lib", + "build:umd": "webpack src/index.js dist/map2tree.js && NODE_ENV=production webpack src/index.js dist/map2tree.min.js", + "lint": "eslint src test", + "test": "babel-node test/map2tree.js | tap-diff", + "check": "npm run lint && npm run test", + "prepublish": "npm run check && npm run clean && npm run build && npm run build:umd" + }, + "repository": { + "type": "git", + "url": "https://github.com/romseguy/map2tree.git" + }, + "keywords": [ + "map2tree", + "map-to-tree", + "mapToTree", + "map", + "tree" + ], + "author": "romseguy", + "license": "MIT", + "bugs": { + "url": "https://github.com/romseguy/map2tree/issues" + }, + "homepage": "https://github.com/romseguy/map2tree", + "devDependencies": { + "babel-cli": "^6.3.15", + "babel-core": "^6.1.20", + "babel-eslint": "4.1.8", + "babel-loader": "^6.2.0", + "babel-preset-es2015-loose": "^6.1.3", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "eslint": "1.10.3", + "immutable": "3.7.6", + "rimraf": "^2.3.4", + "tap-diff": "0.1.1", + "tap-spec": "4.1.1", + "tape": "4.4.0", + "webpack": "1.12.13" + }, + "dependencies": { + "lodash": "^4.2.1" + }, + "npmName": "map2tree", + "npmFileMap": [ + { + "basePath": "/dist/", + "files": [ + "*.js" + ] + } + ] +} diff --git a/packages/map2tree/src/index.js b/packages/map2tree/src/index.js new file mode 100755 index 00000000..83b8b1a6 --- /dev/null +++ b/packages/map2tree/src/index.js @@ -0,0 +1,68 @@ +import isArray from 'lodash/isArray'; +import isPlainObject from 'lodash/isPlainObject'; +import mapValues from 'lodash/mapValues'; + +function visit(parent, visitFn, childrenFn) { + if (!parent) return; + + visitFn(parent); + + const children = childrenFn(parent); + if (children) { + const count = children.length; + for (let i = 0; i < count; i++) { + visit(children[i], visitFn, childrenFn); + } + } +} + +function getNode(tree, key) { + let node = null; + + visit(tree, d => { + if (d.name === key) { + node = d; + } + }, d => d.children); + + return node; +} + +export default function map2tree(root, options = {}, tree = {name: options.key || 'state', children: []}) { + if (!isPlainObject(root) && root && !root.toJS) { + return {}; + } + + const { key: rootNodeKey = 'state', pushMethod = 'push' } = options; + const currentNode = getNode(tree, rootNodeKey); + + if (currentNode === null) { + return {}; + } + + mapValues(root && root.toJS ? root.toJS() : root, (maybeImmutable, key) => { + const value = maybeImmutable && maybeImmutable.toJS ? maybeImmutable.toJS() : maybeImmutable; + let newNode = {name: key}; + + if (isArray(value)) { + newNode.children = []; + + for (let i = 0; i < value.length; i++) { + newNode.children[pushMethod]({ + name: `${key}[${i}]`, + [isPlainObject(value[i]) ? 'object' : 'value']: value[i] + }); + } + } else if (isPlainObject(value)) { + newNode.children = []; + } else { + newNode.value = value; + } + + currentNode.children[pushMethod](newNode); + + map2tree(value, {key, pushMethod}, tree); + }); + + return tree; +} diff --git a/packages/map2tree/test/map2tree.js b/packages/map2tree/test/map2tree.js new file mode 100755 index 00000000..aecd86ab --- /dev/null +++ b/packages/map2tree/test/map2tree.js @@ -0,0 +1,253 @@ +import test from 'tape'; +import map2tree from '../src'; +import immutable from 'immutable'; + +test('# rootNodeKey', assert => { + const map = {}; + const options = {key: 'foo'}; + + assert.equal(map2tree(map, options).name, 'foo'); + assert.end(); +}); + +test('# shallow map', nest => { + nest.test('## null', assert => { + const map = { + a: null + }; + + const expected = { + name: 'state', + children: [ + {name: 'a', value: null} + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); + + nest.test('## value', assert => { + const map = { + a: 'foo', + b: 'bar' + }; + + const expected = { + name: 'state', + children: [ + {name: 'a', value: 'foo'}, + {name: 'b', value: 'bar'} + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); + + nest.test('## object', assert => { + const map = { + a: {aa: 'foo'} + }; + + const expected = { + name: 'state', + children: [ + {name: 'a', children: [{name: 'aa', value: 'foo'}]} + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); + + nest.test('## immutable Map', assert => { + const map = { + a: immutable.fromJS({aa: 'foo', ab: 'bar'}) + }; + + const expected = { + name: 'state', + children: [ + {name: 'a', children: [{name: 'aa', value: 'foo'}, {name: 'ab', value: 'bar'}]} + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.end(); + }) +}); + +test('# deep map', nest => { + nest.test('## null', assert => { + const map = { + a: {aa: null} + }; + + const expected = { + name: 'state', + children: [ + { + name: 'a', + children: [ + { + name: 'aa', + value: null + } + ] + } + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); + + nest.test('## object', assert => { + const map = { + a: {aa: {aaa: 'foo'}} + }; + + const expected = { + name: 'state', + children: [ + { + name: 'a', + children: [ + { + name: 'aa', + children: [ + {name: 'aaa', value: 'foo'} + ] + } + ] + } + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); +}); + +test('# array map', nest => { + const map = { + a: [ + 1, + 2 + ] + }; + + nest.test('## push', assert => { + const expected = { + name: 'state', + children: [{ + name: 'a', + children: [ + {name: 'a[0]', value: 1}, + {name: 'a[1]', value: 2}] + }] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); + + nest.test('## unshift', assert => { + const options = {pushMethod: 'unshift'}; + const expected = { + name: 'state', + children: [{ + name: 'a', + children: [ + {name: 'a[1]', value: 2}, + {name: 'a[0]', value: 1} + ] + }] + }; + + assert.deepEqual(map2tree(map, options), expected); + assert.deepEqual(map2tree(immutable.fromJS(map), options), expected, 'immutable'); + assert.end(); + }); + + nest.test('## null', assert => { + const map = { + a: [ + null + ] + }; + + const expected = { + name: 'state', + children: [{ + name: 'a', + children: [ + {name: 'a[0]', value: null} + ] + }] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }) +}); + +test('# collection map', nest => { + nest.test('## value', assert => { + const map = { + a: [ + {aa: 1}, + {aa: 2} + ] + }; + + const expected = { + name: 'state', + children: [ + { + name: 'a', + children: [ + {name: 'a[0]', object: {aa: 1}}, + {name: 'a[1]', object: {aa: 2}} + ] + } + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }); + + nest.test('## object', assert => { + const map = { + a: [ + {aa: {aaa: 'foo'}} + ] + }; + + const expected = { + name: 'state', + children: [ + { + name: 'a', + children: [ + {name: 'a[0]', object: {aa: {aaa: 'foo'}}} + ] + } + ] + }; + + assert.deepEqual(map2tree(map), expected); + assert.deepEqual(map2tree(immutable.fromJS(map)), expected, 'immutable'); + assert.end(); + }) +}); diff --git a/packages/map2tree/webpack.config.js b/packages/map2tree/webpack.config.js new file mode 100755 index 00000000..dda00392 --- /dev/null +++ b/packages/map2tree/webpack.config.js @@ -0,0 +1,39 @@ +'use strict'; + +var webpack = require('webpack'); + +var plugins = [ + new webpack.optimize.OccurenceOrderPlugin(), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) + }) +]; + +if (process.env.NODE_ENV === 'production') { + plugins.push( + new webpack.optimize.UglifyJsPlugin({ + compressor: { + screw_ie8: true, + warnings: false + } + }) + ); +} + +module.exports = { + module: { + loaders: [{ + test: /\.js$/, + loaders: ['babel-loader'], + exclude: /node_modules/ + }] + }, + output: { + library: 'map2tree', + libraryTarget: 'umd' + }, + plugins: plugins, + resolve: { + extensions: ['', '.js'] + } +};