简体   繁体   中英

How to create a class-less DSL in 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.

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—ie 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', ie 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. eg 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         = ?/

You can use Modules to organize your code. You can add your DSL methods to the Module class using the Module#include method. Here's how RSpec does it. The last two lines being what you are probably looking for. +1 to @meagar about keeping DSL's simple!

Also as @UncleGene points out, RSpec does pollute Kernel with the DSL methods. I'm not sure how to get around that. If there was another DSL with a describe method, it would be hard to determine which describe one was using.

module RSpec
  module Core
    # Adds the `describe` method to the top-level namespace.
    module DSL
      # Generates a subclass of {ExampleGroup}
      #
      # ## Examples:
      #
      #     describe "something" do
      #       it "does something" do
      #         # example code goes here
      #       end
      #     end
      #
      # @see ExampleGroup
      # @see ExampleGroup.describe
      def describe(*args, &example_group_block)
        RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register
      end
    end
  end
end
extend RSpec::Core::DSL
Module.send(:include, RSpec::Core::DSL)

Just define a method called when_string_matches which takes a regex as an argument, tests it against whatever "string" you're talking about, and conditionally yields, passing whatever name is to its block:

def when_string_matches(regex)
   # do whatever is required to produce `my_string` and `name`
   yield(name) if my_string =~ regex
end

This is essentially all Ruby DSLs are: Methods with interesting names that often accept blocks.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM