From 287310a4dece9a70b6a63965a8d3dfe1d385d2d7 Mon Sep 17 00:00:00 2001 From: rubeniskov Date: Mon, 23 Nov 2020 15:02:57 +0100 Subject: [PATCH] init: first commit --- .editorconfig | 19 +++ .eslintrc | 16 +++ .github/workflows/npm-publish.yml | 17 +++ .gitignore | 6 + .npmignore | 1 + .travis.yml | 12 ++ HEADER.md | 11 ++ README.md | 210 +++++++++++++++++++++++++++ index.js | 229 ++++++++++++++++++++++++++++++ package.json | 41 ++++++ test.js | 228 +++++++++++++++++++++++++++++ utils.js | 46 ++++++ 12 files changed, 836 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .github/workflows/npm-publish.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 HEADER.md create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 test.js create mode 100644 utils.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ae5bf21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://editorconfig.org/ + +# top-most EditorConfig file +root = true + +[*.md] +trim_trailing_whitespace = false + +[*.js] +trim_trailing_whitespace = true + +# Unix-style newlines with a newline ending every file +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true +max_line_length = 100 \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..770839c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,16 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "extends": "eslint:recommended", + "rules": { + "semi": [2, "always"], + "comma-dangle": ["error", "always-multiline"], + "no-multiple-empty-lines": "off" + } +} diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..184fd04 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,17 @@ +name: npm-publish +on: + release: + types: [created] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '10.x' + registry-url: 'https://registry.npmjs.org' + - run: npm install + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..134df71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +package-lock.json +.nyc_output +*.tgz +coverage diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..daa6029 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +test.js diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4ead2ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +dist: xenial +language: node_js +cache: npm +os: +- linux +env: + global: +node_js: +- '10' +script: +- npm test +- npm run coverage diff --git a/HEADER.md b/HEADER.md new file mode 100644 index 0000000..1c230ec --- /dev/null +++ b/HEADER.md @@ -0,0 +1,11 @@ +# traverse-json + +[![Build Status](https://travis-ci.org/rubeniskov/traverse-json.svg?branch=master)](https://travis-ci.org/rubeniskov/traverse-json) +![npm-publish](https://github.com/rubeniskov/traverse-json/workflows/npm-publish/badge.svg) +[![Downloads](https://img.shields.io/npm/dw/traverse-json)](https://www.npmjs.com/package/traverse-json) + +A complete traverse json function with `iterable` interface. + +## Motivation + +Many time I've encontered with the difficult task of mutate a object with an with nested properties by filtering properties using a single function, so a `traverse-json` solves this using multiple options for traversing. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f6e5cf --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# traverse-json + +[![Build Status](https://travis-ci.org/rubeniskov/traverse-json.svg?branch=master)](https://travis-ci.org/rubeniskov/traverse-json) +![npm-publish](https://github.com/rubeniskov/traverse-json/workflows/npm-publish/badge.svg) +[![Downloads](https://img.shields.io/npm/dw/traverse-json)](https://www.npmjs.com/package/traverse-json) + +A complete traverse json function with `iterable` interface. + +## Motivation + +Many time I've encontered with the difficult task of mutate a object with an with nested properties by filtering properties using a single function, so a `traverse-json` solves this using multiple options for traversing. +## Functions + +
+
traverseJson(obj, [opts])TraverseIterator
+

Create a function which traverses an object by its keys and values recursively

+
+
createIterator(obj, [opts])Iterable
+

Returns a traverseJson iterable, usefull for use it in a for loop.

+
+
+ +## Typedefs + +
+
TraverseJsonOptions : Object
+
+
TraverseIteratorResult : Object
+
+
TraverseIteratorTraverseIteratorResult
+
+
+ + + +## traverseJson(obj, [opts]) ⇒ [TraverseIterator](#TraverseIterator) +Create a function which traverses an object by its keys and values recursively + +**Kind**: global function + +| Param | Type | +| --- | --- | +| obj | Object | +| [opts] | [TraverseJsonOptions](#TraverseJsonOptions) | + +**Example** +```javascript +const traverseJson = require('.'); + +const options = {...}; + +const iterator = traverseJson({ + foo: 0, + nested: { + depth: 1, + nested: { + depth: 2, + nested: { + depth: 3, + nested: { + depth: 4, + }, + }, + }, + }, + bar: 1, +}, options); + +for (;;) { + const { done, value } = iterator(); + if (done) + break; + console.log(value); +} +``` +### Outputs +`{}` +``` +[ '/foo', 0 ] +[ '/nested/depth', 1 ] +[ '/nested/nested/depth', 2 ] +[ '/nested/nested/nested/depth', 3 ] +[ '/nested/nested/nested/nested/depth', 4 ] +[ '/bar', 1 ] +``` +`{ nested: true }` +``` +[ '/foo', 0 ] +[ '/nested', + { depth: 1, nested: { depth: 2, nested: [Object] } } ] +[ '/nested/depth', 1 ] +[ '/nested/nested', + { depth: 2, nested: { depth: 3, nested: [Object] } } ] +[ '/nested/nested/depth', 2 ] +[ '/nested/nested/nested', { depth: 3, nested: { depth: 4 } } ] +[ '/nested/nested/nested/depth', 3 ] +[ '/nested/nested/nested/nested', { depth: 4 } ] +[ '/nested/nested/nested/nested/depth', 4 ] +[ '/bar', 1 ] +``` +`{ recursive: false }` +``` +[ '/foo', 0 ] +[ '/nested', + { depth: 1, nested: { depth: 2, nested: [Object] } } ] +[ '/bar', 1 ] +``` +`{ step: 2 }` +``` +[ '/foo', 0 ] +[ '/bar', 1 ] +``` +`{ test: /depth$/ }` +``` +[ '/nested/depth', 1 ] +[ '/nested/nested/depth', 2 ] +[ '/nested/nested/nested/depth', 3 ] +[ '/nested/nested/nested/nested/depth', 4 ] +``` +`{ test: /nested$/, nested: true }` +``` +[ '/nested', + { depth: 1, nested: { depth: 2, nested: [Object] } } ] +[ '/nested/nested', + { depth: 2, nested: { depth: 3, nested: [Object] } } ] +[ '/nested/nested/nested', { depth: 3, nested: { depth: 4 } } ] +[ '/nested/nested/nested/nested', { depth: 4 } ] +``` +`{ test: "**\/{depth,foo}" }` +``` +[ '/foo', 0 ] +[ '/nested/depth', 1 ] +[ '/nested/nested/depth', 2 ] +[ '/nested/nested/nested/depth', 3 ] +[ '/nested/nested/nested/nested/depth', 4 ] +``` + + +## createIterator(obj, [opts]) ⇒ Iterable +Returns a traverseJson iterable, usefull for use it in a for loop. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| obj | Object | +| [opts] | [TraverseJsonOptions](#TraverseJsonOptions) | + +**Example** +```javascript +const { createIterator } = require('traverse-json'); +const options = +const ientries = createIterator({ + foo: 0, + nested: { + depth: 1, + nested: { + depth: 2, + nested: { + depth: 3, + nested: { + depth: 4, + }, + }, + }, + }, + bar: 1, +}, {}); + +for (let [k, v] of ientries) { + console.log(k, v); +} +```` +### Output +``` +/foo 0 +/nested/depth 1 +/nested/nested/depth 2 +/nested/nested/nested/depth 3 +/nested/nested/nested/nested/depth 4 +/bar 1 +``` + + +## TraverseJsonOptions : Object +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| [opts.recursive] | Boolean | enable/disable nested arrays and objects recursion | +| [opts.nested] | Boolean | also emit nested array or objects | +| [opts.step] | Boolean | the step to increment, default 1 | +| [opts.test] | Boolean | regexp, string minimatch or function to filter properties | + + + +## TraverseIteratorResult : Object +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| value | Array.<String, any> | +| done | Boolean | + + + +## TraverseIterator ⇒ [TraverseIteratorResult](#TraverseIteratorResult) +**Kind**: global typedef diff --git a/index.js b/index.js new file mode 100644 index 0000000..8c6faed --- /dev/null +++ b/index.js @@ -0,0 +1,229 @@ +const { + isTraversable, + createMatcher, + wrapIterator, + entries, +} = require('./utils'); + +/** + * @typedef {Object} TraverseJsonOptions + * @prop {Boolean} [opts.recursive] enable/disable nested arrays and objects recursion + * @prop {Boolean} [opts.nested] also emit nested array or objects + * @prop {Boolean} [opts.step] the step to increment, default 1 + * @prop {Boolean} [opts.test] regexp, string minimatch or function to filter properties + */ + +/** + * @typedef {Object} TraverseIteratorResult + * @prop {Array} value + * @prop {Boolean} done + */ + +/** + * @callback TraverseIterator + * @returns {TraverseIteratorResult} + */ + +/** + * Create a function which traverses an object by its keys and values recursively + * + * @param {Object} obj + * @param {TraverseJsonOptions} [opts] + * @returns {TraverseIterator} + * @example + * ```javascript + * const traverseJson = require('.'); + * + * const options = {...}; + * + * const iterator = traverseJson({ + * foo: 0, + * nested: { + * depth: 1, + * nested: { + * depth: 2, + * nested: { + * depth: 3, + * nested: { + * depth: 4, + * }, + * }, + * }, + * }, + * bar: 1, + * }, options); + * + * for (;;) { + * const { done, value } = iterator(); + * if (done) + * break; + * console.log(value); + * } + * ``` + * ### Outputs + * `{}` + * ``` + * [ '/foo', 0 ] + * [ '/nested/depth', 1 ] + * [ '/nested/nested/depth', 2 ] + * [ '/nested/nested/nested/depth', 3 ] + * [ '/nested/nested/nested/nested/depth', 4 ] + * [ '/bar', 1 ] + * ``` + * `{ nested: true }` + * ``` + * [ '/foo', 0 ] + * [ '/nested', + * { depth: 1, nested: { depth: 2, nested: [Object] } } ] + * [ '/nested/depth', 1 ] + * [ '/nested/nested', + * { depth: 2, nested: { depth: 3, nested: [Object] } } ] + * [ '/nested/nested/depth', 2 ] + * [ '/nested/nested/nested', { depth: 3, nested: { depth: 4 } } ] + * [ '/nested/nested/nested/depth', 3 ] + * [ '/nested/nested/nested/nested', { depth: 4 } ] + * [ '/nested/nested/nested/nested/depth', 4 ] + * [ '/bar', 1 ] + * ``` + * `{ recursive: false }` + * ``` + * [ '/foo', 0 ] + * [ '/nested', + * { depth: 1, nested: { depth: 2, nested: [Object] } } ] + * [ '/bar', 1 ] + * ``` + * `{ step: 2 }` + * ``` + * [ '/foo', 0 ] + * [ '/bar', 1 ] + * ``` + * `{ test: /depth$/ }` + * ``` + * [ '/nested/depth', 1 ] + * [ '/nested/nested/depth', 2 ] + * [ '/nested/nested/nested/depth', 3 ] + * [ '/nested/nested/nested/nested/depth', 4 ] + * ``` + * `{ test: /nested$/, nested: true }` + * ``` + * [ '/nested', + * { depth: 1, nested: { depth: 2, nested: [Object] } } ] + * [ '/nested/nested', + * { depth: 2, nested: { depth: 3, nested: [Object] } } ] + * [ '/nested/nested/nested', { depth: 3, nested: { depth: 4 } } ] + * [ '/nested/nested/nested/nested', { depth: 4 } ] + * ``` + * `{ test: "**\/{depth,foo}" }` + * ``` + * [ '/foo', 0 ] + * [ '/nested/depth', 1 ] + * [ '/nested/nested/depth', 2 ] + * [ '/nested/nested/nested/depth', 3 ] + * [ '/nested/nested/nested/nested/depth', 4 ] + * ``` + */ +const traverseJson = (obj, opts) => { + const { + recursive = true, + nested = false, + test = null, + step = 1, + } = { ...opts }; + + let filter = createMatcher(test); + let overall = []; + let cursor = 0; + + const dive = (value, prefix) => { + const remain = overall.slice(cursor + 1); + + overall = entries(value, prefix); + + for(let i = 0; i < remain.length; i++) { + overall.push(remain[i]); + } + return overall[cursor = 0]; + }; + + dive(obj); + + const next = () => { + + if (cursor < overall.length) { + let entry = overall[cursor]; + if (recursive) { + const [prefix, value] = entry || []; + if (isTraversable(value)) { + dive(value, prefix); + if (!nested) { + return next(); + } + } else { + cursor += step; + } + } else { + cursor += step; + } + + if (typeof filter === 'function' && !filter(entry)) { + return next(); + } + + return { value: entry, done: false }; + } + return { done: true }; + }; + + return next; +}; + +/** + * + * Returns a traverseJson iterable, usefull for use it in a for loop. + * + * @param {Object} obj + * @param {TraverseJsonOptions} [opts] + * @returns {Iterable} + * + * @example + * ```javascript + * const { createIterator } = require('traverse-json'); + * const options = + * const ientries = createIterator({ + * foo: 0, + * nested: { + * depth: 1, + * nested: { + * depth: 2, + * nested: { + * depth: 3, + * nested: { + * depth: 4, + * }, + * }, + * }, + * }, + * bar: 1, + * }, {}); + * + * for (let [k, v] of ientries) { + * console.log(k, v); + * } + * ```` + * ### Output + * ``` + * /foo 0 + * /nested/depth 1 + * /nested/nested/depth 2 + * /nested/nested/nested/depth 3 + * /nested/nested/nested/nested/depth 4 + * /bar 1 + * ``` + */ +const createIterator = (obj, opts) => { + return wrapIterator(traverseJson(obj, opts)); +}; + + +module.exports = traverseJson; +module.exports.createIterator = createIterator; diff --git a/package.json b/package.json new file mode 100644 index 0000000..acb91bd --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "traverse-json", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test": "nyc ava -v", + "docs": "{ cat HEADER.md; jsdoc2md index.js; } > README.md", + "coverage": "nyc report --reporter=lcov --reporter=text", + "report": "nyc report --reporter=html && http-server -o coverage" + }, + "husky": { + "hooks": { + "pre-commit": "npm run lint", + "pre-push": "npm run lint && npm test" + } + }, + "devDependencies": { + "ava": "^3.13.0", + "eslint": "^7.12.1", + "http-server": "^0.12.3", + "jsdoc-to-markdown": "^6.0.1", + "nyc": "^15.1.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rubeniskov/traverse-json.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/rubeniskov/traverse-json/issues" + }, + "homepage": "https://github.com/rubeniskov/traverse-json#readme", + "dependencies": { + "is-plain-object": "^5.0.0", + "minimatch": "^3.0.4" + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..46f3338 --- /dev/null +++ b/test.js @@ -0,0 +1,228 @@ +const test = require('ava'); +const traverseObject = require('.'); + +const oneDepthObject = { + a: 0, + b: 1, + c: 2, +}; + +const nestedObject = { + a: 0, + b: 1, + c: { + foo: { + bar: [1, 2, 3, { + value: { + foo: 'bar', + }, + }], + }, + }, + d: 3, +}; + +const recursiveObject = { + foo: 0, + nested: { + depth: 1, + nested: { + depth: 2, + nested: { + depth: 3, + nested: { + depth: 4, + }, + }, + }, + }, + bar: 1, +}; + +const iterateEqual = (t, iterator, expected) => { + let result; + for(let i = 0; i < expected.length; i ++) { + const { value } = iterator(); + t.deepEqual(value, expected[i]); + } + result = iterator(); + t.true(result.done); +}; + +test('should iterate through all entries of the given 1 depth object', (t) => { + + const expected = Object.entries(oneDepthObject).map(([k, v]) => [`/${k}`, v]); + const ientries = traverseObject(oneDepthObject); + + iterateEqual(t, ientries, expected); +}); + +test('should iterate through all entries of the given object recursively', (t) => { + + const expected = [ + ['/a', 0], + ['/b', 1], + ['/c/foo/bar/0', nestedObject.c.foo.bar[0]], + ['/c/foo/bar/1', nestedObject.c.foo.bar[1]], + ['/c/foo/bar/2', nestedObject.c.foo.bar[2]], + ['/c/foo/bar/3/value/foo', nestedObject.c.foo.bar[3].value.foo], + ['/d', 3], + ]; + + const ientries = traverseObject(nestedObject); + + iterateEqual(t, ientries, expected); +}); + +test('should iterate through all entries of the given object recursively including nested', (t) => { + + const expected = [ + ['/a', 0], + ['/b', 1], + ['/c', nestedObject.c], + ['/c/foo', nestedObject.c.foo], + ['/c/foo/bar', nestedObject.c.foo.bar], + ['/c/foo/bar/0', nestedObject.c.foo.bar[0]], + ['/c/foo/bar/1', nestedObject.c.foo.bar[1]], + ['/c/foo/bar/2', nestedObject.c.foo.bar[2]], + ['/c/foo/bar/3', nestedObject.c.foo.bar[3]], + ['/c/foo/bar/3/value', nestedObject.c.foo.bar[3].value], + ['/c/foo/bar/3/value/foo', nestedObject.c.foo.bar[3].value.foo], + ['/d', 3], + ]; + + const ientries = traverseObject(nestedObject, { nested: true }); + + iterateEqual(t, ientries, expected); +}); + +test('should iterate through 1 depth entries of the given object nested object', (t) => { + + const expected = [ + ['/a', 0], + ['/b', 1], + ['/c', nestedObject.c], + ['/d', 3], + ]; + + const ientries = traverseObject(nestedObject, { nested: true, recursive: false }); + + iterateEqual(t, ientries, expected); +}); + +test('should iterate filtering the path starting with "nested" though the flatten entries of the given nested object', (t) => { + + const expected = [ + ['/nested/depth', 1], + ['/nested/nested/depth', 2], + ['/nested/nested/nested/depth', 3], + ['/nested/nested/nested/nested/depth', 4], + ]; + + const ientries = traverseObject(recursiveObject, { + test: /\/nested/, + }); + + iterateEqual(t, ientries, expected, true); +}); + +test('should iterate filtering the path starting with "nested" though the entries of the given nested object', (t) => { + + const expected = [ + ['/nested', recursiveObject.nested], + ['/nested/depth', 1], + ['/nested/nested', recursiveObject.nested.nested], + ['/nested/nested/depth', 2], + ['/nested/nested/nested', recursiveObject.nested.nested.nested], + ['/nested/nested/nested/depth', 3], + ['/nested/nested/nested/nested', recursiveObject.nested.nested.nested.nested], + ['/nested/nested/nested/nested/depth', 4], + ]; + + const ientries = traverseObject(recursiveObject, { + nested: true, + test: /\/nested/, + }); + + iterateEqual(t, ientries, expected, true); +}); + +test('should iterate filtering the path with ending with "nested" though the entries of the given nested object', (t) => { + + const expected = [ + ['/nested', recursiveObject.nested], + ['/nested/nested', recursiveObject.nested.nested], + ['/nested/nested/nested', recursiveObject.nested.nested.nested], + ['/nested/nested/nested/nested', recursiveObject.nested.nested.nested.nested], + ]; + + const ientries = traverseObject(recursiveObject, { + nested: true, + test: /nested$/, + }); + + iterateEqual(t, ientries, expected, true); +}); + + +test('should iterate filtering with minimatch though the entries of the given object nested object', (t) => { + + const expected = [ + [ '/foo', 0 ], + [ '/nested/depth', 1 ], + [ '/nested/nested/depth', 2 ], + [ '/nested/nested/nested/depth', 3 ], + [ '/nested/nested/nested/nested/depth', 4 ], + [ '/c/foo/bar/3/value/foo', 'bar' ], + ]; + + const merged = { + ...recursiveObject, ...nestedObject, + }; + + const ientries = traverseObject(merged, { + test: "**/{depth,foo}", + }); + + iterateEqual(t, ientries, expected, true); +}); + +test('should iterate filtering with minimatch including options though the entries of the given object nested object', (t) => { + + const expected = []; + + const merged = { + ...recursiveObject, ...nestedObject, + }; + + const ientries = traverseObject(merged, { + test: ["**/{depth,foo}", { nobrace: true }], + }); + + iterateEqual(t, ientries, expected, true); +}); + + + +test('should works as iterable', (t) => { + + const { createIterator } = traverseObject; + const expected = [ + ['/nested', recursiveObject.nested], + ['/nested/nested', recursiveObject.nested.nested], + ['/nested/nested/nested', recursiveObject.nested.nested.nested], + ['/nested/nested/nested/nested', recursiveObject.nested.nested.nested.nested], + ]; + + const ientries = createIterator(recursiveObject, { + nested: true, + test: /nested$/, + }); + + let i = 0; + for(let [k, v] of ientries) { + t.deepEqual([k, v], expected[i++]); + } +}); + + diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..d12e087 --- /dev/null +++ b/utils.js @@ -0,0 +1,46 @@ +const { isPlainObject } = require('is-plain-object'); +const minimatch = require('minimatch'); + +const isTraversable = (value) => Array.isArray(value) || isPlainObject(value); + +const createMatcher = (test) => { + if (test && test.length) { + const [pattern, opts] = !Array.isArray(test) ? [test] : test; + test = ([path]) => minimatch(path, pattern,opts); + } + + return test && test.test ? ([path]) => test.test(path) : test; +}; + +const formatJsonPath = (prefix, key) => [prefix, key].join('/'); + +/** + * Wraps a function iteratior to become an iterable + * @param {Function} next + * @returns {Iterable} + */ +const wrapIterator = (next) => ({ + next, + [Symbol.iterator]: function() { return this; }, +}); + +const entries = (nested, prefix) => { + const target = []; + const entries = Object.entries(nested); + let i, len; + for(len = entries.length, i = 0; i < len; i++) { + const path = formatJsonPath(prefix, entries[i][0]); + const value = entries[i][1]; + const entry = [path, value]; + target.push(entry); + } + + return target; +}; + +module.exports = { + isTraversable, + createMatcher, + wrapIterator, + entries, +};