├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── compositor.json ├── docs ├── README.md ├── addChildBy.md ├── changeParent.md ├── each.md ├── filter.md ├── findOr.md ├── flatten.md ├── flattenToIds.md ├── flattenToMap.md ├── hardMap.md ├── hardMapBy.md ├── hasId.md ├── reduce.md ├── reduceAncestryBy.md ├── reject.md ├── replaceChildrenBy.md ├── softMap.md ├── softMapBy.md └── superflatten.md ├── package-lock.json ├── package.json └── src ├── __tests__ ├── addChildBy.test.js ├── changeParent.test.js ├── each.test.js ├── filter.test.js ├── findOr.test.js ├── flatten.test.js ├── flattenToIds.test.js ├── flattenToMap.test.js ├── hardMap.test.js ├── hardMapBy.test.js ├── hasId.test.js ├── reduce.test.js ├── reduceAncestryBy.test.js ├── reject.test.js ├── replaceChildrenBy.test.js ├── softMap.test.js ├── softMapBy.test.js └── superflatten.test.js ├── addChildBy.js ├── changeParent.js ├── each.js ├── filter.js ├── findOr.js ├── flatten.js ├── flattenToIds.js ├── flattenToMap.js ├── hardMap.js ├── hardMapBy.js ├── hasId.js ├── index.js ├── reduce.js ├── reduceAncestryBy.js ├── reject.js ├── replaceChildrenBy.js ├── softMap.js ├── softMapBy.js └── superflatten.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "es2017", "stage-3"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | extends: [ 6 | 'airbnb-base', 7 | 'plugin:import/errors', 8 | 'plugin:import/warnings', 9 | 'plugin:lodash-fp/recommended', 10 | ], 11 | parser: "babel-eslint", 12 | parserOptions: { 13 | ecmaFeatures: { 14 | experimentalObjectRestSpread: true, 15 | }, 16 | }, 17 | plugins: [ 18 | 'lodash-fp', 19 | ], 20 | rules: { 21 | 'import/no-extraneous-dependencies': [ 22 | 'error', 23 | { 24 | devDependencies: [ 25 | '**/*.stories.js', 26 | '**/*.test.js', 27 | '**/webpack-*.config.js', 28 | 'wallaby.js', 29 | ], 30 | }, 31 | ], 32 | 'import/prefer-default-export': 0, 33 | 'linebreak-style': 0, 34 | 'new-cap': 0, 35 | 'no-use-before-define': [ 36 | 'error', 37 | { 38 | classes: true, 39 | functions: false, 40 | }, 41 | ], 42 | 'prefer-const': 2, 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | dist 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc.js 3 | .gitignore 4 | docs 5 | src 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - '8' 9 | - '7' 10 | - '6' 11 | before_script: 12 | - npm prune 13 | script: 14 | - npm run lint 15 | - npm run testonce 16 | - npm run build 17 | after_success: 18 | - npm run semantic-release 19 | - './node_modules/.bin/nyc report --reporter=text-lcov > coverage.lcov && ./node_modules/.bin/codecov' 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nick Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is a treecko? 7 | 8 | # treecko 9 | 10 | [![npm](https://img.shields.io/npm/v/treecko.svg?style=flat-square)](https://www.npmjs.com/package/treecko) 11 | [![Travis](https://img.shields.io/travis/nickjohnson-dev/treecko.svg?style=flat-square)](https://travis-ci.org/nickjohnson-dev/treecko) 12 | [![Codecov](https://img.shields.io/codecov/c/github/nickjohnson-dev/treecko.svg?style=flat-square)](https://codecov.io/gh/nickjohnson-dev/treecko) 13 | 14 | A collection of **functional** and **immutable** helpers for working with **tree** data structures. 15 | 16 | - Both **Trees : Object** and **Tree Lists : Array\** are supported. 17 | - **Curried** for easy partial application. 18 | 19 | ### Mapping 20 | 21 | - [softMap](docs/softMap.md) 22 | - [softMapBy](docs/softMapBy.md) 23 | - [hardMap](docs/hardMap.md) 24 | - [hardMapBy](docs/hardMapBy.md) 25 | 26 | 27 | ### Reducing 28 | 29 | - [reduce](docs/reduce.md) 30 | - [reduceAncestryBy](docs/reduceAncestryBy.md) 31 | 32 | 33 | ### Finding 34 | 35 | - [findOr](docs/findOr.md) 36 | 37 | 38 | ### Filtering 39 | 40 | - [filter](docs/filter.md) 41 | - [reject](docs/reject.md) 42 | 43 | 44 | ### Side Effects 45 | 46 | - [each](docs/each.md) 47 | 48 | 49 | ### Flattening 50 | 51 | - [flatten](docs/flatten.md) 52 | - [superflatten](docs/superflatten.md) 53 | - [flattenToIds](docs/flattenToIds.md) 54 | - [flattenToMap](docs/flattenToMap.md) 55 | 56 | 57 | ### Restructuring 58 | 59 | - [replaceChildrenBy](docs/replaceChildrenBy.md) 60 | - [addChildBy](docs/addChildBy.md) 61 | - [changeParent](docs/changeParent.md) 62 | 63 | 64 | ### Misc 65 | 66 | - [hasId](docs/hasId.md) 67 | 68 | 69 | # TODO 70 | 71 | - [ ] Support different values for `children` key 72 | - [ ] Support different values for `id` key in `flattenToMap` and `changeParent` 73 | - [ ] Support different values for `parentId` key in `changeParent` 74 | - [ ] Add breadth-first versions of methods 75 | -------------------------------------------------------------------------------- /compositor.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nickjohnson-dev/treecko", 3 | "version": "0.1.4", 4 | "libraries": { 5 | "xv": "^1.1.25" 6 | }, 7 | "title": "Treecko", 8 | "branch": "master", 9 | "style": { 10 | "name": "Williamsburg", 11 | "componentSet": { 12 | "nav": "nav/BasicNav", 13 | "header": "header/LightBannerHeader", 14 | "article": "article/ReaderArticle", 15 | "footer": "footer/BasicFooter" 16 | }, 17 | "fontFamily": "Montserrat, sans-serif", 18 | "heading": { 19 | "fontWeight": 600, 20 | "letterSpacing": "0.1em" 21 | }, 22 | "colors": { 23 | "text": "#346", 24 | "background": "#fff", 25 | "primary": "#0099e0", 26 | "secondary": "#ab61ff", 27 | "highlight": "#f7b", 28 | "muted": "#fffab3", 29 | "border": "#ccd" 30 | } 31 | }, 32 | "content": [ 33 | { 34 | "component": "nav", 35 | "links": [ 36 | { 37 | "href": "https://github.com/nickjohnson-dev/treecko", 38 | "text": "GitHub" 39 | }, 40 | { 41 | "href": "https://npmjs.com/package/treecko", 42 | "text": "npm" 43 | } 44 | ] 45 | }, 46 | { 47 | "component": "header", 48 | "heading": "treecko", 49 | "subhead": "A collection of functional and immutable helpers for working with tree data structures.", 50 | "children": [ 51 | { 52 | "component": "ui/TweetButton", 53 | "text": "treecko: A collection of functional and immutable helpers for working with tree data structures.", 54 | "url": "" 55 | }, 56 | { 57 | "component": "ui/GithubButton", 58 | "user": "nickjohnson-dev", 59 | "repo": "treecko" 60 | } 61 | ], 62 | "text": "v0.0.0-development" 63 | }, 64 | { 65 | "component": "article", 66 | "metadata": { 67 | "source": "github.readme" 68 | }, 69 | "html": "

\n\n

\n\n

\n

A collection of functional and immutable helpers for working with tree data structures.

\n\n

Mapping

\n\n

Reducing

\n\n

Finding

\n\n

Filtering

\n\n

Side Effects

\n\n

Flattening

\n\n

Restructuring

\n\n

Misc

\n\n

TODO

\n\n" 70 | }, 71 | { 72 | "component": "footer", 73 | "links": [ 74 | { 75 | "href": "https://github.com/nickjohnson-dev/treecko", 76 | "text": "GitHub" 77 | }, 78 | { 79 | "href": "https://github.com/nickjohnson-dev", 80 | "text": "nickjohnson-dev" 81 | } 82 | ] 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - Both **Trees : Object** and **Tree Lists : Array\** are supported. 4 | - **Curried** for partial application. 5 | 6 | ## Mapping 7 | 8 | - [softMap](softMap.md) 9 | - [softMapBy](softMapBy.md) 10 | - [hardMap](hardMap.md) 11 | - [hardMapBy](hardMapBy.md) 12 | 13 | 14 | ## Reducing 15 | 16 | - [reduce](reduce.md) 17 | - [reduceAncestryBy](reduceAncestryBy.md) 18 | 19 | 20 | ## Finding 21 | 22 | - [findOr](findOr.md) 23 | 24 | 25 | ## Filtering 26 | 27 | - [filter](filter.md) 28 | - [reject](reject.md) 29 | 30 | 31 | ## Side Effects 32 | 33 | - [each](each.md) 34 | 35 | 36 | ## Flattening 37 | 38 | - [flatten](flatten.md) 39 | - [superflatten](superflatten.md) 40 | - [flattenToIds](flattenToIds.md) 41 | - [flattenToMap](flattenToMap.md) 42 | 43 | 44 | ## Restructuring 45 | 46 | - [replaceChildrenBy](replaceChildrenBy.md) 47 | - [addChildBy](addChildBy.md) 48 | - [changeParent](changeParent.md) 49 | 50 | 51 | ## Misc 52 | 53 | - [hasId](hasId.md) 54 | -------------------------------------------------------------------------------- /docs/addChildBy.md: -------------------------------------------------------------------------------- 1 | # addChildBy 2 | 3 | 4 | Returns a copy of the input `data` structure with the return value of `getChild` invoked with the node added to the children of the first node that satisfies the `predicate`. 5 | 6 | 7 | ```javascript 8 | import treecko from 'treecko'; 9 | 10 | const data = { 11 | id: '0', 12 | name: 'users', 13 | parentId: '', 14 | children: [ 15 | { 16 | id: '1', 17 | name: 'treecko', 18 | parentId: '0', 19 | children: [], 20 | }, 21 | ], 22 | }; 23 | 24 | const predicate = node => node.id === '1'; 25 | 26 | const getChild = parent => ({ 27 | id: '2', 28 | name: 'documents', 29 | parentId: parent.id, 30 | children: [], 31 | }); 32 | 33 | const result = treecko.addChildBy(predicate, getChild, data); 34 | // { 35 | // id: '0', 36 | // name: 'users', 37 | // parentId: '', 38 | // children: [ 39 | // { 40 | // id: '1', 41 | // name: 'treecko', 42 | // parentId: '0', 43 | // children: [ 44 | // { 45 | // id: '2', 46 | // name: 'documents', 47 | // parentId: '1', 48 | // children: [], 49 | // } 50 | // ], 51 | // }, 52 | // ], 53 | // } 54 | ``` 55 | 56 | The `getChild` and `predicate` functions receive a metadata object as their second arguments, with the following interface. 57 | 58 | ``` 59 | type metadata = { 60 | parent: Object; 61 | }; 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/changeParent.md: -------------------------------------------------------------------------------- 1 | # changeParent 2 | 3 | Returns a copy of the input `data` structure with a node moved from under one parent to under another. Uses the `predicate` to locate and remove the child, and the `newParentPredicate` to designate the new parent node. Updates the `parentId` property on the child with the `id` property of the new parent. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: '0', 10 | name: 'users', 11 | children: [ 12 | { 13 | id: '1', 14 | name: 'treecko', 15 | children: [ 16 | { 17 | id: '2', 18 | name: 'documents', 19 | parentId: '1', 20 | children: [], 21 | }, 22 | ], 23 | }, 24 | { 25 | id: '3', 26 | name: 'shared', 27 | children: [], 28 | }, 29 | ], 30 | }; 31 | 32 | const newParentPredicate = node => node.id === '3'; 33 | 34 | const predicate = node => node.id === '2'; 35 | 36 | const result = treecko.changeParent( 37 | newParentPredicate, 38 | predicate, 39 | data, 40 | ); 41 | // { 42 | // id: '0', 43 | // name: 'users', 44 | // children: [ 45 | // { 46 | // id: '1', 47 | // name: 'treecko', 48 | // children: [], 49 | // }, 50 | // { 51 | // id: '3', 52 | // name: 'shared', 53 | // children: [ 54 | // { 55 | // id: '2', 56 | // name: 'documents', 57 | // parentId: '3', 58 | // children: [], 59 | // }, 60 | // ], 61 | // }, 62 | // ], 63 | // } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/each.md: -------------------------------------------------------------------------------- 1 | # each 2 | 3 | Invokes the `iteratee` with each node in the input `data` structure, moving in a depth-first fashion. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: 'a', 10 | children: [ 11 | { 12 | id: 'b', 13 | children: [ 14 | { 15 | id: 'c', 16 | children: [], 17 | }, 18 | ], 19 | }, { 20 | id: 'd', 21 | children: [], 22 | }, 23 | ], 24 | }; 25 | 26 | const iteratee = node => { 27 | console.log('Node ID: ', node.id); 28 | }; 29 | 30 | treecko.each(iteratee, data); 31 | // Node ID: a 32 | // Node ID: b 33 | // Node ID: c 34 | // Node ID: d 35 | ``` 36 | 37 | The `iteratee` function receives a metadata object as its second argument, with the following interface. 38 | 39 | ``` 40 | type metadata = { 41 | parent: Object; 42 | }; 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/filter.md: -------------------------------------------------------------------------------- 1 | # filter 2 | 3 | Returns a copy of the input `data` structure with only items that satisfy the `predicate`. **This will often drop off large branches of a tree.** 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | age: 25, 10 | children: [ 11 | { 12 | age: 40, 13 | children: [], 14 | }, 15 | { 16 | age: 150, 17 | children: [], 18 | }, 19 | ], 20 | }; 21 | 22 | const predicate = node => node.age < 100; 23 | 24 | const result = treecko.filter(predicate, data); 25 | // { 26 | // age: 25, 27 | // children: [ 28 | // { 29 | // age: 40, 30 | // children: [], 31 | // }, 32 | // ], 33 | // } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/findOr.md: -------------------------------------------------------------------------------- 1 | # findOr 2 | 3 | Returns the first node in the input `data` structure that matches the `predicate`, or the `defaultValue` if no match is found. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: '0', 10 | name: 'users', 11 | children: [ 12 | { 13 | id: '1', 14 | name: 'treecko', 15 | children: [ 16 | { 17 | id: '2', 18 | name: 'documents', 19 | parentId: '1', 20 | children: [], 21 | }, 22 | ], 23 | }, 24 | { 25 | id: '3', 26 | name: 'shared', 27 | children: [], 28 | }, 29 | ], 30 | }; 31 | 32 | const defaultValue = { error: 'Node not found' }; 33 | 34 | const predicate = node => node.name === 'documents'; 35 | 36 | const result = treecko.findOr(defaultValue, predicate, data); 37 | // { 38 | // id: '2', 39 | // name: 'documents', 40 | // parentId: '1', 41 | // children: [], 42 | // } 43 | 44 | const badPredicate = node => node.name === 'picture'; 45 | 46 | const badResult = treecko.findOr(defaultValue, badPredicate, data); 47 | // { error: 'Node not found' } 48 | ``` 49 | 50 | The `predicate` function receives a metadata object as its second argument, with the following interface. 51 | 52 | ``` 53 | type metadata = { 54 | parent: Object; 55 | }; 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/flatten.md: -------------------------------------------------------------------------------- 1 | # flatten 2 | 3 | Returns the nodes of the input `data` structure in an array, ordered depth-first, and retains children properties on nodes. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: 'a', 10 | parentId: '', 11 | children: [ 12 | { 13 | id: 'b', 14 | parentId: 'a', 15 | children: [ 16 | { 17 | id: 'c', 18 | parentId: 'b', 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | { 24 | id: 'd', 25 | parentId: 'a', 26 | children: [], 27 | }, 28 | ], 29 | }; 30 | 31 | const result = treecko.flatten(data); 32 | // [ 33 | // { 34 | // id: 'a', 35 | // parentId: '', 36 | // children: [ 37 | // { 38 | // id: 'b', 39 | // parentId: 'a', 40 | // children: [ 41 | // { 42 | // id: 'c', 43 | // parentId: 'b', 44 | // children: [], 45 | // }, 46 | // ], 47 | // }, 48 | // { 49 | // id: 'd', 50 | // parentId: 'a', 51 | // children: [], 52 | // }, 53 | // ], 54 | // }, 55 | // { 56 | // id: 'b', 57 | // parentId: 'a', 58 | // children: [ 59 | // { 60 | // id: 'c', 61 | // parentId: 'b', 62 | // children: [], 63 | // }, 64 | // ], 65 | // }, 66 | // { 67 | // id: 'c', 68 | // parentId: 'b', 69 | // children: [], 70 | // }, 71 | // { 72 | // id: 'd', 73 | // parentId: 'a', 74 | // children: [], 75 | // }, 76 | // ] 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/flattenToIds.md: -------------------------------------------------------------------------------- 1 | # flattenToIds 2 | 3 | Returns an array containing the `id` properties of each node in the input data structure, ordered depth-first. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: 'a', 10 | parentId: '', 11 | children: [ 12 | { 13 | id: 'b', 14 | parentId: 'a', 15 | children: [ 16 | { 17 | id: 'c', 18 | parentId: 'b', 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | { 24 | id: 'd', 25 | parentId: 'a', 26 | children: [], 27 | }, 28 | ], 29 | }; 30 | 31 | const result = treecko.flattenToIds(data); 32 | // ['a', 'b', 'c', 'd'] 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/flattenToMap.md: -------------------------------------------------------------------------------- 1 | # flattenToMap 2 | 3 | Returns an object map with the nodes of the input `data` structure assigned to keys equal to their `id` property. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: 'a', 10 | parentId: '', 11 | children: [ 12 | { 13 | id: 'b', 14 | parentId: 'a', 15 | children: [ 16 | { 17 | id: 'c', 18 | parentId: 'b', 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | { 24 | id: 'd', 25 | parentId: 'a', 26 | children: [], 27 | }, 28 | ], 29 | }; 30 | 31 | const result = treecko.flattenToMap(data); 32 | // { 33 | // 'a': { 34 | // id: 'a', 35 | // parentId: '', 36 | // children: [ 37 | // { 38 | // id: 'b', 39 | // parentId: 'a', 40 | // children: [ 41 | // { 42 | // id: 'c', 43 | // parentId: 'b', 44 | // children: [], 45 | // }, 46 | // ], 47 | // }, 48 | // { 49 | // id: 'd', 50 | // parentId: 'a', 51 | // children: [], 52 | // }, 53 | // ], 54 | // }, 55 | // 'b': { 56 | // id: 'b', 57 | // parentId: 'a', 58 | // children: [ 59 | // { 60 | // id: 'c', 61 | // parentId: 'b', 62 | // children: [], 63 | // }, 64 | // ], 65 | // }, 66 | // 'c': { 67 | // id: 'c', 68 | // parentId: 'b', 69 | // children: [], 70 | // }, 71 | // 'd': { 72 | // id: 'd', 73 | // parentId: 'a', 74 | // children: [], 75 | // }, 76 | // } 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/hardMap.md: -------------------------------------------------------------------------------- 1 | # hardMap 2 | 3 | Returns a copy of the input `data` structure with the `iteratee` applied to each node. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | value: 5, 10 | children: [ 11 | { 12 | value: 10, 13 | children: [], 14 | }, 15 | ], 16 | }; 17 | 18 | const iteratee = node => ({ value: node.value * 2 }); 19 | 20 | const result = treecko.hardMap(iteratee, data); 21 | // { 22 | // value: 10, 23 | // } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/hardMapBy.md: -------------------------------------------------------------------------------- 1 | # hardMapBy 2 | 3 | Returns a copy of the input `data` structure with the `iteratee` applied to any node that satisfies the predicate. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | value: 5, 10 | children: [ 11 | { 12 | value: 10, 13 | children: [], 14 | }, 15 | ], 16 | }; 17 | 18 | const predicate = node => node.value >= 10; 19 | 20 | const iteratee = node => ({ value: node.value * 2 }); 21 | 22 | const result = treecko.hardMapBy(predicate, iteratee, data); 23 | // { 24 | // value: 5, 25 | // children: [ 26 | // { 27 | // value: 20, 28 | // }, 29 | // ], 30 | // } 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/hasId.md: -------------------------------------------------------------------------------- 1 | # hasId 2 | 3 | Returns a boolean indicating whether or not the input object has an `id` property equal to `targetId`. Provided to clean up some common use cases with other helpers. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const resultA = treecko.hasId('a', { id: 'a' }); 9 | // true 10 | 11 | const resultB = treecko.hasId('a', { id: 'b' }); 12 | // false 13 | 14 | const resultC = treecko.hasId('a', { name: 'treecko' }); 15 | // false 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/reduce.md: -------------------------------------------------------------------------------- 1 | # reduce 2 | 3 | Returns the result of a depth-first reduce operation on the input `data` structure. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | value: 'a', 10 | children: [ 11 | { 12 | value: 'b', 13 | children: [ 14 | { 15 | value: 'c', 16 | children: [], 17 | }, 18 | ], 19 | }, 20 | { 21 | value: 'd', 22 | children: [], 23 | }, 24 | ], 25 | }; 26 | 27 | const reducer = (acc, cur) => `${acc}${cur.value}`; 28 | 29 | const startingValue = ''; 30 | 31 | const result = treecko.reduce(reducer, startingValue, data); 32 | // abcd 33 | ``` 34 | 35 | The `reducer` function receives a metadata object as its second argument, with the following interface. 36 | 37 | ``` 38 | type metadata = { 39 | parent: Object; 40 | }; 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/reduceAncestryBy.md: -------------------------------------------------------------------------------- 1 | # reduceAncestryBy 2 | 3 | Returns the result of a reduce operation starting with the first node that satisfies the `predicate` and moving up its ancestry to end with the root of the input `data` structure. Must have matching `id` and `parentId` values to work. 4 | 5 | A good use for this method is building the full path for an item in a file structure. 6 | 7 | ```javascript 8 | import treecko from 'treecko'; 9 | 10 | const data = { 11 | id: '0', 12 | parentId: '', 13 | name: 'users', 14 | children: [ 15 | { 16 | id: '1', 17 | parentId: '0', 18 | name: 'treecko', 19 | children: [ 20 | { 21 | id: '2', 22 | parentId: '1', 23 | name: 'documents', 24 | children: [], 25 | }, 26 | ], 27 | }, 28 | ], 29 | }; 30 | 31 | const predicate = node => node.id === '2'; 32 | 33 | const reducer = (acc, cur) => `/${cur.name}${acc}`; 34 | 35 | const startingValue = ''; 36 | 37 | const result = reduceAncestryBy( 38 | predicate, 39 | reducer, 40 | startingValue, 41 | data, 42 | ); 43 | // /users/treecko/documents 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/reject.md: -------------------------------------------------------------------------------- 1 | # reject 2 | 3 | Returns a copy of the input `data` structure without items that satisfy the `predicate`. **This will often drop off large branches of a tree.** 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | age: 25, 10 | children: [ 11 | { 12 | age: 40, 13 | children: [], 14 | }, 15 | { 16 | age: 150, 17 | children: [], 18 | }, 19 | ], 20 | }; 21 | 22 | const predicate = node => node.age > 100; 23 | 24 | const result = treecko.reject(predicate, data); 25 | 26 | /* 27 | { 28 | age: 25, 29 | children: [ 30 | { 31 | age: 40, 32 | children: [], 33 | }, 34 | ], 35 | } 36 | */ 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/replaceChildrenBy.md: -------------------------------------------------------------------------------- 1 | # replaceChildrenBy 2 | 3 | Returns a copy of the input `data` structure with the children of the first node that satisfies the predicate replaced by the return value of `getNewChildren` invoked with the node. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: '0', 10 | name: 'users', 11 | parentId: '', 12 | children: [ 13 | { 14 | id: '1', 15 | name: 'treecko', 16 | parentId: '0', 17 | children: [ 18 | { 19 | id: '2', 20 | name: 'documents', 21 | parentId: '1', 22 | children: [], 23 | }, 24 | ], 25 | }, 26 | ], 27 | }; 28 | 29 | const getChildren = parent => [ 30 | { 31 | id: '3', 32 | name: 'music', 33 | parentId: parent.id, 34 | children: [], 35 | }, 36 | { 37 | id: '4', 38 | name: 'pictures', 39 | parentId: parent.id, 40 | children: [], 41 | }, 42 | ]; 43 | 44 | const predicate = node => node.id === '1'; 45 | 46 | const result = treecko.replaceChildrenBy( 47 | predicate, 48 | getChildren, 49 | data, 50 | ); 51 | // { 52 | // id: '0', 53 | // name: 'users', 54 | // parentId: '', 55 | // children: [ 56 | // { 57 | // id: '1', 58 | // name: 'treecko', 59 | // parentId: '0', 60 | // children: [ 61 | // { 62 | // id: '3', 63 | // name: 'music', 64 | // parentId: '1', 65 | // children: [], 66 | // }, 67 | // { 68 | // id: '4', 69 | // name: 'pictures', 70 | // parentId: '1', 71 | // children: [], 72 | // }, 73 | // ], 74 | // }, 75 | // ], 76 | // } 77 | ``` 78 | 79 | The `getNewChildren` and `predicate` functions receive a metadata object as their second arguments, with the following interface. 80 | 81 | ``` 82 | type metadata = { 83 | parent: Object; 84 | }; 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/softMap.md: -------------------------------------------------------------------------------- 1 | # softMap 2 | 3 | Returns a copy of the input `data` structure with the `iteratee` applied to each node. The `iteratee` is prevented from affecting the `children` property so that overall structure is preserved. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | value: 5, 10 | children: [ 11 | { 12 | value: 10, 13 | children: [], 14 | }, 15 | ], 16 | }; 17 | 18 | const iteratee = node => ({ value: node.value * 2 }); 19 | 20 | const result = treecko.softMap(iteratee, data); 21 | // { 22 | // value: 10, 23 | // children: [ 24 | // { 25 | // value: 20, 26 | // children: [], 27 | // }, 28 | // ], 29 | // } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/softMapBy.md: -------------------------------------------------------------------------------- 1 | # softMapBy 2 | 3 | Returns a copy of the input `data` structure with the `iteratee` applied to any node that satisfies the predicate. The `iteratee` is prevented from affecting the `children` property so that overall structure is preserved. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | value: 5, 10 | children: [ 11 | { 12 | value: 10, 13 | children: [], 14 | }, 15 | ], 16 | }; 17 | 18 | const predicate = node => node.value >= 10; 19 | 20 | const iteratee = node => ({ value: node.value * 2 }); 21 | 22 | const result = treecko.softMapBy(predicate, iteratee, data); 23 | // { 24 | // value: 5, 25 | // children: [ 26 | // { 27 | // value: 20, 28 | // children: [], 29 | // }, 30 | // ], 31 | // } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/superflatten.md: -------------------------------------------------------------------------------- 1 | # superflatten 2 | 3 | Returns the nodes of the input `data` structure in an array, ordered depth-first, and drops children properties to reduce memory footprint. 4 | 5 | ```javascript 6 | import treecko from 'treecko'; 7 | 8 | const data = { 9 | id: 'a', 10 | parentId: '', 11 | children: [ 12 | { 13 | id: 'b', 14 | parentId: 'a', 15 | children: [ 16 | { 17 | id: 'c', 18 | parentId: 'b', 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | { 24 | id: 'd', 25 | parentId: 'a', 26 | children: [], 27 | }, 28 | ], 29 | }; 30 | 31 | const result = treecko.superflatten(data); 32 | // [ 33 | // { 34 | // id: 'a', 35 | // parentId: '', 36 | // }, 37 | // { 38 | // id: 'b', 39 | // parentId: 'a', 40 | // }, 41 | // { 42 | // id: 'c', 43 | // parentId: 'b', 44 | // }, 45 | // { 46 | // id: 'd', 47 | // parentId: 'a', 48 | // }, 49 | // ] 50 | ``` 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treecko", 3 | "version": "0.0.0-development", 4 | "description": "Functional tree helpers", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir dist --ignore test.js", 8 | "clean": "rimraf dist", 9 | "cm": "git-cz", 10 | "cmretry": "git-cz --retry", 11 | "lint": "eslint src", 12 | "prebuild": "npm run clean -s", 13 | "precommit": "npm run lint && npm run testonce", 14 | "test": "ava --watch", 15 | "testonce": "nyc ava", 16 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/nickjohnson-dev/treecko.git" 21 | }, 22 | "author": { 23 | "name": "Nick Johnson", 24 | "email": "nickjohnson.dev@gmail.com" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/nickjohnson-dev/treecko/issues" 29 | }, 30 | "homepage": "https://github.com/nickjohnson-dev/treecko#readme", 31 | "devDependencies": { 32 | "ava": "0.18.1", 33 | "babel-cli": "6.22.2", 34 | "babel-core": "6.22.0", 35 | "babel-eslint": "7.1.1", 36 | "babel-preset-es2015": "6.22.0", 37 | "babel-preset-es2017": "6.22.0", 38 | "babel-preset-stage-3": "6.22.0", 39 | "codecov": "2.3.0", 40 | "commitizen": "2.9.6", 41 | "cz-conventional-changelog": "2.0.0", 42 | "eslint": "3.15.0", 43 | "eslint-config-airbnb-base": "11.1.0", 44 | "eslint-plugin-import": "2.2.0", 45 | "eslint-plugin-lodash-fp": "2.1.3", 46 | "husky": "0.14.3", 47 | "nyc": "11.2.1", 48 | "rimraf": "^2.6.1", 49 | "semantic-release": "^6.3.6", 50 | "sinon": "^2.2.0" 51 | }, 52 | "ava": { 53 | "require": [ 54 | "babel-register" 55 | ], 56 | "babel": "inherit" 57 | }, 58 | "dependencies": { 59 | "lodash": "^4.17.5" 60 | }, 61 | "config": { 62 | "commitizen": { 63 | "path": "./node_modules/cz-conventional-changelog" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/__tests__/addChildBy.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import addChildBy from '../addChildBy'; 4 | 5 | test('should return tree with first item that satisfies predicate having its children replaced by return value of getChild', (t) => { 6 | const child = { id: '100' }; 7 | const data = { 8 | id: '0', 9 | name: 'users', 10 | children: [ 11 | { 12 | id: '1', 13 | name: 'treecko', 14 | children: [], 15 | }, 16 | ], 17 | }; 18 | const expected = { 19 | id: '0', 20 | name: 'users', 21 | children: [ 22 | { 23 | id: '1', 24 | name: 'treecko', 25 | children: [ 26 | child, 27 | ], 28 | }, 29 | ], 30 | }; 31 | const result = addChildBy( 32 | x => x.id === '1', 33 | () => child, 34 | data, 35 | ); 36 | t.deepEqual(result, expected); 37 | }); 38 | 39 | test('should work with an array', (t) => { 40 | const child = { id: '100' }; 41 | const data = [ 42 | { 43 | id: '0', 44 | name: 'users', 45 | children: [ 46 | { 47 | id: '1', 48 | name: 'treecko', 49 | children: [], 50 | }, 51 | ], 52 | }, { 53 | id: '2', 54 | name: 'Applications', 55 | children: [ 56 | { 57 | id: '3', 58 | name: 'utilities', 59 | children: [], 60 | }, 61 | ], 62 | }, 63 | ]; 64 | const expected = [ 65 | { 66 | id: '0', 67 | name: 'users', 68 | children: [ 69 | { 70 | id: '1', 71 | name: 'treecko', 72 | children: [ 73 | child, 74 | ], 75 | }, 76 | ], 77 | }, { 78 | id: '2', 79 | name: 'Applications', 80 | children: [ 81 | { 82 | id: '3', 83 | name: 'utilities', 84 | children: [], 85 | }, 86 | ], 87 | }, 88 | ]; 89 | const result = addChildBy( 90 | x => x.id === '1', 91 | () => child, 92 | data, 93 | ); 94 | t.deepEqual(result, expected); 95 | }); 96 | 97 | test('should work with currying', (t) => { 98 | const child = { id: '100' }; 99 | const data = { 100 | id: '0', 101 | name: 'users', 102 | children: [ 103 | { 104 | id: '1', 105 | name: 'treecko', 106 | children: [], 107 | }, 108 | ], 109 | }; 110 | const expected = { 111 | id: '0', 112 | name: 'users', 113 | children: [ 114 | { 115 | id: '1', 116 | name: 'treecko', 117 | children: [ 118 | child, 119 | ], 120 | }, 121 | ], 122 | }; 123 | const addTo1 = addChildBy(x => x.id === '1'); 124 | const result = addTo1( 125 | () => child, 126 | data, 127 | ); 128 | t.deepEqual(result, expected); 129 | }); 130 | 131 | test('should invoke getChild with metadata', (t) => { 132 | const getChild = sinon.spy(); 133 | const data = { 134 | id: '0', 135 | name: 'users', 136 | children: [ 137 | { 138 | id: '1', 139 | name: 'treecko', 140 | children: [], 141 | }, 142 | ], 143 | }; 144 | const expected = [ 145 | { 146 | id: '1', 147 | name: 'treecko', 148 | children: [], 149 | }, 150 | { 151 | parent: { 152 | id: '0', 153 | name: 'users', 154 | children: [ 155 | { 156 | id: '1', 157 | name: 'treecko', 158 | children: [], 159 | }, 160 | ], 161 | }, 162 | }, 163 | ]; 164 | addChildBy( 165 | x => x.id === '1', 166 | getChild, 167 | data, 168 | ); 169 | const result = getChild.getCall(0).args; 170 | t.deepEqual(result, expected); 171 | }); 172 | 173 | test('should invoke predicate with metadata', (t) => { 174 | const predicate = sinon.spy(() => false); 175 | const data = { 176 | id: '0', 177 | name: 'users', 178 | children: [ 179 | { 180 | id: '1', 181 | name: 'treecko', 182 | children: [], 183 | }, 184 | ], 185 | }; 186 | const expected = [ 187 | { 188 | id: '1', 189 | name: 'treecko', 190 | children: [], 191 | }, 192 | { 193 | parent: { 194 | id: '0', 195 | name: 'users', 196 | children: [ 197 | { 198 | id: '1', 199 | name: 'treecko', 200 | children: [], 201 | }, 202 | ], 203 | }, 204 | }, 205 | ]; 206 | addChildBy( 207 | predicate, 208 | () => {}, 209 | data, 210 | ); 211 | const result = predicate.getCall(1).args; 212 | t.deepEqual(result, expected); 213 | }); 214 | -------------------------------------------------------------------------------- /src/__tests__/changeParent.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import changeParent from '../changeParent'; 3 | import hasId from '../hasId'; 4 | 5 | // eslint-disable-next-line max-len 6 | test('should return tree with first item that satisfies predicate in depth first search moved to be a child of first item that satisfies newParentPredicate', (t) => { 7 | const data = { 8 | id: '0', 9 | name: 'users', 10 | children: [ 11 | { 12 | id: '1', 13 | name: 'treecko', 14 | children: [ 15 | { 16 | id: '2', 17 | name: 'documents', 18 | parentId: '1', 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | { 24 | id: '3', 25 | name: 'shared', 26 | children: [], 27 | }, 28 | ], 29 | }; 30 | const expected = { 31 | id: '0', 32 | name: 'users', 33 | children: [ 34 | { 35 | id: '1', 36 | name: 'treecko', 37 | children: [], 38 | }, 39 | { 40 | id: '3', 41 | name: 'shared', 42 | children: [ 43 | { 44 | id: '2', 45 | name: 'documents', 46 | parentId: '3', 47 | children: [], 48 | }, 49 | ], 50 | }, 51 | ], 52 | }; 53 | const result = changeParent( 54 | hasId('3'), 55 | hasId('2'), 56 | data, 57 | ); 58 | t.deepEqual(result, expected); 59 | }); 60 | 61 | test('should work with an array', (t) => { 62 | const data = [ 63 | { 64 | id: '0', 65 | name: 'treecko', 66 | children: [ 67 | { 68 | id: '1', 69 | name: 'documents', 70 | parentId: '0', 71 | children: [], 72 | }, 73 | ], 74 | }, 75 | { 76 | id: '2', 77 | name: 'shared', 78 | children: [], 79 | }, 80 | ]; 81 | const expected = [ 82 | { 83 | id: '0', 84 | name: 'treecko', 85 | children: [], 86 | }, 87 | { 88 | id: '2', 89 | name: 'shared', 90 | children: [ 91 | { 92 | id: '1', 93 | name: 'documents', 94 | parentId: '2', 95 | children: [], 96 | }, 97 | ], 98 | }, 99 | ]; 100 | const result = changeParent( 101 | hasId('2'), 102 | hasId('1'), 103 | data, 104 | ); 105 | t.deepEqual(result, expected); 106 | }); 107 | 108 | test('should work with currying', (t) => { 109 | const data = { 110 | id: '0', 111 | name: 'users', 112 | children: [ 113 | { 114 | id: '1', 115 | name: 'treecko', 116 | children: [ 117 | { 118 | id: '2', 119 | name: 'documents', 120 | parentId: '1', 121 | children: [], 122 | }, 123 | ], 124 | }, 125 | { 126 | id: '3', 127 | name: 'shared', 128 | children: [], 129 | }, 130 | ], 131 | }; 132 | const expected = { 133 | id: '0', 134 | name: 'users', 135 | children: [ 136 | { 137 | id: '1', 138 | name: 'treecko', 139 | children: [], 140 | }, 141 | { 142 | id: '3', 143 | name: 'shared', 144 | children: [ 145 | { 146 | id: '2', 147 | name: 'documents', 148 | parentId: '3', 149 | children: [], 150 | }, 151 | ], 152 | }, 153 | ], 154 | }; 155 | const moveTo3 = changeParent(hasId('3')); 156 | const result = moveTo3(hasId('2'), data); 157 | t.deepEqual(result, expected); 158 | }); 159 | -------------------------------------------------------------------------------- /src/__tests__/each.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import each from '../each'; 4 | 5 | test('should invoke iteratee with each node in tree and metadata, in a depth first fashion', (t) => { 6 | const expected0 = [ 7 | { 8 | children: [ 9 | { 10 | children: [ 11 | { 12 | children: [], 13 | }, 14 | ], 15 | }, 16 | ], 17 | }, 18 | {}, 19 | ]; 20 | const expected1 = [ 21 | { 22 | children: [ 23 | { 24 | children: [], 25 | }, 26 | ], 27 | }, 28 | { 29 | parent: { 30 | children: [ 31 | { 32 | children: [ 33 | { 34 | children: [], 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | }, 41 | ]; 42 | const expected2 = [ 43 | { 44 | children: [], 45 | }, 46 | { 47 | parent: { 48 | children: [ 49 | { 50 | children: [], 51 | }, 52 | ], 53 | }, 54 | }, 55 | ]; 56 | const data = { 57 | children: [ 58 | { 59 | children: [ 60 | { 61 | children: [], 62 | }, 63 | ], 64 | }, 65 | ], 66 | }; 67 | const iteratee = sinon.spy(); 68 | each(iteratee, data); 69 | t.deepEqual(iteratee.getCall(0).args, expected0); 70 | t.deepEqual(iteratee.getCall(1).args, expected1); 71 | t.deepEqual(iteratee.getCall(2).args, expected2); 72 | }); 73 | 74 | test('should work with array', (t) => { 75 | const expected0 = [ 76 | { 77 | children: [ 78 | { 79 | children: [ 80 | { 81 | children: [], 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | {}, 88 | ]; 89 | const expected1 = [ 90 | { 91 | children: [ 92 | { 93 | children: [], 94 | }, 95 | ], 96 | }, 97 | { 98 | parent: { 99 | children: [ 100 | { 101 | children: [ 102 | { 103 | children: [], 104 | }, 105 | ], 106 | }, 107 | ], 108 | }, 109 | }, 110 | ]; 111 | const expected2 = [ 112 | { 113 | children: [], 114 | }, 115 | { 116 | parent: { 117 | children: [ 118 | { 119 | children: [], 120 | }, 121 | ], 122 | }, 123 | }, 124 | ]; 125 | const data = [{ 126 | children: [ 127 | { 128 | children: [ 129 | { 130 | children: [], 131 | }, 132 | ], 133 | }, 134 | ], 135 | }]; 136 | const iteratee = sinon.spy(); 137 | each(iteratee, data); 138 | t.deepEqual(iteratee.getCall(0).args, expected0); 139 | t.deepEqual(iteratee.getCall(1).args, expected1); 140 | t.deepEqual(iteratee.getCall(2).args, expected2); 141 | }); 142 | 143 | test('should work with currying', (t) => { 144 | const expected0 = [ 145 | { 146 | children: [ 147 | { 148 | children: [ 149 | { 150 | children: [], 151 | }, 152 | ], 153 | }, 154 | ], 155 | }, 156 | {}, 157 | ]; 158 | const expected1 = [ 159 | { 160 | children: [ 161 | { 162 | children: [], 163 | }, 164 | ], 165 | }, 166 | { 167 | parent: { 168 | children: [ 169 | { 170 | children: [ 171 | { 172 | children: [], 173 | }, 174 | ], 175 | }, 176 | ], 177 | }, 178 | }, 179 | ]; 180 | const expected2 = [ 181 | { 182 | children: [], 183 | }, 184 | { 185 | parent: { 186 | children: [ 187 | { 188 | children: [], 189 | }, 190 | ], 191 | }, 192 | }, 193 | ]; 194 | const data = { 195 | children: [ 196 | { 197 | children: [ 198 | { 199 | children: [], 200 | }, 201 | ], 202 | }, 203 | ], 204 | }; 205 | const iteratee = sinon.spy(); 206 | each(iteratee)(data); 207 | t.deepEqual(iteratee.getCall(0).args, expected0); 208 | t.deepEqual(iteratee.getCall(1).args, expected1); 209 | t.deepEqual(iteratee.getCall(2).args, expected2); 210 | }); 211 | -------------------------------------------------------------------------------- /src/__tests__/filter.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import filter from '../filter'; 3 | 4 | test('should return tree with only items that satisfy predicate', (t) => { 5 | const data = { 6 | age: 25, 7 | children: [ 8 | { 9 | age: 40, 10 | children: [], 11 | }, 12 | { 13 | age: 150, 14 | children: [], 15 | }, 16 | ], 17 | }; 18 | const expected = { 19 | age: 25, 20 | children: [ 21 | { 22 | age: 40, 23 | children: [], 24 | }, 25 | ], 26 | }; 27 | const result = filter(x => x.age < 100, data); 28 | t.deepEqual(result, expected); 29 | }); 30 | 31 | test('should work with array', (t) => { 32 | const data = [ 33 | { 34 | age: 40, 35 | children: [], 36 | }, 37 | { 38 | age: 150, 39 | children: [], 40 | }, 41 | ]; 42 | const expected = [ 43 | { 44 | age: 40, 45 | children: [], 46 | }, 47 | ]; 48 | const result = filter(x => x.age < 100, data); 49 | t.deepEqual(result, expected); 50 | }); 51 | 52 | test('should work with currying', (t) => { 53 | const data = { 54 | age: 25, 55 | children: [ 56 | { 57 | age: 40, 58 | children: [], 59 | }, 60 | { 61 | age: 150, 62 | children: [], 63 | }, 64 | ], 65 | }; 66 | const expected = { 67 | age: 25, 68 | children: [ 69 | { 70 | age: 40, 71 | children: [], 72 | }, 73 | ], 74 | }; 75 | const filterWithinDate = filter(x => x.age < 100); 76 | const result = filterWithinDate(data); 77 | t.deepEqual(result, expected); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/findOr.test.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/fp/get'; 2 | import test from 'ava'; 3 | import findOr from '../findOr'; 4 | 5 | test('should return first node in tree that satisfies predicate, in a depth first fashion', (t) => { 6 | const grandchild = { children: [], matches: true }; 7 | const data = { 8 | children: [ 9 | { 10 | children: [ 11 | grandchild, 12 | ], 13 | }, 14 | { children: [], matches: true }, 15 | ], 16 | }; 17 | const expected = grandchild; 18 | const result = findOr({}, get('matches'), data); 19 | t.is(result, expected); 20 | }); 21 | 22 | test('should return defaultValue when no node matches predicate', (t) => { 23 | const grandchild = { children: [] }; 24 | const data = { 25 | children: [ 26 | { 27 | children: [ 28 | grandchild, 29 | ], 30 | }, 31 | { 32 | children: [], 33 | }, 34 | ], 35 | }; 36 | const defaultValue = {}; 37 | const expected = defaultValue; 38 | const result = findOr(defaultValue, get('matches'), data); 39 | t.is(result, expected); 40 | }); 41 | 42 | test('should work with array', (t) => { 43 | const grandchild = { children: [], matches: true }; 44 | const data = [{ 45 | children: [ 46 | { 47 | children: [ 48 | grandchild, 49 | ], 50 | }, 51 | { children: [], matches: true }, 52 | ], 53 | }]; 54 | const expected = grandchild; 55 | const result = findOr({}, get('matches'), data); 56 | t.is(result, expected); 57 | }); 58 | 59 | test('should work with currying', (t) => { 60 | const grandchild = { children: [], matches: true }; 61 | const data = { 62 | children: [ 63 | { 64 | children: [ 65 | grandchild, 66 | ], 67 | }, 68 | { children: [], matches: true }, 69 | ], 70 | }; 71 | const expected = grandchild; 72 | const result = findOr({})(get('matches'))(data); 73 | t.is(result, expected); 74 | }); 75 | 76 | test('should invoke predicate with metadata', (t) => { 77 | const child = { name: 'treecko', children: [] }; 78 | const expected = child; 79 | const data = { 80 | name: 'root', 81 | children: [ 82 | { 83 | name: 'users', 84 | children: [ 85 | child, 86 | { name: 'sudo', children: [] }, 87 | ], 88 | }, 89 | ], 90 | }; 91 | const predicate = (x, { parent }) => 92 | get('name', parent) === 'users'; 93 | const result = findOr({}, predicate, data); 94 | t.is(result, expected); 95 | }); 96 | -------------------------------------------------------------------------------- /src/__tests__/flatten.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import flatten from '../flatten'; 3 | 4 | test('should return array with items of tree flattened in a depth first fashion', (t) => { 5 | const grandchild = { 6 | children: [], 7 | }; 8 | const child = { 9 | children: [ 10 | grandchild, 11 | ], 12 | }; 13 | const secondChild = { 14 | children: [], 15 | }; 16 | const data = { 17 | children: [ 18 | child, 19 | secondChild, 20 | ], 21 | }; 22 | const expected = [data, child, grandchild, secondChild]; 23 | const result = flatten(data); 24 | t.deepEqual(result, expected); 25 | }); 26 | 27 | test('should work with an array', (t) => { 28 | const grandchild = { 29 | children: [], 30 | }; 31 | const child = { 32 | children: [ 33 | grandchild, 34 | ], 35 | }; 36 | const secondChild = { 37 | children: [], 38 | }; 39 | const data = [{ 40 | children: [ 41 | child, 42 | secondChild, 43 | ], 44 | }]; 45 | const expected = [data[0], child, grandchild, secondChild]; 46 | const result = flatten(data); 47 | t.deepEqual(result, expected); 48 | }); 49 | -------------------------------------------------------------------------------- /src/__tests__/flattenToIds.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import flattenToIds from '../flattenToIds'; 3 | 4 | test('should return object map with tree items, children omitted, assigned to the key of their id', (t) => { 5 | const data = { 6 | id: 'a', 7 | children: [ 8 | { 9 | id: 'b', 10 | children: [ 11 | { 12 | id: 'c', 13 | children: [], 14 | }, 15 | ], 16 | }, { 17 | id: 'd', 18 | children: [], 19 | }, 20 | ], 21 | }; 22 | const expected = ['a', 'b', 'c', 'd']; 23 | const result = flattenToIds(data); 24 | t.deepEqual(result, expected); 25 | }); 26 | 27 | test('should work with an array', (t) => { 28 | const data = [ 29 | { 30 | id: 'a', 31 | children: [ 32 | { 33 | id: 'b', 34 | children: [ 35 | { 36 | id: 'c', 37 | children: [], 38 | }, 39 | ], 40 | }, { 41 | id: 'd', 42 | children: [], 43 | }, 44 | ], 45 | }, 46 | ]; 47 | const expected = ['a', 'b', 'c', 'd']; 48 | const result = flattenToIds(data); 49 | t.deepEqual(result, expected); 50 | }); 51 | -------------------------------------------------------------------------------- /src/__tests__/flattenToMap.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import flattenToMap from '../flattenToMap'; 3 | 4 | test('should return object map with tree items, children omitted, assigned to the key of their id', (t) => { 5 | const data = { 6 | id: 'a', 7 | parentId: '', 8 | children: [ 9 | { 10 | id: 'b', 11 | parentId: 'a', 12 | children: [ 13 | { 14 | id: 'c', 15 | parentId: 'b', 16 | children: [], 17 | }, 18 | ], 19 | }, 20 | { 21 | id: 'd', 22 | parentId: 'a', 23 | children: [], 24 | }, 25 | ], 26 | }; 27 | const expected = { 28 | a: { id: 'a', parentId: '' }, 29 | b: { id: 'b', parentId: 'a' }, 30 | c: { id: 'c', parentId: 'b' }, 31 | d: { id: 'd', parentId: 'a' }, 32 | }; 33 | const result = flattenToMap(data); 34 | t.deepEqual(result, expected); 35 | }); 36 | 37 | test('should work with an array', (t) => { 38 | const data = [ 39 | { 40 | id: 'a', 41 | parentId: '', 42 | children: [ 43 | { 44 | id: 'b', 45 | parentId: 'a', 46 | children: [], 47 | }, 48 | ], 49 | }, 50 | { 51 | id: 'c', 52 | parentId: '', 53 | children: [], 54 | }, 55 | ]; 56 | const expected = { 57 | a: { id: 'a', parentId: '' }, 58 | b: { id: 'b', parentId: 'a' }, 59 | c: { id: 'c', parentId: '' }, 60 | }; 61 | const result = flattenToMap(data); 62 | t.deepEqual(result, expected); 63 | }); 64 | -------------------------------------------------------------------------------- /src/__tests__/hardMap.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import hardMap from '../hardMap'; 4 | 5 | test('should return tree with iteratee applied to each item', (t) => { 6 | const data = { 7 | value: 5, 8 | children: [ 9 | { 10 | value: 10, 11 | children: [], 12 | }, 13 | ], 14 | }; 15 | const expected = { 16 | value: 10, 17 | }; 18 | const result = hardMap(x => ({ 19 | value: x.value * 2, 20 | }), data); 21 | t.deepEqual(result, expected); 22 | }); 23 | 24 | test('should work with an array', (t) => { 25 | const data = [{ 26 | value: 5, 27 | children: [ 28 | { 29 | value: 10, 30 | children: [], 31 | }, 32 | ], 33 | }]; 34 | const expected = [{ 35 | value: 10, 36 | }]; 37 | const result = hardMap(x => ({ 38 | value: x.value * 2, 39 | }), data); 40 | t.deepEqual(result, expected); 41 | }); 42 | 43 | test('should work with currying', (t) => { 44 | const data = { 45 | value: 5, 46 | children: [ 47 | { 48 | value: 10, 49 | children: [], 50 | }, 51 | ], 52 | }; 53 | const expected = { 54 | value: 10, 55 | }; 56 | const hardMapDoubleValue = hardMap(x => ({ value: x.value * 2 })); 57 | const result = hardMapDoubleValue(data); 58 | t.deepEqual(result, expected); 59 | }); 60 | 61 | test('should invoke iteratee with metadata', (t) => { 62 | const iteratee = sinon.spy(); 63 | const data = { 64 | value: 5, 65 | children: [ 66 | { 67 | value: 10, 68 | children: [], 69 | }, 70 | ], 71 | }; 72 | const expected = [ 73 | { 74 | value: 5, 75 | children: [ 76 | { 77 | value: 10, 78 | children: [], 79 | }, 80 | ], 81 | }, 82 | {}, 83 | ]; 84 | hardMap(iteratee, data); 85 | const result = iteratee.getCall(0).args; 86 | t.deepEqual(result, expected); 87 | }); 88 | -------------------------------------------------------------------------------- /src/__tests__/hardMapBy.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import hardMapBy from '../hardMapBy'; 4 | 5 | test('should return tree with iteratee applied to items that satisfy predicate', (t) => { 6 | const data = { 7 | value: 5, 8 | children: [ 9 | { 10 | value: 10, 11 | children: [], 12 | }, 13 | ], 14 | }; 15 | const expected = { 16 | value: 5, 17 | children: [ 18 | { 19 | value: 20, 20 | }, 21 | ], 22 | }; 23 | const result = hardMapBy( 24 | x => x.value >= 10, 25 | x => ({ value: x.value * 2 }), 26 | data, 27 | ); 28 | t.deepEqual(result, expected); 29 | }); 30 | 31 | test('should work with an array', (t) => { 32 | const data = [{ 33 | value: 5, 34 | children: [ 35 | { 36 | value: 10, 37 | children: [], 38 | }, 39 | ], 40 | }]; 41 | const expected = [{ 42 | value: 5, 43 | children: [ 44 | { 45 | value: 20, 46 | }, 47 | ], 48 | }]; 49 | const result = hardMapBy( 50 | x => x.value >= 10, 51 | x => ({ value: x.value * 2 }), 52 | data, 53 | ); 54 | t.deepEqual(result, expected); 55 | }); 56 | 57 | test('should work with currying', (t) => { 58 | const data = { 59 | value: 5, 60 | children: [ 61 | { 62 | value: 10, 63 | children: [], 64 | }, 65 | ], 66 | }; 67 | const expected = { 68 | value: 5, 69 | children: [ 70 | { 71 | value: 20, 72 | }, 73 | ], 74 | }; 75 | const mapDoubleDigits = hardMapBy(x => x.value >= 10); 76 | const result = mapDoubleDigits( 77 | x => ({ value: x.value * 2 }), 78 | data, 79 | ); 80 | t.deepEqual(result, expected); 81 | }); 82 | 83 | test('should invoke iteratee with metadata', (t) => { 84 | const iteratee = sinon.spy(); 85 | const data = { 86 | value: 5, 87 | children: [ 88 | { 89 | value: 10, 90 | children: [], 91 | }, 92 | ], 93 | }; 94 | const expected = [ 95 | { 96 | value: 10, 97 | children: [], 98 | }, 99 | { 100 | parent: { 101 | value: 5, 102 | children: [ 103 | { 104 | value: 10, 105 | children: [], 106 | }, 107 | ], 108 | }, 109 | }, 110 | ]; 111 | hardMapBy( 112 | x => x.value >= 10, 113 | iteratee, 114 | data, 115 | ); 116 | const result = iteratee.getCall(0).args; 117 | t.deepEqual(result, expected); 118 | }); 119 | 120 | test('should invoke predicate with metadata', (t) => { 121 | const predicate = sinon.spy(() => false); 122 | const data = { 123 | value: 5, 124 | children: [ 125 | { 126 | value: 10, 127 | children: [], 128 | }, 129 | ], 130 | }; 131 | const expected = [ 132 | { 133 | value: 10, 134 | children: [], 135 | }, 136 | { 137 | parent: { 138 | value: 5, 139 | children: [ 140 | { 141 | value: 10, 142 | children: [], 143 | }, 144 | ], 145 | }, 146 | }, 147 | ]; 148 | hardMapBy( 149 | predicate, 150 | x => x, 151 | data, 152 | ); 153 | const result = predicate.getCall(1).args; 154 | t.deepEqual(result, expected); 155 | }); 156 | -------------------------------------------------------------------------------- /src/__tests__/hasId.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import hasId from '../hasId'; 3 | 4 | test('should return true when data id is equal to id', (t) => { 5 | const data = { 6 | id: 'a', 7 | }; 8 | const result = hasId('a', data); 9 | t.is(result, true); 10 | }); 11 | 12 | test('should return false when data id is not equal to id', (t) => { 13 | const data = { 14 | id: 'b', 15 | }; 16 | const result = hasId('a', data); 17 | t.is(result, false); 18 | }); 19 | 20 | test('should work with currying', (t) => { 21 | const data = { 22 | id: 'a', 23 | }; 24 | const hasIdA = hasId('a'); 25 | const result = hasIdA(data); 26 | t.is(result, true); 27 | }); 28 | -------------------------------------------------------------------------------- /src/__tests__/reduce.test.js: -------------------------------------------------------------------------------- 1 | import getOr from 'lodash/fp/getOr'; 2 | import test from 'ava'; 3 | import reduce from '../reduce'; 4 | 5 | test('should reduce the items in the tree to a single value, in a depth first fashion', (t) => { 6 | const data = { 7 | value: 'a', 8 | children: [ 9 | { 10 | value: 'b', 11 | children: [ 12 | { 13 | value: 'c', 14 | children: [], 15 | }, 16 | ], 17 | }, 18 | { 19 | value: 'd', 20 | children: [], 21 | }, 22 | ], 23 | }; 24 | const expected = 'abcd'; 25 | const result = reduce( 26 | (acc, cur) => `${acc}${cur.value}`, 27 | '', 28 | data, 29 | ); 30 | t.deepEqual(result, expected); 31 | }); 32 | 33 | test('should work with an array', (t) => { 34 | const data = [{ 35 | value: 'a', 36 | children: [ 37 | { 38 | value: 'b', 39 | children: [ 40 | { 41 | value: 'c', 42 | children: [], 43 | }, 44 | ], 45 | }, 46 | { 47 | value: 'd', 48 | children: [], 49 | }, 50 | ], 51 | }]; 52 | const expected = 'abcd'; 53 | const result = reduce( 54 | (acc, cur) => `${acc}${cur.value}`, 55 | '', 56 | data, 57 | ); 58 | t.deepEqual(result, expected); 59 | }); 60 | 61 | test('should work currying', (t) => { 62 | const data = { 63 | value: 'a', 64 | children: [ 65 | { 66 | value: 'b', 67 | children: [ 68 | { 69 | value: 'c', 70 | children: [], 71 | }, 72 | ], 73 | }, 74 | { 75 | value: 'd', 76 | children: [], 77 | }, 78 | ], 79 | }; 80 | const expected = 'abcd'; 81 | const sumReduce = reduce((acc, cur) => `${acc}${cur.value}`); 82 | const result = sumReduce( 83 | '', 84 | data, 85 | ); 86 | t.deepEqual(result, expected); 87 | }); 88 | 89 | test('should invoke reducer with metadata', (t) => { 90 | const data = { 91 | value: 'a', 92 | children: [ 93 | { 94 | value: 'b', 95 | children: [ 96 | { 97 | value: 'c', 98 | children: [], 99 | }, 100 | ], 101 | }, 102 | { 103 | value: 'd', 104 | children: [], 105 | }, 106 | ], 107 | }; 108 | const expected = '(a)(ab)(bc)(ad)'; 109 | const result = reduce( 110 | (acc, cur, { parent }) => `${acc}(${getOr('', 'value', parent)}${cur.value})`, 111 | '', 112 | data, 113 | ); 114 | t.deepEqual(result, expected); 115 | }); 116 | -------------------------------------------------------------------------------- /src/__tests__/reduceAncestryBy.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import reduceAncestryBy from '../reduceAncestryBy'; 3 | 4 | test('should return single value created by reducing ancestry starting with first item in a depth first search that satisfies predicate', (t) => { 5 | const data = { 6 | id: '0', 7 | parentId: '', 8 | name: 'users', 9 | children: [ 10 | { 11 | id: '1', 12 | parentId: '0', 13 | name: 'treecko', 14 | children: [ 15 | { 16 | id: '2', 17 | parentId: '1', 18 | name: 'documents', 19 | children: [], 20 | }, 21 | ], 22 | }, 23 | ], 24 | }; 25 | const expected = '/users/treecko/documents'; 26 | const result = reduceAncestryBy( 27 | x => x.id === '2', 28 | (acc, cur) => `/${cur.name}${acc}`, 29 | '', 30 | data, 31 | ); 32 | t.deepEqual(result, expected); 33 | }); 34 | 35 | test('should work with an array', (t) => { 36 | const data = [ 37 | { 38 | id: '1', 39 | parentId: '0', 40 | name: 'users', 41 | children: [ 42 | { 43 | id: '2', 44 | parentId: '1', 45 | name: 'treecko', 46 | children: [], 47 | }, 48 | ], 49 | }, 50 | { 51 | id: '4', 52 | parentId: '0', 53 | name: 'applications', 54 | children: [ 55 | { 56 | id: '5', 57 | parentId: '4', 58 | name: 'atom', 59 | children: [], 60 | }, 61 | ], 62 | }, 63 | ]; 64 | const expected = '/users/treecko'; 65 | const result = reduceAncestryBy( 66 | x => x.id === '2', 67 | (acc, cur) => `/${cur.name}${acc}`, 68 | '', 69 | data, 70 | ); 71 | t.deepEqual(result, expected); 72 | }); 73 | 74 | test('should work with currying', (t) => { 75 | const data = { 76 | id: '0', 77 | parentId: '', 78 | name: 'users', 79 | children: [ 80 | { 81 | id: '1', 82 | parentId: '0', 83 | name: 'treecko', 84 | children: [ 85 | { 86 | id: '2', 87 | parentId: '1', 88 | name: 'documents', 89 | children: [], 90 | }, 91 | ], 92 | }, 93 | ], 94 | }; 95 | const expected = '/users/treecko/documents'; 96 | const getFullPath = reduceAncestryBy(x => x.id === '2'); 97 | const result = getFullPath( 98 | (acc, cur) => `/${cur.name}${acc}`, 99 | '', 100 | data, 101 | ); 102 | t.deepEqual(result, expected); 103 | }); 104 | -------------------------------------------------------------------------------- /src/__tests__/reject.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import reject from '../reject'; 3 | 4 | test('should return tree without items that satisfy predicate', (t) => { 5 | const data = { 6 | age: 25, 7 | children: [ 8 | { 9 | age: 40, 10 | children: [], 11 | }, 12 | { 13 | age: 150, 14 | children: [], 15 | }, 16 | ], 17 | }; 18 | const expected = { 19 | age: 25, 20 | children: [ 21 | { 22 | age: 40, 23 | children: [], 24 | }, 25 | ], 26 | }; 27 | const result = reject(x => x.age > 100, data); 28 | t.deepEqual(result, expected); 29 | }); 30 | 31 | test('should work with array', (t) => { 32 | const data = [ 33 | { 34 | age: 40, 35 | children: [], 36 | }, 37 | { 38 | age: 150, 39 | children: [], 40 | }, 41 | ]; 42 | const expected = [ 43 | { 44 | age: 40, 45 | children: [], 46 | }, 47 | ]; 48 | const result = reject(x => x.age > 100, data); 49 | t.deepEqual(result, expected); 50 | }); 51 | 52 | test('should work with currying', (t) => { 53 | const data = { 54 | age: 25, 55 | children: [ 56 | { 57 | age: 40, 58 | children: [], 59 | }, 60 | { 61 | age: 150, 62 | children: [], 63 | }, 64 | ], 65 | }; 66 | const expected = { 67 | age: 25, 68 | children: [ 69 | { 70 | age: 40, 71 | children: [], 72 | }, 73 | ], 74 | }; 75 | const rejectOutdated = reject(x => x.age > 100); 76 | const result = rejectOutdated(data); 77 | t.deepEqual(result, expected); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/replaceChildrenBy.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import replaceChildrenBy from '../replaceChildrenBy'; 4 | 5 | test('should return tree with first item that satisfies predicate having its children replaced by return value of getNewChildren function', (t) => { 6 | const replacementChildren = [ 7 | { id: 'a' }, 8 | ]; 9 | const data = { 10 | id: '0', 11 | parentId: '', 12 | name: 'users', 13 | children: [ 14 | { 15 | id: '1', 16 | parentId: '0', 17 | name: 'treecko', 18 | children: [ 19 | { 20 | id: '2', 21 | parentId: '1', 22 | name: 'documents', 23 | children: [], 24 | }, 25 | ], 26 | }, 27 | ], 28 | }; 29 | const expected = { 30 | id: '0', 31 | parentId: '', 32 | name: 'users', 33 | children: [ 34 | { 35 | id: '1', 36 | parentId: '0', 37 | name: 'treecko', 38 | children: replacementChildren, 39 | }, 40 | ], 41 | }; 42 | const result = replaceChildrenBy( 43 | x => x.id === '1', 44 | () => replacementChildren, 45 | data, 46 | ); 47 | t.deepEqual(result, expected); 48 | }); 49 | 50 | test('should work with an array', (t) => { 51 | const replacementChildren = [ 52 | { id: 'a' }, 53 | ]; 54 | const data = [{ 55 | id: '0', 56 | parentId: '', 57 | name: 'users', 58 | children: [ 59 | { 60 | id: '1', 61 | parentId: '0', 62 | name: 'treecko', 63 | children: [ 64 | { 65 | id: '2', 66 | parentId: '1', 67 | name: 'documents', 68 | children: [], 69 | }, 70 | ], 71 | }, 72 | ], 73 | }]; 74 | const expected = [{ 75 | id: '0', 76 | parentId: '', 77 | name: 'users', 78 | children: [ 79 | { 80 | id: '1', 81 | parentId: '0', 82 | name: 'treecko', 83 | children: replacementChildren, 84 | }, 85 | ], 86 | }]; 87 | const result = replaceChildrenBy( 88 | x => x.id === '1', 89 | () => replacementChildren, 90 | data, 91 | ); 92 | t.deepEqual(result, expected); 93 | }); 94 | 95 | test('should work with currying', (t) => { 96 | const replacementChildren = [ 97 | { id: 'a' }, 98 | ]; 99 | const data = { 100 | id: '0', 101 | parentId: '', 102 | name: 'users', 103 | children: [ 104 | { 105 | id: '1', 106 | parentId: '0', 107 | name: 'treecko', 108 | children: [ 109 | { 110 | id: '2', 111 | parentId: '1', 112 | name: 'documents', 113 | children: [], 114 | }, 115 | ], 116 | }, 117 | ], 118 | }; 119 | const expected = { 120 | id: '0', 121 | parentId: '', 122 | name: 'users', 123 | children: [ 124 | { 125 | id: '1', 126 | parentId: '0', 127 | name: 'treecko', 128 | children: replacementChildren, 129 | }, 130 | ], 131 | }; 132 | const replaceId1Children = replaceChildrenBy(x => x.id === '1'); 133 | const result = replaceId1Children( 134 | () => replacementChildren, 135 | data, 136 | ); 137 | t.deepEqual(result, expected); 138 | }); 139 | 140 | test('should invoke getNewChildren with metadata', (t) => { 141 | const getNewChildren = sinon.spy(() => []); 142 | const data = { 143 | id: '0', 144 | parentId: '', 145 | name: 'users', 146 | children: [ 147 | { 148 | id: '1', 149 | parentId: '0', 150 | name: 'treecko', 151 | children: [], 152 | }, 153 | ], 154 | }; 155 | const expected = [ 156 | { 157 | id: '1', 158 | parentId: '0', 159 | name: 'treecko', 160 | children: [], 161 | }, 162 | { 163 | parent: { 164 | id: '0', 165 | parentId: '', 166 | name: 'users', 167 | children: [ 168 | { 169 | id: '1', 170 | parentId: '0', 171 | name: 'treecko', 172 | children: [], 173 | }, 174 | ], 175 | }, 176 | }, 177 | ]; 178 | replaceChildrenBy( 179 | x => x.id === '1', 180 | getNewChildren, 181 | data, 182 | ); 183 | const result = getNewChildren.getCall(0).args; 184 | t.deepEqual(result, expected); 185 | }); 186 | 187 | test('should invoke predicate with metadata', (t) => { 188 | const predicate = sinon.spy(() => false); 189 | const data = { 190 | id: '0', 191 | parentId: '', 192 | name: 'users', 193 | children: [ 194 | { 195 | id: '1', 196 | parentId: '0', 197 | name: 'treecko', 198 | children: [], 199 | }, 200 | ], 201 | }; 202 | const expected = [ 203 | { 204 | id: '1', 205 | parentId: '0', 206 | name: 'treecko', 207 | children: [], 208 | }, 209 | { 210 | parent: { 211 | id: '0', 212 | parentId: '', 213 | name: 'users', 214 | children: [ 215 | { 216 | id: '1', 217 | parentId: '0', 218 | name: 'treecko', 219 | children: [], 220 | }, 221 | ], 222 | }, 223 | }, 224 | ]; 225 | replaceChildrenBy( 226 | predicate, 227 | x => x.children, 228 | data, 229 | ); 230 | const result = predicate.getCall(1).args; 231 | t.deepEqual(result, expected); 232 | }); 233 | -------------------------------------------------------------------------------- /src/__tests__/softMap.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import softMap from '../softMap'; 4 | 5 | test('should return tree with iteratee applied to each item, preserving children', (t) => { 6 | const data = { 7 | value: 5, 8 | children: [ 9 | { 10 | value: 10, 11 | children: [], 12 | }, 13 | ], 14 | }; 15 | const expected = { 16 | value: 10, 17 | children: [ 18 | { 19 | value: 20, 20 | children: [], 21 | }, 22 | ], 23 | }; 24 | const result = softMap(x => ({ 25 | value: x.value * 2, 26 | }), data); 27 | t.deepEqual(result, expected); 28 | }); 29 | 30 | test('should work with an array', (t) => { 31 | const data = [{ 32 | value: 5, 33 | children: [ 34 | { 35 | value: 10, 36 | children: [], 37 | }, 38 | ], 39 | }]; 40 | const expected = [{ 41 | value: 10, 42 | children: [ 43 | { 44 | value: 20, 45 | children: [], 46 | }, 47 | ], 48 | }]; 49 | const result = softMap(x => ({ 50 | value: x.value * 2, 51 | }), data); 52 | t.deepEqual(result, expected); 53 | }); 54 | 55 | test('should work with currying', (t) => { 56 | const data = { 57 | value: 5, 58 | children: [ 59 | { 60 | value: 10, 61 | children: [], 62 | }, 63 | ], 64 | }; 65 | const expected = { 66 | value: 10, 67 | children: [ 68 | { 69 | value: 20, 70 | children: [], 71 | }, 72 | ], 73 | }; 74 | const softMapDoubleValue = softMap(x => ({ value: x.value * 2 })); 75 | const result = softMapDoubleValue(data); 76 | t.deepEqual(result, expected); 77 | }); 78 | 79 | test('should invoke iteratee with metadata', (t) => { 80 | const iteratee = sinon.spy(); 81 | const data = { 82 | value: 5, 83 | children: [ 84 | { 85 | value: 10, 86 | children: [], 87 | }, 88 | ], 89 | }; 90 | const expected = [ 91 | { 92 | value: 10, 93 | children: [], 94 | }, 95 | { 96 | parent: { 97 | value: 5, 98 | children: [ 99 | { 100 | value: 10, 101 | children: [], 102 | }, 103 | ], 104 | }, 105 | }, 106 | ]; 107 | softMap(iteratee, data); 108 | const result = iteratee.getCall(1).args; 109 | t.deepEqual(result, expected); 110 | }); 111 | -------------------------------------------------------------------------------- /src/__tests__/softMapBy.test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import test from 'ava'; 3 | import softMapBy from '../softMapBy'; 4 | 5 | test('should return tree with iteratee applied to items that satisfy predicate, preserving children', (t) => { 6 | const data = { 7 | value: 5, 8 | children: [ 9 | { 10 | value: 10, 11 | children: [], 12 | }, 13 | ], 14 | }; 15 | const expected = { 16 | value: 5, 17 | children: [ 18 | { 19 | value: 20, 20 | children: [], 21 | }, 22 | ], 23 | }; 24 | const result = softMapBy( 25 | x => x.value >= 10, 26 | x => ({ value: x.value * 2 }), 27 | data, 28 | ); 29 | t.deepEqual(result, expected); 30 | }); 31 | 32 | test('should work with an array', (t) => { 33 | const data = [{ 34 | value: 5, 35 | children: [ 36 | { 37 | value: 10, 38 | children: [], 39 | }, 40 | ], 41 | }]; 42 | const expected = [{ 43 | value: 5, 44 | children: [ 45 | { 46 | value: 20, 47 | children: [], 48 | }, 49 | ], 50 | }]; 51 | const result = softMapBy( 52 | x => x.value >= 10, 53 | x => ({ value: x.value * 2 }), 54 | data, 55 | ); 56 | t.deepEqual(result, expected); 57 | }); 58 | 59 | test('should work with currying', (t) => { 60 | const data = { 61 | value: 5, 62 | children: [ 63 | { 64 | value: 10, 65 | children: [], 66 | }, 67 | ], 68 | }; 69 | const expected = { 70 | value: 5, 71 | children: [ 72 | { 73 | value: 20, 74 | children: [], 75 | }, 76 | ], 77 | }; 78 | const mapDoubleDigits = softMapBy(x => x.value >= 10); 79 | const result = mapDoubleDigits( 80 | x => ({ value: x.value * 2 }), 81 | data, 82 | ); 83 | t.deepEqual(result, expected); 84 | }); 85 | 86 | test('should invoke iteratee with metadata', (t) => { 87 | const iteratee = sinon.spy(); 88 | const data = { 89 | value: 5, 90 | children: [ 91 | { 92 | value: 10, 93 | children: [], 94 | }, 95 | ], 96 | }; 97 | const expected = [ 98 | { 99 | value: 10, 100 | children: [], 101 | }, 102 | { 103 | parent: { 104 | value: 5, 105 | children: [ 106 | { 107 | value: 10, 108 | children: [], 109 | }, 110 | ], 111 | }, 112 | }, 113 | ]; 114 | softMapBy( 115 | x => x.value >= 10, 116 | iteratee, 117 | data, 118 | ); 119 | const result = iteratee.getCall(0).args; 120 | t.deepEqual(result, expected); 121 | }); 122 | 123 | test('should invoke predicate with metadata', (t) => { 124 | const predicate = sinon.spy(); 125 | const data = { 126 | value: 5, 127 | children: [ 128 | { 129 | value: 10, 130 | children: [], 131 | }, 132 | ], 133 | }; 134 | const expected = [ 135 | { 136 | value: 10, 137 | children: [], 138 | }, 139 | { 140 | parent: { 141 | value: 5, 142 | children: [ 143 | { 144 | value: 10, 145 | children: [], 146 | }, 147 | ], 148 | }, 149 | }, 150 | ]; 151 | softMapBy( 152 | predicate, 153 | x => x, 154 | data, 155 | ); 156 | const result = predicate.getCall(1).args; 157 | t.deepEqual(result, expected); 158 | }); 159 | -------------------------------------------------------------------------------- /src/__tests__/superflatten.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import superflatten from '../superflatten'; 3 | 4 | test('should return array with items of tree in a depth first fashion, omitting children from the items', (t) => { 5 | const data = { 6 | id: '0', 7 | children: [ 8 | { 9 | id: '1', 10 | children: [ 11 | { 12 | id: '2', 13 | children: [], 14 | }, 15 | ], 16 | }, 17 | { 18 | id: '3', 19 | children: [], 20 | }, 21 | ], 22 | }; 23 | const expected = [ 24 | { id: '0' }, 25 | { id: '1' }, 26 | { id: '2' }, 27 | { id: '3' }, 28 | ]; 29 | const result = superflatten(data); 30 | t.deepEqual(result, expected); 31 | }); 32 | 33 | test('should work with an array', (t) => { 34 | const data = [ 35 | { 36 | id: '0', 37 | children: [ 38 | { 39 | id: '1', 40 | children: [], 41 | }, 42 | ], 43 | }, 44 | { 45 | id: '2', 46 | children: [], 47 | }, 48 | ]; 49 | const expected = [ 50 | { id: '0' }, 51 | { id: '1' }, 52 | { id: '2' }, 53 | ]; 54 | const result = superflatten(data); 55 | t.deepEqual(result, expected); 56 | }); 57 | -------------------------------------------------------------------------------- /src/addChildBy.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import replaceChildrenBy from './replaceChildrenBy'; 6 | 7 | function addChildBy(predicate, getChild, xs) { 8 | return replaceChildrenBy( 9 | predicate, 10 | (x, metadata) => [...x.children, getChild(x, metadata)], 11 | xs, 12 | ); 13 | } 14 | 15 | export default curry((predicate, getChild, data) => { 16 | if (isArray(data)) { 17 | return addChildBy(predicate, getChild, data); 18 | } 19 | 20 | if (isObject(data)) { 21 | return first(addChildBy(predicate, getChild, [data])); 22 | } 23 | 24 | return undefined; 25 | }); 26 | -------------------------------------------------------------------------------- /src/changeParent.js: -------------------------------------------------------------------------------- 1 | import compose from 'lodash/fp/compose'; 2 | import curry from 'lodash/fp/curry'; 3 | import first from 'lodash/fp/first'; 4 | import isArray from 'lodash/fp/isArray'; 5 | import isEqual from 'lodash/fp/isEqual'; 6 | import isObject from 'lodash/fp/isObject'; 7 | import addChildBy from './addChildBy'; 8 | import findOr from './findOr'; 9 | import reject from './reject'; 10 | 11 | function changeParent(newParentPredicate, predicate, xs) { 12 | const target = findOr('notfound', predicate, xs); 13 | 14 | if (target === 'notfound') return xs; 15 | 16 | return compose( 17 | addChildBy( 18 | newParentPredicate, 19 | newParent => ({ ...target, parentId: newParent.id }), 20 | ), 21 | reject(isEqual(target)), 22 | )(xs); 23 | } 24 | 25 | export default curry((newParentPredicate, predicate, data) => { 26 | if (isArray(data)) { 27 | return changeParent(newParentPredicate, predicate, data); 28 | } 29 | 30 | if (isObject(data)) { 31 | return first(changeParent(newParentPredicate, predicate, [data])); 32 | } 33 | 34 | return undefined; 35 | }); 36 | -------------------------------------------------------------------------------- /src/each.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import isArray from 'lodash/fp/isArray'; 3 | import isObject from 'lodash/fp/isObject'; 4 | import lodashEach from 'lodash/fp/each'; 5 | 6 | function each(iteratee, xs, metadata = {}) { 7 | lodashEach((x) => { 8 | iteratee(x, metadata); 9 | each(iteratee, x.children, { parent: x }); 10 | }, xs); 11 | } 12 | 13 | export default curry((iteratee, data) => { 14 | if (isArray(data)) { 15 | each(iteratee, data); 16 | } 17 | 18 | if (isObject(data)) { 19 | each(iteratee, [data]); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import lodashFilter from 'lodash/fp/filter'; 6 | import map from 'lodash/fp/map'; 7 | 8 | function filter(predicate, xs) { 9 | return map(x => ({ 10 | ...x, 11 | children: filter(predicate, x.children), 12 | }), lodashFilter(predicate, xs)); 13 | } 14 | 15 | export default curry((predicate, data) => { 16 | if (isArray(data)) { 17 | return filter(predicate, data); 18 | } 19 | 20 | if (isObject(data)) { 21 | return first(filter(predicate, [data])); 22 | } 23 | 24 | return undefined; 25 | }); 26 | -------------------------------------------------------------------------------- /src/findOr.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import isArray from 'lodash/fp/isArray'; 3 | import isEmpty from 'lodash/fp/isEmpty'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import reduce from 'lodash/fp/reduce'; 6 | 7 | function findOr(defaultValue, predicate, xs, metadata = {}) { 8 | return reduce((acc, cur) => { 9 | if (acc !== defaultValue) return acc; 10 | 11 | if (predicate(cur, metadata)) return cur; 12 | 13 | if (isEmpty(cur.children)) return acc; 14 | 15 | return findOr(acc, predicate, cur.children, { parent: cur }); 16 | }, defaultValue, xs); 17 | } 18 | 19 | export default curry((defaultValue, predicate, data) => { 20 | if (isArray(data)) { 21 | return findOr(defaultValue, predicate, data); 22 | } 23 | 24 | if (isObject(data)) { 25 | return findOr(defaultValue, predicate, [data]); 26 | } 27 | 28 | return undefined; 29 | }); 30 | -------------------------------------------------------------------------------- /src/flatten.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/fp/isArray'; 2 | import isObject from 'lodash/fp/isObject'; 3 | import reduce from 'lodash/fp/reduce'; 4 | 5 | const flatten = reduce((acc, cur) => [ 6 | ...acc, 7 | cur, 8 | ...flatten(cur.children), 9 | ], []); 10 | 11 | export default (data) => { 12 | if (isArray(data)) { 13 | return flatten(data); 14 | } 15 | 16 | if (isObject(data)) { 17 | return flatten([data]); 18 | } 19 | 20 | return undefined; 21 | }; 22 | -------------------------------------------------------------------------------- /src/flattenToIds.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/fp/isArray'; 2 | import isObject from 'lodash/fp/isObject'; 3 | import reduce from 'lodash/fp/reduce'; 4 | 5 | const flattenToIds = reduce((acc, cur) => [ 6 | ...acc, 7 | cur.id, 8 | ...flattenToIds(cur.children), 9 | ], []); 10 | 11 | export default (data) => { 12 | if (isArray(data)) { 13 | return flattenToIds(data); 14 | } 15 | 16 | if (isObject(data)) { 17 | return flattenToIds([data]); 18 | } 19 | 20 | return undefined; 21 | }; 22 | -------------------------------------------------------------------------------- /src/flattenToMap.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/fp/isArray'; 2 | import isObject from 'lodash/fp/isObject'; 3 | import omit from 'lodash/fp/omit'; 4 | import reduce from 'lodash/fp/reduce'; 5 | 6 | const flattenToMap = reduce((acc, cur) => ({ 7 | ...acc, 8 | [cur.id]: omit('children', cur), 9 | ...flattenToMap(cur.children), 10 | }), {}); 11 | 12 | export default (data) => { 13 | if (isArray(data)) { 14 | return flattenToMap(data); 15 | } 16 | 17 | if (isObject(data)) { 18 | return flattenToMap([data]); 19 | } 20 | 21 | return undefined; 22 | }; 23 | -------------------------------------------------------------------------------- /src/hardMap.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import map from 'lodash/fp/map'; 6 | 7 | function hardMap(iteratee, xs, metadata = {}) { 8 | return map(x => iteratee(x, metadata), xs); 9 | } 10 | 11 | export default curry((iteratee, data) => { 12 | if (isArray(data)) { 13 | return hardMap(iteratee, data); 14 | } 15 | 16 | if (isObject(data)) { 17 | return first(hardMap(iteratee, [data])); 18 | } 19 | 20 | return undefined; 21 | }); 22 | -------------------------------------------------------------------------------- /src/hardMapBy.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import map from 'lodash/fp/map'; 6 | 7 | function hardMapBy(predicate, iteratee, xs, metadata = {}) { 8 | return map(x => (predicate(x, metadata) 9 | ? iteratee(x, metadata) 10 | : { 11 | ...x, 12 | children: hardMapBy( 13 | predicate, 14 | iteratee, 15 | x.children, 16 | { 17 | parent: x, 18 | }), 19 | }), 20 | xs, 21 | ); 22 | } 23 | 24 | export default curry((predicate, iteratee, data) => { 25 | if (isArray(data)) { 26 | return hardMapBy(predicate, iteratee, data); 27 | } 28 | 29 | if (isObject(data)) { 30 | return first(hardMapBy(predicate, iteratee, [data])); 31 | } 32 | 33 | return undefined; 34 | }); 35 | -------------------------------------------------------------------------------- /src/hasId.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import getOr from 'lodash/fp/getOr'; 3 | import isObject from 'lodash/fp/isObject'; 4 | 5 | function hasId(id, data) { 6 | if (isObject(data)) { 7 | return getOr('', 'id', data) === id; 8 | } 9 | 10 | return undefined; 11 | } 12 | 13 | export default curry(hasId); 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import addChildBy from './addChildBy'; 2 | import changeParent from './changeParent'; 3 | import each from './each'; 4 | import findOr from './findOr'; 5 | import flatten from './flatten'; 6 | import flattenToMap from './flattenToMap'; 7 | import hardMap from './hardMap'; 8 | import hardMapBy from './hardMapBy'; 9 | import hasId from './hasId'; 10 | import reduce from './reduce'; 11 | import reduceAncestryBy from './reduceAncestryBy'; 12 | import reject from './reject'; 13 | import replaceChildrenBy from './replaceChildrenBy'; 14 | import softMap from './softMap'; 15 | import softMapBy from './softMapBy'; 16 | import superflatten from './superflatten'; 17 | 18 | export default { 19 | addChildBy, 20 | changeParent, 21 | each, 22 | findOr, 23 | flatten, 24 | flattenToMap, 25 | hardMap, 26 | hardMapBy, 27 | hasId, 28 | reduce, 29 | reduceAncestryBy, 30 | reject, 31 | replaceChildrenBy, 32 | softMap, 33 | softMapBy, 34 | superflatten, 35 | }; 36 | -------------------------------------------------------------------------------- /src/reduce.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import isArray from 'lodash/fp/isArray'; 3 | import isObject from 'lodash/fp/isObject'; 4 | import lodashReduce from 'lodash/fp/reduce'; 5 | 6 | function reduce(reducer, startingValue, xs, metadata = {}) { 7 | return lodashReduce((acc, cur) => 8 | reduce( 9 | reducer, 10 | reducer(acc, cur, metadata), 11 | cur.children, 12 | { parent: cur }, 13 | ), 14 | startingValue, 15 | xs, 16 | ); 17 | } 18 | 19 | export default curry((reducer, startingValue, data) => { 20 | if (isArray(data)) { 21 | return reduce(reducer, startingValue, data); 22 | } 23 | 24 | if (isObject(data)) { 25 | return reduce(reducer, startingValue, [data]); 26 | } 27 | 28 | return undefined; 29 | }); 30 | -------------------------------------------------------------------------------- /src/reduceAncestryBy.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import isArray from 'lodash/fp/isArray'; 3 | import isObject from 'lodash/fp/isObject'; 4 | import findOr from './findOr'; 5 | 6 | function reduceAncestryBy(predicate, reducer, acc, xs) { 7 | const cur = findOr('notfound', predicate, xs); 8 | 9 | if (cur === 'notfound') return acc; 10 | 11 | return reduceAncestryBy( 12 | x => x.id === cur.parentId, 13 | reducer, 14 | reducer(acc, cur), 15 | xs, 16 | ); 17 | } 18 | 19 | export default curry((predicate, reducer, startingValue, data) => { 20 | if (isArray(data)) { 21 | return reduceAncestryBy(predicate, reducer, startingValue, data); 22 | } 23 | 24 | if (isObject(data)) { 25 | return reduceAncestryBy(predicate, reducer, startingValue, [data]); 26 | } 27 | 28 | return undefined; 29 | }); 30 | -------------------------------------------------------------------------------- /src/reject.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import lodashReject from 'lodash/fp/reject'; 6 | import map from 'lodash/fp/map'; 7 | 8 | function reject(predicate, xs) { 9 | return map(x => ({ 10 | ...x, 11 | children: reject(predicate, x.children), 12 | }), lodashReject(predicate, xs)); 13 | } 14 | 15 | export default curry((predicate, data) => { 16 | if (isArray(data)) { 17 | return reject(predicate, data); 18 | } 19 | 20 | if (isObject(data)) { 21 | return first(reject(predicate, [data])); 22 | } 23 | 24 | return undefined; 25 | }); 26 | -------------------------------------------------------------------------------- /src/replaceChildrenBy.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import hardMapBy from './hardMapBy'; 6 | 7 | function replaceChildrenBy(predicate, getNewChildren, xs) { 8 | return hardMapBy( 9 | predicate, 10 | (x, metadata) => ({ ...x, children: getNewChildren(x, metadata) }), 11 | xs, 12 | ); 13 | } 14 | 15 | export default curry((predicate, getNewChildren, data) => { 16 | if (isArray(data)) { 17 | return replaceChildrenBy(predicate, getNewChildren, data); 18 | } 19 | 20 | if (isObject(data)) { 21 | return first(replaceChildrenBy(predicate, getNewChildren, [data])); 22 | } 23 | 24 | return undefined; 25 | }); 26 | -------------------------------------------------------------------------------- /src/softMap.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import map from 'lodash/fp/map'; 6 | 7 | function softMap(iteratee, xs, metadata = {}) { 8 | return map(x => ({ 9 | ...iteratee(x, metadata), 10 | children: softMap(iteratee, x.children, { 11 | parent: x, 12 | }), 13 | }), xs); 14 | } 15 | 16 | export default curry((iteratee, data) => { 17 | if (isArray(data)) { 18 | return softMap(iteratee, data); 19 | } 20 | 21 | if (isObject(data)) { 22 | return first(softMap(iteratee, [data])); 23 | } 24 | 25 | return undefined; 26 | }); 27 | -------------------------------------------------------------------------------- /src/softMapBy.js: -------------------------------------------------------------------------------- 1 | import curry from 'lodash/fp/curry'; 2 | import first from 'lodash/fp/first'; 3 | import isArray from 'lodash/fp/isArray'; 4 | import isObject from 'lodash/fp/isObject'; 5 | import map from 'lodash/fp/map'; 6 | 7 | function softMapBy(predicate, iteratee, xs, metadata = {}) { 8 | return map(x => ({ 9 | ...(predicate(x, metadata) ? iteratee(x, metadata) : x), 10 | children: softMapBy(predicate, iteratee, x.children, { 11 | parent: x, 12 | }), 13 | }), xs); 14 | } 15 | 16 | export default curry((predicate, iteratee, data) => { 17 | if (isArray(data)) { 18 | return softMapBy(predicate, iteratee, data); 19 | } 20 | 21 | if (isObject(data)) { 22 | return first(softMapBy(predicate, iteratee, [data])); 23 | } 24 | 25 | return undefined; 26 | }); 27 | -------------------------------------------------------------------------------- /src/superflatten.js: -------------------------------------------------------------------------------- 1 | import isArray from 'lodash/fp/isArray'; 2 | import isObject from 'lodash/fp/isObject'; 3 | import omit from 'lodash/fp/omit'; 4 | import reduce from 'lodash/fp/reduce'; 5 | 6 | const superflatten = reduce((acc, cur) => [ 7 | ...acc, 8 | omit('children', cur), 9 | ...superflatten(cur.children), 10 | ], []); 11 | 12 | export default (data) => { 13 | if (isArray(data)) { 14 | return superflatten(data); 15 | } 16 | 17 | if (isObject(data)) { 18 | return superflatten([data]); 19 | } 20 | 21 | return undefined; 22 | }; 23 | --------------------------------------------------------------------------------