Rebuild Rails Part 3
Let’s continue our rebuild rails series by supporting controllers. We simplify things
by assuming a controller/action
path format and only support GET
request.
We adjust our .call
implementation to handle the new path format. We also introduce
another method #render_controller_action
that inspects the path_info
and instantiates the right controller using Rails NameController
convention.
# lib/tracks.rb
def self.call(env)
path_info = env["PATH_INFO"]
if path_info == "/"
text = Tracks::Controller.render_default_root
else
text = Tracks::Controller.render_controller_action(env)
end
[200, {"Content-Type" => "text/html"}, [text] ]
end
# lib/tracks/controller.rb
def self.render_controller_action(env)
path_info = env["PATH_INFO"]
_, controller, action, after = path_info.split("/")
controller_class_name = controller.capitalize + "Controller"
controller_class = Object.const_get(controller_class_name)
controller_class.new.send(action)
end
$ ruby spec/application_spec.rb
Run options: --seed 55835
# Running:
.E
Finished in 0.015837s, 126.2865 runs/s, 126.2865 assertions/s.
1) Error:
CrazyApp::Application#test_0002_should respond with different path:
NameError: uninitialized constant PostsController
/code/crazy/lib/tracks/controller.rb:13:in `const_get'
When we run the test, you see it failed on PostsController
which is what
we expect since we haven’t implemented PostsController
yet. Let’s add the
controller now.
# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
def index
"hello from tracks /posts/index"
end
end
# config/application.rb
require "./app/controllers/posts_controller"
$ ruby spec/application_spec.rb
Run options: --seed 2475
# Running:
..
Finished in 0.019152s, 104.4277 runs/s, 208.8555 assertions/s.
2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
Automatic loading
Our tests pass but something’s not right. In Rails, there is no need to require every controller (or pretty much anything) to make it work. To support this feature in our framework, we need 2 things:
- convert PostsController to posts_controller.rb; and
- auto-require ‘posts_controller’
To tie these 2 together, we tap into Object.const_missing
so our framework would
know if a class has been used but not yet loaded.
We also update the $LOAD_PATH
to include app/controllers
folder so Ruby knows
where to look.
# lib/tracks.rb
require File.expand_path("../tracks/helper", __FILE__)
require File.expand_path("../tracks/object", __FILE__)
# config/application.rb
require './lib/tracks'
$LOAD_PATH << File.expand_path("../../app/controllers", __FILE__)
module CrazyApp
class Application < Tracks::Application
end
end
# lib/tracks/helper.rb
module Tracks
module Helper
def self.to_underscore(string)
string.scan(/[A-Z][a-z]+/).
join('_').
downcase
end
end
end
# lib/tracks/object.rb
class Object
def self.const_missing(c)
require Tracks::Helper.to_underscore(c.to_s)
const_get(c)
end
end
Our implementation of .to_underscore
is limited compared to what’s supported in Rails.
irb(main):019:0> s = "PostsController"
=> "PostsController"
irb(main):020:0> s.scan(/[A-Z][a-z]+/).join('_').downcase
=> "posts_controller"
Also, since we call const_get
inside const_missing
, you will run into serious
trouble if your file does not contain the expected class. Try changing the name of the
class inside app/controllers/posts_controller.rb
into something else and you will get this
error.
$ ruby spec/application_spec.rb
Run options: --seed 60433
# Running:
.E
Finished in 0.043152s, 46.3478 runs/s, 46.3478 assertions/s.
1) Error:
CrazyApp::Application#test_0002_should respond with different path:
SystemStackError: stack level too deep
/Users/greg/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/forwardable.rb:174
2 runs, 2 assertions, 0 failures, 1 errors, 0 skips