Ever met those instructions :
__FILE__
__method__
__callee__
?
I think we all met at least the first one while playing with Ruby within our early experimentations ... and maybe used the second one for debugging processes - if so I propose you to try a debugger like pry, byebug, pry-byebug you'll get lots of the information right away (if you are using Ruby >3.1 you even get an integrated one).
But do you know what does the last one ?
The difference with method
First of all, let's identify what __method__
is doing :
def world
p "hello #{__method__}"
end
world
Executing this file will produce:
$ > hello world
But why ?
Simply put : __method__
is returning as a symbol the name of the method the instruction is in.
Here the method is world, so it puts the interpolation of the string and the method symbol which gives : hello world.
So what is the difference with __callee__
?
Well __callee__
is returning the actual name of the called method dynamically as a symbol.
Let's example ourselves
class Foo
def root_method
p "you did #{__method__}"
end
def root_callee
p "you did #{__calle__}"
end
end
Foo.new.tap do |this|
this.root_method
this.root_callee
end
File execution :
$ > you did root_method
> you did root_callee
Here nothing big differentiate both behaviours : both return their method name.
Let's then put the __callee__
specificities to the test :
class Foo
alias_method :aliased_method, :root_method
alias_method :aliased_callee, :root_callee
end
Foo.new.tap do |this|
this.aliased_method
this.aliased_callee
end
File execution :
$ > you did root_method
> you did aliased_callee
So what happened ?
- The aliased method that uses
__method__
interpolates the root method name. - The aliased method that uses
__callee__
interpolates with the aliased method name.
__callee__
instead of __method__
is looking up for the actual method name, not the one it originated from !
Ok ... but how can I use this ?
That's a good question !
Let's imagine a situation where we send automatic mailing to update an asynchronous task for our users .
We will always send the same data structure, and the only thing that will change is the corpus of the mail.
We could go the naive way and produce a Mailer
with every specific methods we need for every possible situations. But that's tedious and imply a lot of repetition - which we don't want !
So how can we use __callee__
here to simplify our day to day life ?
A one Mailer to rule them all
So we need a Mailer that will always send emails with 3 parameters : email
, user_name
, a job_id
, to notify an asynchronous event to the users.
The only things that will change depending on the event is :
- the subject of the mail
- the corpus of the mail
So first we need something that will be able to structure the needed data as we need it :
class AsynchronUpdateMailer < ActionMailer::Base
private
def default_mail_action(email:, user_name:, jid:)
@email = email
@name = user_name
@jid = jid
mail(
to: ["#{@name} <#{@email}>"],
subject: "#{__callee__.to_s.gsub('_', ' ')}: #{jid}",
&:html
)
end
end
We now have an AsynchronUpdateMailer
that is able to receive parameters to structure a default email and uses __callee__
and the job id to structure the subject. That's Good !
But we can't do anything with it yet.
We turn the method private because this should never get called : no need to send a default mail, what we need is a simple first structure to be able to quickly extend its behaviour with the less possible code
So let's extend it a bit to turn it a little bit more usefull :
class AsynchronUpdateMailer < ActionMailer::Base
VALID_ACTIONS = %i[
foo_success
foo_failure
bar_success
]
VALID_ACTIONS.each do |valid_alias|
alias_method valid_alias, :default_mail_action
end
private
[...]
end
What did we do here ?
- We defined a constant that identify which actions this mailer is able to produce.
- when this Mailer is loaded by Rails, it will programmatically define an alias for every "valid action" that aliases
default_mail_action
, and each of those aliases will be public
Ok ... that's good and all. But what about the corpus ? What is the interest of this proposition if everyone of the sent emails does the same ?
Well, the answer is simple : we're going to use the amazing powers of Rails views system to resolve our situation !
Rails views to the rescue
Every time a ActionMailer::Base
uses the mail
method it will rely on the called method name and the application file structure to obtain the view that matches :
- the Mailer name to find the folder within the app/views folder
- the given action name to find the corresponding .haml file
So let's add our files where they need to be !
Within the app/views folder we create a folder that is te machine name of our Mailer : app/views/asynchronous_update_mailer
. We then create a .haml
for every action we need - corresponding to the aliases we defined :
foo_success.haml
foo_failure.haml
bar_success.haml
Each one of them is a common haml file and icing on the cake : we event get access to the state of the Mailer within this haml file.
So we can do things like :
%html
%body
%section
%p
= "Hello #{@user_name}!"
%p
= "You can use the number given within the subject of this mail to contact the support if need be !"
...
So ? Can you feel the maintenability and scalability of this setup tickling your brain ?
Obviously you can always go one step further, but here we potentially saved a lot of time and maintenance using the power of Rails !
Conclusion
__callee__
is one of those tool Ruby proposes that we don't always need ... but can still be of great use in the right context.
It always comes down to what we have and what we do with it. I'm sure you already made great use of it and can share with us in the comment some of your experiences with its usage !
That's it for today !
Thank you for reading this article, and I hope it got you something interesting out of it. At least I had a blast typing it.
I'm sure you already made great use of the __callee__
method, so don't hesitate to give your insights and personal examples in the comments : they will be greatly appreciated.
Enjoy your skills and have a great day !
Top comments (0)