Building a DSL in JavaScript similar to a Ruby DSL

Viewed 1888

I am wanting to venture more into JavaScript wanted to see if I could create a Domain Specific Language that was similar to that of something I could make in Ruby using instance_eval and block_given?. I mostly don't know what terms to search and when I search DSL I usually get some hugely meta stuff I'm not familiar with. I originally got real lucky with my Ruby search and found a good blog that helped me out. My examples in Ruby are:

# frozen_string_literal: true

require 'phil_dsl/version'

module PhilDsl
  class Error < StandardError; end

  # The DSL Class.
  class Pipeline
    def initialize(&block)
      if block_given? # rubocop:disable Style/GuardClause
        if block.arity == 1
          yield self
        else
          instance_eval(&block)
        end
      end
    end

    def pipeline(&block)
      if block_given? # rubocop:disable Style/GuardClause
        if block.arity == 1
          yield self
        else
          instance_eval(&block)
        end
      end
    end

    def print_stuff(name = 'bob', &block)
      puts name.to_s

      if block_given? # rubocop:disable Style/GuardClause
        if block.arity == 1
          yield self
        else
          instance_eval(&block)
        end
      end
    end
  end
end

and then the actual usage of the DSL, once installed

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'phil_dsl'

PhilDsl::Pipeline.new.pipeline do
  print_stuff('hello') do
    print_stuff
  end
end

Looking at MochaJS is what I want to emulate, but I'm not sure where to start or go for figuring out how to do DSL creation similar to what I can do in Ruby.

My Ruby DSL full example: https://github.com/FilBot3/phil_dsl

1 Answers

It's not as pretty in JS because you always need the this keyword, but the core part of a DSL is that instance_exec functionality, which is very easy to do in JS using bind, apply, or arrow functions.

Rather than use a class (which doesn't support arrow functions) I'll build a new scope using an IIFE:

MyDsl = (function(){
  return this;
}).apply({});

Then I'll add a single-function DSL:

MyDsl = (function(){
  this.print_stuff = (arg, callback) => {
    console.log(arg);
    callback && callback.apply(this);
  };
  return this;
}).apply({});

Since I define print_stuff as an arrow function I guarantee that inside of it, this will always refer to MyDsl.

I can then use the DSL like so:

MyDsl.print_stuff("foo", function() {
  this.print_stuff("bar");
});

Note that we cannot use an error function here, because then the .apply(this) would be ignored.

Related