package/index.js0000755000175000017500000000717111610520202014334 0ustar substacksubstackvar Traverse = require('traverse'); var EventEmitter = require('events').EventEmitter; module.exports = Chainsaw; function Chainsaw (builder) { var saw = Chainsaw.saw(builder, {}); var r = builder.call(saw.handlers, saw); if (r !== undefined) saw.handlers = r; saw.record(); return saw.chain(); }; Chainsaw.light = function ChainsawLight (builder) { var saw = Chainsaw.saw(builder, {}); var r = builder.call(saw.handlers, saw); if (r !== undefined) saw.handlers = r; return saw.chain(); }; Chainsaw.saw = function (builder, handlers) { var saw = new EventEmitter; saw.handlers = handlers; saw.actions = []; saw.chain = function () { var ch = Traverse(saw.handlers).map(function (node) { if (this.isRoot) return node; var ps = this.path; if (typeof node === 'function') { this.update(function () { saw.actions.push({ path : ps, args : [].slice.call(arguments) }); return ch; }); } }); process.nextTick(function () { saw.emit('begin'); saw.next(); }); return ch; }; saw.pop = function () { return saw.actions.shift(); }; saw.next = function () { var action = saw.pop(); if (!action) { saw.emit('end'); } else if (!action.trap) { var node = saw.handlers; action.path.forEach(function (key) { node = node[key] }); node.apply(saw.handlers, action.args); } }; saw.nest = function (cb) { var args = [].slice.call(arguments, 1); var autonext = true; if (typeof cb === 'boolean') { var autonext = cb; cb = args.shift(); } var s = Chainsaw.saw(builder, {}); var r = builder.call(s.handlers, s); if (r !== undefined) s.handlers = r; // If we are recording... if ("undefined" !== typeof saw.step) { // ... our children should, too s.record(); } cb.apply(s.chain(), args); if (autonext !== false) s.on('end', saw.next); }; saw.record = function () { upgradeChainsaw(saw); }; ['trap', 'down', 'jump'].forEach(function (method) { saw[method] = function () { throw new Error("To use the trap, down and jump features, please "+ "call record() first to start recording actions."); }; }); return saw; }; function upgradeChainsaw(saw) { saw.step = 0; // override pop saw.pop = function () { return saw.actions[saw.step++]; }; saw.trap = function (name, cb) { var ps = Array.isArray(name) ? name : [name]; saw.actions.push({ path : ps, step : saw.step, cb : cb, trap : true }); }; saw.down = function (name) { var ps = (Array.isArray(name) ? name : [name]).join('/'); var i = saw.actions.slice(saw.step).map(function (x) { if (x.trap && x.step <= saw.step) return false; return x.path.join('/') == ps; }).indexOf(true); if (i >= 0) saw.step += i; else saw.step = saw.actions.length; var act = saw.actions[saw.step - 1]; if (act && act.trap) { // It's a trap! saw.step = act.step; act.cb(); } else saw.next(); }; saw.jump = function (step) { saw.step = step; saw.next(); }; }; package/.npmignore0000644000175000017500000000001511610520202014651 0ustar substacksubstacknode_modules package/package.json0000644000175000017500000000115211610520202015143 0ustar substacksubstack{ "name" : "chainsaw", "version" : "0.1.0", "description" : "Build chainable fluent interfaces the easy way... with a freakin' chainsaw!", "main" : "./index.js", "repository" : { "type" : "git", "url" : "http://github.com/substack/node-chainsaw.git" }, "dependencies" : { "traverse" : ">=0.3.0 <0.4" }, "keywords" : [ "chain", "fluent", "interface", "monad", "monadic" ], "author" : "James Halliday (http://substack.net)", "license" : "MIT/X11", "engine" : { "node" : ">=0.4.0" } } package/README.markdown0000644000175000017500000000756411610520202015373 0ustar substacksubstackChainsaw ======== Build chainable fluent interfaces the easy way in node.js. With this meta-module you can write modules with chainable interfaces. Chainsaw takes care of all of the boring details and makes nested flow control super simple too. Just call `Chainsaw` with a constructor function like in the examples below. In your methods, just do `saw.next()` to move along to the next event and `saw.nest()` to create a nested chain. Examples ======== add_do.js --------- This silly example adds values with a chainsaw. var Chainsaw = require('chainsaw'); function AddDo (sum) { return Chainsaw(function (saw) { this.add = function (n) { sum += n; saw.next(); }; this.do = function (cb) { saw.nest(cb, sum); }; }); } AddDo(0) .add(5) .add(10) .do(function (sum) { if (sum > 12) this.add(-10); }) .do(function (sum) { console.log('Sum: ' + sum); }) ; Output: Sum: 5 prompt.js --------- This example provides a wrapper on top of stdin with the help of [node-lazy](https://github.com/pkrumins/node-lazy) for line-processing. var Chainsaw = require('chainsaw'); var Lazy = require('lazy'); module.exports = Prompt; function Prompt (stream) { var waiting = []; var lines = []; var lazy = Lazy(stream).lines.map(String) .forEach(function (line) { if (waiting.length) { var w = waiting.shift(); w(line); } else lines.push(line); }) ; var vars = {}; return Chainsaw(function (saw) { this.getline = function (f) { var g = function (line) { saw.nest(f, line, vars); }; if (lines.length) g(lines.shift()); else waiting.push(g); }; this.do = function (cb) { saw.nest(cb, vars); }; }); } And now for the new Prompt() module in action: var util = require('util'); var stdin = process.openStdin(); Prompt(stdin) .do(function () { util.print('x = '); }) .getline(function (line, vars) { vars.x = parseInt(line, 10); }) .do(function () { util.print('y = '); }) .getline(function (line, vars) { vars.y = parseInt(line, 10); }) .do(function (vars) { if (vars.x + vars.y < 10) { util.print('z = '); this.getline(function (line) { vars.z = parseInt(line, 10); }) } else { vars.z = 0; } }) .do(function (vars) { console.log('x + y + z = ' + (vars.x + vars.y + vars.z)); process.exit(); }) ; Installation ============ With [npm](http://github.com/isaacs/npm), just do: npm install chainsaw or clone this project on github: git clone http://github.com/substack/node-chainsaw.git To run the tests with [expresso](http://github.com/visionmedia/expresso), just do: expresso Light Mode vs Full Mode ======================= `node-chainsaw` supports two different modes. In full mode, every action is recorded, which allows you to replay actions using the `jump()`, `trap()` and `down()` methods. However, if your chainsaws are long-lived, recording every action can consume a tremendous amount of memory, so we also offer a "light" mode where actions are not recorded and the aforementioned methods are disabled. To enable light mode simply use `Chainsaw.light()` to construct your saw, instead of `Chainsaw()`. package/examples/add_do.js0000644000175000017500000000071111610520202016243 0ustar substacksubstackvar Chainsaw = require('chainsaw'); function AddDo (sum) { return Chainsaw(function (saw) { this.add = function (n) { sum += n; saw.next(); }; this.do = function (cb) { saw.nest(cb, sum); }; }); } AddDo(0) .add(5) .add(10) .do(function (sum) { if (sum > 12) this.add(-10); }) .do(function (sum) { console.log('Sum: ' + sum); }) ; package/examples/prompt.js0000644000175000017500000000324511610520202016357 0ustar substacksubstackvar Chainsaw = require('chainsaw'); var Lazy = require('lazy'); module.exports = Prompt; function Prompt (stream) { var waiting = []; var lines = []; var lazy = Lazy(stream).lines.map(String) .forEach(function (line) { if (waiting.length) { var w = waiting.shift(); w(line); } else lines.push(line); }) ; var vars = {}; return Chainsaw(function (saw) { this.getline = function (f) { var g = function (line) { saw.nest(f, line, vars); }; if (lines.length) g(lines.shift()); else waiting.push(g); }; this.do = function (cb) { saw.nest(cb, vars); }; }); } var util = require('util'); if (__filename === process.argv[1]) { var stdin = process.openStdin(); Prompt(stdin) .do(function () { util.print('x = '); }) .getline(function (line, vars) { vars.x = parseInt(line, 10); }) .do(function () { util.print('y = '); }) .getline(function (line, vars) { vars.y = parseInt(line, 10); }) .do(function (vars) { if (vars.x + vars.y < 10) { util.print('z = '); this.getline(function (line) { vars.z = parseInt(line, 10); }) } else { vars.z = 0; } }) .do(function (vars) { console.log('x + y + z = ' + (vars.x + vars.y + vars.z)); process.exit(); }) ; } package/test/chainsaw.js0000644000175000017500000002327111610520202015775 0ustar substacksubstackvar assert = require('assert'); var Chainsaw = require('../index'); exports.getset = function () { var to = setTimeout(function () { assert.fail('builder never fired'); }, 1000); var ch = Chainsaw(function (saw) { clearTimeout(to); var num = 0; this.get = function (cb) { cb(num); saw.next(); }; this.set = function (n) { num = n; saw.next(); }; var ti = setTimeout(function () { assert.fail('end event not emitted'); }, 50); saw.on('end', function () { clearTimeout(ti); assert.equal(times, 3); }); }); var times = 0; ch .get(function (x) { assert.equal(x, 0); times ++; }) .set(10) .get(function (x) { assert.equal(x, 10); times ++; }) .set(20) .get(function (x) { assert.equal(x, 20); times ++; }) ; }; exports.nest = function () { var ch = (function () { var vars = {}; return Chainsaw(function (saw) { this.do = function (cb) { saw.nest(cb, vars); }; }); })(); var order = []; var to = setTimeout(function () { assert.fail("Didn't get to the end"); }, 50); ch .do(function (vars) { vars.x = 'y'; order.push(1); this .do(function (vs) { order.push(2); vs.x = 'x'; }) .do(function (vs) { order.push(3); vs.z = 'z'; }) ; }) .do(function (vars) { vars.y = 'y'; order.push(4); }) .do(function (vars) { assert.eql(order, [1,2,3,4]); assert.eql(vars, { x : 'x', y : 'y', z : 'z' }); clearTimeout(to); }) ; }; exports.nestWait = function () { var ch = (function () { var vars = {}; return Chainsaw(function (saw) { this.do = function (cb) { saw.nest(cb, vars); }; this.wait = function (n) { setTimeout(function () { saw.next(); }, n); }; }); })(); var order = []; var to = setTimeout(function () { assert.fail("Didn't get to the end"); }, 1000); var times = {}; ch .do(function (vars) { vars.x = 'y'; order.push(1); this .do(function (vs) { order.push(2); vs.x = 'x'; times.x = Date.now(); }) .wait(50) .do(function (vs) { order.push(3); vs.z = 'z'; times.z = Date.now(); var dt = times.z - times.x; assert.ok(dt >= 50 && dt < 75); }) ; }) .do(function (vars) { vars.y = 'y'; order.push(4); times.y = Date.now(); }) .wait(100) .do(function (vars) { assert.eql(order, [1,2,3,4]); assert.eql(vars, { x : 'x', y : 'y', z : 'z' }); clearTimeout(to); times.end = Date.now(); var dt = times.end - times.y; assert.ok(dt >= 100 && dt < 125) }) ; }; exports.nestNext = function () { var ch = (function () { var vars = {}; return Chainsaw(function (saw) { this.do = function (cb) { saw.nest(false, function () { var args = [].slice.call(arguments); args.push(saw.next); cb.apply(this, args); }, vars); }; }); })(); var order = []; var to = setTimeout(function () { assert.fail("Didn't get to the end"); }, 500); var times = []; ch .do(function (vars, next_) { vars.x = 'y'; order.push(1); this .do(function (vs, next) { order.push(2); vs.x = 'x'; setTimeout(next, 30); }) .do(function (vs, next) { order.push(3); vs.z = 'z'; setTimeout(next, 10); }) .do(function () { setTimeout(next_, 20); }) ; }) .do(function (vars, next) { vars.y = 'y'; order.push(4); setTimeout(next, 5); }) .do(function (vars) { assert.eql(order, [1,2,3,4]); assert.eql(vars, { x : 'x', y : 'y', z : 'z' }); clearTimeout(to); }) ; }; exports.builder = function () { var cx = Chainsaw(function (saw) { this.x = function () {}; }); assert.ok(cx.x); var cy = Chainsaw(function (saw) { return { y : function () {} }; }); assert.ok(cy.y); var cz = Chainsaw(function (saw) { return { z : function (cb) { saw.nest(cb) } }; }); assert.ok(cz.z); var to = setTimeout(function () { assert.fail("Nested z didn't run"); }, 50); cz.z(function () { clearTimeout(to); assert.ok(this.z); }); }; this.attr = function () { var to = setTimeout(function () { assert.fail("attr chain didn't finish"); }, 50); var xy = []; var ch = Chainsaw(function (saw) { this.h = { x : function () { xy.push('x'); saw.next(); }, y : function () { xy.push('y'); saw.next(); assert.eql(xy, ['x','y']); clearTimeout(to); } }; }); assert.ok(ch.h); assert.ok(ch.h.x); assert.ok(ch.h.y); ch.h.x().h.y(); }; exports.down = function () { var error = null; var s; var ch = Chainsaw(function (saw) { s = saw; this.raise = function (err) { error = err; saw.down('catch'); }; this.do = function (cb) { cb.call(this); }; this.catch = function (cb) { if (error) { saw.nest(cb, error); error = null; } else saw.next(); }; }); var to = setTimeout(function () { assert.fail(".do() after .catch() didn't fire"); }, 50); ch .do(function () { this.raise('pow'); }) .do(function () { assert.fail("raise didn't skip over this do block"); }) .catch(function (err) { assert.equal(err, 'pow'); }) .do(function () { clearTimeout(to); }) ; }; exports.trap = function () { var error = null; var ch = Chainsaw(function (saw) { var pars = 0; var stack = []; var i = 0; this.par = function (cb) { pars ++; var j = i ++; cb.call(function () { pars --; stack[j] = [].slice.call(arguments); saw.down('result'); }); saw.next(); }; this.join = function (cb) { saw.trap('result', function () { if (pars == 0) { cb.apply(this, stack); saw.next(); } }); }; this.raise = function (err) { error = err; saw.down('catch'); }; this.do = function (cb) { cb.call(this); }; this.catch = function (cb) { if (error) { saw.nest(cb, error); error = null; } else saw.next(); }; }); var to = setTimeout(function () { assert.fail(".do() after .join() didn't fire"); }, 100); var tj = setTimeout(function () { assert.fail('.join() never fired'); }, 100); var joined = false; ch .par(function () { setTimeout(this.bind(null, 1), 50); }) .par(function () { setTimeout(this.bind(null, 2), 25); }) .join(function (x, y) { assert.equal(x[0], 1); assert.equal(y[0], 2); clearTimeout(tj); joined = true; }) .do(function () { clearTimeout(to); assert.ok(joined); }) ; }; exports.jump = function () { var to = setTimeout(function () { assert.fail('builder never fired'); }, 50); var xs = [ 4, 5, 6, -4, 8, 9, -1, 8 ]; var xs_ = []; var ch = Chainsaw(function (saw) { this.x = function (i) { xs_.push(i); saw.next(); }; this.y = function (step) { var x = xs.shift(); if (x > 0) saw.jump(step); else saw.next(); }; saw.on('end', function () { clearTimeout(to); assert.eql(xs, [ 8 ]); assert.eql(xs_, [ 1, 1, 1, 1, 2, 3, 2, 3, 2, 3 ]); }); }); ch .x(1) .y(0) .x(2) .x(3) .y(2) ; };