I recently bought 37signals' first ONCE product, Campfire. I think it's a fantastic deal to get to read the source code of a Rails app running in production, from a world-class Rails development team and DHH himself. I also attended a code walkthrough with David on Friday, and seeing his energy and passion about the nuts and bolts of the code alone was worth the entry price of $299.
- David Heinemeier Hansson, in Patek levels of finishing
I don't plan to use the chat application right away, and haven't even installed it yet, as I've been busy digging into the source code for the past few days, which is incredibly simple yet powerful and very, very expressive.
One of the very first things I do whenever I come across a new Rails codebase is to open the routes file and see what are the points of interaction (i.e. route endpoints) with the application.
Routing is such an important concept in Rails that I wrote a ~5000 word post on it: Understanding the Rails Router: Why, What, and How.
Anyway, while reading Campfire's routes.rb
file, I came across a new method called direct
that I'd never seen before.
# config/routes.rb
direct :fresh_user_avatar do |user, options|
route_for :user_avatar, user, v: user.updated_at.to_fs(:number)
end
direct :fresh_account_logo do |options|
route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number)
end
You may have seen and used this method before, but I'm sure there're many like me who don't know what it does and have never used it before. So, in this post, we'll take a look at this direct
method. What is it, and what does it do?
Let's dig in.
Named Routes and Route Helpers
You know that whenever you create standard routes, Rails provides two helper methods such as xxx_path
and xxx_url
which generate the corresponding URL for that route, using the provided parameters. For example,
photos_path
returns/photos
new_photo_path
returns/photos/new
edit_photo_path(:id)
returns/photos/:id/edit
(e.g.edit_photo_path(10)
returns/photos/10/edit
)
In addition, you can pass the :as
option to a route to give a specific name for the route. When you name a route, Rails will automatically generate helper methods to generate URLs for that route. These methods are called name_url
or name_path
, where name
is the value you gave to the :as
option.
For example, the following route definition will generate logout_path
and logout_url
helper methods that return /exit
and https://localhost:3000/exit
respectively.
get 'exit', to: 'sessions#destroy', as: :logout
With that background, let's try to understand the direct
method.
What is the direct Method in the Rails Router?
The direct
method lets you define custom URL helpers. You define the helper name first, and then define the URL that this helper will return when called. Any parameters passed to the helper method are directly passed to the block.
direct :course_landing_page do
"https://courses.writesoftwarewell.com/"
end
# >> course_landing_page_url
# => "https://courses.writesoftwarewell.com/"
direct :greeting_page do |user|
"https://writesoftwarewell.com/#{user.name}"
end
# >> greeting_page_path(matz)
# => "/yukihiro"
Using the direct
method, you can override and/or replace the default behavior of routing helpers in Rails.
The above example was very simple: just define the helper name and return the URL. However, as with everything in Rails, this simple API is also very flexible. In addition to the string URL, which is treated as a generated URL, you can also return one of the following options, for more powerful customizations:
- A Hash, e.g.
{ controller: "page", action: "index" }
- An Array, which is passed to
polymorphic_url
- An Active Model instance, or
- An Active Model class
In fact, the return value of the block must be a valid argument for the url_for
method. This method actually builds the URL string.
direct :previewable do |file|
[ file, slug: file.slug ]
end
direct :sales do
{ controller: 'LandingPages', action: 'sales', subdomain: 'pay' }
end
To learn more about how you can use the above options, I suggest reading the documentation for the url_for
method.
What's more, you can even call other URL helpers. However, an important thing to keep in mind is not to call the same custom URL helper you're defining. Otherwise, it will keep calling itself, resulting in the notorious Stack Overflow error.
direct :course_landing_page do
# Warning: Don't Do This!!!
course_landing_page_path
end
Behind the Scenes: Reading the Source
As always, let's open the Rails source code and see how this method is implemented. Surprisingly, it's not long, just four lines of code:
# actionpack/lib/action_dispatch/routing/mapper.rb
def direct(name, options = {}, &block)
unless @scope.root?
raise RuntimeError, "The direct method can't be used inside a routes scope block"
end
@set.add_url_helper(name, options, &block)
end
The very first thing we notice is that the direct
method cannot be used inside of a scope block such as namespace
or scope
. If you do it anyway, Rails will raise an error.
Finally, direct
delegates to the add_url_helper
method to create a custom URL helper, which is an internal method. It defines the name_path
and name_url
helpers, given a name
.
If you're interested in learning about the internals of the Rails Router, check out the following section: How Rails implements the beautiful routing DSL.
Revisiting Campfire
Let's get back to the two Campfire routes we saw earlier. With everything we've just learned, can you try to guess what these routes do and in what context they can be used?
# config/routes.rb
direct :fresh_user_avatar do |user, options|
route_for :user_avatar, user, v: user.updated_at.to_fs(:number)
end
direct :fresh_account_logo do |options|
route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number)
end
Let's take the first route. It defines two helper methods named fresh_user_avatar_path
and fresh_user_avatar_url
. In addition, it accepts two parameters, user
and options
.
Inside the block, it calls the route_for
method. Let's inspect the Rails source to see what this method does:
# action_dispatch/routing/url_for.rb
def route_for(name, *args)
public_send(:"#{name}_url", *args)
end
It simply calls the helper method, passing any arguments passed to it. So our helper fresh_user_avatar_url
is indirectly calling the user_avatar_url
helper. The route for this helper is defined just a few lines above:
resources :users, only: :show do
scope module: "users" do
resource :avatar, only: %i[ show destroy ]
end
end
Here's an example where it's used in the codebase, to display the user's avatar:
# app/views/users/show.html.erb
<%= image_tag fresh_user_avatar_path(@user), role: "presentation", class: "avatar" %>
And that's how Campfire uses the direct
method in the Rails router to create a custom helper that returns the user's avatar URL.
P.S.: I still haven't figured out the purpose of the option passed to the helper, v: user.updated_at.to_fs(:number)
as I just couldn't find out where it's being used in the codebase. If any 37signals developers are reading this post and know the answer, please enlighten me.
v
option is used for invalidating the cached image and fetching the latest version of the avatar. Whenever the user updates their profile picture, the generated URL will be different, which will bust the cache and return a fresh copy of the avatar.That's a wrap. I hope you found this article helpful and you learned something new.
chat.writesoftwarewell.com
- so that I could chat with you all awesome software writers. We've a pretty sizeable community of Rails developers here and I think some may find it useful. (Let me know if you'd be interested in something like that.)As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.