|
|
|
Sponsored Link •
|
|
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.
|
Sponsored Links
|