├── .gitignore ├── bower.json ├── package.json ├── LICENSE ├── safe-access.js ├── test └── test.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe-access", 3 | "main": "safe-access.js", 4 | "version": "0.2.0", 5 | "homepage": "https://github.com/erictrinh/safe-access", 6 | "authors": [ 7 | "Eric Trinh" 8 | ], 9 | "description": "A utility to allow for safe accessing of nested properties", 10 | "keywords": [ 11 | "safe", 12 | "accessor", 13 | "nested" 14 | ], 15 | "license": "MIT", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe-access", 3 | "version": "0.2.0", 4 | "description": "A utility to allow for safe accessing of nested properties", 5 | "main": "safe-access.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec" 8 | }, 9 | "keywords": [ 10 | "safe", 11 | "accessor", 12 | "nested" 13 | ], 14 | "author": "Eric Trinh", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "mocha": "^1.17.1", 18 | "chai": "^1.9.0" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/erictrinh/safe-access.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/erictrinh/safe-access/issues" 26 | }, 27 | "homepage": "https://github.com/erictrinh/safe-access" 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Eric Trinh 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /safe-access.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if(typeof define === "function" && define.amd) { 3 | define(factory); 4 | } else if(typeof module === "object" && module.exports) { 5 | module.exports = factory(); 6 | } else { 7 | root.safeAccess = factory(); 8 | } 9 | }(this, function() { 10 | 11 | return function access(obj, accessStr) { 12 | // auto-curry here 13 | if (isUndefined(accessStr)) { 14 | return access.bind(null, obj); 15 | } 16 | 17 | var funcArgs = Array.prototype.slice.call(arguments, 2); 18 | return helper(obj, tokenize(accessStr), null, funcArgs); 19 | }; 20 | 21 | function helper(obj, tokens, ctx, fnArgs) { 22 | if (tokens.length === 0) { 23 | return obj; 24 | } 25 | 26 | var currentToken = tokens[0]; 27 | 28 | if (isUndefined(obj) || isNull(obj) || 29 | (isTokenFunctionCall(currentToken) && !isFunction(obj))) { 30 | return undefined; 31 | } 32 | 33 | if (isTokenFunctionCall(currentToken)) { 34 | 35 | return helper(obj[isArray(fnArgs[0]) ? 'apply' : 'call'](ctx, fnArgs[0]), 36 | tokens.slice(1), 37 | // clear context because consecutive fn calls execute in global context 38 | // e.g. `a.b()()` 39 | null, 40 | fnArgs.slice(1)); 41 | 42 | } else if (isTokenArrayAccess(currentToken)) { 43 | 44 | return helper(obj[parseInt(currentToken.substr(1), 10)], 45 | tokens.slice(1), 46 | // lookahead two tokens for function calls 47 | isTokenFunctionCall(tokens[1]) ? obj : ctx, 48 | fnArgs); 49 | 50 | } else { 51 | 52 | return helper(obj[currentToken], 53 | tokens.slice(1), 54 | // lookahead two tokens for function calls 55 | isTokenFunctionCall(tokens[1]) ? obj : ctx, 56 | fnArgs); 57 | 58 | } 59 | } 60 | 61 | function isUndefined(a) { 62 | return a === void 0; 63 | } 64 | 65 | function isNull(a) { 66 | return a === null; 67 | } 68 | 69 | function isArray(a) { 70 | return Array.isArray(a); 71 | } 72 | 73 | function isFunction(a) { 74 | return typeof a === 'function'; 75 | } 76 | 77 | function isTokenFunctionCall(token) { 78 | return token === '()'; 79 | } 80 | 81 | function isTokenArrayAccess(token) { 82 | return /^\[\d+\]$/.test(token); 83 | } 84 | 85 | function tokenize(str) { 86 | return str.split(/\.|(\(\))|(\[\d+?])/).filter(function(t) { return t; }); 87 | } 88 | 89 | })); 90 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var access = require('../safe-access'); 3 | 4 | describe('safe-access', function() { 5 | var a = { 6 | b: { 7 | c: { 8 | d: function() { 9 | return 'hi!'; 10 | } 11 | }, 12 | add: function(a, b) { 13 | return a + b; 14 | }, 15 | f: null, 16 | e: false, 17 | g: '' 18 | }, 19 | returnThis: function() { 20 | return this; 21 | }, 22 | returnReturnThis: function() { 23 | return function() { 24 | return this; 25 | }; 26 | }, 27 | arr: [{key: 'hey'}] 28 | }; 29 | 30 | var b = ['one', function() { return this; }]; 31 | 32 | var aDot = access(a); 33 | var bDot = access(b); 34 | 35 | describe('property access', function() { 36 | it('should return undefined if the initial object is undefined', function() { 37 | expect(access(undefined, 'b.c.d')).to.be.undefined; 38 | }); 39 | 40 | it('should access 1 level down properly', function() { 41 | expect(aDot('b')).to.equal(a.b); 42 | }); 43 | 44 | it('should return the right value even if the value is falsey', function() { 45 | expect(aDot('b.e')).to.be.false; 46 | }); 47 | 48 | it('should return undefined if property doesn\'t exist', function() { 49 | expect(aDot('yippee')).to.be.undefined; 50 | }); 51 | 52 | it('should access 2 levels down', function() { 53 | expect(aDot('b.c')).to.equal(a.b.c); 54 | }); 55 | 56 | it('should not freak out if a property in the chain is non-existent', function() { 57 | expect(aDot('yippee.c')).to.be.undefined; 58 | expect(aDot('b.yippee.c')).to.be.undefined; 59 | }); 60 | 61 | it('should return the null property', function() { 62 | expect(aDot('b.f')).to.be.null; 63 | }); 64 | 65 | it('should behave correctly with properties that are null', function() { 66 | expect(aDot('b.f.e')).to.be.undefined; 67 | }); 68 | }); 69 | 70 | 71 | describe('array access', function() { 72 | it('should access arrays', function() { 73 | expect(aDot('arr[0].key')).to.equal('hey'); 74 | }); 75 | 76 | it('should soak faulty array accesses', function() { 77 | expect(aDot('arr[1].key')).to.be.undefined; 78 | }); 79 | 80 | it('should work if the first token is an array access', function() { 81 | expect(bDot('[0]')).to.equal('one'); 82 | }); 83 | }); 84 | 85 | describe('function calls', function() { 86 | it('should call a function', function() { 87 | expect(aDot('b.c.d()')).to.be.equal('hi!'); 88 | }); 89 | 90 | it('should access past the falsey value', function() { 91 | expect(aDot('b.g.concat()', 'boop!')).to.equal('boop!'); 92 | }); 93 | 94 | it('should returned undefined if trying to call a non-function', function() { 95 | expect(aDot('b.c()')).to.be.undefined; 96 | }); 97 | 98 | it('should call the function with the rest args', function() { 99 | expect(aDot('b.add()', [1, 2])).to.equal(3); 100 | }); 101 | 102 | it('should call the function with more than 1 rest args with correct context', function() { 103 | expect(aDot('b.add().toFixed()', [1, 2], 2)).to.equal('3.00'); 104 | }); 105 | 106 | it('should call a shallow function in the context of the primary object', function() { 107 | expect(aDot('returnThis()')).to.equal(a); 108 | }); 109 | 110 | it('should call a function-returning function in the global context', function() { 111 | expect(aDot('returnReturnThis()()')).to.equal(global); 112 | }); 113 | 114 | it('should call a function in an array with the array as context', function() { 115 | expect(bDot('[1]()')).to.equal(b); 116 | }); 117 | }); 118 | 119 | }); 120 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Safe Access 2 | 3 | `safe-access` is a Javascript utility to allow for safe accessing of nested properties by soaking up nulls, inspired by Coffeescript's [existential operator](http://coffeescript.org/#operators). 4 | 5 | ## Install 6 | 7 | ### Node 8 | 9 | ``` 10 | npm install safe-access 11 | ``` 12 | 13 | ### Bower 14 | 15 | ``` 16 | bower install safe-access 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Node 22 | 23 | ```javascript 24 | var access = require('safe-access'); 25 | var nestedThang = access(obj, 'that.is.very.nested'); 26 | ``` 27 | 28 | ### Require.js 29 | 30 | ```javascript 31 | require.config({ 32 | paths: { 33 | "safe-access": "path/to/safe-access", 34 | } 35 | }); 36 | ``` 37 | 38 | ```javascript 39 | define(["safe-access"], function (access) { 40 | var nestedThang = access(obj, 'that.is.very.nested'); 41 | }); 42 | ``` 43 | 44 | ### Browser 45 | 46 | ```html 47 | 48 | 51 | ``` 52 | 53 | ## I know Coffeescript. Why should I use this? 54 | 55 | Are you writing Javascript and miss doing this in Coffeescript? 56 | 57 | ```coffeescript 58 | very?.nested?.property?.and?.array?[0]?.func?() 59 | ``` 60 | 61 | Well, now you can do that without all the question marks: 62 | 63 | ```javascript 64 | var access = require('safe-access'); 65 | access(very, 'nested.property.and.array[0].func()'); 66 | ``` 67 | 68 | ## I don't know Coffeescript. Why should I use this? 69 | 70 | When accessing deeply nested properties in Javascript, it's important to guard against accessing non-existent properties in the middle of a chain. For example, `obj.that.is.very.nested` will throw an error if the property `that` doesn't exist. This is bad because it halts your program altogether (unless you have a try/catch in place). In Javascript, one way to guard against this is with long `&&` chains: 71 | 72 | ```javascript 73 | var nestedThang = obj.that && obj.that.is && obj.that.is.very && obj.that.is.very.nested; 74 | ``` 75 | 76 | `nestedThang` will simply be `undefined` if `that` doesn't exist (instead of throwing an error). But, this gets quite messy (and annoying to type out). 77 | 78 | The equivalent, using `safe-access` (in Node): 79 | 80 | ```javascript 81 | var access = require('safe-access'); 82 | var nestedThang = access(obj, 'that.is.very.nested'); 83 | ``` 84 | 85 | `safe-access` can even be used to safely access arrays and call functions: 86 | 87 | ```javascript 88 | var obscenelyNested = access(obj, 'leading.to.array[0].andFunc()'); 89 | ``` 90 | 91 | which is the equivalent of this charming thing in Javascript: 92 | 93 | ```javascript 94 | var obscenelyNested = obj && 95 | obj.leading && 96 | obj.leading.to && 97 | obj.leading.to.array && 98 | obj.leading.to.array[0] && 99 | obj.leading.to.array[0].andFunc && 100 | (typeof obj.leading.to.array[0].andFunc === 'function' ? 101 | obj.leading.to.array[0].andFunc() : 102 | undefined); 103 | ``` 104 | 105 | ## Calling functions with arguments 106 | 107 | Sometimes, it's necessary to call functions with some arguments. Every argument after the accessor string (3rd argument and beyond) will be used as the arguments to each function call in the accessor string. Like this: 108 | 109 | ```javascript 110 | // equivalent of `obj.thing.add(1, 2);` 111 | access(obj, 'thing.add()', [1, 2]); 112 | ``` 113 | 114 | Or maybe you have multiple function calls that receive arguments: 115 | 116 | ```javascript 117 | // equivalent of `thing.add(1, 2).toFixed(1).substr(2);` 118 | access(obj, 'thing.add().toFixed().substr()', [1, 2], 1, 2); 119 | ``` 120 | 121 | Notice that if you need to pass in multiple arguments (like in the `add` function), you'll need to pass the arguments as an array. The caveat is **if you need to pass in an array as an argument, you'll need to pass in a nested array**. 122 | 123 | An example, passing in an array as an argument: 124 | 125 | ```javascript 126 | access(window._, 'compact()', [[ false, 'boop', 'beep', '', 'meep' ]]); 127 | // returns [ 'boop', 'beep', 'meep' ] OR undefined if window._ doesn't exist 128 | ``` 129 | 130 | ## Automatic Currying 131 | `safe-access` auto-curries, which means omitting the second argument will return a function that you can use to access the same object over and over again. This can be useful if you are accessing many different nested properties on an object. 132 | 133 | ```javascript 134 | var objDot = access(obj); 135 | objDot('nested.thing'); // obj.nested.thing 136 | objDot('other.nested.thing'); // obj.other.nested.thing 137 | ``` 138 | 139 | ## License 140 | 141 | `safe-access` is freely distributable under the terms of the [MIT license](LICENSE). 142 | --------------------------------------------------------------------------------