├── .coveralls.yml ├── .travis.yml ├── lib ├── is-last-key.js ├── key-to-string.js ├── key-requires-bracket-notation.js ├── default-shallow-clone.js ├── is-primitive.js ├── state-to-at-keypath.js ├── normalized-keypath.js ├── operations │ ├── get.js │ ├── in.js │ ├── del.js │ ├── has.js │ ├── set.js │ ├── immutable-del.js │ └── immutable-set.js ├── error-messages │ ├── key-of-undefined.js │ ├── has-operator.js │ ├── in-operator.js │ ├── cannot-shallow-clone.js │ ├── keypath-syntax.js │ └── setting-key.js ├── operations.js ├── shallow-clone-obj.js ├── create-obj.js ├── type-conversion-check.js └── keypath-reducer.js ├── del.js ├── in.js ├── get.js ├── has.js ├── set.js ├── __test__ ├── fixtures │ ├── test-function.js │ └── flatten-expand-test-cases.js ├── flatten.test.js ├── expand.test.js ├── legacy │ ├── dot-notation.legacy.js │ ├── del.legacy.test.js │ ├── mixed.legacy.test.js │ ├── expand.legacy.test.js │ └── get.legacy.test.js ├── del.test.js ├── in.test.js ├── has.test.js ├── get.test.js ├── immutable-del.test.js ├── set.test.js └── immutable-set.test.js ├── immutable-del.js ├── immutable-set.js ├── .gitignore ├── LICENSE ├── package.json ├── CHANGELOG.md ├── flatten.js ├── expand.js └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: xv6kv8SOjn9YYYT7AEVLGGGvKTITZNR9O 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "8" 6 | - "node" 7 | -------------------------------------------------------------------------------- /lib/is-last-key.js: -------------------------------------------------------------------------------- 1 | module.exports = function isLastKey (state) { 2 | return (state.i + 1) >= state.keypath.length 3 | } 4 | -------------------------------------------------------------------------------- /lib/key-to-string.js: -------------------------------------------------------------------------------- 1 | module.exports = function keyToString (key) { 2 | return typeof key === 'string' 3 | ? ("'" + key + "'") 4 | : key 5 | } 6 | -------------------------------------------------------------------------------- /lib/key-requires-bracket-notation.js: -------------------------------------------------------------------------------- 1 | var dotNotationKey = /^[A-Za-z_$][A-Za-z0-9_$]{0,}$/ 2 | 3 | module.exports = function keyRequiresBracketNotation (key) { 4 | return !dotNotationKey.test(key) 5 | } 6 | -------------------------------------------------------------------------------- /lib/default-shallow-clone.js: -------------------------------------------------------------------------------- 1 | var shallowClone = require('shallow-clone') 2 | 3 | module.exports = function defaultShallowClone (obj) { 4 | return shallowClone(typeof obj.toJSON === 'function' ? obj.toJSON() : obj) 5 | } 6 | -------------------------------------------------------------------------------- /lib/is-primitive.js: -------------------------------------------------------------------------------- 1 | module.exports = function isPrimitive (obj) { 2 | return ( 3 | typeof obj === 'undefined' || 4 | typeof obj === 'boolean' || 5 | typeof obj === 'number' || 6 | typeof obj === 'string' 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /lib/state-to-at-keypath.js: -------------------------------------------------------------------------------- 1 | module.exports = function stateToAtKeypath (state) { 2 | var offset = state.insideBracketString ? 2 : 1 3 | offset = Math.max(offset, 0) 4 | var keyEnd = state.keyStart - offset 5 | keyEnd = Math.max(keyEnd, 0) 6 | return state.keypath.slice(0, keyEnd) 7 | } 8 | -------------------------------------------------------------------------------- /del.js: -------------------------------------------------------------------------------- 1 | var delOperation = require('./lib/operations/del.js') 2 | var keypathReducer = require('./lib/keypath-reducer.js') 3 | 4 | module.exports = del 5 | 6 | function del (ctx, keypath, opts) { 7 | return keypathReducer({ 8 | ctx: ctx, 9 | keypath: keypath 10 | }, delOperation, opts) 11 | } 12 | -------------------------------------------------------------------------------- /in.js: -------------------------------------------------------------------------------- 1 | var inOperation = require('./lib/operations/in.js') 2 | var keypathReducer = require('./lib/keypath-reducer.js') 3 | 4 | module.exports = keypathIn 5 | 6 | function keypathIn (ctx, keypath, opts) { 7 | return keypathReducer({ 8 | ctx: ctx, 9 | keypath: keypath 10 | }, inOperation, opts) 11 | } 12 | -------------------------------------------------------------------------------- /get.js: -------------------------------------------------------------------------------- 1 | var getOperation = require('./lib/operations/get.js') 2 | var keypathReducer = require('./lib/keypath-reducer.js') 3 | 4 | module.exports = getKeypath 5 | 6 | function getKeypath (ctx, keypath, opts) { 7 | return keypathReducer({ 8 | ctx: ctx, 9 | keypath: keypath 10 | }, getOperation, opts) 11 | } 12 | -------------------------------------------------------------------------------- /has.js: -------------------------------------------------------------------------------- 1 | var hasOperation = require('./lib/operations/has.js') 2 | var keypathReducer = require('./lib/keypath-reducer.js') 3 | 4 | module.exports = keypathHas 5 | 6 | function keypathHas (ctx, keypath, opts) { 7 | return keypathReducer({ 8 | ctx: ctx, 9 | keypath: keypath 10 | }, hasOperation, opts) 11 | } 12 | -------------------------------------------------------------------------------- /set.js: -------------------------------------------------------------------------------- 1 | var keypathReducer = require('./lib/keypath-reducer.js') 2 | var setOperation = require('./lib/operations/set.js') 3 | 4 | module.exports = setKeypath 5 | 6 | function setKeypath (ctx, keypath, val, opts) { 7 | return keypathReducer({ 8 | ctx: ctx, 9 | keypath: keypath, 10 | val: val 11 | }, setOperation, opts) 12 | } 13 | -------------------------------------------------------------------------------- /lib/normalized-keypath.js: -------------------------------------------------------------------------------- 1 | // i just needed a way to create a unique string from keypath split, 2 | // that would not collide with other keypath split strings. 3 | // i didnt want a simple join, as that would be more likely to cause collisions 4 | module.exports = function normalizedKeypath (keypathSplit) { 5 | return keypathSplit.reduce(function (str, key, i) { 6 | str += i + ':' + key + ',' 7 | return str 8 | }, '') 9 | } 10 | -------------------------------------------------------------------------------- /lib/operations/get.js: -------------------------------------------------------------------------------- 1 | var exists = require('101/exists') 2 | var undefinedErrMessage = require('../error-messages/key-of-undefined') 3 | 4 | module.exports = function getOperation (obj, key, state, opts) { 5 | // if obj exists 6 | if (exists(obj)) return obj[key] 7 | // obj does not exist 8 | if (opts.force) return undefined 9 | // obj does not exist, and no "force" 10 | throw new TypeError(undefinedErrMessage(key, state)) 11 | } 12 | -------------------------------------------------------------------------------- /lib/error-messages/key-of-undefined.js: -------------------------------------------------------------------------------- 1 | var keyToString = require('../key-to-string') 2 | var stateToAtKeypath = require('../state-to-at-keypath') 3 | 4 | module.exports = function keyOfUndefinedErrorMessage (key, state) { 5 | var atKeypath = stateToAtKeypath(state) 6 | return "Cannot read property $keyStr of undefined (at keypath '$atKeypath' of '$keypath')" 7 | .replace(/\$atKeypath/, atKeypath) 8 | .replace(/\$keypath/, state.keypath) 9 | .replace(/\$keyStr/, keyToString(key)) 10 | } 11 | -------------------------------------------------------------------------------- /lib/error-messages/has-operator.js: -------------------------------------------------------------------------------- 1 | var keyToString = require('../key-to-string') 2 | var stateToAtKeypath = require('../state-to-at-keypath') 3 | 4 | module.exports = function keyOfUndefinedErrorMessage (msg, key, state) { 5 | var atKeypath = stateToAtKeypath(state) 6 | return "$msg (hasOwnProperty($keyStr) errored at keypath '$atKeypath' of '$keypath')" 7 | .replace(/\$atKeypath/, atKeypath) 8 | .replace(/\$keypath/, state.keypath) 9 | .replace(/\$keyStr/, keyToString(key)) 10 | .replace(/\$msg/, msg + '') 11 | } 12 | -------------------------------------------------------------------------------- /lib/error-messages/in-operator.js: -------------------------------------------------------------------------------- 1 | var keyToString = require('../key-to-string') 2 | var stateToAtKeypath = require('../state-to-at-keypath') 3 | 4 | module.exports = function keyOfUndefinedErrorMessage (key, obj, state) { 5 | var atKeypath = stateToAtKeypath(state) 6 | return "Cannot use 'in' operator to search for $keyStr in $obj (at '$atKeypath' of '$keypath')" 7 | .replace(/\$atKeypath/, atKeypath) 8 | .replace(/\$keypath/, state.keypath) 9 | .replace(/\$keyStr/, keyToString(key)) 10 | .replace(/\$obj/, obj + '') 11 | } 12 | -------------------------------------------------------------------------------- /__test__/fixtures/test-function.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | module.exports = function testFunction (fn, args, expectedVal) { 3 | if (expectedVal instanceof Error || expectedVal instanceof RegExp) { 4 | test('should error: ' + fn.name + '("' + args[1] + '")', function () { 5 | expect(function () { 6 | fn.apply(null, args) 7 | }).toThrow(expectedVal) 8 | }) 9 | } else { 10 | test('should ' + fn.name + '("' + args[1] + '")', function () { 11 | expect(fn.apply(null, args)).toBe(expectedVal) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /immutable-del.js: -------------------------------------------------------------------------------- 1 | var defaults = require('101/defaults') 2 | 3 | var defaultShallowClone = require('./lib/default-shallow-clone') 4 | var keypathReducer = require('./lib/keypath-reducer.js') 5 | var delOperation = require('./lib/operations/immutable-del.js') 6 | 7 | module.exports = immutableDelKeypath 8 | 9 | function immutableDelKeypath (ctx, keypath, opts) { 10 | opts = defaults(opts, { 11 | shallowClone: defaultShallowClone 12 | }) 13 | return keypathReducer({ 14 | ctx: ctx, 15 | keypath: keypath 16 | }, delOperation, opts) 17 | } 18 | -------------------------------------------------------------------------------- /lib/error-messages/cannot-shallow-clone.js: -------------------------------------------------------------------------------- 1 | var stateToAtKeypath = require('../state-to-at-keypath') 2 | 3 | module.exports = function cannotShallowClone (value, state, isDefault) { 4 | var atKeypath = stateToAtKeypath(state) 5 | var extra = isDefault ? ' (use opts.shallowClone for advanced cloning)' : '' 6 | return "Shallow clone returned original value ($value) at keypath '$atKeypath' of '$keypath'$extra" 7 | .replace(/\$atKeypath/, atKeypath) 8 | .replace(/\$keypath/, state.keypath) 9 | .replace(/\$value/, value) 10 | .replace(/\$extra/, extra) 11 | } 12 | -------------------------------------------------------------------------------- /__test__/flatten.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var flatten = require('../flatten') 3 | var testCases = require('./fixtures/flatten-expand-test-cases.js').common.concat( 4 | require('./fixtures/flatten-expand-test-cases.js').flatten 5 | ) 6 | 7 | describe('flatten', function () { 8 | testCases.forEach(function (testCase) { 9 | var full = testCase.full 10 | var flat = testCase.flat 11 | var opts = testCase.opts 12 | 13 | test('flatten: ' + JSON.stringify(full), function () { 14 | expect(flatten(full, opts)).toEqual(flat) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /immutable-set.js: -------------------------------------------------------------------------------- 1 | var defaults = require('101/defaults') 2 | 3 | var defaultShallowClone = require('./lib/default-shallow-clone') 4 | var keypathReducer = require('./lib/keypath-reducer.js') 5 | var setOperation = require('./lib/operations/immutable-set.js') 6 | 7 | module.exports = immutableSetKeypath 8 | 9 | function immutableSetKeypath (ctx, keypath, val, opts) { 10 | opts = defaults(opts, { 11 | shallowClone: defaultShallowClone 12 | }) 13 | return keypathReducer({ 14 | ctx: ctx, 15 | keypath: keypath, 16 | val: val 17 | }, setOperation, opts) 18 | } 19 | -------------------------------------------------------------------------------- /lib/operations/in.js: -------------------------------------------------------------------------------- 1 | var exists = require('101/exists') 2 | 3 | var getOperation = require('./get.js') 4 | var isLastKey = require('../is-last-key') 5 | var inErrMessage = require('../error-messages/in-operator') 6 | 7 | module.exports = function inOperation (obj, key, state, opts) { 8 | if (!isLastKey(state)) { 9 | return getOperation(obj, key, state, opts) 10 | } 11 | if (exists(obj)) { 12 | return key in obj 13 | } else if (opts.force) { 14 | return false 15 | } else { 16 | // no obj, no force 17 | throw new TypeError(inErrMessage(key, obj, state)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/operations/del.js: -------------------------------------------------------------------------------- 1 | var exists = require('101/exists') 2 | 3 | var getOperation = require('./get.js') 4 | var undefinedErrMessage = require('../error-messages/key-of-undefined') 5 | 6 | module.exports = function delOperation (obj, key, state, opts) { 7 | if (state.i < state.keypath.length - 1) { 8 | return getOperation(obj, key, state, opts) 9 | } 10 | if (exists(obj)) { 11 | return delete obj[key] 12 | } else if (opts.force) { 13 | // no obj, force del returns true 14 | return true 15 | } else { 16 | // no obj, no force 17 | throw new TypeError(undefinedErrMessage(key, state)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /__test__/expand.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var expand = require('../expand') 3 | var testCases = require('./fixtures/flatten-expand-test-cases.js').common 4 | 5 | describe('expand', function () { 6 | testCases.forEach(function (testCase) { 7 | var full = testCase.full 8 | var flat = testCase.flat 9 | var opts = testCase.opts 10 | var expandAssert = testCase.expandAssert 11 | 12 | test('expand: ' + JSON.stringify(flat), function () { 13 | var result = expand(flat, opts) 14 | expect(result).toEqual(full) 15 | if (expandAssert) expandAssert(testCase, result) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /lib/error-messages/keypath-syntax.js: -------------------------------------------------------------------------------- 1 | var exists = require('101/exists') 2 | 3 | module.exports = function keypathSyntaxErrorMessage (state, extra, i) { 4 | var character = exists(i) ? state.keypath.charAt(i) : state.character 5 | if (character === 'END') { 6 | return "Unexpected end of keypath '$keypath' ($extra)" 7 | .replace(/\$extra/g, extra) 8 | .replace(/\$keypath/g, state.keypath) 9 | } 10 | return "Unexpected token '$character' in keypath '$keypath' at position $i ($extra)" 11 | .replace(/\$extra/g, extra) 12 | .replace(/\$character/g, character) 13 | .replace(/\$i/g, exists(i) ? i : state.i) 14 | .replace(/\$keypath/g, state.keypath) 15 | } 16 | -------------------------------------------------------------------------------- /__test__/legacy/dot-notation.legacy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var get = require('../../get') 3 | 4 | describe('legacy tests: dot notation', function () { 5 | var obj 6 | describe("get(obj, 'foo')", function () { 7 | beforeEach(function () { 8 | obj = { 9 | foo: Math.random() 10 | } 11 | }) 12 | 13 | it('should get the value', function () { 14 | expect(get(obj, 'foo')).toEqual(obj.foo) 15 | }) 16 | }) 17 | describe("get(obj, 'foo.bar')", function () { 18 | beforeEach(function () { 19 | obj = { 20 | foo: { 21 | bar: Math.random() 22 | } 23 | } 24 | }) 25 | 26 | it('should get the value', function () { 27 | expect(get(obj, 'foo.bar')).toEqual(obj.foo.bar) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /lib/operations/has.js: -------------------------------------------------------------------------------- 1 | var exists = require('101/exists') 2 | 3 | var getOperation = require('./get.js') 4 | var isLastKey = require('../is-last-key') 5 | var hasErrMessage = require('../error-messages/has-operator') 6 | 7 | module.exports = function hasOperation (obj, key, state, opts) { 8 | // if not last key update ctx using get 9 | if (!isLastKey(state)) return getOperation(obj, key, state, opts) 10 | // obj does not exist w/ force, return true 11 | if (!exists(obj) && opts.force) return false 12 | // is last key 13 | try { 14 | return obj.hasOwnProperty(key) 15 | } catch (_err) { 16 | var isError = _err instanceof Error 17 | var msg = (isError ? _err.message : _err) 18 | var err = new Error(hasErrMessage(msg, key, state)) 19 | err.source = _err 20 | throw err 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/operations.js: -------------------------------------------------------------------------------- 1 | var exists = require('101/exists') 2 | 3 | module.exports = { 4 | del: require('./operations/del'), 5 | get: require('./operations/get'), 6 | in: require('./operations/in'), 7 | set: require('./operations/set'), 8 | has: function (obj, key, state, opts) { 9 | if (state.i === state.keypath.length - 1) { 10 | if (exists(obj) && obj.hasOwnProperty) { 11 | return obj.hasOwnProperty(key) 12 | } else if (opts.force) { 13 | return false 14 | } else { 15 | // no obj, no force 16 | var atKeypath = state.keypath.slice(0, state.bracketStart) 17 | throw new TypeError( 18 | "Cannot read property '" + key + "' of undefined (at keypath '" + atKeypath + "')" 19 | ) 20 | } 21 | } else { 22 | return this.get.apply(this, arguments) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/shallow-clone-obj.js: -------------------------------------------------------------------------------- 1 | var last = require('101/last') 2 | 3 | var isPrimitive = require('./is-primitive') 4 | var defaultShallowClone = require('./default-shallow-clone') 5 | var shallowCloneErrMessage = require('./error-messages/cannot-shallow-clone') 6 | 7 | module.exports = function shallowCloneObj (_obj, key, state, opts, log) { 8 | var obj = opts.shallowClone(_obj) 9 | 10 | if (!isPrimitive(_obj) && obj === _obj) { 11 | throw new Error(shallowCloneErrMessage(_obj, state, opts.shallowClone === defaultShallowClone)) 12 | } 13 | 14 | if (state.ctx === state.rootCtx) { 15 | // root object 16 | state.originalRootCtx = state.rootCtx 17 | state.rootCtx = state.ctx = obj 18 | } else { 19 | // not root object 20 | var objKey = last(state.keypathSplit) 21 | state.parentCtx[objKey] = state.ctx = obj 22 | } 23 | 24 | return obj 25 | } 26 | -------------------------------------------------------------------------------- /lib/create-obj.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('keypather:operations:set') 2 | var exists = require('101/exists') 3 | var last = require('101/last') 4 | 5 | module.exports = function createObj (obj, key, state, opts) { 6 | /* istanbul ignore next */ 7 | // this shouldn't happen: if obj exists, createOperation should not be called 8 | if (exists(obj)) throw new Error('createOperation should not be called if obj exists') 9 | var objKey = last(state.keypathSplit) 10 | /* istanbul ignore next */ 11 | // this shouldn't happen: objKey will exist 12 | if (!exists(objKey)) throw new Error('invalid state.keypathSplit') 13 | 14 | // normalize keypath 15 | var objKeypath = state.normalizedKeypath() 16 | var isArrayKey = typeof key === 'number' 17 | // create object 18 | obj = isArrayKey ? [] : {} 19 | state.parentCtx[objKey] = state.ctx = obj 20 | // mark object as created, for expand 21 | state.createOperation.createdKeypaths[objKeypath] = true 22 | 23 | debug('createOperation %O %O %O', { obj: obj, key: key }, opts, state) 24 | return obj 25 | } 26 | -------------------------------------------------------------------------------- /lib/error-messages/setting-key.js: -------------------------------------------------------------------------------- 1 | var isObject = require('101/is-object') 2 | 3 | var keyToString = require('../key-to-string') 4 | var stateToAtKeypath = require('../state-to-at-keypath') 5 | 6 | module.exports = function settingKey (obj, key, state) { 7 | var atKeypath = stateToAtKeypath(state) 8 | var type = typeString(obj) 9 | var keyDesc = typeof key === 'number' 10 | ? 'number key' 11 | : (type === 'array' ? 'string key' : 'key') 12 | var keyStr = typeof key === 'number' 13 | ? ('(' + keyToString(key) + ')') 14 | : keyToString(key) 15 | return "Setting $keyDesc $keyStr on $type at keypath '$atKeypath' of '$keypath')" 16 | .replace(/\$atKeypath/, atKeypath) 17 | .replace(/\$keypath/, state.keypath) 18 | .replace(/\$keyDesc/, keyDesc) 19 | .replace(/\$keyStr/, keyStr) 20 | .replace(/\$type/, type) 21 | } 22 | 23 | function typeString (obj) { 24 | if (isObject(obj)) return 'object' 25 | if (Array.isArray(obj)) return 'array' 26 | if (typeof obj === 'string') return "string '" + obj + "'" 27 | return (typeof obj) + ' ' + obj 28 | } 29 | -------------------------------------------------------------------------------- /lib/operations/set.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('keypather:operations:set') 2 | var exists = require('101/exists') 3 | 4 | var typeConversionCheck = require('../type-conversion-check') 5 | var isLastKey = require('../is-last-key') 6 | var undefinedErrMessage = require('../error-messages/key-of-undefined') 7 | 8 | module.exports = function setOperation (obj, key, state, opts) { 9 | debug('setOperation %O %O %O', { obj: obj, key: key }, opts, state) 10 | // if obj exists, set val 11 | if (exists(obj)) { 12 | obj = typeConversionCheck(obj, key, state, opts) 13 | 14 | if (isLastKey(state)) { 15 | debug('SET %O %O %O', obj, state.keypath, state.val) 16 | obj[key] = state.val 17 | } else if (!exists(obj[key]) && opts.force) { 18 | debug('TMP CREATE %O %O %O', obj, key, {}, state.keypath) 19 | var keypath = state.normalizedKeypath(state.keypathSplit.concat(key)) 20 | state.createOperation.createdKeypaths[keypath] = true 21 | obj[key] = {} 22 | } 23 | 24 | return obj[key] 25 | } 26 | 27 | throw new TypeError(undefinedErrMessage(key, state)) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tejesh Mehta 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keypather", 3 | "version": "3.1.0", 4 | "description": "Get or set a deep value using a keypath string. Supports bracket and dot notation", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard", 8 | "test-watch": "jest --coverage --watch", 9 | "test": "jest --coverage" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/tjmehta/value-for-keypath.git" 14 | }, 15 | "keywords": [ 16 | "keypath", 17 | "deep", 18 | "get", 19 | "set", 20 | "check", 21 | "existance", 22 | "value", 23 | "traversal", 24 | "dot", 25 | "bracket", 26 | "notation", 27 | "path", 28 | "array", 29 | "object", 30 | "node", 31 | "module", 32 | "key", 33 | "keys", 34 | "string" 35 | ], 36 | "author": "Tejesh Mehta", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/tjmehta/value-for-keypath/issues" 40 | }, 41 | "homepage": "https://github.com/tjmehta/value-for-keypath", 42 | "dependencies": { 43 | "101": "^1.6.2", 44 | "debug": "^3.1.0", 45 | "escape-string-regexp": "^1.0.5", 46 | "shallow-clone": "^3.0.0", 47 | "string-reduce": "^1.0.0" 48 | }, 49 | "devDependencies": { 50 | "deep-freeze-strict": "^1.1.1", 51 | "fast-deep-equal": "^3.0.0", 52 | "jest": "^21.2.1", 53 | "standard": "^12.0.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | - feature: added immutable-set 5 | - feature: should immutable-del 6 | - patch: fix issue w/ setting number indexes 7 | - patch: improved error messages 8 | - patch: improved readme 9 | 10 | ## 3.0.0 - rewrote keypather as a string parser! 11 | - breaking!: dropped support for functions 12 | - breaking!: expand doesn't support custom delimeters larger than 1 character 13 | - breaking!: export keypather's modules separately 14 | - feature: bundling keypather into web apps should be much smaller 15 | - feature: should be much more performant 16 | - patch: expand was not creating nested arrays properly [see tests](https://github.com/tjmehta/keypather/pull/26/files) 17 | - patch: this should fix various inconsistencies with parsing old 18 | 19 | ## 2.0.1 20 | - patch: bug-fix flatten removes empty nested objects 21 | 22 | ## 2.0.0 23 | - breaking!: default non-existant values to `undefined` vs `null` 24 | - patch: updated 101@v1.5.0 25 | 26 | ## 1.10.1 27 | - patch: `delimeter` option documentation for `expand` and `flatten` 28 | 29 | ## 1.10.0 30 | - feature: `keypather.expand` 31 | 32 | ## 1.9.0 33 | - feature: `keypather.flatten` 34 | - patch: fixed readme typos and examples 35 | 36 | ## 1.8.1 37 | - patch: fixed bug w/ setting keypaths w/ dots w/in brackets [see added tests](https://github.com/tjmehta/keypather/commit/0904fe7aa0f6556879170424d2781281976e7b28) 38 | 39 | ## 1.8.0 40 | - feature: added support for keypaths w/ dots w/in brackets 41 | 42 | ## 1.7.5 (and 1.7.4, same tag, whoops!) 43 | - patch: fixed bug w/ keypaths that have depth greater than three, eg. foo.bar.qux.korge 44 | 45 | ... more 46 | -------------------------------------------------------------------------------- /__test__/legacy/del.legacy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var del = require('../../del') 3 | 4 | describe('legacy tests: del', function () { 5 | describe("del(obj, 'foo.bar')", delTest('foo.bar')) 6 | describe("del(obj, '['foo'].bar')", delTest("['foo'].bar")) 7 | describe("del(obj, 'foo['bar']')", delTest("foo['bar']")) 8 | describe("del(obj, '['foo']['bar']')", delTest("['foo']['bar']")) 9 | describe("del(obj, 'foo.no.no')", delTest('foo.no.no', true)) 10 | describe("del(obj, '['foo'].no.no')", delTest("['foo'].no.no", true)) 11 | describe("del(obj, 'foo['no']['no']')", delTest("foo['no']['no']", true)) 12 | describe("del(obj, '['foo']['no']['no']')", delTest("['foo']['no']['no']", true)) 13 | describe("del(obj, 'foo.bar.boom')", delTest('foo.bar.boom', false, true)) 14 | }) 15 | 16 | function delTest (keypath, missing, nestedMissing) { 17 | var obj 18 | return function () { 19 | beforeEach(function () { 20 | obj = { 21 | foo: { 22 | bar: { 23 | boom: 'orig-value' 24 | }, 25 | qux: 1 26 | } 27 | } 28 | }) 29 | 30 | test('should del the value', function () { 31 | expect(del(obj, keypath)).toEqual(true) 32 | if (missing) { 33 | expect(obj).toEqual({ 34 | foo: { 35 | bar: { 36 | boom: 'orig-value' 37 | }, 38 | qux: 1 39 | } 40 | }) 41 | } else if (nestedMissing) { 42 | expect(obj).toEqual({ 43 | foo: { 44 | bar: {}, 45 | qux: 1 46 | } 47 | }) 48 | } else { 49 | expect(obj).toEqual({ 50 | foo: { 51 | qux: 1 52 | } 53 | }) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flatten.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('keypather:expand') 2 | var defaults = require('101/defaults') 3 | var exists = require('101/exists') 4 | var isObject = require('101/is-object') 5 | 6 | var keyRequiresBracketNotation = require('./lib/key-requires-bracket-notation') 7 | 8 | module.exports = function flatten (obj, opts) { 9 | debug('flatten %O %O', obj, opts) 10 | opts = exists(opts) ? opts : {} 11 | if (typeof opts === 'string') { 12 | opts = { delimeter: opts } 13 | } 14 | defaults(opts, { 15 | delimeter: '.', 16 | dest: {} 17 | }) 18 | 19 | var isArray = Array.isArray(obj) 20 | var keys = Object.keys(obj) 21 | 22 | return keys.reduce(function (flat, key) { 23 | var val = obj[key] 24 | 25 | // convert key to bracket key if necessary 26 | var isBracketKey = false 27 | if (isArray && /^[0-9]+$/.test(key)) { 28 | // obj is array, use bracket key 29 | isBracketKey = true 30 | key = '[' + key + ']' 31 | } else if (keyRequiresBracketNotation(key)) { 32 | // key starts with invalid char for dot key, use bracket key 33 | isBracketKey = true 34 | key = '["' + key + '"]' 35 | } 36 | 37 | // create keypath 38 | var keypath = exists(opts.parentKeypath) 39 | ? [ opts.parentKeypath, key ].join(isBracketKey ? '' : opts.delimeter) 40 | : key 41 | 42 | // check if value is flattenable 43 | if (Array.isArray(val) || isObject(val)) { 44 | // value is flattenable, continue flattenning 45 | flatten(val, { 46 | delimeter: opts.delimeter, 47 | parentKeypath: keypath, 48 | dest: flat 49 | }) 50 | } else { 51 | // value is not flattenable, set flat key 52 | flat[keypath] = val 53 | } 54 | 55 | // return flattenned object 56 | return flat 57 | }, opts.dest) 58 | } 59 | -------------------------------------------------------------------------------- /lib/operations/immutable-del.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('keypather:operations:set') 2 | var exists = require('101/exists') 3 | 4 | var getOperation = require('./get.js') 5 | var undefinedErrMessage = require('../error-messages/key-of-undefined') 6 | var shallowCloneObj = require('../shallow-clone-obj') 7 | 8 | module.exports = function immutableDelOperation (obj, key, state, opts) { 9 | debug('immutableDelOperation %O %O %O', { obj: obj, key: key }, opts, state) 10 | var objExists = exists(obj) 11 | var objKeypath = state.normalizedKeypath() 12 | var clonedObj = objExists && !state.createOperation.createdKeypaths[objKeypath] 13 | // obj exists and was not newly created 14 | ? shallowCloneObj(obj, key, state, opts) 15 | // obj dne or was newly created 16 | : obj 17 | 18 | // check if the current key is the last key 19 | if (state.i < state.keypath.length - 1) { 20 | // current key is not the last key 21 | return getOperation(clonedObj, key, state, opts) 22 | } 23 | 24 | // current key is the last key 25 | if (objExists) { 26 | // obj exists, delete the value 27 | if (key in clonedObj) { 28 | // finally delete the last key in the keypath 29 | delete clonedObj[key] 30 | if (!(key in clonedObj)) { 31 | // object was modified, return modified rootCtx 32 | debug('del success, return modified') 33 | return state.rootCtx 34 | } else { 35 | // object was NOT modified, return original rootCtx 36 | debug('del failed, return original') 37 | return state.originalRootCtx 38 | } 39 | } else { 40 | // object was NOT modified, return original rootCtx 41 | return state.originalRootCtx 42 | } 43 | } else if (opts.force) { 44 | // object was NOT modified, return original rootCtx 45 | return state.originalRootCtx 46 | } 47 | 48 | throw new TypeError(undefinedErrMessage(key, state)) 49 | } 50 | -------------------------------------------------------------------------------- /expand.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('keypather:expand') 2 | var exists = require('101/exists') 3 | var defaults = require('101/defaults') 4 | 5 | var createObj = require('./lib/create-obj') 6 | var keypathReducer = require('./lib/keypath-reducer.js') 7 | var setOperation = require('./lib/operations/set.js') 8 | 9 | module.exports = function keypatherExpand (obj, opts) { 10 | debug('expand %O %O', obj, opts) 11 | opts = exists(opts) ? opts : {} 12 | if (typeof opts === 'string') { 13 | opts = { delimeter: opts } 14 | } 15 | defaults(opts, { 16 | delimeter: '.' 17 | }) 18 | // expand requires force to create paths 19 | opts.force = true 20 | var expanded = null 21 | var sharedState = { 22 | // hack: for root key, so that the root type created 23 | // the root key will be autocreated as array or object based on key values 24 | parentCtx: { $root: expanded }, 25 | // share createOperation between "sets" 26 | createOperation: { 27 | createdKeypaths: {} 28 | } 29 | } 30 | return Object.keys(obj).reduce(function (expanded, keypath) { 31 | var val = obj[keypath] 32 | return expandKeypath(expanded, keypath, val, sharedState, opts) 33 | }, expanded) 34 | } 35 | 36 | function expandKeypath (ctx, keypath, val, sharedState, opts) { 37 | debug('expandKeypath %O', keypath) 38 | keypathReducer({ 39 | ctx: ctx, 40 | keypath: keypath, 41 | val: val, 42 | keypathSplit: ['$root'], 43 | // shared state 44 | parentCtx: sharedState.parentCtx, 45 | createOperation: sharedState.createOperation 46 | }, function expandOperation (obj, key, state, opts) { 47 | // check if a created keypath needs to be converted to an array 48 | if (!exists(obj)) { 49 | // if obj does not exist and opts.force, create 50 | obj = createObj(obj, key, state, opts) 51 | } 52 | var result = setOperation(obj, key, state, opts) 53 | return result 54 | }, opts) 55 | return sharedState.parentCtx.$root 56 | } 57 | -------------------------------------------------------------------------------- /lib/operations/immutable-set.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('keypather:operations:set') 2 | var exists = require('101/exists') 3 | 4 | var typeConversionCheck = require('../type-conversion-check') 5 | var isLastKey = require('../is-last-key') 6 | var shallowCloneObj = require('../shallow-clone-obj') 7 | var undefinedErrMessage = require('../error-messages/key-of-undefined') 8 | 9 | module.exports = function immutableSetOperation (obj, key, state, opts) { 10 | debug('immutableSetOperation %O %O %O', { obj: obj, key: key }, opts, state) 11 | 12 | if (exists(obj)) { 13 | // check if the key can be set on the obj 14 | obj = typeConversionCheck(obj, key, state, opts) 15 | 16 | // if obj exists, clone it (bc it will be modified) 17 | var objKeypath = state.normalizedKeypath() 18 | var clonedObj = state.createOperation.createdKeypaths[objKeypath] 19 | ? obj // obj was newly created, no need to clone 20 | : shallowCloneObj(obj, key, state, opts) 21 | 22 | if (isLastKey(state)) { 23 | debug('SET %O %O %O', obj, key, state.keypath, state.val, state.createOperation.createdKeypaths) 24 | // finally, set the value 25 | if (clonedObj[key] !== state.val) { 26 | clonedObj[key] = state.val 27 | // object was modified, return modified rootCtx 28 | if (clonedObj[key] === state.val) { 29 | debug('set success, return modified') 30 | return state.rootCtx 31 | } else { 32 | // object was NOT modified, return original rootCtx 33 | debug('set failed, return original') 34 | return state.originalRootCtx 35 | } 36 | } else { 37 | // object was NOT modified, return original rootCtx 38 | debug('return original') 39 | return state.originalRootCtx 40 | } 41 | } else if (!exists(clonedObj[key]) && opts.force) { 42 | debug('TMP CREATE %O %O %O', obj, key, {}, state.keypath) 43 | var keypath = state.normalizedKeypath(state.keypathSplit.concat(key)) 44 | state.createOperation.createdKeypaths[keypath] = true 45 | clonedObj[key] = {} 46 | } 47 | 48 | return clonedObj[key] 49 | } 50 | 51 | throw new TypeError(undefinedErrMessage(key, state)) 52 | } 53 | -------------------------------------------------------------------------------- /lib/type-conversion-check.js: -------------------------------------------------------------------------------- 1 | var last = require('101/last') 2 | 3 | var settingKey = require('./error-messages/setting-key') 4 | 5 | /* istanbul ignore next */ 6 | var warn = (console.warn || console.log).bind(console) 7 | 8 | // this methods check if the current key is a number 9 | // and if obj was "force" created as object. if it was, 10 | // convert it to an array. 11 | module.exports = function arrayConversionCheck (obj, key, state, opts) { 12 | // obj existance is checked outside 13 | // check for an array key (number) being set on an object 14 | 15 | var isArrayKey = typeof key === 'number' 16 | var objType = typeof obj 17 | 18 | if (isArrayKey) { 19 | // is array-key (number) 20 | if (Array.isArray(obj)) { 21 | // obj is array, return unmodified 22 | return obj 23 | } 24 | if (objType === 'object' || objType === 'function') { 25 | // obj is object or function 26 | var keypath = state.normalizedKeypath() 27 | if (state.createOperation.createdKeypaths[keypath]) { 28 | // obj was created as object, convert it to an array 29 | return convertToArray(obj, state) 30 | } 31 | // warn: setting number-key on object/function 32 | if (!opts.silent) warn(settingKey(obj, key, state)) 33 | return obj 34 | } 35 | // obj is boolean, number, or string 36 | if (opts.overwritePrimitives) { 37 | // overwrite w/ [] 38 | return setObj(state, []) 39 | } 40 | // warn: setting array-key on primitive 41 | if (!opts.silent) warn(settingKey(obj, key, state)) 42 | 43 | return obj 44 | } else { 45 | // object-key (string) 46 | if (Array.isArray(obj)) { 47 | // obj is array or function 48 | // warn: setting string-key on array/function 49 | if (!opts.silent) warn(settingKey(obj, key, state)) 50 | return obj 51 | } 52 | if (objType === 'object' || objType === 'function') { 53 | // obj is object, return unmodified 54 | return obj 55 | } 56 | // obj is boolean, number, or string 57 | if (opts.overwritePrimitives) { 58 | // overwrite w/ {} 59 | return setObj(state, {}) 60 | } 61 | // warn: setting key on primitive 62 | if (!opts.silent) warn(settingKey(obj, key, state)) 63 | 64 | return obj 65 | } 66 | } 67 | 68 | function convertToArray (val, state) { 69 | // convert the created object to an array 70 | val = Object.keys(val).reduce(function (arr, key) { 71 | arr[key] = val[key] 72 | return arr 73 | }, []) 74 | return setObj(state, val) 75 | } 76 | 77 | function setObj (state, val) { 78 | var objKey = last(state.keypathSplit) 79 | state.parentCtx[objKey] = state.ctx = val 80 | var keypath = state.normalizedKeypath() 81 | state.createOperation.createdKeypaths[keypath] = true 82 | return val 83 | } 84 | -------------------------------------------------------------------------------- /__test__/legacy/mixed.legacy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var get = require('../../get') 3 | 4 | describe('mixed', function () { 5 | var obj 6 | describe("get(obj, 'foo['bar'].baz')", function () { 7 | beforeEach(function () { 8 | obj = { 9 | foo: { 10 | bar: { 11 | baz: 'val' 12 | } 13 | } 14 | } 15 | }) 16 | 17 | it('should get the value', function () { 18 | expect(get(obj, "foo['bar'].baz")).toEqual(obj.foo.bar.baz) 19 | }) 20 | }) 21 | describe("get(obj, 'foo.bar['baz']')", function () { 22 | beforeEach(function () { 23 | obj = { 24 | foo: { 25 | bar: { 26 | baz: 'val' 27 | } 28 | } 29 | } 30 | }) 31 | 32 | it('should get the value', function () { 33 | expect(get(obj, "foo.bar['baz']")).toEqual(obj.foo.bar.baz) 34 | }) 35 | }) 36 | describe("get(obj, '['foo'].bar.baz')", function () { 37 | beforeEach(function () { 38 | obj = { 39 | foo: { 40 | bar: { 41 | baz: 'val' 42 | } 43 | } 44 | } 45 | }) 46 | 47 | it('should get the value', function () { 48 | expect(get(obj, "['foo'].bar.baz")).toEqual(obj.foo.bar.baz) 49 | }) 50 | }) 51 | describe("get(obj, '['foo'].bar.baz')", function () { 52 | beforeEach(function () { 53 | obj = { 54 | foo: { 55 | bar: { 56 | baz: 'val' 57 | } 58 | } 59 | } 60 | }) 61 | 62 | it('should get the value', function () { 63 | expect(get(obj, "['foo'].bar.baz")).toEqual(obj.foo.bar.baz) 64 | }) 65 | }) 66 | describe("get(obj, 'foo.bar['baz']')", function () { 67 | beforeEach(function () { 68 | obj = { 69 | foo: { 70 | bar: 'val' 71 | } 72 | } 73 | }) 74 | 75 | it('should get the value', function () { 76 | expect(get(obj, "foo.bar['baz']")).toEqual(obj.foo.bar.baz) 77 | }) 78 | }) 79 | describe("get(obj, 'foo['bar'].baz')", function () { 80 | beforeEach(function () { 81 | obj = { 82 | foo: { 83 | bar: { 84 | baz: 'val' 85 | } 86 | } 87 | } 88 | }) 89 | 90 | it('should get the value', function () { 91 | expect(get(obj, "foo['bar'].baz")).toEqual(obj.foo.bar.baz) 92 | }) 93 | }) 94 | describe('most complicated get', function () { 95 | beforeEach(function () { 96 | obj = { 97 | NetworkSettings: { 98 | Ports: { 99 | '15000/tcp': [ { 100 | HostIp: '0.0.0.0', 101 | HostPort: '49166' 102 | } ] 103 | } 104 | } 105 | } 106 | }) 107 | 108 | it('should get the value', function () { 109 | expect(get(obj, "NetworkSettings.Ports['15000/tcp'][0].HostPort")).toEqual(obj.NetworkSettings.Ports['15000/tcp'][0].HostPort) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /__test__/legacy/expand.legacy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var expand = require('../../expand') 3 | 4 | describe('expand', function () { 5 | var obj 6 | describe('expand(obj)', function () { 7 | describe('simple', function () { 8 | beforeEach(function () { 9 | obj = { 10 | foo: Math.random() 11 | } 12 | }) 13 | 14 | it('should get the value', function () { 15 | expect(expand(obj)).toEqual({ 16 | foo: obj.foo 17 | }) 18 | }) 19 | }) 20 | describe('nested', function () { 21 | beforeEach(function () { 22 | obj = { 23 | 'foo.bar': Math.random() 24 | } 25 | }) 26 | 27 | it('should get the value', function () { 28 | expect(expand(obj)).toEqual({ 29 | foo: { 30 | bar: obj['foo.bar'] 31 | } 32 | }) 33 | }) 34 | }) 35 | describe('mixed', function () { 36 | beforeEach(function () { 37 | obj = { 38 | 'foo.qux': 10, 39 | 'bar[0]': 1, 40 | 'bar[1].yolo[0]': 1, 41 | 'yolo': {} 42 | } 43 | }) 44 | 45 | it('should get the value', function () { 46 | expect(expand(obj)).toEqual({ 47 | foo: { 48 | qux: 10 49 | }, 50 | bar: [ 51 | 1, 52 | { 53 | yolo: [1] 54 | } 55 | ], 56 | 'yolo': {} 57 | }) 58 | }) 59 | }) 60 | }) 61 | describe('expand(arr)', function () { 62 | describe('simple', function () { 63 | beforeEach(function () { 64 | obj = { 65 | '[0]': 'foo', 66 | '[1]': 'bar' 67 | } 68 | }) 69 | 70 | it('should get the value', function () { 71 | expect(expand(obj)).toEqual([ 72 | 'foo', 73 | 'bar' 74 | ]) 75 | }) 76 | }) 77 | describe('mixed', function () { 78 | beforeEach(function () { 79 | obj = { 80 | '[0].foo.qux': 10, 81 | '[0].bar[0]': 1, 82 | '[0].bar[1].yolo': true, 83 | '[1]': 'hello' 84 | } 85 | }) 86 | 87 | it('should get the value', function () { 88 | expect(expand(obj)).toEqual([ 89 | { 90 | foo: { 91 | qux: 10 92 | }, 93 | bar: [ 94 | 1, 95 | { 96 | yolo: true 97 | } 98 | ] 99 | }, 100 | 'hello' 101 | ]) 102 | }) 103 | }) 104 | describe('delimeter', function () { 105 | beforeEach(function () { 106 | obj = { 107 | 'foo_qux': 'hello' 108 | } 109 | }) 110 | 111 | it('should expand the object', function (done) { 112 | expect(expand(obj, '_')).toEqual({ 113 | foo: { 114 | qux: 'hello' 115 | } 116 | }) 117 | done() 118 | }) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /__test__/fixtures/flatten-expand-test-cases.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | module.exports.common = [ 3 | // dot keys 4 | { 5 | full: { foo: 1 }, 6 | flat: { foo: 1 } 7 | }, 8 | 9 | { 10 | full: { 11 | _a: 1, 12 | $b: 2, 13 | Ac: 3, 14 | Zd: 4 15 | }, 16 | flat: { 17 | _a: 1, 18 | $b: 2, 19 | Ac: 3, 20 | Zd: 4 21 | } 22 | }, 23 | 24 | // bracket keys 25 | { 26 | full: [1, 2, 3], 27 | flat: { 28 | '[0]': 1, 29 | '[1]': 2, 30 | '[2]': 3 31 | } 32 | }, 33 | 34 | { 35 | full: { 36 | '!a': 1, 37 | '@b': 2, 38 | '#c': 3, 39 | '&d': 4 40 | }, 41 | flat: { 42 | '["!a"]': 1, 43 | '["@b"]': 2, 44 | '["#c"]': 3, 45 | '["&d"]': 4 46 | } 47 | }, 48 | 49 | // complex 50 | { 51 | full: { 52 | foo: { a: 1, b: 2, c: 3 }, 53 | bar: [1, 2, 3] 54 | }, 55 | flat: { 56 | 'foo.a': 1, 57 | 'foo.b': 2, 58 | 'foo.c': 3, 59 | 'bar[0]': 1, 60 | 'bar[1]': 2, 61 | 'bar[2]': 3 62 | } 63 | }, 64 | 65 | { 66 | full: { 67 | foo: { a: 1, b: 2, c: 3 }, 68 | bar: [{ a: 1 }, { b: 2 }, { c: 3 }] 69 | }, 70 | flat: { 71 | 'foo.a': 1, 72 | 'foo.b': 2, 73 | 'foo.c': 3, 74 | 'bar[0].a': 1, 75 | 'bar[1].b': 2, 76 | 'bar[2].c': 3 77 | } 78 | }, 79 | 80 | { 81 | full: [ 82 | { 83 | foo: { bar: 100 }, 84 | qux: [200, { baz: true }] 85 | }, 86 | 'hello' 87 | ], 88 | flat: { 89 | '[0].foo.bar': 100, 90 | '[0].qux[0]': 200, 91 | '[0].qux[1].baz': true, 92 | '[1]': 'hello' 93 | } 94 | }, 95 | 96 | { 97 | full: [ 98 | { 99 | foo: { 'bar.qux': 100 } 100 | }, 101 | 'hello' 102 | ], 103 | flat: { 104 | '[0].foo["bar.qux"]': 100, 105 | '[1]': 'hello' 106 | } 107 | }, 108 | 109 | { 110 | full: [ 111 | { 112 | foo: { bar: 100 }, 113 | qux: Object.assign([200, { baz: true }], { foo: 'foo' }) 114 | }, 115 | 'hello' 116 | ], 117 | flat: { 118 | '[0].foo.bar': 100, 119 | '[0].qux.foo': 'foo', 120 | '[0].qux[0]': 200, 121 | '[0].qux[1].baz': true, 122 | '[1]': 'hello' 123 | }, 124 | expandAssert: function (testCase, expanded) { 125 | expect(expanded).toEqual(expect.arrayContaining(testCase.full)) 126 | expect(expanded[0].qux).toEqual(expect.arrayContaining(testCase.full[0].qux)) 127 | } 128 | }, 129 | 130 | // opts: delimeter 131 | { 132 | full: { 133 | foo: { a: 1, b: 2, c: 3 }, 134 | bar: [1, 2, 3] 135 | }, 136 | flat: { 137 | 'foo-a': 1, 138 | 'foo-b': 2, 139 | 'foo-c': 3, 140 | 'bar[0]': 1, 141 | 'bar[1]': 2, 142 | 'bar[2]': 3 143 | }, 144 | opts: '-' 145 | } 146 | ] 147 | 148 | module.exports.flatten = [ 149 | // opts: 150 | // delimeter 151 | { 152 | full: { foo: 1 }, 153 | flat: { foo: 1 }, 154 | opts: { 155 | delimeter: ',' 156 | } 157 | }, 158 | 159 | { 160 | full: { foo: { bar: 1 } }, 161 | flat: { 'foo,bar': 1 }, 162 | opts: { 163 | delimeter: ',' 164 | } 165 | }, 166 | 167 | { 168 | full: { foo: { bar: 1 } }, 169 | flat: { 'foo,bar': 1 }, 170 | opts: ',' 171 | } 172 | ] 173 | -------------------------------------------------------------------------------- /__test__/legacy/get.legacy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var get = require('../../get') 3 | 4 | describe('legacy tests: bracket notation', function () { 5 | var obj 6 | describe("get(obj, 'foo['bar']')", function () { 7 | beforeEach(function () { 8 | obj = { 9 | foo: { 10 | bar: Math.random() 11 | } 12 | } 13 | }) 14 | 15 | it('should get the value', function () { 16 | expect(get(obj, "foo['bar']")).toEqual(obj.foo.bar) 17 | }) 18 | }) 19 | describe("get(obj, 'foo['bar']['baz']')", function () { 20 | beforeEach(function () { 21 | obj = { 22 | foo: { 23 | bar: { 24 | baz: Math.random() 25 | } 26 | } 27 | } 28 | }) 29 | 30 | it('should get the value', function () { 31 | expect(get(obj, "foo['bar']['baz']")).toEqual(obj.foo.bar.baz) 32 | }) 33 | }) 34 | describe("get(obj, '['foo.bar.baz']')", function () { 35 | beforeEach(function () { 36 | obj = { foo: { bar: { baz: 'foo.bar.bas' } } } 37 | obj['foo.bar.baz'] = Math.random() 38 | }) 39 | 40 | it('should get the value', function () { 41 | expect(get(obj, "['foo.bar.baz']")).toEqual(obj['foo.bar.baz']) 42 | }) 43 | }) 44 | describe("get(obj, 'some['foo.bar.baz']')", function () { 45 | beforeEach(function () { 46 | obj = { foo: { bar: { baz: 'foo.bar.bas' } } } 47 | obj['foo.bar.baz'] = Math.random() 48 | obj.some = { 'foo.bar.baz': Math.random() } 49 | }) 50 | 51 | it('should get the value', function () { 52 | expect(get(obj, "some['foo.bar.baz']")).toEqual(obj.some['foo.bar.baz']) 53 | }) 54 | }) 55 | describe("get(obj, '['foo.bar.baz'].some')", function () { 56 | beforeEach(function () { 57 | obj = { foo: { bar: { baz: 'foo.bar.bas' } } } 58 | obj['foo.bar.baz'] = { 59 | one: Math.random(), 60 | some: Math.random() 61 | } 62 | obj.some = { 'foo.bar.baz': Math.random() } 63 | }) 64 | 65 | it('should get the value', function () { 66 | expect(get(obj, "['foo.bar.baz'].some")).toEqual(obj['foo.bar.baz'].some) 67 | }) 68 | }) 69 | describe("get(obj, '['foo.bar.baz']['his.hers']')", function () { 70 | beforeEach(function () { 71 | obj = { foo: { bar: { baz: 'foo.bar.bas' } } } 72 | obj['foo.bar.baz'] = { 73 | one: Math.random(), 74 | some: Math.random(), 75 | 'his.hers': Math.random() 76 | } 77 | obj.some = { 'foo.bar.baz': Math.random() } 78 | }) 79 | 80 | it('should get the value', function () { 81 | expect(get(obj, "['foo.bar.baz']['his.hers']")).toEqual(obj['foo.bar.baz']['his.hers']) 82 | }) 83 | }) 84 | }) 85 | 86 | describe('legacy tests: dot notation', function () { 87 | var obj 88 | describe("get(obj, 'foo')", function () { 89 | beforeEach(function () { 90 | obj = { 91 | foo: Math.random() 92 | } 93 | }) 94 | 95 | it('should get the value', function () { 96 | expect(get(obj, 'foo')).toEqual(obj.foo) 97 | }) 98 | }) 99 | describe("get(obj, 'foo.bar')", function () { 100 | beforeEach(function () { 101 | obj = { 102 | foo: { 103 | bar: Math.random() 104 | } 105 | } 106 | }) 107 | 108 | it('should get the value', function () { 109 | expect(get(obj, 'foo.bar')).toEqual(obj.foo.bar) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /__test__/del.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var del = require('../del') 3 | 4 | function testFunction (fn, args, expectedVal, only) { 5 | var testFn = only ? test.only : test 6 | if (expectedVal instanceof Error || expectedVal instanceof RegExp) { 7 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 8 | expect(function () { 9 | fn.apply(null, args) 10 | }).toThrow(expectedVal) 11 | }) 12 | } else { 13 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 14 | expect(fn.apply(null, args)).toBe(expectedVal) 15 | }) 16 | } 17 | } 18 | 19 | testFunction.only = function (fn, args, expectedVal) { 20 | testFunction(fn, args, expectedVal, true) 21 | } 22 | 23 | var val = {} 24 | 25 | describe('del', function () { 26 | describe('path exists', function () { 27 | describe('dot notation', function () { 28 | testFunction(del, [{ foo: val }, 'foo'], true) 29 | testFunction(del, [{ foo: { bar: val } }, 'foo.bar'], true) 30 | testFunction(del, [{ foo: { bar: { qux: val } } }, 'foo.bar.qux'], true) 31 | }) 32 | 33 | describe('bracket notation', function () { 34 | describe('single quote', function () { 35 | testFunction(del, [{ foo: val }, "['foo']"], true) 36 | testFunction(del, [{ foo: { bar: val } }, "['foo']['bar']"], true) 37 | testFunction(del, [{ foo: { bar: { qux: val } } }, "['foo']['bar']['qux']"], true) 38 | testFunction(del, [{ foo: { 'dot.key': { qux: val } } }, "['foo']['dot.key']['qux']"], true) 39 | testFunction(del, [{ foo: { '[bracket.key]': { qux: val } } }, "['foo']['[bracket.key]']['qux']"], true) 40 | testFunction(del, [{ foo: { '\'quote.key\'': { qux: val } } }, "['foo'][''quote.key'']['qux']"], true) 41 | testFunction(del, [{ '[""]': val }, '["[""]"]'], true) // complex edgecase! 42 | 43 | describe('escaped', function () { 44 | testFunction(del, [{ foo: val }, '[\'foo\']'], true) 45 | testFunction(del, [{ foo: { bar: val } }, '[\'foo\'][\'bar\']'], true) 46 | testFunction(del, [{ foo: { bar: { qux: val } } }, '[\'foo\'][\'bar\'][\'qux\']'], true) 47 | testFunction(del, [{ foo: { 'dot.key': { qux: val } } }, '[\'foo\'][\'dot.key\'][\'qux\']'], true) 48 | testFunction(del, [{ foo: { '[bracket.key]': { qux: val } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']'], true) 49 | testFunction(del, [{ foo: { '\'quote.key\'': { qux: val } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']'], true) 50 | }) 51 | }) 52 | 53 | describe('double quote', function () { 54 | testFunction(del, [{ foo: val }, '["foo"]'], true) 55 | testFunction(del, [{ foo: { bar: val } }, '["foo"]["bar"]'], true) 56 | testFunction(del, [{ foo: { bar: { qux: val } } }, '["foo"]["bar"]["qux"]'], true) 57 | testFunction(del, [{ foo: { 'dot.key': { qux: val } } }, '["foo"]["dot.key"]["qux"]'], true) 58 | testFunction(del, [{ foo: { '[bracket.key]': { qux: val } } }, '["foo"]["[bracket.key]"]["qux"]'], true) 59 | testFunction(del, [{ foo: { '"quote.key"': { qux: val } } }, '["foo"][""quote.key""]["qux"]'], true) 60 | 61 | describe('escaped', function () { 62 | // eslint-disable-next-line quotes 63 | testFunction(del, [{ foo: val }, "[\"foo\"]"], true) 64 | // eslint-disable-next-line quotes 65 | testFunction(del, [{ foo: { bar: val } }, "[\"foo\"][\"bar\"]"], true) 66 | // eslint-disable-next-line quotes 67 | testFunction(del, [{ foo: { bar: { qux: val } } }, "[\"foo\"][\"bar\"][\"qux\"]"], true) 68 | // eslint-disable-next-line quotes 69 | testFunction(del, [{ foo: { '[bracket.key]': { qux: val } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]"], true) 70 | // eslint-disable-next-line quotes 71 | testFunction(del, [{ foo: { '"quote.key"': { qux: val } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]"], true) 72 | }) 73 | }) 74 | }) 75 | }) 76 | 77 | var hasUndeletableProp = {} 78 | Object.defineProperty(hasUndeletableProp, 'qux', { 79 | writable: false, 80 | value: val 81 | }) 82 | 83 | describe('path does not exist', function () { 84 | describe('force: true (default)', function () { 85 | describe('dot notation', function () { 86 | testFunction(del, [{ }, 'foo'], true) 87 | testFunction(del, [{ }, 'foo.bar.qux'], true) 88 | testFunction(del, [{ foo: {} }, 'foo.bar.qux'], true) 89 | testFunction(del, [{ foo: { bar: hasUndeletableProp } }, 'foo.bar.qux'], false) 90 | }) 91 | 92 | describe('bracket notation', function () { 93 | testFunction(del, [{ }, '["foo"]'], true) 94 | testFunction(del, [{ }, '["foo"]["bar"]'], true) 95 | testFunction(del, [{ foo: {} }, '["foo"]["bar"]["qux"]'], true) 96 | testFunction(del, [{ foo: { bar: hasUndeletableProp } }, 'foo.bar.qux'], false) 97 | testFunction(del, [{ }, '["[""]"]'], true) // complex edgecase! 98 | }) 99 | }) 100 | describe('force: false', function () { 101 | describe('dot notation', function () { 102 | testFunction(del, [{ }, 'foo', { force: false }], true) 103 | testFunction(del, [{ }, 'foo.bar.qux', { force: false }], /bar.*foo.bar.qux/) 104 | testFunction(del, [{ foo: {} }, 'foo.bar.qux', { force: false }], /qux.*foo.bar.qux/) 105 | testFunction(del, [{ foo: { bar: hasUndeletableProp } }, 'foo.bar.qux', { force: false }], false) 106 | }) 107 | 108 | describe('bracket notation', function () { 109 | testFunction(del, [{ }, '["foo"]', { force: false }], true) 110 | testFunction(del, [{ }, '["foo"]["bar"]', { force: false }], /bar.*\["foo"\]\["bar"\]/) 111 | testFunction(del, [{ foo: {} }, '["foo"]["bar"]["qux"]', { force: false }], /qux.*\["foo"\]\["bar"\]\["qux"\]/) 112 | testFunction(del, [{ foo: { bar: hasUndeletableProp } }, '["foo"]["bar"]["qux"]', { force: false }], false) 113 | }) 114 | }) 115 | }) 116 | 117 | describe('errors', function () { 118 | describe('invalid dot notation', function () { 119 | testFunction(del, [{ }, '.'], /invalid dot key/) 120 | testFunction(del, [{ }, '9'], /invalid dot key/) 121 | testFunction(del, [{ }, 'foo..bar'], /invalid dot key/) 122 | testFunction(del, [{ }, 'foo...bar'], /invalid dot key/) 123 | }) 124 | 125 | describe('invalid bracket notation', function () { 126 | testFunction(del, [{ }, '['], /Unexpected end of keypath.*invalid bracket key/) 127 | testFunction(del, [{ }, '[]'], /Unexpected token.*in keypath.*at position 1.*invalid bracket key/) 128 | testFunction(del, [{ }, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 129 | testFunction(del, [{ }, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 130 | }) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /__test__/in.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var keypathIn = require('../in') 3 | 4 | function testFunction (fn, args, expectedVal, only) { 5 | var testFn = only ? test.only : test 6 | if (expectedVal instanceof Error || expectedVal instanceof RegExp) { 7 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 8 | expect(function () { 9 | fn.apply(null, args) 10 | }).toThrow(expectedVal) 11 | }) 12 | } else { 13 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 14 | expect(fn.apply(null, args)).toBe(expectedVal) 15 | }) 16 | } 17 | } 18 | 19 | testFunction.only = function (fn, args, expectedVal) { 20 | testFunction(fn, args, expectedVal, true) 21 | } 22 | 23 | var val = {} 24 | 25 | describe('keypathIn', function () { 26 | describe('path exists', function () { 27 | describe('dot notation', function () { 28 | testFunction(keypathIn, [{ foo: val }, 'foo'], true) 29 | testFunction(keypathIn, [{ foo: { bar: val } }, 'foo.bar'], true) 30 | testFunction(keypathIn, [{ foo: { bar: { qux: val } } }, 'foo.bar.qux'], true) 31 | }) 32 | 33 | describe('bracket notation', function () { 34 | describe('single quote', function () { 35 | testFunction(keypathIn, [{ foo: val }, "['foo']"], true) 36 | testFunction(keypathIn, [{ foo: { bar: val } }, "['foo']['bar']"], true) 37 | testFunction(keypathIn, [{ foo: { bar: { qux: val } } }, "['foo']['bar']['qux']"], true) 38 | testFunction(keypathIn, [{ foo: { 'dot.key': { qux: val } } }, "['foo']['dot.key']['qux']"], true) 39 | testFunction(keypathIn, [{ foo: { '[bracket.key]': { qux: val } } }, "['foo']['[bracket.key]']['qux']"], true) 40 | testFunction(keypathIn, [{ foo: { '\'quote.key\'': { qux: val } } }, "['foo'][''quote.key'']['qux']"], true) 41 | testFunction(keypathIn, [{ '[""]': val }, '["[""]"]'], true) // complex edgecase! 42 | 43 | describe('escaped', function () { 44 | testFunction(keypathIn, [{ foo: val }, '[\'foo\']'], true) 45 | testFunction(keypathIn, [{ foo: { bar: val } }, '[\'foo\'][\'bar\']'], true) 46 | testFunction(keypathIn, [{ foo: { bar: { qux: val } } }, '[\'foo\'][\'bar\'][\'qux\']'], true) 47 | testFunction(keypathIn, [{ foo: { 'dot.key': { qux: val } } }, '[\'foo\'][\'dot.key\'][\'qux\']'], true) 48 | testFunction(keypathIn, [{ foo: { '[bracket.key]': { qux: val } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']'], true) 49 | testFunction(keypathIn, [{ foo: { '\'quote.key\'': { qux: val } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']'], true) 50 | }) 51 | }) 52 | 53 | describe('double quote', function () { 54 | testFunction(keypathIn, [{ foo: val }, '["foo"]'], true) 55 | testFunction(keypathIn, [{ foo: { bar: val } }, '["foo"]["bar"]'], true) 56 | testFunction(keypathIn, [{ foo: { bar: { qux: val } } }, '["foo"]["bar"]["qux"]'], true) 57 | testFunction(keypathIn, [{ foo: { 'dot.key': { qux: val } } }, '["foo"]["dot.key"]["qux"]'], true) 58 | testFunction(keypathIn, [{ foo: { '[bracket.key]': { qux: val } } }, '["foo"]["[bracket.key]"]["qux"]'], true) 59 | testFunction(keypathIn, [{ foo: { '"quote.key"': { qux: val } } }, '["foo"][""quote.key""]["qux"]'], true) 60 | 61 | describe('escaped', function () { 62 | // eslint-disable-next-line quotes 63 | testFunction(keypathIn, [{ foo: val }, "[\"foo\"]"], true) 64 | // eslint-disable-next-line quotes 65 | testFunction(keypathIn, [{ foo: { bar: val } }, "[\"foo\"][\"bar\"]"], true) 66 | // eslint-disable-next-line quotes 67 | testFunction(keypathIn, [{ foo: { bar: { qux: val } } }, "[\"foo\"][\"bar\"][\"qux\"]"], true) 68 | // eslint-disable-next-line quotes 69 | testFunction(keypathIn, [{ foo: { '[bracket.key]': { qux: val } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]"], true) 70 | // eslint-disable-next-line quotes 71 | testFunction(keypathIn, [{ foo: { '"quote.key"': { qux: val } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]"], true) 72 | }) 73 | }) 74 | }) 75 | }) 76 | 77 | describe('path does not exist', function () { 78 | describe('force: true (default)', function () { 79 | describe('dot notation', function () { 80 | testFunction(keypathIn, [{ }, 'foo'], false) 81 | testFunction(keypathIn, [{ }, 'foo.bar.qux'], false) 82 | testFunction(keypathIn, [{ foo: {} }, 'foo.bar.qux'], false) 83 | testFunction(keypathIn, [{ foo: { bar: Object.create({ qux: val }) } }, 'foo.bar.qux'], true) 84 | }) 85 | 86 | describe('bracket notation', function () { 87 | testFunction(keypathIn, [{ }, '["foo"]'], false) 88 | testFunction(keypathIn, [{ }, '["foo"]["bar"]'], false) 89 | testFunction(keypathIn, [{ foo: {} }, '["foo"]["bar"]["qux"]'], false) 90 | testFunction(keypathIn, [{ foo: { bar: Object.create({ qux: val }) } }, 'foo.bar.qux'], true) 91 | testFunction(keypathIn, [{ }, '["[""]"]'], false) // complex edgecase! 92 | }) 93 | }) 94 | describe('force: false', function () { 95 | describe('dot notation', function () { 96 | testFunction(keypathIn, [{ }, 'foo', { force: false }], false) 97 | testFunction(keypathIn, [{ }, 'foo.bar.qux', { force: false }], /bar.*foo.bar.qux/) 98 | testFunction(keypathIn, [{ foo: {} }, 'foo.bar.qux', { force: false }], /qux.*foo.bar.qux/) 99 | testFunction(keypathIn, [{ foo: { bar: Object.create({ qux: val }) } }, 'foo.bar.qux', { force: false }], true) 100 | }) 101 | 102 | describe('bracket notation', function () { 103 | testFunction(keypathIn, [{ }, '["foo"]', { force: false }], false) 104 | testFunction(keypathIn, [{ }, '["foo"]["bar"]', { force: false }], /bar.*\["foo"\]\["bar"\]/) 105 | testFunction(keypathIn, [{ foo: {} }, '["foo"]["bar"]["qux"]', { force: false }], /qux.*\["foo"\]\["bar"\]\["qux"\]/) 106 | testFunction(keypathIn, [{ foo: { bar: Object.create({ qux: val }) } }, '["foo"]["bar"]["qux"]', { force: false }], true) 107 | }) 108 | }) 109 | }) 110 | 111 | describe('errors', function () { 112 | describe('invalid dot notation', function () { 113 | testFunction(keypathIn, [{ }, '.'], /0.*invalid dot key/) 114 | testFunction(keypathIn, [{ }, '9'], /0.*invalid dot key/) 115 | testFunction(keypathIn, [{ }, 'foo..bar'], /4.*invalid dot key/) 116 | testFunction(keypathIn, [{ }, 'foo...bar'], /4.*invalid dot key/) 117 | }) 118 | 119 | describe('invalid bracket notation', function () { 120 | testFunction(keypathIn, [{ }, '['], /Unexpected end of keypath.*invalid bracket key/) 121 | testFunction(keypathIn, [{ }, '[]'], /1.*invalid bracket key/) 122 | testFunction(keypathIn, [{ }, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 123 | testFunction(keypathIn, [{ }, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /__test__/has.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var keypathHas = require('../has') 3 | 4 | function testFunction (fn, args, expectedVal, only) { 5 | var testFn = only ? test.only : test 6 | if (expectedVal instanceof Error || expectedVal instanceof RegExp) { 7 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 8 | expect(function () { 9 | fn.apply(null, args) 10 | }).toThrow(expectedVal) 11 | }) 12 | } else { 13 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 14 | expect(fn.apply(null, args)).toBe(expectedVal) 15 | }) 16 | } 17 | } 18 | 19 | testFunction.only = function (fn, args, expectedVal) { 20 | testFunction(fn, args, expectedVal, true) 21 | } 22 | 23 | var val = {} 24 | 25 | describe('keypathHas', function () { 26 | describe('path exists', function () { 27 | describe('dot notation', function () { 28 | testFunction(keypathHas, [{ foo: val }, 'foo'], true) 29 | testFunction(keypathHas, [{ foo: { bar: val } }, 'foo.bar'], true) 30 | testFunction(keypathHas, [{ foo: { bar: { qux: val } } }, 'foo.bar.qux'], true) 31 | }) 32 | 33 | describe('bracket notation', function () { 34 | describe('single quote', function () { 35 | testFunction(keypathHas, [{ foo: val }, "['foo']"], true) 36 | testFunction(keypathHas, [{ foo: { bar: val } }, "['foo']['bar']"], true) 37 | testFunction(keypathHas, [{ foo: { bar: { qux: val } } }, "['foo']['bar']['qux']"], true) 38 | testFunction(keypathHas, [{ foo: { 'dot.key': { qux: val } } }, "['foo']['dot.key']['qux']"], true) 39 | testFunction(keypathHas, [{ foo: { '[bracket.key]': { qux: val } } }, "['foo']['[bracket.key]']['qux']"], true) 40 | testFunction(keypathHas, [{ foo: { '\'quote.key\'': { qux: val } } }, "['foo'][''quote.key'']['qux']"], true) 41 | testFunction(keypathHas, [{ '[""]': val }, '["[""]"]'], true) // complex edgecase! 42 | 43 | describe('escaped', function () { 44 | testFunction(keypathHas, [{ foo: val }, '[\'foo\']'], true) 45 | testFunction(keypathHas, [{ foo: { bar: val } }, '[\'foo\'][\'bar\']'], true) 46 | testFunction(keypathHas, [{ foo: { bar: { qux: val } } }, '[\'foo\'][\'bar\'][\'qux\']'], true) 47 | testFunction(keypathHas, [{ foo: { 'dot.key': { qux: val } } }, '[\'foo\'][\'dot.key\'][\'qux\']'], true) 48 | testFunction(keypathHas, [{ foo: { '[bracket.key]': { qux: val } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']'], true) 49 | testFunction(keypathHas, [{ foo: { '\'quote.key\'': { qux: val } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']'], true) 50 | }) 51 | }) 52 | 53 | describe('double quote', function () { 54 | testFunction(keypathHas, [{ foo: val }, '["foo"]'], true) 55 | testFunction(keypathHas, [{ foo: { bar: val } }, '["foo"]["bar"]'], true) 56 | testFunction(keypathHas, [{ foo: { bar: { qux: val } } }, '["foo"]["bar"]["qux"]'], true) 57 | testFunction(keypathHas, [{ foo: { 'dot.key': { qux: val } } }, '["foo"]["dot.key"]["qux"]'], true) 58 | testFunction(keypathHas, [{ foo: { '[bracket.key]': { qux: val } } }, '["foo"]["[bracket.key]"]["qux"]'], true) 59 | testFunction(keypathHas, [{ foo: { '"quote.key"': { qux: val } } }, '["foo"][""quote.key""]["qux"]'], true) 60 | 61 | describe('escaped', function () { 62 | // eslint-disable-next-line quotes 63 | testFunction(keypathHas, [{ foo: val }, "[\"foo\"]"], true) 64 | // eslint-disable-next-line quotes 65 | testFunction(keypathHas, [{ foo: { bar: val } }, "[\"foo\"][\"bar\"]"], true) 66 | // eslint-disable-next-line quotes 67 | testFunction(keypathHas, [{ foo: { bar: { qux: val } } }, "[\"foo\"][\"bar\"][\"qux\"]"], true) 68 | // eslint-disable-next-line quotes 69 | testFunction(keypathHas, [{ foo: { '[bracket.key]': { qux: val } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]"], true) 70 | // eslint-disable-next-line quotes 71 | testFunction(keypathHas, [{ foo: { '"quote.key"': { qux: val } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]"], true) 72 | }) 73 | }) 74 | }) 75 | }) 76 | 77 | describe('path does not exist', function () { 78 | describe('force: true (default)', function () { 79 | describe('dot notation', function () { 80 | testFunction(keypathHas, [{ }, 'foo'], false) 81 | testFunction(keypathHas, [{ }, 'foo.bar.qux'], false) 82 | testFunction(keypathHas, [{ foo: {} }, 'foo.bar.qux'], false) 83 | testFunction(keypathHas, [{ foo: { bar: Object.create({ qux: val }) } }, 'foo.bar.qux'], false) 84 | testFunction(keypathHas, [{ foo: { hasOwnProperty: function () { throw new Error('boom') } } }, 'foo.bar'], /hasOwnProperty\('bar'\) errored at keypath 'foo' of 'foo.bar'/) 85 | /* eslint-disable */ 86 | testFunction(keypathHas, [{ foo: { hasOwnProperty: function () { throw null } } }, 'foo.bar'], /hasOwnProperty\('bar'\) errored at keypath 'foo' of 'foo.bar'/) 87 | testFunction(keypathHas, [{ foo: { hasOwnProperty: function () { throw { constructor: null } } } }, 'foo.bar'], /hasOwnProperty\('bar'\) errored at keypath 'foo' of 'foo.bar'/) 88 | /* eslint-enable */ 89 | }) 90 | 91 | describe('bracket notation', function () { 92 | testFunction(keypathHas, [{ }, '["foo"]'], false) 93 | testFunction(keypathHas, [{ }, '["foo"]["bar"]'], false) 94 | testFunction(keypathHas, [{ foo: {} }, '["foo"]["bar"]["qux"]'], false) 95 | testFunction(keypathHas, [{ foo: { bar: Object.create({ qux: val }) } }, '["foo"]["bar"]["qux"]'], false) 96 | testFunction(keypathHas, [{ }, '["[""]"]'], false) // complex edgecase! 97 | }) 98 | }) 99 | describe('force: false', function () { 100 | describe('dot notation', function () { 101 | testFunction(keypathHas, [{ }, 'foo', { force: false }], false) 102 | testFunction(keypathHas, [{ }, 'foo.bar.qux', { force: false }], /'bar' of undefined.*at keypath 'foo' of 'foo.bar/) 103 | testFunction(keypathHas, [{ foo: {} }, 'foo.bar.qux', { force: false }], /'hasOwnProperty' of undefined.*at keypath 'foo.bar' of 'foo.bar.qux/) 104 | testFunction(keypathHas, [{ foo: { bar: Object.create({ qux: val }) } }, 'foo.bar.qux', { force: false }], false) 105 | }) 106 | 107 | describe('bracket notation', function () { 108 | testFunction(keypathHas, [{ }, '["foo"]', { force: false }], false) 109 | testFunction(keypathHas, [{ }, '["foo"]["bar"]', { force: false }], /'hasOwnProperty' of undefined.*at keypath '\["foo"\]' of '\["foo"\]\["bar"\]/) 110 | testFunction(keypathHas, [{ foo: {} }, '["foo"]["bar"]["qux"]', { force: false }], /'hasOwnProperty' of undefined.*at keypath '\["foo"\]\["bar"\]' of '\["foo"\]\["bar"\]\["qux"\]/) 111 | testFunction(keypathHas, [{ foo: { bar: Object.create({ qux: val }) } }, '["foo"]["bar"]["qux"]', { force: false }], false) 112 | }) 113 | }) 114 | }) 115 | 116 | describe('errors', function () { 117 | describe('invalid dot notation', function () { 118 | testFunction(keypathHas, [{ }, '.'], /0.*invalid dot key/) 119 | testFunction(keypathHas, [{ }, '9'], /0.*invalid dot key/) 120 | testFunction(keypathHas, [{ }, 'foo..bar'], /4.*invalid dot key/) 121 | testFunction(keypathHas, [{ }, 'foo...bar'], /4.*invalid dot key/) 122 | }) 123 | 124 | var noHasOwn = {} 125 | Object.defineProperty(noHasOwn, 'hasOwnProperty', { 126 | value: null 127 | }) 128 | 129 | describe('invalid bracket notation', function () { 130 | testFunction(keypathHas, [{ }, '['], /Unexpected end of keypath.*invalid bracket key/) 131 | testFunction(keypathHas, [{ }, '[]'], /1.*invalid bracket key/) 132 | testFunction(keypathHas, [{ }, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 133 | testFunction(keypathHas, [{ }, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 134 | testFunction(keypathHas, [{ foo: { bar: noHasOwn } }, 'foo.bar.qux'], /hasOwnProperty.*qux.*foo\.bar\.qux/) 135 | }) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /lib/keypath-reducer.js: -------------------------------------------------------------------------------- 1 | var escRE = require('escape-string-regexp') 2 | var exists = require('101/exists') 3 | var defaults = require('101/defaults') 4 | var debug = require('debug')('keypather:reducer') 5 | var stringReduce = require('string-reduce') 6 | 7 | var normalizedKeypath = require('./normalized-keypath') 8 | var syntaxErrMessage = require('./error-messages/keypath-syntax') 9 | 10 | module.exports = keypatherReducer 11 | 12 | function keypatherReducer (state, operation, opts) { 13 | debug('keypather %O %O', state, opts) 14 | opts = opts || {} 15 | defaults(opts, { 16 | delimeter: '.', 17 | force: true, 18 | overwritePrimitives: true, 19 | silent: false 20 | }) 21 | var dot = opts.delimeter 22 | var keyStartRegex = new RegExp('[' + escRE(opts.delimeter) + '\\[]') 23 | var initialState = { 24 | ctx: state.ctx, 25 | parentCtx: state.parentCtx || null, 26 | rootCtx: state.ctx, 27 | // static args 28 | keypath: state.keypath, 29 | val: state.val, 30 | // loop state 31 | i: null, 32 | character: null, 33 | lastCharacter: null, 34 | // position state 35 | keypathSplit: state.keypathSplit || [], 36 | _normalizedKeypathCache: {}, 37 | normalizedKeypath: function (keypathSplit) { 38 | keypathSplit = keypathSplit || this.keypathSplit 39 | var len = keypathSplit.length 40 | this._normalizedKeypathCache[len] = 41 | this._normalizedKeypathCache[len] || 42 | normalizedKeypath(keypathSplit) 43 | if (len > 2) { 44 | // only keep one extra normalized kp in cache 45 | delete this._normalizedKeypathCache[len - 2] 46 | } 47 | return this._normalizedKeypathCache[len] 48 | }, 49 | keyStart: null, 50 | keyEnd: null, 51 | insideBracket: false, 52 | insideBracketNumber: false, 53 | insideBracketString: false, 54 | quoteCharacter: null, 55 | // operation state 56 | createOperation: state.createOperation || { 57 | createdKeypaths: { 58 | /* : true */ 59 | } 60 | } 61 | } 62 | state = stringReduce(state.keypath, reducer, initialState) 63 | state = reducer(state, 'END', state.keypath.length, initialState.keypath) 64 | function reducer (state, character, i, keypath) { 65 | var key 66 | var endFound 67 | state.i = i 68 | state.lastLastCharacter = keypath.charAt(i - 2) 69 | state.lastCharacter = state.character 70 | state.character = character 71 | debug('step %O %O %O', character, state, i) 72 | if (state.closedBracket) { 73 | endFound = character === 'END' || keyStartRegex.test(character) 74 | if (!endFound) throw new Error(syntaxErrMessage(state, 'invalid bracket key')) 75 | state.closedBracket = false 76 | state.insideBracket = character === '[' 77 | } else if (state.insideBracketString) { 78 | // state: inside bracket 79 | state.keyStart = state.keyStart || i 80 | // greedily check for key end, by checking for next section start 81 | // unfortunately this is the best I could come up with w/out being 82 | // able to check for escaped chars in js 83 | endFound = character === 'END' || keyStartRegex.test(character) 84 | debug('insideBracketString %O %O %O', character, endFound, state.lastCharacter === ']', state.lastLastCharacter === state.quoteCharacter) 85 | if (!endFound) return state 86 | if (state.lastCharacter !== ']') { 87 | if (character === 'END') throw new Error(syntaxErrMessage(state, 'invalid bracket string key')) 88 | return state 89 | } 90 | if (state.lastLastCharacter !== state.quoteCharacter) { 91 | if (character === 'END') throw new Error(syntaxErrMessage(state, 'invalid bracket string key')) 92 | return state 93 | } 94 | // state: closed bracket 95 | state.keyEnd = i - 2 96 | if (state.keyStart > state.keyEnd) throw new Error(syntaxErrMessage(state, 'invalid bracket string key', state.i - 1)) 97 | // state: inside bracket, closed string 98 | key = keypath.substring(state.keyStart, state.keyEnd) 99 | debug('handleKey bracket-string %O %O %O', key, state.ctx, character, state.keyStart, state.keyEnd, state.i) 100 | handleKey(operation, key, state, opts) 101 | state.insideBracket = state.character === '[' 102 | state.insideBracketString = false 103 | state.quoteCharacter = null 104 | } else if (state.insideBracketNumber) { 105 | // state: inside bracket, inside number 106 | if (!/^[0-9\]]$/.test(character)) throw new Error(syntaxErrMessage(state, 'invalid bracket number key')) 107 | if (character === ']') { 108 | // state: closed bracket number 109 | state.keyEnd = i 110 | key = keypath.substring(state.keyStart, state.keyEnd) 111 | key = parseInt(key, 10) 112 | // not necesary bc regex above: 113 | // if (isNaN(key)) throw new Error(syntaxErrMessage(state, 'invalid bracket number key')) 114 | debug('handleKey bracket-number %O %O %O', key, state.ctx, character) 115 | handleKey(operation, key, state, opts) 116 | state.insideBracket = false 117 | state.insideBracketNumber = false 118 | state.closedBracket = true 119 | } 120 | } else if (state.insideBracket) { 121 | // state: inside bracket 122 | if (/^["']$/.test(character)) { 123 | // inside bracket string start 124 | state.insideBracketString = true 125 | state.quoteCharacter = character 126 | } else if (/^[0-9]$/.test(character)) { 127 | // inside bracket number start 128 | state.insideBracketNumber = true 129 | state.keyStart = i 130 | } else { 131 | throw new Error(syntaxErrMessage(state, 'invalid bracket key')) 132 | } 133 | } else { 134 | if (character === 'END') { 135 | // state: keypath end 136 | if (state.lastCharacter === '.') throw new Error(syntaxErrMessage(state, 'invalid dot key')) 137 | if (!exists(state.keyStart)) return state 138 | // hack: follow dot path 139 | character = dot 140 | } 141 | if (character === dot) { 142 | if (!exists(state.keyStart)) throw new Error(syntaxErrMessage(state, 'invalid dot key')) 143 | // state: key end 144 | state.keyEnd = state.keyEnd || i 145 | key = keypath.substring(state.keyStart, state.keyEnd) 146 | debug('handleKey dot %O %O %O', key, state.ctx, character) 147 | handleKey(operation, key, state, opts) 148 | // state: dot key start 149 | } else if (character === '[') { 150 | if (i !== 0 && state.lastCharacter !== ']') { 151 | // state: dot key end 152 | if (!exists(state.keyStart)) throw new Error(syntaxErrMessage(state, 'invalid dot key')) 153 | state.keyEnd = state.keyEnd || i 154 | key = keypath.substring(state.keyStart, state.keyEnd) 155 | debug('handleKey pre-bracket %O %O %O', key, state.ctx, character) 156 | handleKey(operation, key, state, opts) 157 | } // else bracket key start after bracket 158 | // state: bracket key start 159 | state.insideBracket = true 160 | state.closedBracket = false 161 | } else { 162 | if (exists(state.keyStart)) { 163 | // state: dot key continue 164 | if (!/^[A-Za-z0-9_$]$/.test(character)) throw new Error(syntaxErrMessage(state, 'invalid dot key')) 165 | } else { 166 | // state: dot key start 167 | state.keyStart = i 168 | if (!/^[A-Za-z_$]$/.test(character)) throw new Error(syntaxErrMessage(state, 'invalid dot key')) 169 | } 170 | } 171 | } 172 | return state 173 | } 174 | return state.ctx 175 | } 176 | 177 | function handleKey (operation, key, state, opts) { 178 | debug(handleKey, key) 179 | var nextCtx = operation(state.ctx, key, state, opts) 180 | state.parentCtx = state.ctx 181 | state.ctx = nextCtx 182 | state.keyStart = null 183 | state.keyEnd = null 184 | state.keypathSplit.push(key) 185 | debug('NEXT %O', { 186 | parentCtx: state.parentCtx, 187 | ctx: state.ctx, 188 | keyStart: state.keyStart, 189 | keyEnd: state.keyEnd, 190 | keypathSplit: state.keypathSplit 191 | }) 192 | } 193 | -------------------------------------------------------------------------------- /__test__/get.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var get = require('../get') 3 | 4 | function testFunction (fn, args, expectedVal, only) { 5 | var testFn = only ? test.only : test 6 | if (expectedVal instanceof Error || expectedVal instanceof RegExp) { 7 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 8 | expect(function () { 9 | fn.apply(null, args) 10 | }).toThrow(expectedVal) 11 | }) 12 | } else { 13 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 14 | expect(fn.apply(null, args)).toBe(expectedVal) 15 | }) 16 | } 17 | } 18 | 19 | testFunction.only = function (fn, args, expectedVal) { 20 | testFunction(fn, args, expectedVal, true) 21 | } 22 | 23 | var val = {} 24 | 25 | describe('get', function () { 26 | describe('path exists', function () { 27 | describe('dot notation', function () { 28 | testFunction(get, [val, ''], val) 29 | testFunction(get, [{ foo: val }, 'foo'], val) 30 | testFunction(get, [{ foo: { bar: val } }, 'foo.bar'], val) 31 | testFunction(get, [{ foo: { bar: { qux: val } } }, 'foo.bar.qux'], val) 32 | }) 33 | 34 | describe('bracket notation', function () { 35 | describe('no quote', function () { 36 | testFunction(get, [[val], '[0]'], val) 37 | testFunction(get, [[1, 2, 3, 4, 5, 6, 7, 8, 9, 0, val], '[10]'], val) 38 | testFunction(get, [[[val]], '[0][0]'], val) 39 | testFunction(get, [[[[val]]], '[0][0][0]'], val) 40 | }) 41 | describe('single quote', function () { 42 | testFunction(get, [[[val]], '[0][0]'], val) 43 | testFunction(get, [[[val]], '[0][0]'], val) 44 | testFunction(get, [{ foo: val }, "['foo']"], val) 45 | testFunction(get, [{ foo: { bar: val } }, "['foo']['bar']"], val) 46 | testFunction(get, [{ foo: { bar: { qux: val } } }, "['foo']['bar']['qux']"], val) 47 | testFunction(get, [{ foo: { 'dot.key': { qux: val } } }, "['foo']['dot.key']['qux']"], val) 48 | testFunction(get, [{ foo: { '[bracket.key]': { qux: val } } }, "['foo']['[bracket.key]']['qux']"], val) 49 | testFunction(get, [{ foo: { '\'quote.key\'': { qux: val } } }, "['foo'][''quote.key'']['qux']"], val) 50 | testFunction(get, [{ '[""]': val }, '["[""]"]'], val) // complex edgecase! 51 | 52 | describe('escaped', function () { 53 | testFunction(get, [{ foo: val }, '[\'foo\']'], val) 54 | testFunction(get, [{ foo: { bar: val } }, '[\'foo\'][\'bar\']'], val) 55 | testFunction(get, [{ foo: { bar: { qux: val } } }, '[\'foo\'][\'bar\'][\'qux\']'], val) 56 | testFunction(get, [{ foo: { 'dot.key': { qux: val } } }, '[\'foo\'][\'dot.key\'][\'qux\']'], val) 57 | testFunction(get, [{ foo: { '[bracket.key]': { qux: val } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']'], val) 58 | testFunction(get, [{ foo: { '\'quote.key\'': { qux: val } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']'], val) 59 | }) 60 | }) 61 | 62 | describe('double quote', function () { 63 | testFunction(get, [{ foo: val }, '["foo"]'], val) 64 | testFunction(get, [{ foo: { bar: val } }, '["foo"]["bar"]'], val) 65 | testFunction(get, [{ foo: { bar: { qux: val } } }, '["foo"]["bar"]["qux"]'], val) 66 | testFunction(get, [{ foo: { 'dot.key': { qux: val } } }, '["foo"]["dot.key"]["qux"]'], val) 67 | testFunction(get, [{ foo: { '[bracket.key]': { qux: val } } }, '["foo"]["[bracket.key]"]["qux"]'], val) 68 | testFunction(get, [{ foo: { '"quote.key"': { qux: val } } }, '["foo"][""quote.key""]["qux"]'], val) 69 | 70 | describe('escaped', function () { 71 | // eslint-disable-next-line 72 | testFunction(get, [{ foo: val }, "[\"foo\"]"], val) 73 | // eslint-disable-next-line 74 | testFunction(get, [{ foo: { bar: val } }, "[\"foo\"][\"bar\"]"], val) 75 | // eslint-disable-next-line 76 | testFunction(get, [{ foo: { bar: { qux: val } } }, "[\"foo\"][\"bar\"][\"qux\"]"], val) 77 | // eslint-disable-next-line 78 | testFunction(get, [{ foo: { '[bracket.key]': { qux: val } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]"], val) 79 | // eslint-disable-next-line 80 | testFunction(get, [{ foo: { '\"quote.key\"': { qux: val } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]"], val) 81 | }) 82 | }) 83 | 84 | describe('mixed notation', function () { 85 | testFunction(get, [{ foo: { bar: val } }, 'foo["bar"]'], val) 86 | testFunction(get, [{ foo: { bar: val } }, '["foo"].bar'], val) // important 87 | }) 88 | }) 89 | }) 90 | 91 | describe('path does not exist', function () { 92 | describe('force: true (default)', function () { 93 | describe('dot notation', function () { 94 | testFunction(get, [{ }, 'foo'], undefined) 95 | testFunction(get, [{ }, 'foo.bar.qux'], undefined) 96 | testFunction(get, [{ foo: {} }, 'foo.bar.qux'], undefined) 97 | }) 98 | 99 | describe('bracket notation', function () { 100 | testFunction(get, [{ }, '["foo"]'], undefined) 101 | testFunction(get, [{ }, '["foo"]["bar"]'], undefined) 102 | testFunction(get, [{ foo: {} }, '["foo"]["bar"]["qux"]'], undefined) 103 | testFunction(get, [{ }, '["[""]"]'], undefined) // complex edgecase! 104 | }) 105 | }) 106 | describe('force: false', function () { 107 | describe('dot notation', function () { 108 | testFunction(get, [{ }, 'foo', { force: false }], undefined) 109 | testFunction(get, [{ }, 'foo.bar.qux', { force: false }], /'bar' of undefined.*at keypath 'foo' of 'foo.bar.qux'/) 110 | testFunction(get, [{ foo: {} }, 'foo.bar.qux', { force: false }], /'qux' of undefined.*at keypath 'foo.bar' of 'foo.bar.qux'/) 111 | }) 112 | 113 | describe('bracket notation', function () { 114 | testFunction(get, [{ }, '["foo"]', { force: false }], undefined) 115 | testFunction(get, [{ }, '["foo"]["bar"]', { force: false }], /'bar' of undefined.*at keypath '\["foo"\]' of '\["foo"\]\["bar"\]'/) 116 | testFunction(get, [{ foo: {} }, '["foo"]["bar"]["qux"]', { force: false }], /'qux' of undefined.*'\["foo"\]\["bar"\]' of '\["foo"\]\["bar"\]\["qux"\]'/) 117 | }) 118 | }) 119 | }) 120 | 121 | describe('errors', function () { 122 | describe('invalid dot notation', function () { 123 | testFunction(get, [{ }, '.'], /0.*invalid dot key/) 124 | testFunction(get, [{ }, '9'], /0.*invalid dot key/) 125 | testFunction(get, [{ }, 'foo..bar'], /4.*invalid dot key/) 126 | testFunction(get, [{ }, 'foo...bar'], /4.*invalid dot key/) 127 | }) 128 | 129 | describe('invalid bracket notation', function () { 130 | testFunction(get, [{ }, '['], /Unexpected end of keypath.*invalid bracket key/) 131 | testFunction(get, [{ }, '[]'], /Unexpected token.*in keypath.*at position 1.*invalid bracket key/) 132 | testFunction(get, [{ }, '["]'], /Unexpected token.*in keypath.*at position 2.*invalid bracket string key/) 133 | testFunction(get, [{ }, "[']"], /Unexpected token.*in keypath.*at position 2.*invalid bracket string key/) 134 | testFunction(get, [{ }, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 135 | testFunction(get, [{ }, '["g]'], /Unexpected end of keypath.*invalid bracket string key/) 136 | testFunction(get, [{ }, '["g].yo'], /Unexpected end of keypath.*invalid bracket string key/) 137 | testFunction(get, [{ }, 'foo.'], /Unexpected end of keypath.*invalid dot key/) 138 | testFunction(get, [{ }, '[yo]'], /Unexpected token.*in keypath.*at position 1.*invalid bracket key/) 139 | testFunction(get, [{ }, '[0]foo'], /Unexpected token.*in keypath.*at position 3.*invalid bracket key/) 140 | testFunction(get, [{ }, 'foo.[0]'], /Unexpected token.*in keypath.*at position 4.*invalid dot key/) 141 | testFunction(get, [{ }, 'f-'], /Unexpected token.*in keypath.*at position 1.*invalid dot key/) 142 | testFunction(get, [{ }, '.bar'], /Unexpected token.*in keypath.*at position 0.*invalid dot key/) 143 | testFunction(get, [{ }, 'foo..bar'], /Unexpected token.*in keypath.*at position 4.*invalid dot key/) 144 | testFunction(get, [{ }, '["0"]foo'], /Unexpected end of keypath.*invalid bracket string key/) // edge case due to js lack of ability to read escapes 145 | testFunction(get, [{ }, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 146 | }) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /__test__/immutable-del.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var deepEqual = require('fast-deep-equal') 3 | 4 | var immutableDel = require('../immutable-del') 5 | var del = require('../del') 6 | 7 | function testFunction (fn, args, expectedResult, only) { 8 | var testFn = only ? test.only : test 9 | if (expectedResult instanceof Error || expectedResult instanceof RegExp) { 10 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 11 | expect(function () { 12 | fn.apply(null, args) 13 | }).toThrow(expectedResult) 14 | }) 15 | } else { 16 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 17 | var result = fn.apply(null, args) 18 | var obj = args[0] 19 | var objUnchanged = deepEqual(result, obj) 20 | if (!objUnchanged) del.apply(null, args) 21 | expect(result).toEqual(expectedResult) 22 | if (objUnchanged) expect(result).toBe(args[0]) 23 | expect(Array.isArray(result)).toEqual(Array.isArray(expectedResult)) 24 | }) 25 | } 26 | } 27 | 28 | testFunction.only = function (fn, args, expectedVal) { 29 | testFunction(fn, args, expectedVal, true) 30 | } 31 | 32 | var val = {} 33 | var hasUndeletableProp = {} 34 | Object.defineProperty(hasUndeletableProp, 'qux', { 35 | enumerable: true, 36 | writable: false, 37 | value: val 38 | }) 39 | var hasUnclonableProp 40 | (function () { 41 | hasUnclonableProp = arguments 42 | })(1, 2, 3) 43 | 44 | var hasToJSON = { 45 | foo: 10, 46 | toJSON: function () { 47 | return { foo: 10, bar: 10 } 48 | } 49 | } 50 | function shallowClone (obj) { 51 | var out = require('shallow-clone')(obj) 52 | for (var k in obj) { 53 | Object.defineProperty(out, k, Object.getOwnPropertyDescriptor(obj, k)) 54 | } 55 | return out 56 | } 57 | function shallowCloneFail (obj) { 58 | return obj 59 | } 60 | 61 | describe('immutableDel', function () { 62 | describe('path exists', function () { 63 | describe('dot notation', function () { 64 | testFunction(immutableDel, [{ foo: val }, 'foo'], {}) 65 | testFunction(immutableDel, [hasToJSON, 'foo'], { bar: 10 }) 66 | testFunction(immutableDel, [{ foo: { bar: val } }, 'foo.bar'], { foo: {} }) 67 | testFunction(immutableDel, [{ foo: { bar: { qux: val } } }, 'foo.bar.qux'], { foo: { bar: {} } }) 68 | testFunction(immutableDel, [{ foo: { baz: hasUndeletableProp } }, 'foo.baz.qux'], { foo: { baz: {} } }) 69 | testFunction(immutableDel, [{ foo: { baz: hasUndeletableProp } }, 'foo.baz.qux', { shallowClone: shallowClone }], { foo: { baz: hasUndeletableProp } }) 70 | testFunction(immutableDel, [{ foo: { baz: hasUnclonableProp } }, 'foo.baz.qux'], /Shallow clone returned original.*opts.shallow/) 71 | testFunction(immutableDel, [{ foo: { baz: hasUndeletableProp } }, 'foo.baz.qux', { shallowClone: shallowCloneFail }], /Shallow clone returned original/) 72 | }) 73 | 74 | describe('bracket notation', function () { 75 | describe('single quote', function () { 76 | testFunction(immutableDel, [{ foo: val }, "['foo']"], {}) 77 | testFunction(immutableDel, [{ foo: { bar: val } }, "['foo']['bar']"], { foo: {} }) 78 | testFunction(immutableDel, [{ foo: { bar: { qux: val } } }, "['foo']['bar']['qux']"], { foo: { bar: {} } }) 79 | testFunction(immutableDel, [{ foo: { 'dot.key': { qux: val } } }, "['foo']['dot.key']['qux']"], { foo: { 'dot.key': {} } }) 80 | testFunction(immutableDel, [{ foo: { '[bracket.key]': { qux: val } } }, "['foo']['[bracket.key]']['qux']"], { foo: { '[bracket.key]': {} } }) 81 | testFunction(immutableDel, [{ foo: { '\'quote.key\'': { qux: val } } }, "['foo'][''quote.key'']['qux']"], { foo: { '\'quote.key\'': {} } }) 82 | testFunction(immutableDel, [{ '[""]': val }, '["[""]"]'], {}) // complex edgecase! 83 | testFunction(immutableDel, [{ foo: { baz: hasUndeletableProp } }, '["foo"]["baz"]["qux"]'], { foo: { baz: {} } }) 84 | 85 | describe('escaped', function () { 86 | testFunction(immutableDel, [{ foo: val }, '[\'foo\']'], {}) 87 | testFunction(immutableDel, [{ foo: { bar: val } }, '[\'foo\'][\'bar\']'], { foo: {} }) 88 | testFunction(immutableDel, [{ foo: { bar: { qux: val } } }, '[\'foo\'][\'bar\'][\'qux\']'], { foo: { bar: {} } }) 89 | testFunction(immutableDel, [{ foo: { 'dot.key': { qux: val } } }, '[\'foo\'][\'dot.key\'][\'qux\']'], { foo: { 'dot.key': {} } }) 90 | testFunction(immutableDel, [{ foo: { '[bracket.key]': { qux: val } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']'], { foo: { '[bracket.key]': {} } }) 91 | testFunction(immutableDel, [{ foo: { '\'quote.key\'': { qux: val } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']'], { foo: { '\'quote.key\'': {} } }) 92 | }) 93 | }) 94 | 95 | describe('double quote', function () { 96 | testFunction(immutableDel, [{ foo: val }, '["foo"]'], {}) 97 | testFunction(immutableDel, [{ foo: { bar: val } }, '["foo"]["bar"]'], { foo: {} }) 98 | testFunction(immutableDel, [{ foo: { bar: { qux: val } } }, '["foo"]["bar"]["qux"]'], { foo: { bar: {} } }) 99 | testFunction(immutableDel, [{ foo: { 'dot.key': { qux: val } } }, '["foo"]["dot.key"]["qux"]'], { foo: { 'dot.key': {} } }) 100 | testFunction(immutableDel, [{ foo: { '[bracket.key]': { qux: val } } }, '["foo"]["[bracket.key]"]["qux"]'], { foo: { '[bracket.key]': {} } }) 101 | testFunction(immutableDel, [{ foo: { '"quote.key"': { qux: val } } }, '["foo"][""quote.key""]["qux"]'], { foo: { '"quote.key"': {} } }) 102 | 103 | describe('escaped', function () { 104 | // eslint-disable-next-line quotes 105 | testFunction(immutableDel, [{ foo: val }, "[\"foo\"]"], {}) 106 | // eslint-disable-next-line quotes 107 | testFunction(immutableDel, [{ foo: { bar: val } }, "[\"foo\"][\"bar\"]"], { foo: {} }) 108 | // eslint-disable-next-line quotes 109 | testFunction(immutableDel, [{ foo: { bar: { qux: val } } }, "[\"foo\"][\"bar\"][\"qux\"]"], { foo: { bar: {} } }) 110 | // eslint-disable-next-line quotes 111 | testFunction(immutableDel, [{ foo: { '[bracket.key]': { qux: val } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]"], { foo: { '[bracket.key]': {} } }) 112 | // eslint-disable-next-line quotes 113 | testFunction(immutableDel, [{ foo: { '"quote.key"': { qux: val } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]"], { foo: { '"quote.key"': {} } }) 114 | }) 115 | }) 116 | }) 117 | }) 118 | 119 | describe('path does not exist', function () { 120 | describe('force: true (default)', function () { 121 | describe('dot notation', function () { 122 | testFunction(immutableDel, [{ }, 'foo'], { }) 123 | testFunction(immutableDel, [{ }, 'foo.bar.qux'], { }) 124 | testFunction(immutableDel, [{ foo: {} }, 'foo.bar.qux'], { foo: {} }) 125 | }) 126 | 127 | describe('bracket notation', function () { 128 | testFunction(immutableDel, [{ }, '["foo"]'], { }) 129 | testFunction(immutableDel, [{ }, '["foo"]["bar"]'], { }) 130 | testFunction(immutableDel, [{ foo: {} }, '["foo"]["bar"]["qux"]'], { foo: {} }) 131 | testFunction(immutableDel, [{ }, '["[""]"]'], { }) // complex edgecase! 132 | }) 133 | }) 134 | describe('force: false', function () { 135 | describe('dot notation', function () { 136 | testFunction(immutableDel, [{ }, 'foo', { force: false }], { }) 137 | testFunction(immutableDel, [{ }, 'foo.bar.qux', { force: false }], /bar.*foo.bar.qux/) 138 | testFunction(immutableDel, [{ foo: {} }, 'foo.bar.qux', { force: false }], /qux.*foo.bar.qux/) 139 | }) 140 | 141 | describe('bracket notation', function () { 142 | testFunction(immutableDel, [{ }, '["foo"]', { force: false }], { }) 143 | testFunction(immutableDel, [{ }, '["foo"]["bar"]', { force: false }], /bar.*\["foo"\]\["bar"\]/) 144 | testFunction(immutableDel, [{ foo: {} }, '["foo"]["bar"]["qux"]', { force: false }], /qux.*\["foo"\]\["bar"\]\["qux"\]/) 145 | }) 146 | }) 147 | }) 148 | 149 | describe('errors', function () { 150 | describe('invalid dot notation', function () { 151 | testFunction(immutableDel, [{ }, '.'], /invalid dot key/) 152 | testFunction(immutableDel, [{ }, '9'], /invalid dot key/) 153 | testFunction(immutableDel, [{ }, 'foo..bar'], /invalid dot key/) 154 | testFunction(immutableDel, [{ }, 'foo...bar'], /invalid dot key/) 155 | }) 156 | 157 | describe('invalid bracket notation', function () { 158 | testFunction(immutableDel, [{ }, '['], /Unexpected end of keypath.*invalid bracket key/) 159 | testFunction(immutableDel, [{ }, '[]'], /Unexpected token.*in keypath.*at position 1.*invalid bracket key/) 160 | testFunction(immutableDel, [{ }, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 161 | testFunction(immutableDel, [{ }, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 162 | }) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /__test__/set.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var debug = require('debug')('keypather:set.test') 3 | var set = require('../set') 4 | 5 | var old = {} 6 | var val = {} 7 | 8 | function testFunction (fn, args, expected, only) { 9 | var testFn = only ? test.only : test 10 | if (expected instanceof Error || expected instanceof RegExp) { 11 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 12 | expect(function () { 13 | fn.apply(null, args) 14 | }).toThrow(expected) 15 | }) 16 | } else { 17 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 18 | var result = fn.apply(null, args) 19 | var opts = args[3] || {} 20 | var expectedResult = opts.overwritePrimitives === false ? undefined : args[2] 21 | debug({ 22 | result: result, 23 | expectedResult: expectedResult, 24 | object: args[0], 25 | expectedObject: expected 26 | }) 27 | expect(val).toEqual({}) // safety check, since val is static 28 | expect(result).toBe(expectedResult) 29 | expect(args[0]).toEqual(expected) 30 | expect(Array.isArray(args[0])).toBe(Array.isArray(expected)) 31 | }) 32 | } 33 | } 34 | 35 | testFunction.only = function (fn, args, expected) { 36 | testFunction(fn, args, expected, true) 37 | } 38 | 39 | describe('set', function () { 40 | describe('path exists', function () { 41 | describe('dot notation', function () { 42 | testFunction(set, [{ foo: old, zfoo: 1 }, 'foo', val], { foo: val, zfoo: 1 }) 43 | testFunction(set, [{ foo: { bar: old, zbar: 2 }, zfoo: 1 }, 'foo.bar', val], { foo: { bar: val, zbar: 2 }, zfoo: 1 }) 44 | testFunction(set, [{ foo: { bar: { qux: old, zqux: 2 }, zbar: 2 }, zfoo: 1 }, 'foo.bar.qux', val], { foo: { bar: { qux: val, zqux: 2 }, zbar: 2 }, zfoo: 1 }) 45 | }) 46 | 47 | describe('bracket notation', function () { 48 | describe('single quote', function () { 49 | testFunction(set, [{ foo: old }, "['foo']", val], { foo: val }) 50 | testFunction(set, [[], '[0]', val], [val]) 51 | testFunction(set, [{}, '[0]', val], { '0': val }) 52 | testFunction(set, [[], 'foo', val], Object.assign([], {foo: val})) 53 | testFunction(set, [{}, 'foo', val], { 'foo': val }) 54 | testFunction(set, [{ foo: { bar: old } }, "['foo']['bar']", val], { foo: { bar: val } }) 55 | testFunction(set, [{ foo: { bar: { qux: old } } }, "['foo']['bar']['qux']", val], { foo: { bar: { qux: val } } }) 56 | testFunction(set, [{ foo: { 'dot.key': { qux: old } } }, "['foo']['dot.key']['qux']", val], { foo: { 'dot.key': { qux: val } } }) 57 | testFunction(set, [{ foo: { '[bracket.key]': { qux: old } } }, "['foo']['[bracket.key]']['qux']", val], { foo: { '[bracket.key]': { qux: val } } }) 58 | testFunction(set, [{ foo: { '\'quote.key\'': { qux: old } } }, "['foo'][''quote.key'']['qux']", val], { foo: { '\'quote.key\'': { qux: val } } }) 59 | 60 | describe('escaped', function () { 61 | testFunction(set, [{ foo: old }, '[\'foo\']', val], { foo: val }) 62 | testFunction(set, [{ foo: { bar: old } }, '[\'foo\'][\'bar\']', val], { foo: { bar: val } }) 63 | testFunction(set, [{ foo: { bar: { qux: old } } }, '[\'foo\'][\'bar\'][\'qux\']', val], { foo: { bar: { qux: val } } }) 64 | testFunction(set, [{ foo: { 'dot.key': { qux: old } } }, '[\'foo\'][\'dot.key\'][\'qux\']', val], { foo: { 'dot.key': { qux: val } } }) 65 | testFunction(set, [{ foo: { '[bracket.key]': { qux: old } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']', val], { foo: { '[bracket.key]': { qux: val } } }) 66 | testFunction(set, [{ foo: { '\'quote.key\'': { qux: old } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']', val], { foo: { '\'quote.key\'': { qux: val } } }) 67 | }) 68 | }) 69 | 70 | describe('double quote', function () { 71 | testFunction(set, [{ foo: old }, '["foo"]', val], { foo: val }) 72 | testFunction(set, [{ foo: { bar: old } }, '["foo"]["bar"]', val], { foo: { bar: val } }) 73 | testFunction(set, [{ foo: { bar: { qux: old } } }, '["foo"]["bar"]["qux"]', val], { foo: { bar: { qux: val } } }) 74 | testFunction(set, [{ foo: { 'dot.key': { qux: old } } }, '["foo"]["dot.key"]["qux"]', val], { foo: { 'dot.key': { qux: val } } }) 75 | testFunction(set, [{ foo: { '[bracket.key]': { qux: old } } }, '["foo"]["[bracket.key]"]["qux"]', val], { foo: { '[bracket.key]': { qux: val } } }) 76 | testFunction(set, [{ foo: { '"quote.key"': { qux: old } } }, '["foo"][""quote.key""]["qux"]', val], { foo: { '"quote.key"': { qux: val } } }) 77 | 78 | describe('escaped', function () { 79 | // eslint-disable-next-line quotes 80 | testFunction(set, [{ foo: old }, "[\"foo\"]", val], { foo: val }) 81 | // eslint-disable-next-line quotes 82 | testFunction(set, [{ foo: { bar: old } }, "[\"foo\"][\"bar\"]", val], { foo: { bar: val } }) 83 | // eslint-disable-next-line quotes 84 | testFunction(set, [{ foo: { bar: { qux: old } } }, "[\"foo\"][\"bar\"][\"qux\"]", val], { foo: { bar: { qux: val } } }) 85 | // eslint-disable-next-line quotes 86 | testFunction(set, [{ foo: { '[bracket.key]': { qux: old } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]", val], { foo: { '[bracket.key]': { qux: val } } }) 87 | // eslint-disable-next-line quotes 88 | testFunction(set, [{ foo: { '"quote.key"': { qux: old } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]", val], { foo: { '"quote.key"': { qux: val } } }) 89 | }) 90 | }) 91 | }) 92 | 93 | describe('overwrite', () => { 94 | testFunction(set, [{ foo: 1 }, 'foo.bar', val], { foo: { bar: val } }) 95 | testFunction(set, [{ foo: 1 }, 'foo.qux', val, {overwritePrimitives: false}], { foo: 1 }) 96 | testFunction(set, [{ foo: 1 }, 'foo[0]', val], { foo: [val] }) 97 | testFunction(set, [{ foo: 1 }, 'foo[0]', val, {overwritePrimitives: false}], { foo: 1 }) 98 | testFunction(set, [{ foo: 'str' }, 'foo.bar', val, {overwritePrimitives: false}], { foo: 'str' }) 99 | }) 100 | 101 | describe('silent', () => { 102 | var opts1 = {silent: true} 103 | var opts2 = {overwritePrimitives: false, silent: true} 104 | testFunction(set, [{ foo: [] }, 'foo.qux', val, opts1], { foo: Object.assign([], { qux: val }) }) 105 | testFunction(set, [{ foo: {} }, 'foo[0]', val, opts1], { foo: {'0': val} }) 106 | testFunction(set, [{ foo: 1 }, 'foo.qux', val, opts2], { foo: 1 }) 107 | testFunction(set, [{ foo: 1 }, 'foo[0]', val, opts2], { foo: 1 }) 108 | }) 109 | }) 110 | 111 | describe('path does not exist', function () { 112 | describe('force: true (default)', function () { 113 | describe('dot notation', function () { 114 | testFunction(set, [{}, 'foo', val], { foo: val }) 115 | testFunction(set, [{}, 'foo.bar', val], { foo: { bar: val } }) 116 | testFunction(set, [{ foo: {} }, 'foo.bar.qux', val], { foo: { bar: { qux: val } } }) 117 | testFunction(set, [{}, 'foo[0]', val], { foo: [val] }) 118 | testFunction(set, [{foo: {}}, 'foo[0]', val], { foo: { '0': val } }) 119 | }) 120 | 121 | describe('bracket notation', function () { 122 | testFunction(set, [{}, '["foo"]', val], { foo: val }) 123 | testFunction(set, [{}, '["foo"]["bar"]', val], { foo: { bar: val } }) 124 | testFunction(set, [{ foo: {} }, '["foo"]["bar"]["qux"]', val], { foo: { bar: { qux: val } } }) 125 | testFunction(set, [{}, '["[""]"]', val], { '[""]': val }) // complex edgecase! 126 | }) 127 | }) 128 | 129 | describe('force: false', function () { 130 | describe('dot notation', function () { 131 | testFunction(set, [{}, 'foo', val, { force: false }], { foo: val }) 132 | testFunction(set, [{}, 'foo.bar.qux', val, { force: false }], /'bar' of undefined.*at keypath 'foo' of 'foo.bar.qux'/) 133 | testFunction(set, [{ foo: {} }, 'foo.bar.qux', val, { force: false }], /'qux' of undefined.*at keypath 'foo.bar' of 'foo.bar.qux'/) 134 | }) 135 | 136 | describe('bracket notation', function () { 137 | testFunction(set, [{}, '["foo"]', val, { force: false }], { foo: val }) 138 | testFunction(set, [{}, '["foo"]["bar"]', val, { force: false }], /'bar' of undefined.*at keypath '\["foo"\]' of '\["foo"\]\["bar"\]'/) 139 | testFunction(set, [{ foo: {} }, '["foo"]["bar"]["qux"]', val, { force: false }], /'qux' of undefined.*at keypath '\["foo"\]\["bar"\]' of '\["foo"\]\["bar"\]\["qux"\]'/) 140 | }) 141 | }) 142 | }) 143 | 144 | describe('errors', function () { 145 | describe('invalid dot notation', function () { 146 | testFunction(set, [{}, '.'], /0.*invalid dot key/) 147 | testFunction(set, [{}, '9'], /0.*invalid dot key/) 148 | testFunction(set, [{}, 'foo..bar'], /4.*invalid dot key/) 149 | testFunction(set, [{}, 'foo...bar'], /4.*invalid dot key/) 150 | }) 151 | 152 | describe('invalid bracket notation', function () { 153 | testFunction(set, [{}, '['], /Unexpected end of keypath.*invalid bracket key/) 154 | testFunction(set, [{}, '[]'], /1.*invalid bracket key/) 155 | testFunction(set, [{}, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 156 | testFunction(set, [{}, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 157 | }) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /__test__/immutable-set.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | var debug = require('debug')('keypather:set.test') 3 | var exists = require('101/exists') 4 | var deepEqual = require('fast-deep-equal') 5 | var deepFreeze = require('deep-freeze-strict') 6 | 7 | var set = require('../set') 8 | var immutableSet = require('../immutable-set') 9 | 10 | var old = { old: 'old' } 11 | var val = {} 12 | 13 | function testFunction (fn, args, expectedResult, only) { 14 | var testFn = only ? test.only : test 15 | if (expectedResult instanceof Error || expectedResult instanceof RegExp) { 16 | testFn('should error: ' + fn.name + '("' + args[1] + '")', function () { 17 | expect(function () { 18 | fn.apply(null, args) 19 | }).toThrow(expectedResult) 20 | }) 21 | } else { 22 | testFn('should ' + fn.name + '("' + args[1] + '")', function () { 23 | var objUnchanged = deepEqual(args[0], expectedResult) 24 | var result = fn.apply(null, args) 25 | if (!objUnchanged) set.apply(null, args) 26 | var object = args[0] 27 | debug({ 28 | object: object, 29 | keypath: args[1], 30 | result: result, 31 | expectedResult: expectedResult, 32 | objUnchanged: objUnchanged 33 | }) 34 | expect(val).toEqual({}) // safety check, since val is static 35 | // should not be the original object 36 | if (objUnchanged) { 37 | expect(result).toBe(object) 38 | } else { 39 | expect(result).not.toBe(object) 40 | } 41 | // should deep equal object modified w/ set 42 | expect(result).toEqual(expectedResult) 43 | expect(result).toEqual(object) // modified object 44 | expect(Array.isArray(result)).toBe(Array.isArray(object)) 45 | expectNoSharedChildren(result, expectedResult, val) 46 | }) 47 | } 48 | } 49 | 50 | testFunction.only = function (fn, args, expected) { 51 | testFunction(fn, args, expected, true) 52 | } 53 | 54 | describe('immutable-set', function () { 55 | describe('path exists', function () { 56 | describe('object unchanged', () => { 57 | testFunction(immutableSet, [deepFreeze({ foo: { bar: val }, zfoo: 1 }), 'foo.bar', val], { foo: { bar: val }, zfoo: 1 }) 58 | }) 59 | 60 | describe('dot notation', function () { 61 | testFunction(immutableSet, [{ foo: old, zfoo: 1 }, 'foo', val], { foo: val, zfoo: 1 }) 62 | testFunction(immutableSet, [{ foo: { bar: old, zbar: 2 }, zfoo: 1 }, 'foo.bar', val], { foo: { bar: val, zbar: 2 }, zfoo: 1 }) 63 | testFunction(immutableSet, [{ foo: { bar: { qux: old, zqux: 2 }, zbar: 2 }, zfoo: 1 }, 'foo.bar.qux', val], { foo: { bar: { qux: val, zqux: 2 }, zbar: 2 }, zfoo: 1 }) 64 | }) 65 | 66 | describe('bracket notation', function () { 67 | describe('single quote', function () { 68 | testFunction(immutableSet, [{ foo: old }, "['foo']", val], { foo: val }) 69 | testFunction(immutableSet, [{ foo: old }, "['foo']", val], { foo: val }) 70 | testFunction(immutableSet, [[], '[0]', val], [val]) 71 | testFunction(immutableSet, [{}, '[0]', val], { '0': val }) 72 | testFunction(immutableSet, [{ foo: { bar: old } }, "['foo']['bar']", val], { foo: { bar: val } }) 73 | testFunction(immutableSet, [{ foo: { bar: { qux: old } } }, "['foo']['bar']['qux']", val], { foo: { bar: { qux: val } } }) 74 | testFunction(immutableSet, [{ foo: { 'dot.key': { qux: old } } }, "['foo']['dot.key']['qux']", val], { foo: { 'dot.key': { qux: val } } }) 75 | testFunction(immutableSet, [{ foo: { '[bracket.key]': { qux: old } } }, "['foo']['[bracket.key]']['qux']", val], { foo: { '[bracket.key]': { qux: val } } }) 76 | testFunction(immutableSet, [{ foo: { '\'quote.key\'': { qux: old } } }, "['foo'][''quote.key'']['qux']", val], { foo: { '\'quote.key\'': { qux: val } } }) 77 | 78 | describe('escaped', function () { 79 | testFunction(immutableSet, [{ foo: old }, '[\'foo\']', val], { foo: val }) 80 | testFunction(immutableSet, [{ foo: { bar: old } }, '[\'foo\'][\'bar\']', val], { foo: { bar: val } }) 81 | testFunction(immutableSet, [{ foo: { bar: { qux: old } } }, '[\'foo\'][\'bar\'][\'qux\']', val], { foo: { bar: { qux: val } } }) 82 | testFunction(immutableSet, [{ foo: { 'dot.key': { qux: old } } }, '[\'foo\'][\'dot.key\'][\'qux\']', val], { foo: { 'dot.key': { qux: val } } }) 83 | testFunction(immutableSet, [{ foo: { '[bracket.key]': { qux: old } } }, '[\'foo\'][\'[bracket.key]\'][\'qux\']', val], { foo: { '[bracket.key]': { qux: val } } }) 84 | testFunction(immutableSet, [{ foo: { '\'quote.key\'': { qux: old } } }, '[\'foo\'][\'\'quote.key\'\'][\'qux\']', val], { foo: { '\'quote.key\'': { qux: val } } }) 85 | }) 86 | }) 87 | 88 | describe('double quote', function () { 89 | testFunction(immutableSet, [{ foo: old }, '["foo"]', val], { foo: val }) 90 | testFunction(immutableSet, [{ foo: { bar: old } }, '["foo"]["bar"]', val], { foo: { bar: val } }) 91 | testFunction(immutableSet, [{ foo: { bar: { qux: old } } }, '["foo"]["bar"]["qux"]', val], { foo: { bar: { qux: val } } }) 92 | testFunction(immutableSet, [{ foo: { 'dot.key': { qux: old } } }, '["foo"]["dot.key"]["qux"]', val], { foo: { 'dot.key': { qux: val } } }) 93 | testFunction(immutableSet, [{ foo: { '[bracket.key]': { qux: old } } }, '["foo"]["[bracket.key]"]["qux"]', val], { foo: { '[bracket.key]': { qux: val } } }) 94 | testFunction(immutableSet, [{ foo: { '"quote.key"': { qux: old } } }, '["foo"][""quote.key""]["qux"]', val], { foo: { '"quote.key"': { qux: val } } }) 95 | 96 | describe('escaped', function () { 97 | // eslint-disable-next-line quotes 98 | testFunction(immutableSet, [{ foo: old }, "[\"foo\"]", val], { foo: val }) 99 | // eslint-disable-next-line quotes 100 | testFunction(immutableSet, [{ foo: { bar: old } }, "[\"foo\"][\"bar\"]", val], { foo: { bar: val } }) 101 | // eslint-disable-next-line quotes 102 | testFunction(immutableSet, [{ foo: { bar: { qux: old } } }, "[\"foo\"][\"bar\"][\"qux\"]", val], { foo: { bar: { qux: val } } }) 103 | // eslint-disable-next-line quotes 104 | testFunction(immutableSet, [{ foo: { '[bracket.key]': { qux: old } } }, "[\"foo\"][\"[bracket.key]\"][\"qux\"]", val], { foo: { '[bracket.key]': { qux: val } } }) 105 | // eslint-disable-next-line quotes 106 | testFunction(immutableSet, [{ foo: { '"quote.key"': { qux: old } } }, "[\"foo\"][\"\"quote.key\"\"][\"qux\"]", val], { foo: { '"quote.key"': { qux: val } } }) 107 | }) 108 | }) 109 | }) 110 | 111 | describe('overwrite', () => { 112 | testFunction(immutableSet, [{ foo: 1 }, 'foo.bar', val], { foo: { bar: val } }) 113 | }) 114 | 115 | describe('overwrite: false', () => { 116 | testFunction(immutableSet, [{ foo: 1, zfoo: 1 }, 'foo.bar', val, { overwritePrimitives: false }], { foo: 1, zfoo: 1 }) 117 | testFunction(immutableSet, [{ foo: 1, zfoo: 1 }, 'foo[0]', val, { overwritePrimitives: false }], { foo: 1, zfoo: 1 }) 118 | }) 119 | }) 120 | 121 | describe('path does not exist', function () { 122 | describe('force: true (default)', function () { 123 | describe('dot notation', function () { 124 | testFunction(immutableSet, [{}, 'foo', val], { foo: val }) 125 | testFunction(immutableSet, [{}, 'foo.bar', val], { foo: { bar: val } }) 126 | testFunction(immutableSet, [{ foo: { bar: {} } }, 'foo.bar.qux.duck', val], { foo: { bar: { qux: { duck: val } } } }) 127 | testFunction(immutableSet, [{}, 'foo[0]', val], { foo: [val] }) 128 | }) 129 | 130 | describe('bracket notation', function () { 131 | testFunction(immutableSet, [{}, '["foo"]', val], { foo: val }) 132 | testFunction(immutableSet, [{}, '["foo"]["bar"]', val], { foo: { bar: val } }) 133 | testFunction(immutableSet, [{ foo: {} }, '["foo"]["bar"]["qux"]', val], { foo: { bar: { qux: val } } }) 134 | testFunction(immutableSet, [{}, '["[""]"]', val], { '[""]': val }) // complex edgecase! 135 | }) 136 | }) 137 | 138 | describe('force: false', function () { 139 | describe('dot notation', function () { 140 | testFunction(immutableSet, [{}, 'foo', val, { force: false }], { foo: val }) 141 | testFunction(immutableSet, [{}, 'foo.bar.qux', val, { force: false }], /'bar' of undefined.*at keypath 'foo' of 'foo.bar.qux'/) 142 | testFunction(immutableSet, [{ foo: {} }, 'foo.bar.qux', val, { force: false }], /'qux' of undefined.*at keypath 'foo.bar' of 'foo.bar.qux'/) 143 | }) 144 | 145 | describe('bracket notation', function () { 146 | testFunction(immutableSet, [{}, '["foo"]', val, { force: false }], { foo: val }) 147 | testFunction(immutableSet, [{}, '["foo"]["bar"]', val, { force: false }], /'bar' of undefined.*at keypath '\["foo"\]' of '\["foo"\]\["bar"\]'/) 148 | testFunction(immutableSet, [{ foo: {} }, '["foo"]["bar"]["qux"]', val, { force: false }], /'qux' of undefined.*at keypath '\["foo"\]\["bar"\]' of '\["foo"\]\["bar"\]\["qux"\]'/) 149 | }) 150 | }) 151 | }) 152 | 153 | describe('errors', function () { 154 | describe('invalid dot notation', function () { 155 | testFunction(immutableSet, [{}, '.'], /0.*invalid dot key/) 156 | testFunction(immutableSet, [{}, '9'], /0.*invalid dot key/) 157 | testFunction(immutableSet, [{}, 'foo..bar'], /4.*invalid dot key/) 158 | testFunction(immutableSet, [{}, 'foo...bar'], /4.*invalid dot key/) 159 | }) 160 | 161 | describe('invalid bracket notation', function () { 162 | testFunction(immutableSet, [{}, '['], /Unexpected end of keypath.*invalid bracket key/) 163 | testFunction(immutableSet, [{}, '[]'], /1.*invalid bracket key/) 164 | testFunction(immutableSet, [{}, '[""'], /Unexpected end of keypath.*invalid bracket string key/) 165 | testFunction(immutableSet, [{}, '[2'], /Unexpected end of keypath.*invalid bracket number key/) 166 | }) 167 | }) 168 | }) 169 | 170 | function expectNoSharedChildren (obj1, obj2, endVal) { 171 | var keys = dedupe(Object.keys(obj1).concat(obj2)) 172 | keys.forEach(function (key) { 173 | var val1 = obj1[key] 174 | var val2 = obj2[key] 175 | if (!exists(val1) || val1 === endVal) return 176 | if (typeof val1 === 'object') { 177 | expect(val1).not.toBe(val2) 178 | expectNoSharedChildren(val1, val2, endVal) 179 | } 180 | }) 181 | } 182 | function dedupe (keys) { 183 | return keys.filter(function (key1, i) { 184 | return keys.some(function (key2, j) { 185 | if (i === j) return true 186 | return key1 !== key2 187 | }) 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![keypather-logo](https://i.imgur.com/wFm1N25.png) 2 | 3 | # keypather [![Build Status](https://travis-ci.org/tjmehta/keypather.png?branch=master)](https://travis-ci.org/tjmehta/keypather) [![Coverage Status](https://coveralls.io/repos/github/tjmehta/keypather/badge.svg?branch=immutable-methods)](https://coveralls.io/github/tjmehta/keypather?branch=immutable-methods) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 4 | 5 | Get, set, or delete a deep value using a keypath string (supports immutable operations) and more. 6 | 7 | A collection of keypath utilities: get, set, delete, in, has, immutable set/delete, flatten, and expand. 8 | 9 | Lightweight and parses keypaths using vanilla JS - No ```eval``` or ```new Function``` hacks! 10 | 11 | # Installation 12 | ```bash 13 | npm install keypather 14 | ``` 15 | 16 | # Usage 17 | 18 | ## Examples 19 | 20 | ### Import 21 | ```js 22 | // modular imports, so you can keep your bundle lean 23 | const get = require('keypather/get') 24 | const set = require('keypather/set') 25 | const del = require('keypather/del') 26 | const immutableSet = require('keypather/immutable-set') 27 | const immutableDel = require('keypather/immutable-del') 28 | const keypathIn = require('keypather/in') 29 | const hasKeypath = require('keypather/has') 30 | const expand = require('keypather/expand') 31 | const flatten = require('keypather/flatten') 32 | ``` 33 | 34 | ### GET, SET, DEL Example 35 | ```js 36 | const get = require('keypather/get') 37 | const set = require('keypather/set') 38 | const del = require('keypather/del') 39 | 40 | let obj 41 | 42 | // Objects 43 | obj = { foo: { bar: 100 } } 44 | get(obj, 'foo.bar') // returns 100 45 | del(obj, '["foo"]["bar"]') // returns true, obj becomes { foo: {} } 46 | set(obj, 'foo.bar.qux', 200) // returns 200, obj becomes { foo: { bar: { qux: 200 } } } 47 | get(obj, 'foo["bar"].qux') // returns 200 48 | 49 | // Arrays 50 | obj = {} 51 | set(obj, 'foo[0]', 100) // obj is { foo: [ 100 ] } 52 | ``` 53 | 54 | ### Immutable SET, DEL Example 55 | ```js 56 | const set = require('keypather/immutable-set') 57 | const del = require('keypather/immutable-del') 58 | 59 | let obj 60 | let out 61 | 62 | // Objects 63 | obj = { foo: { bar: 100 } } 64 | out = set(obj, 'foo.bar', 100) // returns obj 65 | // out === obj, 66 | // since it was not modified 67 | out = set(obj, 'foo.bar.qux', 200) // returns { foo: { bar: { qux: 200 } } } 68 | // out !== obj, 69 | // obj is still { foo: { bar: 100 } } 70 | out = del(obj, 'one.two.three') // returns obj 71 | // out === obj, 72 | // since it was not modified 73 | out = del(obj, 'foo.bar.qux') // returns { foo: { bar: {} } } 74 | // out !== obj, 75 | // obj is still { foo: { bar: { qux: 200 } } } 76 | 77 | // Arrays 78 | obj = {} 79 | out = set(obj, 'foo[0]', 100) // returns { foo: [ 100 ] } (new) 80 | // out !== obj, obj is still { foo: { bar: 100 } } 81 | ``` 82 | 83 | ### HAS, IN Example 84 | ```js 85 | const hasKeypath = require('keypather/has') 86 | const keypathIn = require('keypather/in') 87 | 88 | const obj = { foo: Object.create({ bar: 100 }) } 89 | 90 | hasKeypath(obj, 'foo.bar') // returns false (bar is on proto) 91 | keypathIn(obj, 'foo.bar') // returns true 92 | hasKeypath(obj, 'foo') // returns true 93 | ``` 94 | 95 | ### FLATTEN, EXPAND Example 96 | ```js 97 | const expand = require('keypather/expand') 98 | const flatten = require('keypather/flatten') 99 | 100 | const obj = expand({ 101 | 'foo.bar': 1, 102 | 'foo.qux[0]': 100, 103 | 'foo["qux"][1]': 200, 104 | 'foo.qux.wut': 'val' 105 | }) 106 | // obj is { foo { bar: 1, qux: [ 100, 200, wut: 'val' ] } } 107 | const flat = flatten(obj) 108 | // flat is { 'foo.bar': 1, 'foo.qux': 2 } } 109 | ``` 110 | 111 | ### Errors Example 112 | ```js 113 | /* Missing deep values w/ "force: false" */ 114 | get({}, 'foo.bar', { force: false }) 115 | set({}, 'foo.bar', 100, { force: false }) 116 | del({}, 'foo.bar', { force: false }) 117 | immutableSet({}, 'foo.bar', 100, { force: false }) 118 | immutableDel({}, 'foo.bar', { force: false }) 119 | // TypeError: Cannot read property 'bar' of undefined (at keypath 'foo' of 'foo.bar') 120 | get({ foo: {} }, 'foo.bar', { force: false }) 121 | set({ foo: {} }, 'foo.bar', 100, { force: false }) 122 | del({ foo: {} }, 'foo.bar', { force: false }) 123 | immutableSet({ foo: {} }, 'foo.bar', 100, { force: false }) 124 | immutableDel({ foo: {} }, 'foo.bar', { force: false }) 125 | // TypeError: Cannot read property 'bar' of undefined (at keypath 'foo.bar' of 'foo.bar.qux') 126 | hasKeypath({}, 'foo.bar', { force: false }) 127 | // TypeError: Cannot read property 'hasOwnProperty' of undefined (hasOwnProperty('bar') errored at keypath 'foo' of 'foo.bar') 128 | keypathIn({}, 'foo.bar', { force: false }) 129 | // TypeError: Cannot use 'in' operator to search for 'bar' in undefined (at 'foo' of 'foo.bar') 130 | keypathIn({}, 'foo.bar.qux', { force: false }) 131 | hasKeypath({}, 'foo.bar.qux', { force: false }) 132 | // TypeError: Cannot read property 'bar' of undefined (at keypath 'foo' of 'foo.bar.qux') 133 | 134 | /* Warnings for set and immutable-set */ 135 | // by default, set will overwrite primitives (string, number or regexp) to an object or array. 136 | // when overwritePrimitives is set to false, sets will warn when settings a key on a primitive 137 | // to disable all warnings use the option { warn: false } 138 | set({}, '[0]', 'val', { overwritePrimitives: false }) 139 | // log: Setting number key (0) on object at keypath '' of '[0]') 140 | set([], 'key', 'val', { overwritePrimitives: false }) 141 | // log: Setting string key 'foo' on array at keypath '' of 'foo') 142 | set({ foo: 1 }, 'foo.qux', 'val', { overwritePrimitives: false }) 143 | // log: Setting key 'qux' on number 1 at keypath 'foo' of 'foo.qux') 144 | set({ foo: 1 }, 'foo[0]', 'val', { overwritePrimitives: false }) 145 | // log: Setting number key (0) on number 1 at keypath 'foo' of 'foo[0]') 146 | set({ foo: 'str' }, 'foo.bar', 'val', { overwritePrimitives: false }) 147 | // log: Setting key 'bar' on string 'str' at keypath 'foo' of 'foo.bar') 148 | set({ foo: {} }, 'foo[0]', 'val', { overwritePrimitives: false }) 149 | // log: Setting number key (0) on object at keypath 'foo' of 'foo[0]') 150 | 151 | /* Invalid keypaths */ 152 | get({}, 'foo.1bar') 153 | // Error: Unexpected token '1' in keypath 'foo.1bar' at position 4 (invalid dot key) 154 | get({}, 'foo[]') 155 | // Error: Unexpected token ']' in keypath 'foo[]' at position 4 (invalid bracket key) 156 | get({}, 'foo["]') 157 | // Error: Unexpected token ']' in keypath 'foo[]' at position 5 (invalid bracket string key) 158 | get({}, 'foo.') 159 | // Error: Unexpected end of keypath 'foo.' (invalid dot key) 160 | get({}, 'foo[') 161 | // Error: Unexpected end of keypath 'foo[' (invalid bracket key) 162 | get({}, "foo['") 163 | // Error: Unexpected end of keypath 'foo['' (invalid bracket string key) 164 | ``` 165 | 166 | ## Documentation 167 | 168 | ### GET 169 | Returns value at keypath in obj 170 | * @param {any} obj - context to read keypath from 171 | * @param {string} keypath - bracket and/or dot notation keypath string 172 | * @param {?object} opts - optional, defaults to { force: true } 173 | * opts.force - force specifies whether non-existant keypaths should be ignored, defaults to true 174 | * if false, `get` will error when reading a key on a non-existant keypath. 175 | * @returns {any} value at keypath 176 | 177 | ```js 178 | const get = require('keypather/get'); 179 | const obj = { 180 | foo: { 181 | bar: { 182 | baz: 'val' 183 | } 184 | } 185 | }; 186 | get(obj, "foo.bar.baz"); // returns 'val' 187 | get(obj, "foo['bar'].baz"); // returns 'val' 188 | get(obj, "['foo']['bar']['baz']"); // returns 'val' 189 | 190 | get({}, 'foo.two.three', { force: false }) // throws error 191 | // TypeError: Cannot read property 'three' of undefined (at keypath 'foo.two' of 'foo.two.three') 192 | ``` 193 | 194 | ### SET 195 | Sets a value in obj at keypath. If force=true, set will create objects at non-existant keys in the 196 | keypath. If the non-existant key is a number, its value will be initialized as an array. 197 | * @param {any} obj - context to read keypath from 198 | * @param {string} keypath - bracket and/or dot notation keypath string to read from obj 199 | * @returns {any} value - value to set at keypath 200 | * @param {?object} opts - optional, defaults to { force: true, overwritePrimitives: true, warn: true } 201 | * opts.force - whether non-existant keys in keypath should be created, defaults to true. 202 | * if false, `set` will error when reading a key on a non-existant keypath. 203 | * opts.overwritePrimitives - whether primitive keys (booleans, strings, numbers) should be overwritten. 204 | * setting a key on a primitive will convert it to an object or array (if key is string or number). 205 | * if false, `set` will log a warning when setting keys on primitives. 206 | * opts.silent - specifies whether warning logs should be enabled, defaults to false. 207 | * @returns {any} value set at keypath 208 | 209 | ```js 210 | const set = require('keypather/set'); 211 | 212 | let obj = { 213 | foo: { 214 | bar: { 215 | baz: 'val' 216 | } 217 | } 218 | }; 219 | set(obj, "foo['bar'].baz", 'val'); // returns 'val' 220 | set(obj, "foo.bar.baz", 'val'); // returns 'val' 221 | set(obj, "['foo']['bar']['baz']", 'val'); // returns 'val' 222 | 223 | /* By default, set forces creation of non-existant keys */ 224 | obj = {} 225 | set(obj, "foo.bar.baz", 'val'); // returns 'val' 226 | // obj becomes: 227 | // { 228 | // foo: { 229 | // bar: { 230 | // baz: 'val' 231 | // } 232 | // } 233 | // }; 234 | 235 | /* By default, overwrites primitives when setting a key on one */ 236 | obj = { foo: 1 } 237 | set(obj, "foo.bar.baz", 'val'); // returns 'val' 238 | // obj becomes: 239 | // { 240 | // foo: { 241 | // bar: { 242 | // baz: 'val' 243 | // } 244 | // } 245 | // }; 246 | obj = { foo: 1 } 247 | set(obj, "foo[0].baz", 'val'); // returns 'val' 248 | // obj becomes: 249 | // { 250 | // foo: [{ 251 | // baz: 'val' 252 | // }] 253 | // }; 254 | 255 | /* Errors, force=false */ 256 | set({}, "foo.bar.baz", 'val', { force: false }); // throw's an error 257 | // TypeError: Cannot read property 'bar' of undefined (at keypath 'foo' of 'foo.bar.baz') 258 | // see more errors above in the 'Errors' section 259 | 260 | /* Warnings, overwritePrimitives=false */ 261 | set({ foo: 'str' }, 'foo.bar', 'val', { overwritePrimitives: false }) 262 | // log: Setting key 'bar' on string 'str' at keypath 'foo' of 'foo.bar') 263 | // see more warnings above in the 'Errors' section 264 | ``` 265 | 266 | ### DEL 267 | Deletes value a keypath in obj. Similar to `delete obj.key`. 268 | * @param {any} obj - context to read keypath from 269 | * @param {string} keypath - bracket and/or dot notation keypath string to delete from obj 270 | * @param {?object} opts - optional, defaults to { force: true } 271 | * opts.force - whether non-existant keys in keypath should be created, defaults to true. 272 | * if false, `del` will error when reading a key on a non-existant keypath. 273 | * @returns {boolean} true except when the property is non-configurable or in non-strict mode 274 | 275 | ```js 276 | const del = require('keypather/del'); 277 | 278 | const obj = { 279 | foo: { 280 | bar: { 281 | baz: 'val' 282 | } 283 | } 284 | }; 285 | del(obj, "foo['bar'].baz"); // true 286 | del(obj, "foo.bar.baz"); // true 287 | del(obj, "['foo']['bar']['baz']"); // true 288 | // obj becomes: 289 | // { 290 | // foo: { 291 | // bar: {} 292 | // } 293 | // } 294 | 295 | /* Errors, force=false */ 296 | del(obj, "one.two.three", 'val', { force: false }); // throw's an error 297 | // TypeError: Cannot read property 'two' of undefined (at keypath 'one' of 'one.two.three') 298 | // see more errors above in the 'Errors' section 299 | ``` 300 | 301 | ### IMMUTABLE SET 302 | Sets a value in obj at keypath. If force=true, set will create objects at non-existant keys in the 303 | keypath. If the non-existant key is a number, its value will be initialized as an array. 304 | * @param {any} obj - context to read keypath from 305 | * @param {string} keypath - bracket and/or dot notation keypath string to read from obj 306 | * @returns {any} value - value to set at keypath 307 | * @param {?object} opts - optional, defaults to { force: true, overwritePrimitives: true, warn: true } 308 | * opts.force - whether non-existant keys in keypath should be created, defaults to true. 309 | * if false, `immutable-set` will error when reading a key on a non-existant keypath. 310 | * opts.overwritePrimitives - whether primitive keys (booleans, strings, numbers) should be overwritten. 311 | * setting a key on a primitive will convert it to an object or array (if key is string or number). 312 | * if false, `immutable-set` will log a warning when setting keys on primitives. 313 | * opts.silent - specifies whether warning logs should be enabled, defaults to false. 314 | * opts.shallowClone - provide custom shallowClone, defaults to [shallow-clone](https://npmrepo.com/shallow-clone) 315 | * @returns {any} returns same obj if unmodified, otherwise modified clone of obj 316 | 317 | ```js 318 | const set = require('keypather/immutable-set'); 319 | 320 | let obj = { 321 | foo: { 322 | bar: { 323 | baz: 'val' 324 | } 325 | } 326 | }; 327 | let out 328 | out = set(obj, "foo['bar'].baz", 'val'); // returns SAME object, since the value was unchanged 329 | // out === obj 330 | out = set(obj, "foo.bar.baz", 'val2'); // returns { foo: { bar: { baz: 'val2' } } } (new object) 331 | // out !== obj 332 | out = set(obj, "['foo']['bar']['baz']", 'val3'); // returns { foo: { bar: { baz: 'val3' } } } (new object) 333 | // out !== obj 334 | 335 | /* By default, overwrites primitives when setting a key on one */ 336 | obj = { foo: 1 } 337 | out = set(obj, "foo.bar.baz", 'val'); // returns new object 338 | // out !== obj 339 | // out is: 340 | // { 341 | // foo: { 342 | // bar: { 343 | // baz: 'val' 344 | // } 345 | // } 346 | // }; 347 | obj = { foo: 1 } 348 | out = set(obj, "foo[0].baz", 'val'); // returns new object 349 | // out !== obj 350 | // out is: 351 | // { 352 | // foo: [{ 353 | // baz: 'val' 354 | // }] 355 | // }; 356 | 357 | /* Errors, force=false */ 358 | obj = {} 359 | set(obj, "foo.bar.baz", 'val', { force: false }); // throws error 360 | // Error: Cannot read property 'bar' of undefined (at keypath 'foo' of 'foo.bar.baz') 361 | 362 | /* Warnings, force=false */ 363 | obj = { foo: 'str' } 364 | out = set(obj, 'foo.bar', 'val', { overwritePrimitives: false }) 365 | // out === obj, since keys cannot be set on strings or numbers 366 | // log: Setting key 'bar' on string 'str' at keypath 'foo' of 'foo.bar') 367 | ``` 368 | 369 | ### IMMUTABLE DEL 370 | Deletes value a keypath in obj. Similar to `delete obj.key`. 371 | * @param {any} obj - context to read keypath from 372 | * @param {string} keypath - bracket and/or dot notation keypath string to delete from obj 373 | * @param {?object} opts - optional, defaults to { force: true } 374 | * opts.force - whether non-existant keys in keypath should be created, defaults to true. 375 | * if false, `del` will error when reading a key on a non-existant keypath. 376 | * opts.shallowClone - provide custom shallowClone, defaults to [shallow-clone](https://npmrepo.com/shallow-clone) 377 | * @returns {any} returns same obj if unmodified, otherwise modified clone of obj 378 | 379 | ```js 380 | const del = require('keypather/immutable-del'); 381 | const obj = { 382 | foo: { 383 | bar: { 384 | baz: 'val' 385 | } 386 | } 387 | }; 388 | let out 389 | out = del(obj, "foo['bar'].baz"); // true 390 | out = del(obj, "foo.bar.baz"); // true 391 | out = del(obj, "['foo']['bar']['baz']"); // true 392 | // obj becomes: 393 | // { 394 | // foo: { 395 | // bar: {} 396 | // } 397 | // } 398 | 399 | /* Errors, force=false */ 400 | del(obj, "one.two.three", 'val', { force: false }); // throw's an error 401 | // Error: Cannot read property 'two' of undefined (at keypath 'one' of 'one.two.three') 402 | ``` 403 | 404 | ### IN 405 | Returns true if keypath is "in" the obj at the keypath. Similar to "in" operator. 406 | * @param {any} obj - context to read keypath in 407 | * @param {string} keypath - bracket and/or dot notation keypath string to read from obj 408 | * @param {?object} opts - optional, defaults to { force: true } 409 | * opts.force - force specifies whether non-existant keypaths should be ignored, defaults to true 410 | * @returns {boolean} true if the keypath is "in" the obj, else false 411 | 412 | ```js 413 | const keypathIn = require('keypather/in'); 414 | const obj = { 415 | foo: { 416 | bar: { 417 | baz: 'val' 418 | __proto__: { 419 | qux: 'val' 420 | } 421 | } 422 | } 423 | }; 424 | keypathIn(obj, "foo.bar.baz"); // true 425 | keypathIn(obj, "foo.bar.qux"); // true 426 | keypathIn(obj, "foo.bar.bing"); // false 427 | keypathIn(obj, "foo['bar'].baz"); // true 428 | keypathIn(obj, "one.two.three"); // false 429 | 430 | // Errors, force=false 431 | keypathIn(obj, "one.two.three", { force: false }); 432 | // Error: Cannot read property 'two' of undefined (at keypath 'two' of 'one.two.three') 433 | keypathIn(obj, "foo.two.three", { force: false }); 434 | // TypeError: Cannot use 'in' operator to search for 'three' in undefined (at 'foo.two' of 'foo.two.three') 435 | ``` 436 | 437 | ### HAS 438 | Returns true if the obj has the keypath. Similar to `obj.hasOwnProperty`. 439 | * @param {any} obj - context to read keypath in 440 | * @param {string} keypath - bracket and/or dot notation keypath string to read from obj 441 | * @param {?object} opts - optional, defaults to { force: true } 442 | * opts.force - force specifies whether non-existant keypaths should be ignored, defaults to true 443 | * @returns {boolean} true if the keypath is "in" the obj, else false 444 | 445 | ```js 446 | const hasKeypath = require('keypather/has'); 447 | const obj = { 448 | foo: { 449 | bar: { 450 | baz: 'val' 451 | __proto__: { 452 | qux: 'val' 453 | } 454 | } 455 | } 456 | }; 457 | hasKeypath(obj, "foo.bar.baz"); // true 458 | hasKeypath(obj, "foo.bar.qux"); // false 459 | hasKeypath(obj, "['foo']['bar']['baz']"); // true 460 | hasKeypath(obj, "one.two.three"); // false 461 | 462 | // Errors, force=false 463 | hasKeypath(obj, "one.two.three", { force: false }); // throw's an error 464 | // Error: Cannot read property 'two' of undefined (at keypath 'two' of 'one.two.three 465 | hasKeypath(obj, "foo.two.three", { force: false }); 466 | // Error: Cannot read property 'hasOwnProperty' of undefined (hasOwnProperty('three') errored at keypath 'foo.two' of 'foo.two.three') 467 | ``` 468 | 469 | ### FLATTEN 470 | Flatten an object or array into a keypath object 471 | * @param {any} obj - object or array to flatten 472 | 473 | ```js 474 | const flatten = require('keypather/flatten'); 475 | 476 | flatten({ 477 | foo: { 478 | qux: 'hello' 479 | }, 480 | bar: [ 481 | 1, 482 | { 483 | yolo: [1] 484 | } 485 | ] 486 | }); 487 | // returns: 488 | // { 489 | // 'foo.qux': 'hello', 490 | // 'bar[0]': 1, 491 | // 'bar[1].yolo[0]': 1 492 | // } 493 | 494 | /* accepts a delimiter other than '.' as second arg */ 495 | 496 | flatten({ 497 | foo: { 498 | qux: 'hello' 499 | } 500 | }, '_'); 501 | // returns: 502 | // { 503 | // 'foo_qux': 'hello', 504 | // } 505 | 506 | ``` 507 | 508 | ### EXPAND 509 | Expand a flattened object back into an object or array 510 | * @param {any} obj - flattened object or array to be expanded 511 | 512 | ```js 513 | const expand = require('keypather/expand'); 514 | 515 | expand({ 516 | 'foo.qux': 'hello', 517 | 'bar[0]': 1, 518 | 'bar[1].yolo[0]': 1 519 | }); 520 | // returns: 521 | // { 522 | // foo: { 523 | // qux: 'hello' 524 | // }, 525 | // bar: [ 526 | // 1, 527 | // { 528 | // yolo: [1] 529 | // } 530 | // ] 531 | // } 532 | 533 | /* expand will assume an object is an array if any of the keys are numbers */ 534 | 535 | expand({ 536 | '[0]': 1, 537 | '[1].yolo[0]': 1 538 | }); 539 | // returns: 540 | // [ 541 | // 1, 542 | // { 543 | // yolo: [1] 544 | // } 545 | // ] 546 | 547 | /* accepts a delimiter other than '.' as second arg */ 548 | 549 | expand({ 550 | 'foo_qux': 'hello' 551 | }, '_'); 552 | // returns: 553 | // { 554 | // foo: { 555 | // qux: 'hello' 556 | // } 557 | // } 558 | ``` 559 | 560 | # Changelog 561 | [Changelog history](https://github.com/tjmehta/keypather/blob/master/CHANGELOG.md) 562 | 563 | # License 564 | ### MIT 565 | --------------------------------------------------------------------------------