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,

  1. The get method is updated
  2. Two private methods find_controller_action and constantize 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.