├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── drilldown.js └── drilldown.min.js ├── gulpfile.js ├── index.js ├── mocha-helper.js ├── package.json └── src ├── drilldown-spec.js └── drilldown.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | 3 | node_modules/ 4 | npm-debug.log 5 | 6 | *.swp 7 | *~ 8 | *.iml 9 | .idea/ 10 | *.ipr 11 | *.iws 12 | .~* 13 | .settings/ 14 | .project 15 | .metadata 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | ## [0.1.1] - 2016-05-01 5 | ### Changed 6 | * Drilling to functions with custom `.bind` properties now works 7 | 8 | 9 | ## [0.1.0] - 2015-07-03 10 | ### Added 11 | * `set` now creates missing parents to the path of the variable being set 12 | 13 | ### Changed 14 | * For security, `invoke` does not default to console.log anymore 15 | 16 | ### Removed 17 | * Deprecation warnings and deprecated functions 18 | 19 | 20 | ## [0.0.4] - 2015-06-27 21 | ### Changed 22 | * I forgot to bump the bower version before! 23 | 24 | 25 | ## [0.0.3] - 2015-06-27 26 | ### Added 27 | * Bring `set` back with a deprecation warning 28 | * Bring `func` back with a deprecation warning 29 | 30 | 31 | ## [0.0.2] - 2015-06-27 32 | ### Added 33 | * Coverage gulp task 34 | 35 | ### Changed 36 | * Renamed `set` to `update` (to possibly make room for a new `set`) 37 | * Renamed `func` to `invoke` (slightly more obvious API) 38 | * You can now only drill into own properties 39 | 40 | 41 | ## [0.0.1] - 2015-06-23 42 | Initial release 43 | 44 | [unreleased]: https://github.com/d10n/drilldown/compare/v0.1.1...HEAD 45 | [0.1.1]: https://github.com/d10n/drilldown/compare/v0.1.0...0.1.1 46 | [0.1.0]: https://github.com/d10n/drilldown/compare/v0.0.4...v0.1.0 47 | [0.0.4]: https://github.com/d10n/drilldown/compare/v0.0.3...v0.0.4 48 | [0.0.3]: https://github.com/d10n/drilldown/compare/v0.0.2...v0.0.3 49 | [0.0.2]: https://github.com/d10n/drilldown/compare/v0.0.1...v0.0.2 50 | [0.0.1]: https://github.com/d10n/drilldown/compare/fc09a25...v0.0.1 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, d10n 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | drilldown.js 2 | ============ 3 | 4 | Safely accesses deep properties of objects. 5 | 6 | ```JavaScript 7 | // npm install drilldown 8 | // bower install drilldown 9 | var dd = require('drilldown'); 10 | ``` 11 | Strict version locking is recommended during release 0.0.x 12 | 13 | Ever run into this? 14 | ```JavaScript 15 | var foo; 16 | foo.bar; 17 | // TypeError: Cannot read property 'bar' of undefined 18 | ``` 19 | 20 | You can now check deeply nested properties cleanly! 21 | ```JavaScript 22 | var foo = {abc: {def: {ghi: 'jkl'}}}; 23 | dd(foo)('abc')('def')('ghi').val is 'jkl' 24 | dd(foo)('abc')('zzz')('yyy').val is undefined 25 | 26 | // You don't need to use this ugly idiom anymore! 27 | (((foo || {}).abc || {}).def || {}).ghi // no 28 | ``` 29 | 30 | Check if a deep property exists: 31 | ```JavaScript 32 | dd(foo)('abc').exists 33 | ``` 34 | 35 | Works with arrays too: 36 | ```JavaScript 37 | var foo = {abc: [ {bar: 'def'},{bar: 'ghi'} ]}; 38 | dd(foo)('abc')(0)('bar').val is 'def' 39 | ``` 40 | 41 | Safely call functions: 42 | ```JavaScript 43 | var foo = {abc: {addOne: function(x) { return x + 1; }}}; 44 | dd(foo)('abc')('addOne').invoke(5); returns 6 45 | dd(foo)('zzz')('aaa').invoke(5); returns undefined 46 | ``` 47 | 48 | Update values if the original value exists: 49 | ```JavaScript 50 | var foo = {abc: {def: {ghi: 'jkl'}}}; 51 | var newValue = {ping: 'pong'}; 52 | 53 | dd(foo)('abc')('def').update(newValue); 54 | // - foo is now {abc: {def: {ping: 'pong'}}} 55 | // - {ping: 'pong'} is returned 56 | 57 | dd(foo)('abc')('zzz').update(5); 58 | // - foo is unchanged 59 | // - undefined is returned 60 | ``` 61 | 62 | Set values even if the path drilled to does not exist: 63 | ```JavaScript 64 | var foo = {abc: {}}; 65 | dd(foo)('abc')('def')('ghi').set('jkl'); 66 | // - foo is now {abc: {def: {ghi: 'jkl}}} 67 | ``` 68 | 69 | To prevent confusion, only own properties are drilled into. 70 | 71 | Available dd properties: 72 | * val - the value 73 | * exists - true if val is defined 74 | * update function(value) - sets the value if the value exists 75 | * set function(value) - sets the value at any path 76 | * invoke - the value if the value is a function, or else a dummy function 77 | 78 | Alternatives: 79 | * lodash or underscore: `_.get(foo, 'abc.def.ghi')` 80 | * nevernull: `nn(foo)('abc.def')('ghi').val` 81 | * vanilla es5: `(((foo || {}).abc || {}).def || {}).ghi` 82 | 83 | Drilldown works with dots and brackets in property names, which may be useful for drilling with user input. 84 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drilldown", 3 | "main": "dist/drilldown.js", 4 | "version": "0.1.1", 5 | "homepage": "https://github.com/d10n/drilldown", 6 | "description": "Safely accesses deeply nested properties of objects", 7 | "moduleType": [ 8 | "amd" 9 | ], 10 | "authors": [ 11 | "d10n " 12 | ], 13 | "license": "ISC", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /dist/drilldown.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.dd = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/d10n/drilldown/issues" 17 | }, 18 | "homepage": "https://github.com/d10n/drilldown", 19 | "devDependencies": { 20 | "browserify": "^13.0.0", 21 | "gulp": "^3.9.0", 22 | "gulp-istanbul": "^0.10.0", 23 | "gulp-mocha": "^2.1.2", 24 | "gulp-rename": "^1.2.2", 25 | "gulp-uglify": "^1.2.0", 26 | "istanbul": "^0.4.3", 27 | "lodash": "^4.11.1", 28 | "mocha": "^2.2.5", 29 | "must": "^0.13.1", 30 | "sinon": "^1.15.3", 31 | "vinyl-buffer": "^1.0.0", 32 | "vinyl-source-stream": "^1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/drilldown-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dd = require('./drilldown'); 4 | var _ = require('lodash'); 5 | 6 | describe('drilldown', function() { 7 | var example; 8 | beforeEach(function() { 9 | example = { 10 | foo: 'foo value', 11 | bar: sinon.spy(), 12 | baz: [ 13 | {name: 'item 0'}, 14 | {name: 'item 1'}, 15 | {name: 'item 2'} 16 | ], 17 | quux: { 18 | ping: { 19 | pong: true 20 | } 21 | }, 22 | '[id.local]': 1, 23 | person: { 24 | x: 0, 25 | y: 0, 26 | move: function(dx, dy) { 27 | this.x += dx; 28 | this.y += dy; 29 | } 30 | } 31 | }; 32 | }); 33 | 34 | it('should access top-level properties', function() { 35 | expect(dd(example)('foo').val).to.equal(example.foo); 36 | }); 37 | it('should access deeply-nested properties', function() { 38 | expect(dd(example)('quux')('ping').val).to.equal(example.quux.ping); 39 | }); 40 | it('should not fail when accessing non-object properties', function() { 41 | expect(dd(example)('nothing')('here').val).to.be.undefined(); 42 | }); 43 | it('should not fail when the first dd argument is undefined', function() { 44 | expect(dd(undefined)(null)(false).val).to.be.undefined(); 45 | }); 46 | it('should be able to access array indices', function() { 47 | expect(dd(example)('baz')(1)('name').val).to.equal(example.baz[1].name); 48 | }); 49 | it('should be able to update the value of a deeply-nested property', function() { 50 | var originalValue = example.quux.ping.pong; 51 | expect(dd(example)('quux')('ping')('pong').val).to.equal(originalValue); 52 | var newValue = false; 53 | var newResult = dd(example)('quux')('ping')('pong').update(newValue); 54 | expect(newResult).to.equal(newValue); 55 | expect(example.quux.ping.pong).to.equal(newValue); 56 | }); 57 | it('should not update the value of a nonexistent property', function() { 58 | var original = _.cloneDeep(example); 59 | var newResult = dd(example)('quux')('abc')('xyz').update(5); 60 | expect(example).to.eql(original); 61 | expect(newResult).to.be.undefined(); 62 | }); 63 | it('should set the value of a deeply-nested property', function() { 64 | var originalValue = example.quux.ping.pong; 65 | expect(dd(example)('quux')('ping')('pong').val).to.equal(originalValue); 66 | var newValue = false; 67 | var newResult = dd(example)('quux')('ping')('pong').set(newValue); 68 | expect(newResult).to.equal(newValue); 69 | expect(example.quux.ping.pong).to.equal(newValue); 70 | }); 71 | it('should set the value of a nonexistent property', function() { 72 | var expected = _.cloneDeep(example); 73 | expected.quux.abc = {xyz: 5}; 74 | var newResult = dd(example)('quux')('abc')('xyz').set(5); 75 | expect(example).to.eql(expected); 76 | expect(newResult).to.equal(5); 77 | }); 78 | it('should not set a value without a context', function() { 79 | var original = _.cloneDeep(example); 80 | var newExample = dd(example).set(5); 81 | expect(example).to.eql(original); 82 | expect(newExample).to.be.undefined(); 83 | }); 84 | it('should set a deeply-nested value on an empty object', function() { 85 | var myObject = {}; 86 | var newExample = dd(myObject)('req')('query')('page').set(5); 87 | var expected = {req: {query: {page: 5}}}; 88 | expect(myObject).to.eql(expected); 89 | expect(newExample).to.equal(5); 90 | }); 91 | it('should indicate whether a property exists', function() { 92 | expect(dd(example)('foo').exists).to.be.true(); 93 | expect(dd(example)('oof').exists).to.be.false(); 94 | }); 95 | it('should work with dots and brackets in property names', function() { 96 | var idLocal = dd(example)('[id.local]'); 97 | var originalIdLocal = example['[id.local]']; 98 | expect(idLocal.val).to.equal(originalIdLocal); 99 | expect(idLocal.update(originalIdLocal + 1)).to.equal(originalIdLocal + 1); 100 | expect(idLocal('subId').update(originalIdLocal + 2)).to.be.undefined(); 101 | var newSubId = idLocal('subId').set(originalIdLocal + 3); 102 | expect(newSubId).to.equal(originalIdLocal + 3); 103 | expect(example['[id.local]']).to.eql({ 104 | subId: originalIdLocal + 3 105 | }); 106 | }); 107 | it('should not drill into own properties', function() { 108 | expect(dd(example)('__proto__').exists).to.be.false(); 109 | expect(dd(example)('__proto__')('hasOwnProperty').exists).to.be.false(); 110 | expect(dd(Object)('prototype').exists).to.be.true(); 111 | expect(dd(Object)('__proto__').exists).to.be.false(); 112 | }); 113 | it('should call functions which exist', function() { 114 | var exampleBar = dd(example)('bar'); 115 | var arg = 'string argument'; 116 | exampleBar.invoke(arg); 117 | expect(exampleBar.val.calledWith(arg)).to.be.true(); 118 | }); 119 | it('should call functions which exist with this', function() { 120 | var exampleWalk = dd(example)('person')('move'); 121 | sinon.spy(exampleWalk, 'invoke'); 122 | exampleWalk.invoke(3, 4); 123 | expect(example.person.x).to.equal(3); 124 | expect(example.person.y).to.equal(4); 125 | expect(exampleWalk.invoke.calledWith(3, 4)).to.be.true(); 126 | }); 127 | it('should call functions which exist with .bind properties', function() { 128 | example.bar.bind = sinon.spy(); 129 | var exampleBar = dd(example)('bar'); 130 | var arg = 'string argument'; 131 | exampleBar.invoke(arg); 132 | expect(exampleBar.val.calledWith(arg)).to.be.true(); 133 | }); 134 | it('should preserve custom .bind properties of functions', function() { 135 | example.bar.bind = sinon.spy(); 136 | var exampleBar = dd(example)('bar'); 137 | var bindArgument = 'argument for bind'; 138 | exampleBar.val.bind(bindArgument); 139 | expect(exampleBar.val.bind.calledWith(bindArgument)).to.be.true(); 140 | }); 141 | it('should call the stub function for functions which do not exist', function() { 142 | var exampleBar = dd(example)('bar')('zzzz'); 143 | var noopFunctionString = 'function(){}'; 144 | var invokeFunctionString = exampleBar.invoke.toString().replace(/\s/g, ''); 145 | expect(invokeFunctionString).to.equal(noopFunctionString); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/drilldown.js: -------------------------------------------------------------------------------- 1 | // drilldown.js 2 | // simple nevernull alternative 3 | 'use strict'; 4 | 5 | var isFunction = (function() { 6 | // Thanks to underscore for identifying typeof bugs 7 | var regexTypeofIsCorrect = typeof /./ !== 'function'; 8 | var int8ArrayTypeofIsCorrect = typeof Int8Array !== 'object'; 9 | /* istanbul ignore else */ 10 | if (regexTypeofIsCorrect && int8ArrayTypeofIsCorrect) { 11 | return function(obj) { 12 | return typeof obj === 'function' || false; 13 | }; 14 | } 15 | /* istanbul ignore next */ 16 | return function(obj) { 17 | return Object.prototype.toString.call(obj) === '[object Function]'; 18 | }; 19 | })(); 20 | 21 | /** 22 | * drilldown 23 | * Safely accesses deep properties of objects. 24 | * 25 | * var foo; 26 | * foo.bar; 27 | * // TypeError: Cannot read property 'bar' of undefined 28 | * 29 | * var foo = {abc: {def: {ghi: 'jkl'}}}; 30 | * dd(foo)('abc')('def')('ghi').val is 'jkl' 31 | * dd(foo)('abc')('zzz')('yyy').val is undefined 32 | * 33 | * Check if a deep property exists: 34 | * dd(foo)('abc').exists 35 | * 36 | * Works with arrays too: 37 | * var foo = {abc: [ {bar: 'def'},{bar: 'ghi'} ]}; 38 | * dd(foo)('abc')(0)('bar') is 'def' 39 | * 40 | * Safely call functions: 41 | * var foo = {abc: {addOne: function(x) { return x + 1; }}}; 42 | * dd(foo)('abc')('addOne').invoke(5); returns 6 43 | * dd(foo)('zzz')('aaa').invoke(5); returns undefined 44 | * 45 | * Update values if the original value exists: 46 | * var foo = {abc: {def: {ghi: 'jkl'}}}; 47 | * var newValue = {ping: 'pong'}; 48 | * dd(foo)('abc')('def').update(newValue); 49 | * - foo is now {abc: {def: {ping: 'pong'}}} 50 | * - {ping: 'pong'} is returned 51 | * dd(foo)('abc')('zzz').update(5); 52 | * - foo is unchanged 53 | * - undefined is returned 54 | * 55 | * Set values even if the path drilled to does not exist: 56 | * var foo = {abc: {}}; 57 | * dd(foo)('abc')('def')('ghi').set('jkl'); 58 | * - foo is now {abc: {def: {ghi: 'jkl}}} 59 | * 60 | * To prevent confusion, only own properties are drilled into. 61 | * 62 | * Available properties: 63 | * - val - the value 64 | * - exists - true if val is defined 65 | * - update function(value) - sets the value if the value exists 66 | * - set function(value) - sets the value at any path 67 | * - invoke - the value if the value is a function, or else a dummy function 68 | * 69 | * @param {object} object 70 | * @param _context 71 | * @param _key 72 | * @param _root 73 | * @param _rootPath 74 | * @returns {Function} 75 | */ 76 | function dd(object, _context, _key, _root, _rootPath) { 77 | _root = _root || object; 78 | _rootPath = _rootPath || []; 79 | var drill = function(key) { 80 | var nextObject = ( 81 | object && 82 | object.hasOwnProperty(key) && 83 | object[key] || 84 | undefined 85 | ); 86 | return dd(nextObject, object, key, _root, _rootPath.concat(key)); 87 | }; 88 | drill.val = object; 89 | drill.exists = object !== undefined; 90 | drill.set = function(value) { 91 | if (_rootPath.length === 0) { 92 | return; 93 | } 94 | var contextIterator = _root; 95 | for (var depth = 0; depth < _rootPath.length; depth++) { 96 | var key = _rootPath[depth]; 97 | var isFinalDepth = (depth === _rootPath.length - 1); 98 | if (!isFinalDepth) { 99 | contextIterator[key] = ( 100 | contextIterator.hasOwnProperty(key) && 101 | typeof contextIterator[key] === 'object' ? 102 | contextIterator[key] : {} 103 | ); 104 | contextIterator = contextIterator[key]; 105 | } else { 106 | _context = contextIterator; 107 | _key = key; 108 | } 109 | } 110 | _context[_key] = value; 111 | drill.val = value; 112 | drill.exists = value !== undefined; 113 | return value; 114 | }; 115 | drill.update = function(value) { 116 | if (drill.exists) { 117 | _context[_key] = value; 118 | drill.val = value; 119 | return value; 120 | } 121 | }; 122 | drill.invoke = isFunction(object) ? Function.prototype.bind.call(object, _context) : function () { 123 | }; 124 | 125 | return drill; 126 | } 127 | 128 | module.exports = dd; 129 | 130 | --------------------------------------------------------------------------------