Share
Embed
Ruby on Rails Reading Guide
Slides for RailsConf 2014 talk "Ruby on Rails Reading Guide" http://www.railsconf.com/
Akira Matsuda
April 23, 2014
Transcript
202 slides
-
pp self name: Akira Matsuda GitHub: amatsuda Twitter: @a_matsuda committer
of: Ruby, Rails, Haml from: Tokyo, Japan founder of: Asakusa.rb an organizer of: RubyKaigi -
amatsuda creator of: kaminari, active_decorator, action_args, html5_validators, hocus_pocus, erd, everywhere,
i18n_generators, database_rewinder, rfd... -
Why should we read Rails? We're "Software Writers" (DHH) We'd
better know what's happening underneath our apps You'll occasionally hit Rails bugs -
We're "Software Writers" (DHH) You need to read good software
if you want to be a good "software writer" -
We'd better know what's happening underneath our apps Short code
does a lot of works Magical DSLs -
Short code does a lot of works class UsersController <
ApplicationController def index @users = User.all end end -
Rails.has_many :bugs We often hit them in production apps We
need to x them We need to be familiar with the codebase -
Chapters Let's start reading! How Rails server boots (1) What
is Railties? How Rails server boots (2) Rails and Rack middleware Routes Controllers and actions -
Ruby on Rails is_a Open-source On GitHub 100% Ruby No
C extensions! -
Pro tip: Use gem-src How can you nd the GH
repo for the gem you use? Don't want to manually git-clone every gem that you read or patch? Use gem-src -
amatsuda/gem-src Installation (as an rbenv plugin) % git clone https://github.com/
amatsuda/gem-src.git ~/.rbenv/ plugins/gem-src Con guration % echo "gemsrc_clone_root: ~/ src" >> ~/.gemrc -
gem-src (a social coder's best friend) Usage % gem i
rails #=> automatically git-clone You no more need to memorize the gem author name, or search in GitHub! -
What's included in the rails gem? Take a look at
the gemspec -
rails.gemspec s.add_dependency 'activesupport', version s.add_dependency 'actionpack', version s.add_dependency 'actionview', version
s.add_dependency 'activemodel', version s.add_dependency 'activerecord', version s.add_dependency 'actionmailer', version s.add_dependency 'railties', version -
rails.gemspec It has no code, but instead it de nes
several dependencies It means that the rails gem is a meta package to install these 7 gems -
Directories in the rails project % lsd # aliased to
ls -ld *(-/DN) actionmailer actionpack actionview activemodel activerecord activesupport railties -
7 dependencies You can nd them at the top directory
in the rails repo -
Rails is a full-stack MVC framework All the Rails MVC
components are in this one repository -
Summary: Let's start reding! rails gem is a meta package
that depends on other 7 gems including the whole MVC components -
Rails.root/con g/boot.rb Sets up gems listed in the Gem le
Tweaks the $LOAD_PATH (Bundler) -
railties.gemspec s.summary = 'Tools for creating, working with, and running
Rails applications.' s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.' -
rails/commands/ commands_tasks.rb module Rails class CommandsTasks def server ... require
"rails/commands/server" Rails::Server.new.tap do |server| require APP_PATH ... server.start ennnd -
rails/commands/server.rb ... require 'rails' module Rails class Server < ::Rack::Server
def initialize(*) super ENV["RAILS_ENV"] ||= options[:environment] end def start super ennnd -
rails/railtie.rb Requires 'rails/initializable' Requires 'rails/con guration' De nes Rails::Railtie class
that includes Rails::Initializable -
What we learned Railties de nes these core classes MyApp::Application
< Application < Engine < Railtie < Initializable -
Railties is the core of Rails View ActionView Controller ActionPack
Model ActiveModel ActiveRecord Railties -
Pro tip: Tools to read it through Vim + unite.vim
(unite-outline) rdefs RubyMine -
What is railties? It's a library providing something underneath the
Rails Application Another answer is... -
Railroad tie "a rectangular support for the rails in railroad
tracks" "Generally laid perpendicular to the rails" "Railroad ties were traditionally made of wood" -
Railroad tie "railroad tie/railway tie/crosstie (North America), or railway sleeper
(Europe, Australia & Asia)" -
Railties in Rails % git grep Rails::Railtie actionmailer/lib/action_mailer/railtie.rb: class Railtie
< Rails::Railtie # :nodoc: actionpack/lib/action_controller/railtie.rb: class Railtie < Rails::Railtie #:nodoc: actionpack/lib/action_dispatch/railtie.rb: class Railtie < Rails::Railtie # :nodoc: actionview/lib/action_view/railtie.rb: class Railtie < Rails::Railtie # :nodoc: activemodel/lib/active_model/railtie.rb: class Railtie < Rails::Railtie # :nodoc: activerecord/lib/active_record/railtie.rb: class Railtie < Rails::Railtie # :nodoc: activesupport/lib/active_support/i18n_railtie.rb: class Railtie < Rails::Railtie activesupport/lib/active_support/railtie.rb: class Railtie < Rails::Railtie # :nodoc: -
Each component has its own Railtie inheriting Rails::Railtie module ActionMailer
class Railtie < Rails::Railtie module ActionController class Railtie < Rails::Railtie module ActionDispatch class Railtie < Rails::Railtie module ActionView class Railtie < Rails::Railtie module ActiveModel class Railtie < Rails::Railtie module ActiveRecord class Railtie < Rails::Railtie -
We see Railtie everywhere! View ActionView Controller ActionPack Model ActiveModel
ActiveRecord Railties Railtie Railtie Railtie -
What's written in a Railtie? (active_model/railtie.rb) require "active_model" require "rails"
module ActiveModel class Railtie < Rails::Railtie # :nodoc: config.eager_load_namespaces << ActiveModel initializer "active_model.secure_password" do ActiveModel::SecurePassword.min_cost = Rails.env.test? end ennd -
What's written in a Railtie? Mainly we see two kinds
of class method calls, con g and initializer -
What is con g? (railtie/railtie.rb) module Rails class Railtie def
config @config ||= Railtie::Configuration.new ennnd -
Railtie::Con guration? (railtie/con guration.rb) module Rails class Railtie class Configuration
def method_missing(name, *args, &blk) if name.to_s =~ /=$/ @@options[$`.to_sym] = args.first elsif @@options.key?(name) @@options[name] else super end ennnnd -
Railtie::Con guration? It accepts any method call and stores the
given method name & value in a class level Hash as {method_name: value} -
What is initializer? (rails/initializable.rb) module Rails module Initializable class Initializer
attr_reader :name, :block def initialize(name, context, options, &block) ... @name, @context, @options, @block = name, context, options, block ennd module ClassMethods def initializer(name, opts = {}, &blk) ... initializers << Initializer.new(name, nil, opts, &blk) ennnnd -
What is initializer? It just keeps the given block as
a Proc instance (with a name and options) -
What Railtie is doing Setting some values via con g.foobar
= 'baz' Storing Procs given to initializer method calls -
One more thing about Rails::Railtie (rails/railtie.rb) module Rails class Railtie
... class << self private :new -
One more thing about Rails::Railtie Prohibited to be instantiated from
outside self.instance returns the singleton instance Why is it a class despite you can't instantiate? -
Why is it a class despite you can't instantiate? In
order to be able to be inherited -
What's gonna happen when Rails::Railtie is inherited? (rails/railtle.rb) module Rails
class Railtie class << self def inherited(base) unless base.abstract_railtie? subclasses << base ennnnnd -
We see Railtie everywhere! View ActionView Controller ActionPack Model ActiveModel
ActiveRecord Railties Railtie Railtie Railtie -
Summary: What is railties? Railties is the core of Rails
that provides Railtie, Engine, Application, etc. We can nd "Railtie"s in other Rails components A "Railtie" can keep "con g" and "initializer" values Railties ties up these "Railtie"s -
rails/commands/ commands_tasks.rb module Rails class CommandsTasks def server ... require
"rails/commands/server" Rails::Server.new.tap do |server| require APP_PATH ... server.start ennnd -
rails/commands/server.rb ... require 'rails' module Rails class Server < ::Rack::Server
def initialize(*) super ENV["RAILS_ENV"] ||= options[:environment] end def start super ennnd -
I'm not gonna do rack code reading here But method
calls go on like start => wrapped_app => app => build_app_and_options_from_co n g => Rack::Builder.parse_ le => Rack::Builder.new_from_string -
Rack::Builder.new module Rack class Builder def initialize(default_app = nil,&block) ...
instance_eval(&block) if block_given? sennnd -
Pro tip: `puts caller` Find it difficult to follow the
method calls? Add `puts caller` to con g.ru le and start the server. -
Rails.root/con g.ru # This file is used by Rack-based servers
to start the application. puts caller require ::File.expand_path('../config/environment', __FILE__) run Rails.application -
`puts caller` from con g.ru #{GEM_HOME}/rack-1.5.2/lib/rack/builder.rb:55:in `instance_eval' #{GEM_HOME}/rack-1.5.2/lib/rack/builder.rb:55:in `initialize' #{Rails.root}/config.ru:in
`new' #{Rails.root}/config.ru:in `<main>' #{GEM_HOME}/rack-1.5.2/lib/rack/builder.rb:49:in `eval' #{GEM_HOME}/rack-1.5.2/lib/rack/builder.rb:49:in `new_from_string' #{GEM_HOME}/rack-1.5.2/lib/rack/builder.rb:40:in `parse_file' #{GEM_HOME}/rack-1.5.2/lib/rack/server.rb:126:in `build_app_and_options_from_config' #{GEM_HOME}/rack-1.5.2/lib/rack/server.rb:82:in `app' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/server.rb:50:in `app' #{GEM_HOME}/rack-1.5.2/lib/rack/server.rb:163:in `wrapped_app' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/server.rb:130:in `log_to_stdout' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/server.rb:67:in `start' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/commands_tasks.rb:82:in `block in server' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/commands_tasks.rb:77:in `tap' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/commands_tasks.rb:77:in `server' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands/commands_tasks.rb:41:in `run_command!' #{GEM_HOME}/railties-4.1.0.rc1/lib/rails/commands.rb:17:in `<top (required)>' ./bin/rails:8:in `require' ./bin/rails:8:in `<main>' -
Pro tip: `puts caller` Remember to use caller when you
get lost in the method call hell. -
Rails.root/con g/ application.rb require 'rails/all' Bundler.require(*Rails.groups) module YourApp class Application
< Rails::Application end end -
rails/all.rb require "rails" %w( active_record action_controller action_view action_mailer rails/test_unit sprockets
).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end -
rails/all.rb (edited) require 'active_record/railtie' require 'action_controller/railtie' require 'action_view/railtie' require 'action_mailer/railtie'
require 'rails/test_unit/railtie' require 'sprockets/railtie' -
Rails.application.initialize! (rails/application.rb) module Rails class Application < Engine def initialize!(group=:default)
... run_initializers(group, self) ... ennnd -
run_initializers (rails/initializable.rb) module Rails module Initializable def run_initializers(group=:default, *args) ...
initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) end ... ennd -
How the initializers are run? Calls #run method for each
Initializer object in the initializers collection -
Initializer#run (rails/initializable.rb) module Rails module Initializable class Initializer def run(*args)
@context.instance_exec(*args, &block) ennnnd -
Initializer#run It simply instance_execs the stored Proc in some context
stored in @context @context is actually the application instance -
All the Initializers (rails/application.rb) module Rails class Application < Engine
... def initializers Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self) ennnd -
What's in the initializers collection? (rails/initializable.rb) module Rails module Initializable
module ClassMethods def initializer(name, opts = {}, &blk) ... initializers << Initializer.new(name, nil, opts, &blk) ennnnd -
What's in the initializers collection? Instances of Rails::Initializable::Initializer Each Initializer
just holds a Proc instance (and a name and options) given to Rails::Railtie::Initializable.initializ e method call -
rails/commands/server.rb ... require 'rails' module Rails class Server < ::Rack::Server
def initialize(*) super ENV["RAILS_ENV"] ||= options[:environment] end def start super ennnd -
Summary: How Rails server boots Require all gems in the
Gem le (bundler) Load all Railties and Engines (con g/application.rb) De ne YourApp::Application inheriting Rails::Application (con g/application.rb) Run Railtie#initializer de ned in each Railtie, Engine, and Application Load each Engine's load_path and routes, then load con g/initializers/* Run Rails.application (con g.ru) => The next chapter -
run Rails.application With this de nition, the Rack server calls
`Rails.application.call(env)` -
Rails.application as a Rack application (railties/lib/rails/applicaiton.rb) module Rails class Application
... def call(env) ... super(env) ennnd -
Rails::Engine is also a Rack app (railties/lib/rails/engine.rb) module Rails class
Engine ... def call(env) ... app.call(env) ennnd -
What's app? (railties/lib/rails/engine.rb) module Rails class Engine ... def app
@app ||= begin config.middleware = config.middleware.merge_into(default_middleware_stack) config.middleware.build(endpoint) end ennnd -
What is con g.middleware? (rails/engine/con guration.rb) module Rails class Engine
class Configuration < ::Rails::Railtie::Configuration def middleware @middleware ||= Rails::Configuration::MiddlewareStackProxy.new ennnnd -
What is Rails::Con guration::MiddlewareStackProxy? (rails/con guration.rb) module Rails module Configuration
class MiddlewareStackProxy def initialize @operations = [] ennnnd -
What is Rails::Con guration::MiddlewareStackProxy? Basically it's a stack that proxies
the Rails middleware stack -
app = default_middlewarestack + endpoint (railties/lib/rails/engine.rb) module Rails class Engine
... def app @app ||= begin config.middleware = config.middleware.merge_into(default_middleware_stack) config.middleware.build(endpoint) end ennnd -
What's the endpoint? (rails/engine.rb) module Rails class Engine < Railtie
def endpoint self.class.endpoint || routes end def routes @routes ||= ActionDispatch::Routing::RouteSet.new ... ennnd -
Summary: Building middleware stack The Rack server calls `Rails.application.call` It
calls the Rails default middleware stack and the endpoint The endpoint is "routes" -
Rails.application.routes is also a Rack app (action_dispatch/routing/route_set.rb) module ActionDispatch module
Routing class RouteSet ... def call(env) @router.call(env) ennnnd -
What is @router? (action_dispatch/routing/route_set.rb) module ActionDispatch module Routing class RouteSet
... def initialize(request_class = ActionDispatch::Request) ... @set = Journey::Routes.new @router = Journey::Router.new(@set, { :parameters_key => PARAMETERS_KEY, :request_class => request_class}) @formatter = Journey::Formatter.new @set ennnnd -
Rails.application.routes is also a Rack app This is how Rails.application's
endpoint accepts the Rack request But for now, let's go back to routes.draw DSL -
routes.draw (action_dispatch/routing/route_set.rb) module ActionDispatch module Routing class RouteSet def draw(&block)
... eval_block(block) ... end def eval_block(block) ... mapper = Mapper.new(self) mapper.instance_exec(&block) ennnnd -
routes.draw instance_execs the whole given block with an instance of
ActionDispatch::Routing::Mapper -
AD::Routing::Mapper#get (action_dispatch/routing/mapper.rb) module ActionDispatch module Routing class Mapper def get(*args,
&block) map_method(:get, args, &block) end def map_method(method, args, &block) ... match(*args, options, &block) ... end def match(path, *rest) ... decomposed_match(_path, route_options) end def decomposed_match(path, options) add_route(path, options) ennnnnd -
Mapper#add_route (action_dispatch/routing/mapper.rb) module ActionDispatch module Routing class Mapper ... def
add_route(action, options) ... mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) ennnnnd -
add_routes returns a Journey::Route (action_dispatch/journey/routes.rb) module ActionDispatch module Journey class
Routes def add_route(app, path, conditions, defaults, name = nil) route = Route.new(name, app, path, conditions, defaults) route.precedence = routes.length routes << route ennnnd -
Mapper#add_route Finally adds a new Journey::Route to routes Makes a
Mapper#to_route call -
Mapper#to_route => app (action_dispatch/routing/mapper.rb) module ActionDispatch module Routing class Mapper
... def to_route [ app, conditions, requirements, defaults, options[:as], options[:anchor] ] end def app Constraints.new(endpoint, blocks, @set.request_class) ennnnnnd -
Constraints.new (action_dispatch/routing/mapper.rb) module ActionDispatch module Routing class Mapper class Constraints
def self.new(app, constraints, request = Rack::Request) if constraints.any? super(app, constraints, request) else app end ennnnnd -
What is endpoint? (action_dispatch/routing/mapper.rb) module ActionDispatch module Routing class Mapper
class Mapping ... def endpoint to.respond_to?(:call) ? to : dispatcher ennnnnnd -
to.respond_to?(:call) ?? Possibly An endpoint can be a Rack app
or a mounted Engine -
"foo#bar" does not respond_to :call (action_dispatch/routing/mapper.rb) module ActionDispatch module Routing
class Mapper class Mapping ... def dispatcher Routing::RouteSet::Dispatcher.new( :defaults => defaults) ennnnnnd -
Dispatcher is a Rack app (action_dispatch/routing/route_set.rb) module ActionDispatch module Routing
class RouteSet class Dispatcher def call(env) ... dispatch(controller, params[:action], env) end def dispatch(controller, action, env) controller.action(action).call(env) ennnnnd -
get "/hello" => "foo#bar" Maps "/hello" to a Proc returned
by FooController.action('bar') -
Rails.application.router module ActionDispatch module Routing class RouteSet def initialize(request_class =
ActionDispatch::Request) ... @set = Journey::Routes.new @router = Journey::Router.new(@set, { :parameters_key => PARAMETERS_KEY, :request_class => request_class}) @formatter = Journey::Formatter.new @set ennnnd -
RouteSet#call => Journey::Router#call module ActionDispatch module Routing class RouteSet ...
def call(env) @router.call(env) ennnnd -
Summary: Routes Rails.application.routes is a Rack app Each routes' endpoint
is a Rack app Each controller's each action is a Rack app e.g. 'foo#bar' becomes a Rack app generated by FooController.action('bar') Everything is a Rack app -
controller.action(action).call(env) (action_dispatch/routing/route_set.rb) module ActionDispatch module Routing class RouteSet class Dispatcher
def call(env) ... dispatch(controller, params[:action], env) end def dispatch(controller, action, env) controller.action(action).call(env) ennnnnd -
Creating a Request object, and adding a Proc to the
stack (action_controller/metal.rb) module ActionController class Metal < AbstractController::Base def self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| new.dispatch(name, klass.new(env)) end ennnd -
Creating a Response object (action_controller/metal/rack_delegation.rb) module ActionController module RackDelegation def
dispatch(action, request) set_response!(request) super(action, request) end def set_response!(request) @_response = ActionDispatch::Response.new @_response.request = request ennnd -
AC::Metal#dispatch (action_controller/metal.rb) module ActionController class Metal < AbstractController::Base def dispatch(name,
request) @_request = request @_env = request.env @_env['action_controller.instance'] = self process(name) to_a ennnd -
Controller#process (abstract_controller/base.rb) module AbstractController class Base def process(action, *args) ...
process_action(action_name, *args) end private def process_action(method_name, *args) send_action(method_name, *args) end alias send_action send ennnd -
Summary: Controllers and actions The request goes to FooController.action('bar') FooController.action('bar')
sets the Rails Request and Response objects to the controller instance Then call goes to FooController#bar via `send` call