Building a DSL in Ruby — Part 2
In the previous blog post, we implemented the first version of an example of a DSL library in ruby, a simple FactoryBot clone. In this second part, we’ll extend and improve the library so that we can also build associations between objects. The final syntax of our DSL will look like this:
In contrast to the previous implementation, we don’t create the records as soon as we call some method ( create_structure
in the previous version), instead, we have to explicitly state what we want to be able to create later on, as well as their relationships.
I call this post “Part 2” but, technically, it is a complete re-write of our previous implementation. The idea, however, remains the same: creating a domain language powerful enough to express relationships between objects and their attributes. Ok, enough talking, show me the code!
The register
method
The method register
is the entry point of our library. It allows us to register specific structures that we can, later on, create with the create
method. This method should accept a block and run everything inside it in the context of itself. This context should have an implementation of the method structure
. Remember the instance_eval
from the previous post? We’ll use it now!
I decided to leave the responsibility of handling the DSL syntax to a proper class DSL
. Now, every method called inside the block passed to register
will run in the context of a newDSL
object.
The structure method
We need to implement a method called structure
in the DSL
class:
Woah! That’s a lot of new code. Let’s go through the structure
method line by line. As we can see in the desired syntax (in the top of the post), structure
should accept a name, which defines the name of the structure, and a block. First, we check if we have already registered any structure with the name provided. If so, we raise an error message. This prevents the user from doing something like this:
This is important! This tells us that we need to keep track of which structures were already registered in memory.
We’ll see how this is achieved later on. The next important thing is the Construction
class. This is the core class of our library. A Construction
represents a thing that we register and that we may want to create later. In our desired syntax, we can pass a block to a construction. Just like we did in the first implementation, the block that we pass to thestructure
method should be executed in the context of an object that we’ll create to represent a structure
: A Construction
. It should respond to methods that correspond to the attributes of the object along with their values (static or dynamic, like before). Finally, we register the new structure. We’ll see later on how we complete this final step too.
The construction object
Just like before, we’re writing this library assuming that we want to create active record objects, and, for that, we use ActiveRecord
(the ending of this post justifies this choice, so keep reading!).
There’s a difference between this version of the DSL and the previous one: In the first version, we would run the methods inside the given block in the context of the Model class being created (Owner
, Product
, etc.). We won’t do that in this version. How, then, can we know that the object responds to the methods name
, age
, etc? Here’s the fun part: we don’t. We’ll let the user create the object as he pleases, with whatever relationships and properties, and we assume that the user knows that those properties / relationships will map to valid columns/relations in the underlying database.
This raises another, important thing: a Construction
object will have to keep track of all the relationships and properties specified by the user. However, we don’t know apriori which properties the user will need, and of course, we can’t define methods for all possible properties that an object can have. So we must once again resort to metaprogramming!
All objects in Ruby implement a method call method_missing
which is called whenever we call a method that an object doesn’t respond to:
In this example, we intercept the method method_missing
to log a message when we try to call a method that doesn’t exist in the A
class.
Nothing is stopping us from overriding that method and use it to our advantage here. Let’s see how we can leverage this powerful yet dangerous feature in our library:
Whenever we call a method in the context of a Construction
, we assume that that method refers to a property of the object being created. There’s no special reason for this: it was just the way I chose to do it. We keep the attributes of the construction in memory in an array. Just like we did before, we accept either a static value for the property (for example, name "My Name"
) or a dynamic value (for example, name { "My Name Version #{Time.now.to_i}"}
.
After all the code passed to the block of the new structure object is executed, we finally register the new structure in memory using the register_structure
method:
To keep track of the structures registered, we use a simple array stored as a class variable of the ConstructionGirl
class:
Storing associations
The last thing that we need is a way to store associations. As we’ve seen in the syntax of our DSL, there are two ways to register associations for our objects: we can either call the method with
in the context of a new structure:
or we can simply call the name of an already registered structure when creating a new structure (implicit association):
The with method
To store associations with the with
method, we need to add that method to the Construction
class so that it can be recognized as a method inside the block passed to the structure
method:
The with
method is straightforward: it requires a block and a name, creates a new construction with the name given and executes whatever code we passed inside the block in the context of the construction (just like we did before: creating properties and, of course, other associations) and storing the new object in the associations
array.
Implicit association
Creating implicit associations requires us to change the add_attribute
method in the Construction
class. We’ll assume that if the user called a method but with no arguments or block, then he’s adding a new association (for example, when simply calling owner
in the example above):
We check if the user didn’t pass any block or value. In that case, we’ll check if there’s already any structure registered in memory with the name given and if so, we add that structure to the associations
array. Note that this assumes that the construction must be already registered, which is not the same as the with
method, since we can create a new structure on the fly with that approach.
The create method
Last step! Now we need to be able to take whatever is in memory (in the constructions
class variable of the ConstructionGirl
class) and actually create them. In this post I’ve been assuming that we’re using Rails, so creating records is easy, but the implementation of the create
method may change depending on your circumstances.
If the construction doesn’t exist (it wasn’t registered), we throw an error, otherwise, we call the create
method on the construction with the name passed as argument:
First, we initialize the model. Then we set the attributes of the model using whatever we have stored in the attributes array:
There’s an important thing to note in this method: the difference between dynamic and static properties. Note that if the value stored in the attribute hash responds to call
, then it means that it is a block (this is not 100% safe, as we can implement any object that implements a method call
, but for this example, it is enough) and so the value for that property will be the result of calling that block, otherwise, we just store whatever the value is. Note that we’re using the Rails way of accessing properties of a model: model_instace['some_property']
.
After setting the properties of the new instance, we need to save the record. This is important for the set_associations
method:
For each association, we check if it is either a plural association (like has_many products
or a singular association (like belongs_to owner
). This is a naive implementation on top of Rails but, again, it’s used only for demonstration purposes here.
If it is a plural association, then we create another association for that relation with the new object (in this case, we’re calling create
on an active record relation), otherwise, we just set the singular association. Note that for these two methods to work, the receiver record inst
must already be persisted, which is why we call inst.save
before.
The last two lines of the create
method will try to save the object again and return the instance reloaded from the db (important if new associations were created).
Conclusion
In this two-part blog post we created a simple and naive clone of the FactoryBot library and, hopefully, along the way, we learned a couple of things:
- What a DSL is
- What
instance_eval
is - What
method_missing
is and how to leverage it to create a DSL - How to create a simple DSL using these two metaprogramming features of Ruby
This post came from my desire to learn a bit more about FactoryBot and its inner workings, and I hope you’ve learned something with this too!
Thanks for reading. 😃
Nowadays I work at Runtime Revolution. Working here has been, and continues to be, a great learning experience. I’ve matured professionally as a developer, focusing on building and maintaining large scale Ruby on Rails applications.