[Review] Rebuilding Rails
Book | Rebuilding Rails |
Author | Noah Gibbs |
Link | rebuilding-rails.com |
- 1. Zero to “It Works!”
- 2. Your First Controller
- 3. Rails Automatic Loading
- 4. Rendering Views
- 5. Basic Models
- 6. Request, Response
- 7. The Littlest ORM
- 8. Rack Middleware
- 9. Real Routing
My re-building source code
Work flow diagram
1. Zero to “It Works!”
1 2 3 4 |
|
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).
Youʼll need to go into the rulers directory and git add .
before you rebuild the gem (git add .; gem build rulers.gemspec; gem install rulers-0.0.1.gem
). Thatʼs because rulers.gemspec is actually calling git to find out what files to include in your gem.
Rails structure
-
ActiveSupport is a compatibility library including methods that aren’t necessarily specific to Rails. You’ll see ActiveSupport used by non-Rails libraries because it contains such a lot of useful baseline functionality. ActiveSupport includes methods like how Rails changes words from single to plural, or CamelCase to snake_case. It also includes significantly better time and date support than the Ruby standard library.
-
ActiveModel hooks into features of your models that aren’t really about the database - for instance, if you want a URL for a given model, ActiveModel helps you there. It’s a thin wrapper around many different ActiveModel implementations to tell Rails how to use them. Most commonly, ActiveModel implementations are ORMs (see ActiveRecord, below), but they can also use non-relational storage like MongoDB, Redis, Memcached or even just local machine memory.
-
ActiveRecord is an Object-Relational Mapper (ORM). That means that it maps between Ruby objects and tables in a SQL database. When you query from or write to the SQL database in Rails, you do it through ActiveRecord. ActiveRecord also implements ActiveModel. ActiveRecord supports MySQL and SQLite, plus JDBC, Oracle, PostgreSQL and many others.
-
ActionPack (ActionDispatch, ActionController, Actionview) does routing - the mapping of an incoming URL to a controller and action in Rails. It also sets up your controllers and views, and shepherds a request through its controller action and then through rendering the view. For some of it, ActionPack uses Rack. The template rendering itself is done through an external gem like Erubis for .erb templates, or Haml for .haml templates. ActionPack also handles action- or view-centered functionality like view caching.
-
ActionMailer is used to send out email, especially email based on templates. It works a lot like you’d hope Rails email would, with controllers, actions and “views” - which for email are text- based templates, not regular web-page templates.
2. Your First Controller
Rails encapsulated the Rack information into a “request” object rather than just including the hash right into the controller. Thatʼs a good idea when you want to abstract it a bit – normalize values for certain variables, for instance, or read and set cookies to store session data.
3. Rails Automatic Loading
When debugging or printing error messages I like to use STDERR because itʼs a bit harder to redirect than a normal “puts” and so youʼre more likely to see it even when using a log file, background process or similar.
For simple structures, “inspect” shows them exactly as youʼd type them into Ruby – strings with quotes, numbers bare, symbols with a leading colon and so on.
Reloading Means Convenience
gem "rulers", :path => "../rulers"
This trick actually relies on deep Bundler trickery and requires you to always “bundle exec” before running things like rackup. If you forget that, it can look like the gem isnʼt there or (worse) look like an old version.
CamelCase and snake_case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Put it together
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 |
|
Did it work?
When you load a file called whatever_class.rb, youʼre not actually guaranteed that it contains WhateverClass, or that the constant WhateverClass is actually a class. How would you check?
You might try calling const_get(:WhateverClass)… Except that you just made const_get try to load automatically. If you call it on an unloaded class inside the method call where you try to load, youʼll recurse forever and get a “stack level too deep” and a crash. So const_get isnʼt the full answer.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
But thereʼs a reason I say “hideously hacky.” Think about ways this could break. For instance – think about what would happen if you hit this in multiple threads at once. Oops!
Re-re-reloading
1 2 3 4 5 6 7 8 |
|
Running by bundle exec rerun -- rackup -p 3001
. The “–” is an old Unix trick. It means “thatʼs all the arguments you get, the rest belong to somebody else.” Specifically, it tells rerun to ignore the “-p” later.
reloading rack development server, forking version of rackup.
In Rails
-
rails/activesupport/lib/active_support/dependencies.rb Rails uses ActiveSupport for its const_missing support. Most of the code is installing a const_missing that can call through to non-Rails versions of const_missing in other classes, and can be removed or re-added and is appropriately modular. It also works hard to support nested modules like MyLibrary::SubModule::SomeClass.
-
Rails autoloading — how it works, and when it doesn’t by Simon Coffey.
4. Rendering Views
Erb and Erubis
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Run it with ruby erb_test.rb
1 2 3 4 5 6 7 |
|
The few lines starting with _buf
are interesting. Erubis takes apart our string, appends it to _buf
piece by piece, and adds the variables in as well after calling .to_s
on them. Then it just returns _buf
.
Rack test example
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 |
|
Rake test example
Rake actually ships with a “Rake::TestTask”.
1 2 3 4 5 6 7 8 9 10 |
|
A word of caution: Rake will always run your tests by loading them into the same Ruby process, then running each one in turn. This is a lot faster than running them in individual processes, but it means that your tests can mess with each other in annoying ways. If you find yourself saying, “but I didnʼt set that global variable in this test!” think about whether some other test might have done it. For extra fun, the tests donʼt always run in any predictable order.
In Rails
Rails actually allows registering a number of different template engines at once with a number of different extensions so that Erb files are rendered with Erubis, but .haml files are rendered with the HAML templating engine.
You can find the top-level view code in actionpack/lib/action_view.rb, and the whole big chunk of Rails view code in actionpack/lib/action_view/. From there, look in template/handlers/erb.rb for a pretty compact description of exactly how Rails uses Erubis to render Erb templates. You can see that most of the bulk of Railsʼ version is setup, interface and dealing with string encodings. You save a lot of trouble by knowing that youʼre basically only dealing with ASCII and/or UTF-8 strings.
5. Basic Models
Use multi_json (a generic swappable back-end for JSON handling) to built a simple system of models based on JSON files.
In Rails
ActiveRecord is an Object-Relational Mapper so that each of your objects represents a database row. ActiveModel is the interface that Rails uses to all of storage including non-relational stores like Cassandra or MongoDB, to fit particular object types into Rails.
For a good overview of ActiveModel, have a look at a blog post from Yehuda Katz on that topic: ActiveModel: Make Any Ruby Object Feel Like ActiveRecord
6. Request, Response
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 |
|
In Rails, the return value from the controller is ignored. Instead if you donʼt call render (Railsʼ equivalent of render_response), it will automatically call it for you with the same controller name, and the viewʼs name set to the same name as your action.
Rails doesnʼt return the string when you call “render” (well, usually - some calls to render do!). Instead, it keeps track of the fact that you called render and what you called it on. Then it gives you an error if you call it again, or uses the defaults if you get to the end of a controller action without calling it
Instance Variables
The Rails answer is to set instance variables in the controller, then use them in the view. Try creating a new view object, mostly just to use Erubis to evaluate the view file. Then, make it easy to pass in a hash of instance variables which youʼll set on the view object before doing the evaluation.
1 2 3 4 5 6 7 8 9 10 |
|
In Rails
Rails (more specifically, ActionPack) uses Rack in a very similar way, even exposing the Rack Request object with the “request” method. Especially metal.rb and metal/*.rb. “Rails Metal” is a name for the lower-level Rails which goes mostly straight through to the “bare metal” – that is, to Rack.
You can find a lot of the Rails implementation of Rack in these directories – for instance, metal/redirecting.rb is the implementation of the redirect_to() helper which returns status 302 (redirect) and a location to Rack. You could steal the code and add a redirect_to to Rulers, if you wanted.
You can also find things like forgery (CSRF) protection, multiple renderers (i.e. Erb vs Haml), forcing SSL if requested and cookies in this directory. Some are complex, while others call to Rack very simply and you could move right over to Rulers.
7. The Littlest ORM
migration
1 2 3 4 5 6 7 8 9 10 |
|
sqlite model
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
|
You can add a method to the SQLite model that takes a column name and a type, and then when saving and loading that column, does something type-dependent to it, like the boolean or JSON fields above.
ActiveRecord allows both ways – you can research the before_save
and after_initialize
callbacks for how to do it on save/ load.
In Rails
ActiveRecord contains mappings of operations like our gem, but also migrations, cross-database compatibility and a huge amount of optimization and general complexity. And thatʼs even though they use the ARel gem for most of the heavy lifting!
8. Rack Middleware
With any Ruby web framework, you can modify how it works by adding Rack components around it. I like thinking of them as pancakes, because Rack lets you build your framework and your application like a stack of pancakes.
Built-in middlewares
- Rack::Auth::Basic - HTTP Basic authentication.
- Rack::Auth::Digest - HTTP Digest authentication.
- Rack::Cascade - Pass a request to a series of Rack apps, and use the first request that comes back as good. Itʼs a way to mount one Rack app “on top of” another (or many).
- Rack::Chunked - A Rack interface to HTTP Chunked transfer.
- Rack::CommonLogger - Request logging.
- Rack::ConditionalGet - Implement HTTP If-None-Match and If- Modified-Since with ETags and dates.
- Rack::Config - Call a given block before each request.
- Rack::ContentLength - Set Content-Length automatically.
- Rack::ContentType - Try to guess Content-Type and set it. Rack::Deflater - Compress the response with gzip/deflate.
- Rack::Directory - Add Apache-style directory listings. This is an endpoint not an intermediate layer, so use it with “run.”
- Rack::ETag - Generate ETags from MD5s of the content.
- Rack::Head - Remove response body for HEAD requests.
- Rack::Lint - Check your responses for correctness.
- Rack::Lock - Only allow one thread in at once.
- Rack::Reloader - Reload your app when files change.
- Rack::Runtime - Times the request, sets X-Runtime in response.
- Rack::Sendfile - Use the X-Sendfile header to ask your web server to send a file much faster than Ruby can.
- Rack::ShowExceptions - Show a nice exception page if something breaks.
- Rack::ShowStatus - Show a pretty page if the result is empty.
- Rack::Static - Serve from certain directories as static files instead of calling your framework.
- Rack::URLMap - Route different directories to different apps or different stacks. You can also use this with a “map” block in config.ru.
Rack::URLMap is a way to tell Rack what paths go to what Rack apps - and if thereʼs could be two that match, the longer path always takes precedence.
Rack::ContentType is to set the default HTML content type for everything. Since itʼs at the top, outside the blocks, it applies to all the blocks.
The lobster, by the way, is a simple test app built into Rack. Youʼll see it as an example in many places.
Thrid-party middlewares
In Rails
The primary Rack application object in Rails is called ActionController::Dispatcher.
ActionController::Base allows you to get mini-Rack-apps for each controller action because it inherits from Metal, the basic Rails Rack class. So you can call MyController.action(:myaction) and get a Rack app for that action in your controller.
Calling order of Rack middlewares
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 46 47 48 49 50 51 52 53 54 55 56 57 |
|
Other samples:
9. Real Routing
Rails 3 takes this a half-step farther and makes every action on every controller a full-on Rack app that you can extract and use.
Add RouteObject class.
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
|
Define route
to save rules in an instance of RouteObject, and use get_rack_app
to route to controller actions.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Update Rulers::Controller to use self.action
to initialize rack app, and dispatch
to specific action.
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 |
|
In Rails
Rails connects lots of tiny Rack applications into a single overall application. Itʼs a complicated, multi-layered construction.
Each Rails controller keeps track of a mini-Rack stack of middleware which can be specified per-action like before_filters.