RUBY

Better Routing – Part 4


This is the fourth part of Building an MVC Framework with Ruby. The topics we’ll cover are:

  • Part 1 – Rack Deep Dive

  • Part 2 – Set up a Basic Framework

  • Part 3 – Autoloading and Utility Methods

  • Part 4 – Better Routing

  • Part 5 – Render, Redirect & Before_Action Methods in Controllers

  • Part 6 – Extract Complexities out of View with Helpers

  • Part 7 – Reduce Rework with Generators

  • Part 8 – Set up ORM for Read/Write to Database 

  • Part 9 – Generate Database from Schema

  • Part 10 – Set up Migrations

The source code for this post is available on github.

In this post, we’re going to add a router that handles routing request based on user configuration. Topics that we will be covering include

  • Route configuration
  • Router
  • Mapper
  • Route
  • Application
  • Integration test.

1. Route Configuration

Since we are rebuilding rails, we will target a DSL that matches the rails router. config/routes.rb file will look like this.

Let’s look at how to implement the above DSL.

2. Router

Open lib/zucy/routing/router.rb and paste this code

We are creating a module called Routing that will hold all our routing logic. Class Router has a draw method which uses one(instance_eval) of the many fantastic ruby built-in features for creating Domain Specific Languages (DSLs).  The instance_eval method takes either a string or a block and evaluates the passed block in the context of the object calling instance_eval. You can do this with any object in Ruby, even a String:

Check out this link to know more about instance_eval. With instance eval in the picture, we just have to define get, post, put, patch and delete method in our mapper class.

2.1 get

Open lib/zucy/routing/router.rb and paste this code

There is a lot going on here. Let’s try to break it down a little.

  • get method accepts two parameters (path and a hash containing to key).
  • controller_and_action_for accepts a parameter eg “custom#index” and returns an array containing the controller(in string form) to instantiate and the method to invoke.
  • @route_data is a hash containing all information about the route we are defining.
  • pattern_for method accepts a parameter(the path been defined) and returns an array  containing regular expression pattern for the path and placeholders in the path. Notice that all placeholder in the path are converted to regexp named groups. This will make it easier to capture the value of the placeholder when an actual request that matches the pattern is made. To learn more about named groups, check out this link.
  • endpoints method creates an instance variable(@endpoints) if it has not been created before and/or returns the same instance variable.@endpoints is set to a new instance of a hash and each key of the hash is set with a default value of an empty array. This means anytime you ask for a key in the hash that hasn’t been defined yet, it returns an empty array by default.

2.1.1 write test for the get method

Open spec/unit/router_spec.rb and paste this code.

Notice that we are monkey patching our router class. The draw method was modified to return self, since we want to chain our method call. Try running the test using:

Commit your changes

2.2 post, put, patch, delete methods

Methods post, put, patch and delete are going to be very similar to the get methods.

Open lib/zucy/routing/router.rb and add the following methods to the Router class.

Commit your changes

Notice that the difference between all these methods is the last line of each of the method. Let’s try things up a little bit. We define all the methods (get, post, put, patch, delete) at once using another another ruby magic method(define_method). Check out this link to learn more about define_method.

2.3 Dry up code

Remove get, post, put, patch and delete method and replace them with this code in the body of router class.

Commit your changes

3.0 Mapper

Mapper class will be responsible for finding controller and action for a path when a request is made.

Open lib/zucy/routing/mapper.rb and paste this code.

Here we pass in endpoints from router as a parameter when initializing mapper. map_to_route method returns a route object if we find an endpoint that matches the path of the request. match_path_with_pattern method does the actual comparison between the path and an endpoint. On line 21 – 23, we loop through all placeholders, for each placeholder, we update request params with the placeholder as key and the match data value for the placeholder as value. This was possible because we used named groups when creating our regular expression.

Commit your changes

4.0 Route

We going to be creating the route class we instantiated in mapper.

Open lib/zucy/routing/route.rb and paste this code

Commit your changes

5.0 Application

Let’s tie everything together in our application class.

Open lib/zucy/application.rb and paste this code.

Commit your changes

6.0 Integration test.

Let’s modify our integration test to make use of the new routing.

Open spec/zucy_spec.rb and replace app method with this code.

Open spec/todolist/config/routes and paste this code.

Also open spec/todolist/controllers/todolist_controller.rb and paste this code.

Run your test again using

All test should be passing at this point.

Commit your changes

 

With this, we have built a better routing DSL. However we still have a long way to go. Keep calm and wait for the next post.

The source code for this post is available on github.

In the next post, we will be focusing on controllers and we will see how to render a views, redirect to another route etc.

Ikem Okonkwo

About Ikem Okonkwo

Ruby Evangelist, .NET Advocate. Trainer at @andela. Passionate about education and lifelong learning. Loves good food and soccer.