The Artima Developer Community
Sponsored Link

Ruby Code & Style
Creating DSLs with Ruby
by Jim Freeze
March 16, 2006

<<  Page 3 of 4  >>

Advertisement

Now we need to define a new type of accessor for name and parameter . The trick here is to realize that name without an argument is a reader for @name , and name with one or more arguments is a setter for @name . (Note: it is convenient to use this methodology even when multiple levels of hierarchy are present and context is explicitly declared.) We define the accessors for name and parameter by removing the attr_accessor line and adding the following code:

% cat mydsl3.rb
class MyDSL
  def name(*val)
    if val.empty?
      @name
    else
      @name = val.size == 1 ? val[0] : val
    end
  end

  def parameter(*val)
    if val.empty?
      @parameter
    else
      @parameters = val.size == 1 ? val[0] : val
    end
  end

  def self.load(filename)
    # ... same as before
  end
end#class MyDSL

If either name or parameter is seen without arguments, they will return their value. If arguments are present, they will be assigned the value when a single argument is present, or they will be assigned to an array of values for multiple arguments.

Let’s run our sample parser (changed to require the file mydsl3.rb) to test our handiwork:

% ruby dsl-loader.rb params.dsl
#<MyDSL:0x25edc @parameter=0.55, @name="fred">
["@parameter", "@name"]

Success again! But defining these accessors explicitly is a pain. So let’s define a custom DSL accessor and make it available to all classes. We do this by putting the method in the Module class.

% cat dslhelper.rb
class Module
  def dsl_accessor(*symbols)
    symbols.each { |sym|
      class_eval %{
        def #{sym}(*val)
          if val.empty?
            @#{sym}
          else
            @#{sym} = val.size == 1 ? val[0] : val
          end
        end
      }
    }
  end
end

The above code simply defines the dsl_accessor method which creates our DSL specific accessors. We now plug it into the application and use dsl_accessor instead of attr_accessor to get:

% cat mydsl4.rb
require 'dslhelper'

class MyDSL
  dsl_accessor :name, :parameter

  def self.load(filename)
    # ... same as before
  end
end#class MyDSL
Again, we update the require statement in dsl-loader.rb to load the mydsl4.rb file and run the loader:
% ruby dsl-loader.rb params.dsl 
#<MyDSL:0x25edc @parameter=0.55, @name="fred">
["@parameter", "@name"]

This is all well and good, but what if we don’t know the parameter names in advance? Depending on the use cases for the DSL, parameter names may be generated by the user. Never fear. With Ruby, we have the power of method_missing. A two-line method added to MyDSL will define a DSL attribute with dsl_accessor on demand. That is, if a value is to be assigned to a (thus far) non-existent parameter, method_missing will define the getters and setters and assign the value to the parameter.

% cat mydsl5.rb
require 'dslhelper'

class MyDSL
  def method_missing(sym, *args)
    self.class.dsl_accessor sym
    send(sym, *args)
  end

  def self.load(filename)
    # ... Same as before
  end
end

% head -1 dsl-loader.rb
require 'mydsl5'

% ruby dsl-loader.rb params.dsl
#<MyDSL:0x25b80 @parameter=0.55, @name="fred">
["@parameter", "@name"]

Wow! Doesn't that make you feel good? With just a little bit of code, we have a parser that can read and define an arbitrary number of parameters. Well, almost. What if the end-user doesn't know Ruby and uses parameter names that collide with existing method calls? For example, what if our DSL file contains the following:

% cat params_with_keyword.dsl 
methods %w(one two three)
id      12345

% ruby dsl-loader.rb params_with_keyword.dsl 
params_with_keyword.dsl:2:in `id': wrong number of arguments (1 for 0) (ArgumentError)
Oh, how embarrassing. Well, we can fix this (mostly) in short order with a little help from a class called BlankSlate [0], which was initially conceived by Jim Weirich [1]. The BlankSlate class used here is a little different than the one introduced by Jim simply because we want to keep a little more functionality around. So we keep seven methods. You can experiment with these to see which ones are absolutely required and which ones we are using just to visualize the contents of our MyDSL object.
% cat mydsl6.rb 
require 'dslhelper'

class BlankSlate
  instance_methods.each { |m| undef_method(m) unless %w(
       __send__ __id__ send class 
       inspect instance_eval instance_variables 
       ).include?(m)
  }
end#class BlankSlate

# MyDSL now inherits from BlankSlate
class MyDSL < BlankSlate
  # ... nothing new here, move along...
end#class MyDSL
Now when we try to load the DSL file that is loaded with keywords, we should get something a little more sensible:
% head -1 dsl-loader.rb 
require 'mydsl6'

% ruby dsl-loader.rb params_with_keyword.dsl 
#<MyDSL:0x23538 @id=12345, @methods=["one", "two", "three"]>
["@id", "@methods"]

And sure enough, we do. This is good news that we can remove spurious methods and free up more possibilities of parameter names for our end-users. However, note that we can't give end-users a completely unrestrained license to use any name for a parameter. This is one of the downsides of using a generic-programming language as a DSL, but I think that an end-user being prohibited from using class as a parameter name has only a small risk of being a deal killer.

<<  Page 3 of 4  >>


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us