Sponsored Link •
When we were building our DSL for the stackup file we solved the problem three times. First, we wrote our own parser and decided it was too much work to maintain. Not only the code, but also the documentation. Since our DSL was sufficiently complicated, it wasn’t obvious how to use all its features and therefore it had to be copiously documented.
Next, for a short period, we implemented the DSL in XML. This removed the need for us to write our own parser, as XML is universally understood, but it contained too much noise and obscured the contents of the file. Our engineers found it too difficult to mentally task-switch between thinking about the meaning of the stackup and mentally parsing XML. For me, the lesson learned was that XML is not to be read by humans and probably a bad choice for a DSL, regardless of the parsing benefits.
Finally, we implemented the DSL in Ruby. The implementation was quick since Ruby provides the parsing. Documentation on the parser (i.e. Ruby) was not required since it is already available. And the final DSL was easily understood by humans, yet compact and versatile.
So, let’s build a DSL in Ruby that lets us define ‘parameter = value’ statements. Consider the following hypothetical DSL file.
% cat params_with_equal.dsl name = fred parameter = .55
This is not valid Ruby code, so we need to modify the syntax slightly so Ruby accepts it. Let’s change it to:
% cat params_with_equal.dsl name = "fred" parameter = 0.55
Once we get the DSL to follow valid Ruby syntax, Ruby does all the work to parse the file and hold the data in a way that we can operate on it. Now let’s write some Ruby code to read this DSL.
First we want to encapsulate these parameters somehow. A good way is to put them into a class. We’ll call this class MyDSL.
% cat mydsl.rb class MyDSL ... end#class MyDSL
From the developer’s perspective, we want a simple and straightforward way to parse the DSL file. Something like:
my_dsl = MyDSL.load(filename)
So, let’s write the class method
def self.load(filename) dsl = new dsl.instance_eval(File.read(filename), filename) dsl end
The class method
load creates a
MyDSL object and calls
instance_eval on the
DSL file (params_with_equal.dsl above). The second argument to
instance_eval is optional and allows Ruby to report a filename on parse errors. An optional third argument (not shown) gives you the ability to provide a starting line number for parse error reporting.
% cat dsl-loader.rb require 'mydsl' my_dsl = MyDSL.load(ARGV.shift) # put the DSL filename on the command line p my_dsl p my_dsl.instance_variables % ruby dsl-loader.rb params_with_equal.dsl #<MyDSL:0x89cd8> What happened? Where did
parametergo? Well, since
parameterare on the left hand side of the equals sign, Ruby thinks they are local variables. We can tell Ruby otherwise by writing
self.name = "fred"and
self.parameter = 0.55in the DSL file or we can impose upon the user to do this using the
@name = "fred" @parameter = 0.55
But that is kind of ugly and, to me, about the same as if we had written
$name = "fred" $parameter = 0.55
Another way to let Ruby know the context of these methods is to declare the scope explicitly by yielding
MyDSL object instance) to a block. To do this, we will need to add a top level method to jump start our DSL and put the contents inside of the attached block. Our modified DSL now looks like:
% cat params_with_equal2.dsl define_parameters do |p| p.name = "fred" p.parameter = 0.55 end
where we have defined
define_parameters as an instance method:
% cat mydsl2.rb class MyDSL def define_parameters yield self end def self.load(filename) dsl = new dsl.instance_eval(File.read(filename), filename) dsl end end#class MyDSL
And we change the
require in dsl-loader to use the new version of the MyDSL class in
% cat dsl-loader.rb require 'mydsl2' my_dsl = MyDSL.load(ARGV.shift) p my_dsl p my_dsl.instance_variables
Theoretically, this should work, but let’s test it out just to make sure.
% ruby dsl-loader.rb params_with_equal2.dsl params_with_equal2.dsl:2:in `load': undefined method `name=' for #<MyDSL:0x26300> (NoMethodError)
Oops. We forgot the accessors for
parameter . Let’s add those and look at the complete program:
% cat mydsl2.rb class MyDSL attr_accessor :name, :parameter def define_parameters yield self end def self.load(filename) # ... same as before end endNow, let's test it again.
% ruby dsl-loader.rb params_with_equal2.dsl #<MyDSL:0x25ec8 @name="fred", @parameter=0.55> ["@name", "@parameter"]
Success! This now works, but we have added two extra lines to the
DSL file and have added some noise with the ‘p.’ notation. This notation is better suited when there exists multiple levels of hierarchy in the file and there is
actually a need for and a benefit from explicitly specifying context. In our simple case we can implicitly define context and leave no doubt for Ruby that
parameter are methods. We do this by removing the ’=’ sign and write the
DSL file as
% cat params.dsl name "fred" parameter 0.55