Composing DSLs in JavaScript

Further to my previous post I thought I’d share the approach I’ve been using to compose DSLs in JavaScript. If you want a more involved example, check out the currently in-development Forms module for Ojay.

For this example I’m going to be writing a simple permissions language that sets up rules about when certain methods may be called. It could be applied to a system for playing monopoly:

TheRulesAre(function() { with(this) {

    a(Player).mayNot('buyProperty').unless(function(player, property) {
        return property.owner === null &&
                player.funds >= property.price;
    });

    a(Property).mayNot('addHouse').when(it().isMortgaged());
}});

This illustrates a couple of principles when writing your DSL. First, I like to isolate DSL-based code inside a function. This allows cleaner separation from the rest of your program, so you can write helper functions inside the DSL function and not have them polute the global namespace. Second, you should write some test code first: it will make writing the DSL implementation much easier.

Let’s look at the pattern of the language we’re creating:

a(Class).mayNot(methodName).unless(condition)

Each rule takes a class and needs to modify one of its instance methods to check a condition before allowing the method to be called. The first step in implementing the DSL is to identify the root-level methods, which in this case are simply a(). This method should take a class and return an object with a mayNot method. Let’s also have methods an() (the same as a()) and the(), which will modify a specific object rather than a class.

var TheRulesAre = function(rules) {
    rules.apply(TheRulesAre.Root);
};

TheRulesAre.Root = {
    a: function(klass) {
        return new TheRulesAre.Restriction(klass.prototype);
    },
    an: function(klass) {
        return this.a(klass);
    },
    the: function(object) {
        return new TheRulesAre.Restriction(object);
    }
};

So now that we’ve set up the root methods, we need to implement mayNot(). This method will need to take a method name and return an object with unless() and when() methods. mayNot() will not modify the class itself, it will just need to return an object that will perform the modification. mayNot() is provided by the Restriction objects returned by the root-level methods (you’ll need JS.Class, including MethodChain to allow the it().isMortgaged() syntax):

TheRulesAre.Restriction = JS.Class({
    initialize: function(object) {
        this.object = object;
    },
    mayNot: function(methodName) {
        return new TheRulesAre.Condition(this.object, methodName);
    }
});

You see, we’re just passing the buck along, with some information about which object and which method to modify. The final piece of the puzzle is the Condition class, which implements the unless() and when() methods. Each of these should take a function (check) and modify the given method. The modification will consist of intercepting the method call and checking the receiver and the arguments using check. If check returns true, we allow the original method to run, otherwise we throw an exception.

TheRulesAre.Condition = JS.Class({
    initialize: function(object, methodName) {
        this.object = object;
        this.methodName = methodName;
    },

    unless: function(check) {
        // convert MethodChain objects
        if (check.toFunction) check = check.toFunction();

        var object = this.object, methodName = this.methodName;
        var original = object[methodName];

        object[methodName] = function() {
            var args = Array.from(arguments);
            var allowed = check.apply(null, [this].concat(args));
            if (allowed) return original.apply(this, args);
            else throw new Error('method ' + methodName +
                    '() may not be called with these arguments');
        };
    },

    when: function(check) {
        return this.unless(function() {
            return !check.apply(null, arguments);
        });
    }
});

And there you have it, a little mini-language for adding argument checking to method calls. The basic rule with these things is always, always write some test code first. It’s the only way to sort out your thinking about how the language should work and how it should be implemented.

If you’ve enjoyed this article, you might enjoy my recently published book JavaScript Testing Recipes. It’s full of simple techniques for writing modular, maintainable JavaScript apps in the browser and on the server.