Skip to content

dsulfaro/RailsLite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RailsLite

In this project, I implement my own version of Ruby on Rails. The goals of this project were to gain a better understanding of the following:

  • Know how the HTTP request-response cycle works
  • Know how the server works
  • Understand the ControllerBase class
  • Know how cookies are used by browsers and servers
  • Implement a custom router
  • Understand how the params are built out by the HTTP request's information

Rack - Rails Server

So what happens when you run rails server? Rack is the software that sits in between a web server and a web application framework (Ruby, Sinatra). Since there are different frameworks, servers, etc., Rack makes it easier for all of them to communicate; it translates for them. So by using the Rack module, I can create a functional server.

app = Proc.new do |env|
  req = Rack::Request.new(env)
  res = Rack::Response.new
  res['Content-Type'] = 'text/html'
  res.write(req.path)
  res.finish
end

Rack::Server.start(
  app: app,
  Port: 3000
)

Rack needs an app to call after receiving and processing the request from the web server so that's what I created above. env is the variable generated by Rack::Server containing the request data. Rack also provides Request and Response classes to provide a more friendly API however so that we don't have to go digging through env for the information we need. The Content-Type header tells the browser what the server has given to it in response. More can be specified here, but for this simple demonstration, I'll only be getting HTML back. Finally, Rack::Response#write puts things in the response body and res.finish is called after the res is don't being built so Rack knows to wrap everything up.

I then give the app to the Rack server and start it on port 3000. If you run the file and navigate to localhost:3000 and put some arbitrary url after that, it'll simply print the url as HTML on the screen. This was purely an exercise on how the Rack middleware works on a base level.

ControllerBase Class

This is my version of ActionController::Base in Rails. After the router has determined which controller to use for a request that's been made, the controller's job is to interpret the request and produce the appropriate output.

In the constructor, I save the Rack::Request and Rack::Response as instance variables because the request will be used to help fill out the response in one of the actions of the children classes.

#render_content(content, content_type) is an instance method that populates the response with content and raises an error if the developer tries to double render.

def render_content(content, content_type)
  unless already_built_response?
    @res['Content-Type'] = content_type
    @res.write(content)
    @already_built_response = true
  else
    raise "Double render error"
  end
  session.store_session(@res)
end

Is similar to the previous function, only it also sets the reponse header's location to the specified url.

def redirect_to(url)
  unless already_built_response?
    @res.header["location"] = url
    @res.status = 302
    @already_built_response = true
  else
    raise "Double redirect error"
  end
  session.store_session(@res)
end

Next comes the render method. The path to the template is constructed using the name of the class and the template name which is passed into the method. I used the ERB library to interpret the file's contents and then called .result(binding) to capture the controller's local variables. I then call my #render_content method to write the content.

def render(template_name)
  unless already_built_response?
    template_path = "views/#{self.class.to_s.underscore}/#{template_name}.html.erb"
    contents = File.read(template_path)
    template = ERB.new(contents).result(binding)
    render_content(template, "text/html")
    @already_built_response = true
  else
    raise "Double render error"
  end
end

Session

Servers store information on the browser through cookies which consist of a name and a value. I created a Session class which will handle all of the logic of creating and storing the cookie. The constructor takes in a Rack::Request since cookies can only be used during an incoming request via a method on said class. I use 'rails_lite_app' as the name for the session cookie

def initialize(req)
  if req.cookies != {}
    @cookies = JSON.parse(req.cookies["_rails_lite_app"])
  else
    @cookies = {} #in case there are none
  end
end

The server will only have access to cookies with a path attribute that matches the current pack so I used '/' to indicate all paths. Cookies are added to the browser in the response

def store_session(res)
  json = @cookies.to_json
  cookies_attributes = {path: "/", value: json}
  res.set_cookie("_rails_lite_app", cookies_attributes)
end

I also implemented getters and setters for easy access...

def [](key)
  @cookies[key]
end

def []=(key, val)
  @cookies[key] = val
end

...so that #render_content and #redirect_to store the session information when the response is built.

session.store_session(@res)

Routing

I first start by writing a Route class which is analogous to a single row of the command rake routes. An object of this class knows the path to match(pattern), what controller it belongs to, and what method to run within that controller:

def initialize(pattern, http_method, controller_class, action_name)
  @pattern, @http_method = pattern, http_method
  @controller_class, @action_name = controller_class, action_name
end

Route#matches?(req) is a method that checks to make sure the request patch matches the pattern and that http_method is the same as the request method.

def matches?(req)
  (http_method == req.request_method.downcase.to_sym) && !!(pattern =~ req.path)
end

Finally, Route#run(req, res) gets the params from the request path, instantiates an instance of the controller class, and then calls invoke_action on the action specified. To simplify things, I'm not supporting defining routes as strings like Rails does. Instead, I'm requiring that Route's pattern argument to be a Regexp which is definitely more complicated to write, but this way, Ruby will store all the named capture groups in a MatchData object for easy access.

def run(req, res)
  match_data = @pattern.match(req.path)

  route_params = Hash[match_data.names.zip(match_data.captures)]

  @controller_class
    .new(req, res, route_params)
    .invoke_action(action_name)
end

I then added @params = route_params.merge(req.params) to my ControllerBase initializer to consolidate all of the route params, the query params, and the body params.

The Router class is given a Rack::Request and then figures out which Route matches the requested. It then instantiates the Route's controller and runs the appropriate method. The goal of this class is to be able to write something like the following in the routes.rb file:

# Rails routes.rb example
MyApp::Application.routes.draw do
  get "/posts", "posts_controller#index"
  get "/posts/:id/comments", "comments_controller#index"
end

To do this, I wrote a method called #draw(&proc) which takes in the proc and then I use Object#instance_evalwhich evaluates the block in the context of the object in which it was evaluated. This way, I can call methods get and post in the context of the block of code that's coming in even though they are methods of Router.

Flash

Flash is a special part of the session which persist the stored values to the next request (though flash.now have values only available to the current cycle). The Flash class looks a lot like the Session class except that I have separate instance variables for both @flash and @now.

def initialize(req)
  cookie = req.cookies['_rails_lite_app_flash']
  @now = cookie ? JSON.parse(cookie) : {}
  @flash = {}
end

I have similar getters and setters as my Session class as well as a store_flash function, and I add the flash functionality to my ControllerBase class...

def flash
  @flash ||= Flash.new(@req)
end

...and add flash.store_flash(@res) in redirect_to and render_content to save the flash information.

Conclusion

There are a bunch of different ways to expand on this project. I could expand the middleware to print errors, build out CSRF protection, implement patch and delete requests, implement url helpers, etc. I think this project was a great demonstration of not only rails specifically, but web technologies as a whole.

Project credit: App Academy

About

My own version of rails

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published