Whilst perusing through the code for Berkshelf
I came across this piece of absolute gold, since I've been looking at various approaches to implementing clean DSLs in Ruby. I particular prefer an isolated mixin based approach, and this example does so rather gracefully via an 'annonymous class' upon which the DSL is based on.
Should you have a need to process a file via your DSL, checkout its original implementation for dsl_eval_file
— let's get cracking!
I've taken the liberty of stripping this down in to a single file for prying into with... uh, pry
!
To start off, CleanRoom
is defined within the DSLEval
namespace
class CleanRoom
attr_reader :instance
def initialize(instance)
@instance = instance
end
end
The actual dsl_eval
method has the form of self.class.clean_room.new(self).instance_eval(&block)
where self
is the instance of the class DSLEval
is mixed into — our base _klass
.
In my example above, I've setup an instance of M
— obj
for the purpose of our pry
demonstration — on which I've setup @dependencies
as an instance variable (henceforth, ivar), and instance method cookbook
and it has been setup as a DSL method via expose_method
.
Everything about this mixin in brilliant — even their choice of naming. The CleanRoom
, or at least its eventual instance should be a vanilla PORO (Plain Old Ruby Object), i.e. without any exposed methods for external interaction. Think of it as a keypad or some user-interfacing panel without any buttons!
Consider setup_cleanroom
which calls the class method clean_room
on _klass
, setting up the ivar @clean_room
within _klass
(obj.class
). You'll see that I've already taken a look at this via pry
and the result is #<Class:0x007fd824173500> < Mixin::DSLEval::CleanRoom
— this sets up
It is important to note that the block passed to Class.new { #... }
is evaluated when it is instantiated setting up the DSL methods in the @clean_room
instance.
Our @clean_room
is instantiated with bootstrap_cleanroom(_klass_cleanroom)
taking the result of running setup_cleanroom
as its parameter and it's context of instantiation obj
— this is made evident as #<#<Class:0x007fd824173500>:0x007fd82410be00 @instance=#<M:0x007fd824188a40 @dependencies={}>>
where it can be seen that the ivar @instance
of our @clean_room
instance has been set as obj
.
I also wanted too see what the bootstrapped cleanroom looked like, and we can see below it has now taken on the cookbook
DSL method thanks to define_method
expecting arguments and a block as parameters.
Mixin::DSLEval::CleanRoom#methods: instance
#<Class:0x007f93c90cedd8>#methods: cookbook
instance variables: @instance
It should be noted that instance.send(exposed_method, *args, &block)
could have been also written as self.instance_variable_get(:@instance).send(exposed_method, *args, &block)
allowing us to remove the attr_reader
declared within CleanRoom
.
The final piece of the puzzle is to pass the result of bootstrap_cleanroom(_klass_cleanroom)
to pry_dsl_eval(cleanroom)
as its parameter, thus giving us
> _klass_cleanroom = obj.setup_cleanroom
> cr = obj.bootstrap_cleanroom(_klass_cleanroom)
> obj.dsl_eval(cr)
called cookbook on #<M:0x007f8b0384e4b0> from #<#<Class:0x007f8b02985e60>:0x007f8b0292f510>!
#<M:0x007fd824188a40 @dependencies={["mike"]=>["mike"]}>
Let me know what you think!