Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a class-less DSL in Ruby?

Tags:

ruby

I'm trying to figure out how to create a sort of "class-less DSL" for my Ruby project, similar to how step definitions are defined in a Cucumber step definition file or routes are defined in a Sinatra application.

For example, I want to have a file where all my DSL functions are being called:

#sample.rb

when_string_matches /hello (.+)/ do |name|
    call_another_method(name)
end

I assume it's a bad practice to pollute the global (Kernel) namespace with a bunch of methods that are specific to my project. So the methods when_string_matches and call_another_method would be defined in my library and the sample.rb file would somehow be evaluated in the context of my DSL methods.

Update: Here's an example of how these DSL methods are currently defined:

The DSL methods are defined in a class that is being subclassed (I'd like to find a way to reuse these methods between the simple DSL and the class instances):

module MyMod
  class Action
    def call_another_method(value)
      puts value
    end

    def handle(text)
      # a subclass would be expected to define
      # this method (as an alternative to the 
      # simple DSL approach)
    end
  end
end

Then at some point, during the initialization of my program, I want to parse the sample.rb file and store these actions to be executed later:

module MyMod
  class Parser

    # parse the file, saving the blocks and regular expressions to call later
    def parse_it
      file_contents = File.read('sample.rb')
      instance_eval file_contents
    end

    # doesnt seem like this belongs here, but it won't work if it's not
    def self.when_string_matches(regex, &block)
      MyMod.blocks_for_executing_later << { regex: regex, block: block }
    end
  end
end

# Later...

module MyMod
  class Runner

    def run
      string = 'hello Andrew'
      MyMod.blocks_for_executing_later.each do |action|
        if string =~ action[:regex]
          args = action[:regex].match(string).captures
          action[:block].call(args)
        end
      end
    end

  end
end

The problem with what I have so far (and the various things I've tried that I didn't mention above) is when a block is defined in the file, the instance method is not available (I know that it is in a different class right now). But what I want to do is more like creating an instance and eval'ing in that context rather than eval'ing in the Parser class. But I don't know how to do this.

I hope that makes sense. Any help, experience, or advice would be appreciated.

like image 724
Andrew Avatar asked Jan 03 '12 01:01

Andrew


People also ask

What is DSL Ruby?

The goal of metaprogramming in Ruby is often the creation of domain-specific languages, or DSLs. A DSL is just an extension of Ruby's syntax (with methods that look like keywords) or API that allows you to solve a problem or represent data more naturally than you could otherwise.


1 Answers

It's a bit challenging to give you a pat answer on how to do what you are asking to do. I'd recommend that you take a look at the book Eloquent Ruby because there are a couple chapters in there dealing with DSLs which would probably be valuable to you. You did ask for some info on how these other libraries do what they do, so I can briefly try to give you an overview.

Sinatra

If you look into the sinatra code sinatra/main.rb you'll see that it extends Sinatra::Delegator into the main line of code. Delegator is pretty interesting..

It sets up all the methods that it wants to delegate

delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
         :before, :after, :error, :not_found, :configure, :set, :mime_type,
         :enable, :disable, :use, :development?, :test?, :production?,
         :helpers, :settings

and sets up the class to delegate to as a class variable so that it can be overridden if needed..

self.target = Application

And the delegate method nicely allows you to override these methods by using respond_to? or it calls out to the target class if the method is not defined..

def self.delegate(*methods)
  methods.each do |method_name|
    define_method(method_name) do |*args, &block|
      return super(*args, &block) if respond_to? method_name
      Delegator.target.send(method_name, *args, &block)
    end
    private method_name
  end
end

Cucumber

Cucumber uses the treetop language library. It's a powerful (and complex—i.e. non-trivial to learn) tool for building DSLs. If you anticipate your DSL growing a lot then you might want to invest in learning to use this 'big gun'. It's far too much to describe here.

HAML

You didn't ask about HAML, but it's just another DSL that is implemented 'manually', i.e. it doesn't use treetop. Basically (gross oversimplification here) it reads the haml file and processes each line with a case statement...

def process_line(text, index)
  @index = index + 1

  case text[0]
  when DIV_CLASS; push div(text)
  when DIV_ID
    return push plain(text) if text[1] == ?{
    push div(text)
  when ELEMENT; push tag(text)
  when COMMENT; push comment(text[1..-1].strip)
  ...

I think it used to call out to methods directly, but now it's preprocessing the file and pushing the commands into a stack of sorts. e.g. the plain method

FYI the definition of the constants looks like this..

# Designates an XHTML/XML element.
ELEMENT         = ?%
# Designates a `<div>` element with the given class.
DIV_CLASS       = ?.
# Designates a `<div>` element with the given id.
DIV_ID          = ?#
# Designates an XHTML/XML comment.
COMMENT         = ?/
like image 139
radixhound Avatar answered Oct 10 '22 05:10

radixhound