├── .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 |
--------------------------------------------------------------------------------