Have you ever wondered for a Rails application, when you access an action in a controller, how Rails finds the template to render? For example, when the action index in ArticlesController is accessed, by default the template app/views/articles/index.html.erb will be selected and rendered. Recently I’m digging the source code of Rails, and I invite you to walk through some source code in ActionPack and ActionView with me. And then I will show how the Rubygem versioncake works by modifying the Rails configuration.
In this first part we firstly check how the render works. Notice that we check Rails 4.2 source code. If you look at another version, the implementation may be slightly different.
The entry point for the render is from the AbstractController::Rendering#render method. The AbstractController is a module shared by ActionController and ActionMailer. Since these two modules share a lot of functionalities, Rails extract those same functionalities into AbstractController. Let’s have a look at this method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
In our controller, we could call render method directly. For example, we can call render ‘new’ to render the new.html.erb template. Or if we don’t call render explicitly, there is a module ActionController::ImplicitRender which will call a default render.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The send_action will be called when the action of a controller is triggered. It first calls super, then if in the action it doesn’t render anything, the performed? will return false. So default_render is called. We can see that when call default_render it just calls render without any arguments.
In AbstractController::Rendering#render method, it firstly calls _normalize_render then calls render_to_body. The _normalize_render returns an options object which is a Hash. In this part we will examine the _normalize_render method to see how the options is generated.
Let’s see how _normalize_render is implemented.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
We see that it calls _normalize_args and _normalize_options methods. The _normalize_args and _normalize_options have different purposes.
_normalize_args
The _normalize_args is to convert all args into an options hash. For example when we call render method, we could call like this,
1
|
|
Here the first argument ‘new’ is a String, and _normalize_args is responsible to put this first argument in options hash and give it an appropriate key.
Let’s see how it’s implemented.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
We see that this method by default almost does nothing, if the action is a Hash, it returns action, otherwise if action is for example, a string, it returns the second parameter which is the options hash.
Notice that ApplicationController includes some other modules which override this method, as we will see later.
_normalize_options
The _normalize_options method is for the modules to include other options. For a Rails application, the ApplicationController extends from ActionController::Base, and ActionController::Base includes a lot of modules, each module could override this method and add some other options.
Let’s firstly check how this method is implemented in AbstractController::Rendering.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
By default this method does nothing. But it will be overridden in other modules.
Override _normalize_args
Rails source code is complex, one reason is because there are many modules could override other modules’s methods. For example, in a ArticlesController, let’s see its ancestors,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
|
We can see that for a typical controller, it has a lot of ancestors and most of them are modules. All modules after AbstractController::Rendering could override its methods. I have created a gist to check which ancestors implement a method.
1 2 3 4 5 6 7 |
|
Run the above code in a rails console, then you can call ClassName.ancestors_that_implement_instance_method to check what ancestors implement a method.
Let’s first see what ancestors override the _normalize_args method,
1 2 3 4 |
|
Two modules override this instance method: ActionView::Rendering and ActionController::Rendering. Let’s look them in order from top to down.
Let’s look at ActionView::Rendering first,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
We can see that for the first argument action, if it’s a string and the string include ‘/’, the key for this argument is :file, and if it doesn’t include ‘/’, the key is :action.
So if we call render ‘new’, the options will be { action: ‘new’ }, if we call render ‘articles/new’, the options will be { file: ‘articles/new’ }
Now let’s see how ActionController::Rendering overrides this method
1 2 3 4 5 6 7 8 9 10 11 |
|
We can see that for this override, if the method is passed a block, it will set the block to options[:update]
Override _normalize_options
Just like _normalize_args, let’s examine what modules override _normalize_options. We can see following modules implement _normalize_options: [ActionController::Rendering, ActionView::Layouts, ActionView::Rendering, AbstractController::Rendering].
Let’s check ActionView::Rendering first,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
We can see that it addes three options by default,
- If the options[:partial] is true, then it will set options[:partial] to action_name, the action_name is just the name of the action that is triggered. For example, if the index action of ArticlesController is triggered, action_name will be index.
- If the options doesn’t include :partial, :file or :template, it set options[:prefixes]. Let’s see what’s set for this :prefixes in a minute.
- It set the options[:template]. It’s either passed from the arguments or just use the action_name.
So what’s in options[:prefixes], let’s see how _prefixes method is implemented.
The AbstractController::Rendering module includes ActionView::ViewPaths module. And _prefixes method is implemented there.
ActionView::ViewPaths is an important module which we will examine in more details in later parts. It’s to manage the view paths for controllers. For example, by default Rails will append a view path #{Rails.root}app/views so the application knows to search templates in that specific view path.
For now let’s just focus on _prefixes method.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The _prefixes just calls the class method _prefixes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Notice that Rails 4.2 implements that method and handles some deprecated parent prefixes. In above code I omitted that handling for clarity.
This ActionView::ViewPaths._prefixes calls recursively. it append local_prefixes with superclass’s _prefixes. The local_prefixes has just one element: controller_path.
The controller_path method is very simple, it’s implemented in AbstractController::Base. For example, for a controller ArticlesController, its controller_path will be articles, and for a controller Articles::CommentsController, its controller_path will be articles/comments
So the _prefixes method first gets it’s parent prefixes, and then prepends the current controller_path to the front.
For example, if our application has a ArticlesController, in our rails controller, we can call the following code to show the _prefixes.
1 2 3 4 |
|
We see that the prefixes contains two elements: articles and application. It’s because the ApplicationController extends from ActionController::Base, but ActionController::Base is abstract.
So now we can see that for ArticlesController#index action, when we just call render without any arguments, the options will contains following elements,
- :prefixes : array [“articles”, “application”]
- :template : string “index”
Now let’s see next one, how ActionView::Layouts overrides _normalize_options method,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
We can see that if the options[:layout] is not set, the default layout is :default. And then options[:layout] is set to the result returned by _layout_for_option.
If you are interested you could check how _layout_for_options is implemented. When this module searches for the layout it will search “app/views/layouts/#{class_name.underscore}.rb” first, if it doesn’t found, then it will search super class. Since when a rails application is generated, an application.html.erb will be put in app/views/layouts and since by default all controllers’ parent is ApplicationController, so by default this layout will be used.
Finally let’s see how ActionController::Rendering overrides _normalize_options
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
So this method just process :html, :nothing and :status options, which is straight forward.
So finally let’s see when we call render in ArticlesController#index without any arguments, the options will contains following values,
- :prefixes : array [“articles”, “application”]
- :template : string “index”
- :layout : A Proc when called will return “app/views/layouts/application.html.erb”
Now we know how the options is normalized. And when Rails determines which template to render, it will extract details from the options. And we will look at how Rails determines template in later parts.