Let's modify the router so it recognizes and parses a controller/action
route.
Specifically, we require the articles_controller.rb
file and update the get
method to handle the new routing syntax, i.e. controller/action
while still supporting the older syntax, where we passed the block.
The following code shows only the changes required to the router.rb
file. Specifically,
- The
get
method is updated - Two private methods
find_controller_action
andconstantize
are added.
# router.rb
require_relative 'controllers/articles_controller'
def get(path, &blk)
if blk
@routes[path] = blk
else
@routes[path] = ->(env) {
controller_name, action_name = find_controller_action(path) # 'articles', 'index'
controller_klass = constantize(controller_name) # ArticlesController
controller = controller_klass.new(env) # ArticlesController.new(env)
controller.send(action_name.to_sym) # controller.index
}
end
end
private
# input: '/articles/index'
# output: ['articles', 'index']
def find_controller_action(path)
result = path.match /\/(\w+)\/(\w+)\/?/ # path = '/articles/index'
controller = result[1]
action = result[2]
return controller, action # ['articles', 'index']
end
# input: 'articles'
# output: ArticlesController
def constantize(name)
controller_klass_name = name.capitalize + 'Controller' # "ArticlesController" (a string)
Object.const_get(controller_klass_name) # ArticlesController (a class)
end
I've added comments to make the code self-explanatory, but let's take a closer look at each step.
In the get
method, first we check if a block is provided. This is to support the existing approach of returning the response directly, when a handler block is provided.
if blk
@routes[path] = blk
else
# ...
end
If a handler block was not provided, that means a route path was provided, for example get '/articles/index'
. We have to parse this route to extract the controller and action names.
Why? So that we can instantiate the controller and call the action method on it, which is what Rails does.
First, we create a new lambda block and assign it to @routes[path]
. This lambda will be executed whenever the incoming HTTP request path matches the stored route. We also pass the env
hash representing the HTTP request environment to the controller's constructor.
Inside the lambda, we want to find the corresponding controller and call the appropriate action method on it. For example, given the path /articles/index
, the handler should create a new instance of ArticlesController
and call the index
action on it.
First, we extract the controller and action names using the find_controller_action
method. I'll use the regular expression: \/(\w+)\/(\w+)\/?
which grabs the controller name and the action name.
# input: '/articles/index'
# output: ['articles', 'index']
def find_controller_action(path)
result = path.match /\/(\w+)\/(\w+)\/?/ # path = '/articles/index'
controller = result[1]
action = result[2]
return controller, action # ['articles', 'index']
end
Once we have the controller name as a string, i.e. articles
, we want to get the corresponding controller class, i.e. ArticlesController
. The constantize
method handles that by capitalizing the controller name articles
and appending Controller
to it. So articles
becomes ArticlesController
, which is a String.
Then, we get the corresponding constant for ArticlesController
string using the const_get
method. This is the ArticlesController
class.
# input: 'articles'
# output: ArticlesController
def constantize(name)
controller_klass_name = name.capitalize + 'Controller' # "ArticlesController" (a string)
Object.const_get(controller_klass_name) # ArticlesController (a class)
end
Finally, at the end of the handler block, we invoke the action method by calling the send
method on the controller instance, passing the symbolized name of the action method.
Once invoked, the action method returns the string response, which is returned from the handler block to the router, which sends it to the app.
controller = controller_klass.new(env) # ArticlesController.new(env)
controller.send(action_name.to_sym) # controller.index
If you're wondering how handler lambda can access the variables outside its scope, remember that it's a 'closure' , which gives it access to the controller and the action. To learn more, check out the following post: Blocks, Procs, and Lambdas: A Beginner's Guide to Closures and Anonymous Functions in Ruby
In the next lesson, we'll add the final piece: a route that tells our application to send an incoming request to a specific action method on a specific controller class.