< Return to Blog

Simple Ruby DSL Mixin

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 Mobj 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!