Set up a Basic MVC Framework – Part 2
This is the second 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.
Set up a Basic Framework
In this post we’ll use the knowledge of rack from the previous post and build a basic framework. We’ll cover the below topics:
- Set up our development environment
- Create the framework gem.
- Set up test with rspec
1. Set Up our Development Environment
Youʼll need:
• Ruby 2.0 or greater
• a text editor(sublime or any other)
• a command-line or terminal
• Git
• Bundler.
• SQLite, will be needed in part 8 to 10
If you donʼt have them, youʼll need to install them. You can install from source, from your favorite package manager, from RubyGems, or Google it and follow instructions.
2. Create the Framework gem.
For your framework to be reusable by other applications, you need to package it as a gem. Rails was packages as a gem so that other application can include and build on top of it. We will do the same for our framework. We will be calling our framework zucy
.
Create a new gem called zucy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ bundle gem zucy Code of conduct enabled in config MIT License enabled in config create zucy/Gemfile create zucy/.gitignore create zucy/lib/zucy.rb create zucy/lib/zucy/version.rb create zucy/zucy.gemspec create zucy/Rakefile create zucy/README.md create zucy/bin/console create zucy/bin/setup create zucy/CODE_OF_CONDUCT.md create zucy/LICENSE.txt create zucy/.travis.yml create zucy/.rspec create zucy/spec/spec_helper.rb create zucy/spec/zucy_spec.rb Initializing git repo in /Users/ikem/Projects/zucy |
Let’s commit our changes:
1 2 |
$ git add --all $ git commit -m "initial commit" |
2.1 Customize zucy.gemspec File
Make sure to replace “FIXME” and “TODO” in the descriptions, summary and metadata[‘allowed_push_host’] – You can’t build your gem if you have any of them in your gemspec.
1 2 3 4 5 |
spec.add_development_dependency "bundler", "~> 1.10" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec" spec.add_runtime_dependency "rack", "~> 1.0" |
Each of these adds a runtime dependency (needed to run the gem at all) or a development dependency (needed to develop or test the gem)
Let’s commit our changes:
1 2 |
$ git add --all $ git commit -m "update gemspec to reflect the correct description, summary and dependencies" |
Letʼs build your gem and install it:
1 2 |
$ gem build zucy.gemspec $ gem install zucy-0.1.0.gem |
2.2 Create the Application Class
Open lib/zucy.rb and paste the code below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
require "zucy/version" module Zucy class Application def call(env) @req = Rack::Request.new(env) path = @req.path_info request_method = @req.request_method.downcase return [500, {}, []] if path == "/favicon.ico" controller, action = get_controller_and_action_for(path, request_method) response = controller.new.send(action) [200, {"Content-Type" => "text/html"}, [response]] end def get_controller_and_action_for(path, verb) _, controller, action, others = path.split("/", 4) require "#{controller.downcase}_controller.rb" controller = Object.const_get(controller.capitalize! + "Controller") action = action.nil? ? verb : "#{verb}_#{action}" [controller, action] end end end |
This class is the entry point for all request. You will notice that it has a call method. Some interesting things are happening here. On line 10, a method called get_controller_and_action_for
returns the controller and action method to serve the request. On line 11, the controller is instantiated and the action method is invoked via the send method of the controller object. The send method is used to called a method on an object dynamically. You can learn more about send method here. Finally a rack compatible response is returned on line 12.
How exactly is the controller and action gotten? Let’s explore get_controller_and_action_for
method.
get_controller_and_action_for
method implement’s a very simple routing, so weʼll just get a controller and action as simply as possible. We split the URL on “/”. The “4” just means “split no more than 4 times”. So the split assigns an empty string to “_” from before the first slash, then the controller, then the action, and then everything else un-split in one lump. For now we throw away everything after the second “/” – but itʼs still in the environment, so itʼs not really gone.
The method const_get
is a piece of Ruby magic – it just means get me the constant with this name. In this case, we supply the name of the controller we want in string form. Notice that we are requiring the file that contains the constant just before calling const_get
Also, youʼll sometimes see the underscore used to mean “a value I donʼt care about”, as I do above. Itʼs actually a normal variable and you can use it however you like, but many Rubyists like to use it to mean “something Iʼm ignoring or donʼt want.”
On line 21, we check if the action returned from path is nil and if it is, use the verb as the action method to invoke. This will allow us to issue a GET request to /todolist and it will invoke the get
method in TodolistController. Also if we issue a GET request to /todolist/name, it will invoke the get_name
method in TodolistController.
Commit your changes
1 2 |
$ git add --all $ git commit -m "create basic application class" |
3. Set Up Test with Rspec
Update your zucy.gemspec file with the line of code below.
1 |
spec.add_development_dependency "rack-test", "~> 0.6" |
This gem will give us utility methods for testing our rack app.
3.1 Set Up Example Todolist App
Our generated gem has a folder called spec
. This is where all our test will reside. Since our gem will be used to build other applications, we will test it against a sample application build from the gem. We will be building a todolist example application. Create a folder called todolist in spec folder and set up it’s folder structure to look like this.
In application.rb, paste the code below.
1 2 3 4 5 6 7 8 |
require "zucy" $LOAD_PATH << File.join(File.dirname(__FILE__), "..", "app", "controllers") module Todolist class Application < Zucy::Application end end |
The second line adds app/controllers folder to the load path so that you can require sample_controller.rb
file with just require "sample_controller"
without specifying the folder where sample_controller
lives. In line 5, we created a new class called Application
which inherited from our gem’s Application
class. This will be the starting point of the application. config.ru and Gemfile are added so that you can run the example application directly using
1 |
$ bundle exec rackup |
Before running the example todolist application using the above command, paste the code below in config.ru
1 2 3 4 |
require "./config/application.rb" TodoApplication = Todolist::Application.new run TodoApplication |
and update your Gemfile with the code below before running bundle install
.
1 2 3 |
source "https://rubygems.org" gem "zucy" |
Commit your changes
1 2 |
$ git add --all $ git commit -m "create example todolist application" |
3.2 Connect Todolist App to spec_helper.rb
Update your spec_helper.rb file in spec folder to look like this.
1 2 3 4 5 6 |
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require "todolist/config/application.rb" require 'rspec' require 'rack/test' ENV['RACK_ENV'] = 'test' |
Paste the code below into zucy_spec.rb
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 |
require 'spec_helper' describe 'Todolist App' do include Rack::Test::Methods def app Todolist::Application.new end it "returns a list of all my todos" do get '/todolist' expect(last_response).to be_ok expect(last_response.body).to eq("['Write a book', 'Build a house', 'Get married', 'Buy a car']") end it "returns first item in my todolist" do get '/todolist/first' expect(last_response).to be_ok expect(last_response.body).to eq("Write a book") end it "can respond to post request" do post '/todolist' expect(last_response).to be_ok expect(last_response.body).to eq("Post go swimming") end it "can respond to put request" do put '/todolist' expect(last_response).to be_ok expect(last_response.body).to eq("Put Write a book") end it "can respond to delete request" do delete '/todolist' expect(last_response).to be_ok expect(last_response.body).to eq("Delete Write a book") end end describe Zucy do it 'has a version number' do expect(Zucy::VERSION).not_to be nil end end |
In line 4, we included a module from rack-test
gem. This gave us access to get
, post
, put
and delete
you saw in other parts of the test. Also notice line 6 – 8. The value returned by the app method is the rack app that all request are sent to. In our case, it is the todolist application we just created. Run your test using
1 |
$ rake spec |
Notice that most of your test failed. This is because todolist app do not have TodolistController
. Let’s create it. Create a file called todolist_controller.rb
in the controller folder of the todolist app. Paste the code below into the file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class TodolistController def get "['Write a book', 'Build a house', 'Get married', 'Buy a car']" end def get_first "Write a book" end def post "Post go swimming" end def put "Put Write a book" end def delete "Delete Write a book" end end |
Run your test again. Now all test should be passing. You can test the todolist app manually in your browser by running bundle exec rackup
when you are in spec/todolist
folder. Before running the app, make sure you have built and installed the new version of your gem.
Commit your changes
1 2 |
$ git add --all $ git commit -m "set up testing for rack app and add first set of tests" |
With this, we have a fully tested micro framework for building web application. 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 look at how to autoload classes and we will create some utility methods that will be used throughout this series.
- Ruby on Steroids(DSLs): The Powerful Spell Called DSL - March 14, 2016
- Ruby on Steroids: The Magic of MetaProgramming – Method Spells - March 12, 2016
- Ruby on Steroids: The Magic of MetaProgramming – Fellowship of Spells - February 28, 2016
- Ruby on Steroids: The Magic of MetaProgramming – An Unexpected Journey - February 27, 2016
- How to Delegate Like a Boss - February 24, 2016
- Better Routing – Part 4 - January 28, 2016
- Autoloading and Utility Methods – Part 3 - January 11, 2016
- Set up a Basic MVC Framework – Part 2 - December 22, 2015
- Rack Deep Dive – Part 1 - December 17, 2015