testling - automated browser tests
browserling - interactive browser testing
commit d163e832d0ad3f1a97f1c9ba5d26f1750276053c
Author: James Halliday
Date: Fri Dec 3 05:59:13 2010 +0000

I write a lot of modules with chainable interfaces, if you haven't already noticed. I found myself writing these things so often, I wrote a module to make writing modules with chainable interfaces super simple. Introducing chainsaw!

"Chainable interfaces", you might be thinking... to yourself... in my voice... "Those are easy, just return this and you're done, right?"

Well, only sometimes! Very often you'll want to compose chains out of asynchronous actions that haven't returned yet, so you'll need to push all the actions to a stack and then process them one at a time as the results become available. With chainsaw, all of that chain-stack busywork is taken care of!

In this silly example, the AddDo() function takes an initial sum and then modifies that sum in chainable form through .add() and .do().

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);
        };
    });
}

Then we can use the streaming interface like this:

AddDo(0)
    .add(5)
    .add(10)
    .do(function (sum) {
        if (sum > 12) this.add(-12).add(2);
    })
    .do(function (sum) {
        console.log('Sum: ' + sum);
    })
;

Note that in the .do() blocks, further commands can be nested! saw.nest() takes care of all the complexity for making nested chains work.

And now for a more real-world, asynchronous example: consider this module that provides a chainable interface on top of node-lazy to read lines.

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 we can read stdin with a bunch of .getline()s chained together!

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();
    })
;

Check out the code on github for more. You can install chainsaw with npm by doing:

npm install chainsaw

Happy, um... hacking!

more
git clone http://substack.net/blog.git