First off, what is metaprogramming?

TL;DR…. so,

Metaprogramming is code that writes code.

Neato, now that we’ve got that out of the way, let’s dive into a real use case!

A while back, one of the teams in our company decided to use Pipeline Deals as a sales-CRM-of-sorts to keep track of a few things: the status of a ‘deal’, or what may become a bona-fide Order (or ‘sale’), who the correspondence is with (the client), or User, and what the correspondence has been (the back and forth, emails etc), or, Message.

See where I’m going with this? Pipeline needs some info from our application, and, vice-versa. So, we have three classes (for now, there could be more in the future, right?) that we are working with that are requirements for how we need to connect our app to Pipeline. Right now, those classes are Order, User, and Message.

Since we are (as of now) working with three models, we have created a module to handle the Pipeline-related logic. Modules can be included in models (and included in as many as you want). This gives the model access to all the logic contained in the module. You may see

class Order

include Pipeline

.....

…now any code in the included Pipeline module is now available to any instance of Order.

Here’s our module. For the sake of brevity and not overcomplicating the point I’m trying to get across in this article, I’ve omitted a fair bit of the module’s code. We’ve included this module in the models Order, User and Message:

Okay, yeah. “What’s happening in this module?”, you may ask. Glad you asked!

All these methods basically help us build a request to send off to connect with the Pipeline API. That’s the whole purpose of this Pipeline Module, to send and receive data from our app to Pipeline.

So where’s the “little taste of MetaProgramming in Ruby” you spoke of in the click-baity title? It’s not a lot of code, but it packs a punch. It’s in this method called request.

Okay, so how is this “code that writes code”, you may ask?

Really, there’s two things to notice in this compact method: the interpolated bit (self.class.name), and the constantize method (note, this is a Rails method, not Ruby). We’ll talk a little more about this method below, but first…

What is ‘self’ in this context?

The ‘self’ we see in this ‘request’ method could be one of three things…. got it? Eh?

Right. ‘Self’ is either an instance of Order, User, or Message, the three models we currently have included our module into.

A fairly conventional way to handle this may be something like

if self.class == User

# do x

elsif self.class == Order

# do y

elsif self.class == Message

# and so on

What’s wrong with the above? Not a lot. It’s really just a design choice to not keep building on conditionals (too many conditionals is a code smell), and to do a little metaprogramming instead.

Looking at the interpolated string in the request method, if ‘self’ was coming from the User model, request would return this: ‘Pipeline::UserRequest’

Cool. That’s just a string though. We call constantize on this string which then turns it into a class! We now have a class we can call new on. SWEET. Note, you can’t just make any string name into a class like this. The class has to to exist first. This specific class is already declared in the Pipeline module (see line 24 in the full code snippet above). So, we are good to go.

So now the request method instantiates a class, dynamically, based on whatever self is.

Following along?

the request method now returns an instance of this UserRequest class we’ve defined

Pipeline::UserRequest.new(user)

because self was an instance of User in this case.

So now we could call request.query to pull the needed user’s email from our database, or, request.payload to give us the JSON payload we need to send to Pipeline, and so on. The methods below in UserRequest, again, just help us build a request to the Pipeline API.

You’ll see these methods being used in the create_in_pipeline method. This method makes a POST request to Pipeline, and we are making use of all the methods in the UserRequest class to build a proper request..

Maybe it’d help to see this ‘filled in’. Since our current example is that ‘self’ is an instance of User, we would have this: