├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── index.js ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '4' 5 | after_script: 6 | - 'cat ./coverage/lcov.info | ./node_modules/.bin/coveralls' 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = (options, fn, target) => { 3 | const chainables = options.chainableMethods || {}; 4 | const spread = options.spread; 5 | const defaults = Object.assign({}, options.defaults); 6 | 7 | function extend(target, getter, ctx) { 8 | for (const key of Object.keys(chainables)) { 9 | Object.defineProperty(target, key, { 10 | enumerable: true, 11 | configurable: true, 12 | get() { 13 | return wrap(getter, chainables[key], ctx || this); 14 | } 15 | }); 16 | } 17 | } 18 | 19 | function wrap(createOpts, extensionOpts, ctx) { 20 | function wrappedOpts() { 21 | return Object.assign(createOpts(), extensionOpts); 22 | } 23 | 24 | function wrappedFn() { 25 | let args = new Array(arguments.length); 26 | 27 | for (let i = 0; i < args.length; i++) { 28 | args[i] = arguments[i]; 29 | } 30 | 31 | if (spread) { 32 | args.unshift(wrappedOpts()); 33 | } else { 34 | args = [wrappedOpts(), args]; 35 | } 36 | 37 | return fn.apply(ctx || this, args); 38 | } 39 | 40 | extend(wrappedFn, wrappedOpts, ctx); 41 | 42 | return wrappedFn; 43 | } 44 | 45 | function copyDefaults() { 46 | return Object.assign({}, defaults); 47 | } 48 | 49 | if (target) { 50 | extend(target, copyDefaults); 51 | return target; 52 | } 53 | 54 | return wrap(copyDefaults); 55 | }; 56 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) James Talmage (github.com/jamestalmage) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "option-chain", 3 | "version": "1.0.0", 4 | "description": "Use fluent property chains in lieu of options objects", 5 | "license": "MIT", 6 | "repository": "avajs/option-chain", 7 | "author": { 8 | "name": "James Talmage", 9 | "email": "james@talmage.io", 10 | "url": "github.com/jamestalmage" 11 | }, 12 | "engines": { 13 | "node": ">=4" 14 | }, 15 | "scripts": { 16 | "test": "xo && nyc ava" 17 | }, 18 | "files": [ 19 | "index.js" 20 | ], 21 | "keywords": [ 22 | "option", 23 | "options", 24 | "chain", 25 | "chains", 26 | "chainable", 27 | "fluent" 28 | ], 29 | "devDependencies": { 30 | "ava": "^0.19.1", 31 | "coveralls": "^2.11.6", 32 | "nyc": "^10.3.2", 33 | "xo": "^0.18.2" 34 | }, 35 | "nyc": { 36 | "reporter": [ 37 | "lcov", 38 | "text" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # option-chain [![Build Status](https://travis-ci.org/avajs/option-chain.svg?branch=master)](https://travis-ci.org/avajs/option-chain) [![Coverage Status](https://coveralls.io/repos/github/avajs/option-chain/badge.svg?branch=master)](https://coveralls.io/github/avajs/option-chain?branch=master) 2 | 3 | > Use fluent property chains in lieu of options objects 4 | 5 | ## Install 6 | 7 | ``` 8 | $ npm install option-chain 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | const optionChain = require('option-chain'); 15 | 16 | const optionDefinition = { 17 | defaults: { 18 | bar: false 19 | }, 20 | chainableMethods: { 21 | foo: {foo: true}, 22 | notFoo: {foo: false}, 23 | bar: {bar: true} 24 | } 25 | }; 26 | 27 | function printOptionsAndArgs(options, args) { 28 | console.log(options); 29 | 30 | if (args.length) { 31 | console.log(args); 32 | } 33 | } 34 | 35 | const fn = optionChain(optionDefinition, printOptionsAndArgs); 36 | 37 | fn(); 38 | //=> [{bar: false}] 39 | fn.bar(); 40 | //=> [{bar: true}] 41 | fn.foo.bar(); 42 | //=> [{foo: true, bar: false}] 43 | 44 | fn.foo('a', 'b'); 45 | //=> [{foo: true, bar: false}] 46 | //=> ['a', 'b'] 47 | ``` 48 | 49 | 50 | ## API 51 | 52 | ### optionChain(options, callback, target?) 53 | 54 | #### options 55 | 56 | Type: `object` 57 | 58 | ##### chainableMethods 59 | 60 | *Required*\ 61 | Type: `object` 62 | 63 | A map of chainable property names to the options set by adding property to the chain. 64 | 65 | Given the following: 66 | 67 | ```js 68 | const chainableMethods = { 69 | foo: {foo: true}, 70 | notFoo: {foo: false}, 71 | bar: {bar: true}, 72 | both: {foo: true, bar: true} 73 | } 74 | ``` 75 | 76 | Then: 77 | 78 | - `fn.foo` would set `foo` to `true`. 79 | - `fn.bar` would set `bar` to `true`. 80 | - `fn.both` sets both `foo` and `bar` to `true`. 81 | - The last property in the chain takes precedence, so `fn.foo.notFoo` would result in `foo` being `false`. 82 | 83 | 84 | ##### defaults 85 | 86 | Type: `object`\ 87 | Default: `{}` 88 | 89 | A set of default starting properties. 90 | 91 | ##### spread 92 | 93 | Type: `boolean`\ 94 | Default: `false` 95 | 96 | By default, any arguments passed to the wrapper are passed as an array to the second argument of the wrapped function. When this is `true`, additional arguments will be spread out as additional arguments: 97 | 98 | ```js 99 | function withoutSpread(opts, args) { 100 | let foo = args[0]; 101 | let bar = args[1]; 102 | // … 103 | } 104 | 105 | function withSpread(opts, foo, bar) { 106 | // … 107 | } 108 | ``` 109 | 110 | #### callback 111 | 112 | Type: `Function` 113 | 114 | This callback is called with the accumulated options as the first argument. Depending on the value of `options.spread`, arguments passed to the wrapper will either be an array as the second argument or spread out as the 2nd, 3rd, 4th... arguments. 115 | 116 | #### target 117 | 118 | If supplied, the `target` object is extended with the property getters and returned. Otherwise a wrapper function is created for `options.defaults`, then that wrapper is extended and returned. 119 | 120 | *Hint:* If you want to extend a `target` and add a method that simply uses the defaults, add a chainable method definition with an empty spec: 121 | 122 | ```js 123 | const chainableMethods = { 124 | defaultMethodName: {} 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import fn from '.'; 3 | 4 | test('defaults and args are passed', t => { 5 | t.plan(2); 6 | 7 | fn({ 8 | defaults: {foo: 'bar'} 9 | }, (opts, args) => { 10 | t.deepEqual(opts, {foo: 'bar'}); 11 | t.deepEqual(args, ['uni', 'corn']); 12 | })('uni', 'corn'); 13 | }); 14 | 15 | test('chainableMethods extend the options passed', t => { 16 | t.plan(2); 17 | 18 | fn({ 19 | defaults: {foo: 'bar'}, 20 | chainableMethods: { 21 | moo: {cow: true} 22 | } 23 | }, (opts, args) => { 24 | t.deepEqual(opts, {foo: 'bar', cow: true}); 25 | t.deepEqual(args, ['duck', 'goose']); 26 | }).moo('duck', 'goose'); 27 | }); 28 | 29 | test('last item in the chain takes precedence', t => { 30 | t.plan(4); 31 | 32 | const config = { 33 | chainableMethods: { 34 | foo: {foo: true}, 35 | notFoo: {foo: false} 36 | } 37 | }; 38 | 39 | let expected = true; 40 | 41 | function isExpected(opts) { 42 | t.is(opts.foo, expected); 43 | } 44 | 45 | const notFoo = fn(config, isExpected).notFoo; 46 | const foo = fn(config, isExpected).foo; 47 | 48 | foo(); 49 | notFoo.foo(); 50 | 51 | expected = false; 52 | 53 | notFoo(); 54 | foo.notFoo(); 55 | }); 56 | 57 | test('can extend a target object', t => { 58 | const ctx = {}; 59 | 60 | const result = fn({ 61 | chainableMethods: { 62 | def: {}, 63 | foo: {foo: true}, 64 | notFoo: {foo: false}, 65 | bar: {bar: true} 66 | } 67 | }, (opts, args) => [opts, args], ctx); 68 | 69 | t.is(result, ctx); 70 | t.deepEqual(ctx.def(), [{}, []]); 71 | t.deepEqual(ctx.foo('baz'), [{foo: true}, ['baz']]); 72 | t.deepEqual(ctx.notFoo('quz'), [{foo: false}, ['quz']]); 73 | t.deepEqual(ctx.bar.foo.notFoo(), [{foo: false, bar: true}, []]); 74 | }); 75 | 76 | test('this is preserved', t => { 77 | const ctx = {}; 78 | 79 | fn({ 80 | chainableMethods: { 81 | def: {}, 82 | foo: {foo: true}, 83 | notFoo: {foo: false}, 84 | bar: {bar: true} 85 | } 86 | }, function (opts) { 87 | t.is(this, ctx); 88 | return opts; 89 | }, ctx); 90 | 91 | t.deepEqual(ctx.def(), {}); 92 | t.deepEqual(ctx.foo.bar(), {foo: true, bar: true}); 93 | }); 94 | 95 | test('this is preserved correctly using prototypes', t => { 96 | function Constructor() {} 97 | 98 | fn({ 99 | chainableMethods: { 100 | def: {}, 101 | foo: {foo: true}, 102 | notFoo: {foo: false}, 103 | bar: {bar: true} 104 | } 105 | }, function (opts) { 106 | return [this, opts]; 107 | }, Constructor.prototype); 108 | 109 | const c1 = new Constructor(); 110 | const c2 = new Constructor(); 111 | 112 | t.is(c1.def()[0], c1); 113 | t.is(c1.foo.bar()[0], c1); 114 | t.is(c2.def()[0], c2); 115 | t.is(c2.bar.foo()[0], c2); 116 | }); 117 | 118 | test('spread option spreads arguments', t => { 119 | const def = fn({ 120 | spread: true, 121 | chainableMethods: { 122 | foo: {foo: true} 123 | } 124 | }, function () { 125 | return Array.prototype.slice.call(arguments); 126 | }); 127 | 128 | t.deepEqual(def('a', 'b'), [{}, 'a', 'b']); 129 | t.deepEqual(def.foo('c', 'd'), [{foo: true}, 'c', 'd']); 130 | }); 131 | --------------------------------------------------------------------------------