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::Delegatorinto the main line of code. Delegator is pretty interesting..It sets up all the methods that it wants to delegate
and sets up the class to delegate to as a class variable so that it can be overridden if needed..
And the delegate method nicely allows you to override these methods by using
respond_to?or it calls out to thetargetclass if the method is not defined..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…
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
plainmethodFYI the definition of the constants looks like this..