├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── index.js ├── license ├── package.json ├── readme.md └── test └── spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{md,jade}] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "regexp": true, 14 | "undef": true, 15 | "unused": true, 16 | "strict": true, 17 | "trailing": true, 18 | "smarttabs": true, 19 | "asi": true 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function id (x) { return x } 4 | function second (x,y) { return y } 5 | 6 | var deadLogger = { 7 | log: function() { 8 | throw new Error('Cannot log because console is not defined and no logger was provided.') 9 | } 10 | } 11 | 12 | function shackles(host, options) { 13 | 14 | host = host || {} 15 | options = options || {} 16 | var logger = options.logger || (typeof console !== undefined ? console : deadLogger) 17 | 18 | // a mutating value which the host function wrappers will have closure over 19 | var value 20 | 21 | // when enabled, all chained functions will invoke the logger 22 | var logAll = false 23 | 24 | // the chaining object that houses all the wrapped functions 25 | var container = { 26 | value: function() { 27 | return value 28 | }, 29 | toString: function() { 30 | return value.toString() 31 | }, 32 | tap: function(f) { 33 | var result = f(value) 34 | if(result !== undefined) { 35 | value = result 36 | } 37 | return container 38 | }, 39 | log: function(enable) { 40 | 41 | // if a flag was passed, toggle logging on all chain methods 42 | if(enable !== undefined) { 43 | logAll = enable 44 | } 45 | 46 | if(enable !== false || logAll) { 47 | logger.log(value) 48 | } 49 | 50 | return container 51 | } 52 | } 53 | 54 | // a wrapper function that invokes the given function with the mutator value as the first argument 55 | // attaches to the container for chaining 56 | // must be part of a separate create method so that f is retained (it gets lost in the for loop iteration otherwise) 57 | function createMutator(f) { 58 | return function() { 59 | var args = Array.prototype.concat.apply([value], [Array.prototype.slice.call(arguments)]) 60 | value = f.apply(null, args) 61 | if(logAll) { 62 | container.log() 63 | } 64 | // console.log('arguments', arguments, 'args', args, 'value', value) 65 | return container 66 | } 67 | } 68 | 69 | // helper method to add a value at the specified key to the container 70 | function addToContainer(key, value) { 71 | 72 | var f = typeof value === 'function' ? 73 | value : 74 | id.bind(null, value) 75 | 76 | container[key] = createMutator(f) 77 | } 78 | 79 | // copy each property from the host to the container as a chainable function with the same name 80 | function addAllPropertiesToContainer() { 81 | 82 | for(var key in host) { 83 | 84 | // protect the value property 85 | if(key in container) { 86 | console.warn(key + ' is reserved. The host object\'s property will be ignored.') 87 | continue 88 | } 89 | 90 | addToContainer(key, host[key]) 91 | } 92 | } 93 | 94 | // initialize 95 | addAllPropertiesToContainer() 96 | 97 | // return a function that can set an initial value and start the chain 98 | return createMutator(second) 99 | } 100 | 101 | module.exports = shackles 102 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The ISC License (ISC) 2 | 3 | Copyright (c) Raine Lourie (https://github.com/metaraine) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shackles", 3 | "version": "0.2.0", 4 | "description": "Minimal chaining library with tapping and logging", 5 | "license": "ISC", 6 | "repository": "metaraine/shackles", 7 | "author": { 8 | "name": "Raine Lourie", 9 | "email": "raine@collegecoding.com", 10 | "url": "https://github.com/metaraine" 11 | }, 12 | "engines": { 13 | "node": ">=0.10.0" 14 | }, 15 | "scripts": { 16 | "test": "mocha", 17 | "browser": "browserify -s $npm_package_name -o browser.js ." 18 | }, 19 | "files": [ 20 | "index.js" 21 | ], 22 | "keywords": [ 23 | "chain", 24 | "chaining", 25 | "method", 26 | "tap", 27 | "tapping", 28 | "log", 29 | "logging", 30 | "spy", 31 | "spying" 32 | ], 33 | "devDependencies": { 34 | "browserify": "^7.0.0", 35 | "insist": "^0.2.3", 36 | "lodash": "^2.4.1", 37 | "mocha": "^2.0.1" 38 | }, 39 | "dependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # shackles 2 | [![Build Status](https://travis-ci.org/metaraine/shackles.svg?branch=master)](https://travis-ci.org/metaraine/shackles) 3 | [![NPM version](https://badge.fury.io/js/shackles.svg)](http://badge.fury.io/js/shackles) 4 | 5 | > A minimal chaining library with tapping and logging 6 | 7 | 8 | ## Install 9 | 10 | ```sh 11 | $ npm install --save shackles 12 | ``` 13 | 14 | 15 | ## Basic Usage 16 | 17 | Add chaining to a library: 18 | 19 | ```js 20 | var stringlib = { 21 | prepend: function(str, chr) { 22 | return chr + str 23 | }, 24 | append: function(str, chr) { 25 | return str + chr 26 | } 27 | } 28 | 29 | var chain = shackles(stringlib) 30 | 31 | var result = chain('Hello') 32 | .prepend('(') 33 | .append('!') 34 | .append(')') 35 | .value() // (Hello!) 36 | ``` 37 | 38 | If underscore didn't have chaining, we could easily add it: 39 | 40 | ```js 41 | var chain = shackles(_) 42 | 43 | var result = chain([1,2,3]) 44 | .map(function (x) { return x*x }) 45 | .filter(function (x) { return x > 2 }) 46 | .value() // [4,9] 47 | ``` 48 | 49 | Scalar properties become chainable methods that override the underlying value: 50 | 51 | ```js 52 | var chain = shackles({ 53 | inc: function(x) { return x+1 } 54 | pi: 3.141592654 55 | }) 56 | 57 | var result = chain(0) 58 | .inc() 59 | .inc() 60 | .pi() 61 | .inc() 62 | .value() // 4.141592654 63 | ``` 64 | 65 | ## Tapping 66 | 67 | You can transform the value at any point in the chain: 68 | 69 | ```js 70 | var chain = shackles(/* lib */) 71 | 72 | var result = chain(10) 73 | .tap(function(value) { 74 | return value * 2; 75 | }) 76 | .value() // 20 77 | ``` 78 | 79 | ## Logging 80 | 81 | You can log the value at any point in the chain: 82 | The default logger method is `console`: 83 | 84 | ```js 85 | var chain = shackles({ 86 | inc: function(x) { return x+1 } 87 | }) 88 | 89 | var result = chain(0) 90 | .inc() 91 | .log() // 1 92 | .inc() 93 | .log() // 2 94 | .inc() 95 | .inc() 96 | .value() // 4 97 | ``` 98 | 99 | You can override the default logger: 100 | 101 | ```js 102 | var doubled = null 103 | 104 | var chain = shackles({}, { 105 | logger: { 106 | log: function(value) { 107 | doubled = value * 2 108 | } 109 | } 110 | }) 111 | 112 | var result = chain(10) 113 | .log() 114 | .value() // 10 115 | 116 | console.log(doubled) // 20 117 | ``` 118 | 119 | You can enable/disable logging for longer sections of the chain: 120 | 121 | ```js 122 | var history = [] 123 | 124 | var stringlib = { 125 | prepend: function(str, chr) { 126 | return chr + str 127 | }, 128 | append: function(str, chr) { 129 | return str + chr 130 | } 131 | } 132 | 133 | var chain = shackles(stringlib, { 134 | logger: { 135 | log: function(value) { 136 | history.push(value) 137 | } 138 | } 139 | }) 140 | 141 | var result = chain('Hello') 142 | .log(true) 143 | .prepend('(') 144 | .append('!') 145 | .append(')') 146 | .log(false) 147 | .append('?') 148 | .append('?') 149 | .append('?') 150 | .value() // (Hello!)??? 151 | 152 | console.log(history) 153 | /* [ 154 | 'Hello', 155 | '(Hello', 156 | '(Hello!', 157 | '(Hello!)' 158 | ]) */ 159 | ``` 160 | 161 | ## License 162 | 163 | ISC © [Raine Lourie](https://github.com/metaraine) 164 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('insist') 4 | var shackles = require('../index.js') 5 | var _ = require('lodash') 6 | 7 | describe('shackles', function() { 8 | 9 | it('should be able to set an initial value and retrieve it with .value()', function () { 10 | var chain = shackles({}) 11 | var result = chain('test').value() 12 | assert.equal(result, 'test') 13 | }) 14 | 15 | it('should default to empty object if no host object is provided', function () { 16 | var chain = shackles() 17 | var result = chain('test').value() 18 | assert.equal(result, 'test') 19 | }) 20 | 21 | it('should have a toString method that returns the string represention of the boxed value', function () { 22 | var chain = shackles() 23 | var result = chain('test').toString() 24 | assert.equal(result, 'test') 25 | }) 26 | 27 | it('should chain a string library', function () { 28 | var stringlib = { 29 | prepend: function(str, chr, chr2) { 30 | return (chr || '') + (chr2 || '') + str 31 | }, 32 | append: function(str, chr, chr2) { 33 | return str + (chr || '') + (chr2 || '') 34 | } 35 | } 36 | 37 | var chain = shackles(stringlib) 38 | 39 | var result = chain('Hello') 40 | .prepend('(') 41 | .append('!', '?') 42 | .append(')') 43 | .value() 44 | 45 | assert.equal(result, '(Hello!?)') 46 | }) 47 | 48 | it('should chain lodash', function () { 49 | 50 | var chain = shackles(_) 51 | 52 | var result = chain([1,2,3]) 53 | .map(function (x) { return x*x }) 54 | .filter(function (x) { return x > 2 }) 55 | .value() 56 | 57 | assert.deepEqual(result, [4, 9]) 58 | }) 59 | 60 | it('should override the boxed value with any scalar properties that are called as chained functions', function () { 61 | 62 | var chain = shackles({ 63 | num: 10 64 | }) 65 | 66 | var result = chain('dummy') 67 | .num() 68 | .value() 69 | 70 | assert.equal(result, 10) 71 | }) 72 | 73 | describe('tap', function() { 74 | 75 | it('should have a chainable tap function that passes the value to a function', function () { 76 | 77 | var chain = shackles() 78 | 79 | var myval = null 80 | 81 | var result = chain(10) 82 | .tap(function(value) { 83 | myval = value * 2 84 | }) 85 | .value() 86 | 87 | assert.equal(result, 10) 88 | assert.equal(myval, 20) 89 | }) 90 | 91 | it('should override the boxed value with the value that the tap callback returns', function () { 92 | 93 | var chain = shackles() 94 | 95 | var result = chain(10) 96 | .tap(function(value) { 97 | return value/2 98 | }) 99 | .value() 100 | 101 | assert.equal(result, 5) 102 | }) 103 | }) 104 | 105 | describe('log', function() { 106 | 107 | it('should have a chainable log function that uses an overrideable logger', function () { 108 | 109 | var doubled = null 110 | 111 | var chain = shackles({}, { 112 | logger: { 113 | log: function(value) { 114 | doubled = value * 2 115 | } 116 | } 117 | }) 118 | 119 | var result = chain(10) 120 | .log() 121 | .value() 122 | 123 | assert.equal(result, 10) 124 | assert.equal(doubled, 20) 125 | }) 126 | 127 | it('should enable/disable logging on all chained functions', function () { 128 | 129 | var history = [] 130 | 131 | var stringlib = { 132 | prepend: function(str, chr, chr2) { 133 | return (chr || '') + (chr2 || '') + str 134 | }, 135 | append: function(str, chr, chr2) { 136 | return str + (chr || '') + (chr2 || '') 137 | } 138 | } 139 | 140 | var chain = shackles(stringlib, { 141 | logger: { 142 | log: function(value) { 143 | history.push(value) 144 | } 145 | } 146 | }) 147 | 148 | var result = chain('Hello') 149 | .log(true) 150 | .prepend('(') 151 | .append('!', '?') 152 | .log(false) 153 | .append(')') 154 | .value() 155 | 156 | assert.deepEqual(history, [ 157 | 'Hello', 158 | '(Hello', 159 | '(Hello!?', 160 | ]) 161 | 162 | assert.equal(result, '(Hello!?)') 163 | }) 164 | }) 165 | 166 | }) 167 | --------------------------------------------------------------------------------