├── .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 | 7 | 8 | # treecko 9 | 10 | [](https://www.npmjs.com/package/treecko) 11 | [](https://travis-ci.org/nickjohnson-dev/treecko) 12 | [](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\nA collection of functional and immutable helpers for working with tree data structures.\n\nBoth Trees : Object and Tree Lists : Array\\ are supported.\nCurried for easy partial application.\n\nMapping\n\nsoftMap\nsoftMapBy\nhardMap\nhardMapBy\n\nReducing\n\nreduce\nreduceAncestryBy\n\nFinding\n\nfindOr\n\nFiltering\n\nfilter\nreject\n\nSide Effects\n\neach\n\nFlattening\n\nflatten\nsuperflatten\nflattenToIds\nflattenToMap\n\nRestructuring\n\nreplaceChildrenBy\naddChildBy\nchangeParent\n\nMisc\n\nhasId\n\nTODO\n\n[ ] Support different values for children key\n[ ] Support different values for id key in flattenToMap and changeParent\n[ ] Support different values for parentId key in changeParent\n[ ] Add breadth-first versions of methods\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 | --------------------------------------------------------------------------------
\n\n
A collection of functional and immutable helpers for working with tree data structures.
children
id
flattenToMap
changeParent
parentId