Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ruby on Rails Hacking Guide

Ruby on Rails Hacking Guide

Slides for RubyConf India 2014 talk "Ruby on Rails Hacking Guide" http://rubyconfindia.org/2014/

Akira Matsuda

March 23, 2014
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. pp self GitHub: amatsuda Twitter: @a_matsuda committer of: Ruby, Rails,

    Haml from: Tokyo, Japan founder of: Asakusa.rb an organizer of: RubyKaigi
  2. Rails.has_many :bugs I often hit them in my production apps

    I need to x them I need to understand the code
  3. How could I understand the code? I read it, and

    read it, and I read it I spent too much time reading and learning the code I don't want all other contributors to repeat that waste of time
  4. I spent too much time reading and learning the code

    Because I learned it without a guide I wish I had a good guide like "Ruby Hacking Guide" by Minero Aoki So I'm speaking and writing
  5. How could I hit Rails bug so often? Use Edge

    Rails In your hobby apps Don't trust Rails Rails should work as you expect Remove silencers!
  6. Chapters Let's start reding! What is Railties? How Rails server

    boots Rails and Rack middleware Routes Controllers and actions ... and more
  7. 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
  8. 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
  9. 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!
  10. What's included in the rails gem? When you want to

    know what's included in the gem, take a look at the gemspec
  11. 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
  12. 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
  13. Directories in the rails project % lsd # aliased to

    ls -ld *(-/DN) actionmailer actionpack actionview activemodel activerecord activesupport railties
  14. Rails is a full-stack MVC framework All the Rails MVC

    components are in this one repository
  15. 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
  16. Back to the dependencies list You'll notice one gem that

    is not included in any of the MVC Which is called railties
  17. Summary: Let's start reding! rails gem is a meta package

    that depends on the whole MVC components There's a gem that is not included in any of the MVC components, which is called railties
  18. railties.gemspec s.summary = 'Tools for creating, working with, and running

    Rails applications.' s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.'
  19. What we learned Railties de nes these core classes MyApp::Application

    < Application < Engine < Railtie < Initializable
  20. Pro tip: Tools to read it through Vim + unite.vim

    (unite-outline) rdefs RubyMine
  21. Railroad tie "a rectangular support for the rails in railroad

    tracks" "Generally laid perpendicular to the rails" "Railroad ties were traditionally made of wood"
  22. 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:
  23. Each gems has their 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
  24. 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
  25. What's written in a Railtie? Mainly we see two kinds

    of class method calls, con g and initializer
  26. What is con g? (railtie/railtie.rb) module Rails class Railtie def

    config @config ||= Railtie::Configuration.new ennnd
  27. 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
  28. Railtie::Con guration? It accepts any method call and stores the

    given method name & value in a class level Hash as {method_name: value}
  29. 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
  30. What is initializer? It just holds the given block as

    a Proc instance (with a name and options)
  31. What Railtie is doing Sets some values via con g.foobar

    = 'baz' Stores Procs given to initializer method calls
  32. 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?
  33. Why is it a class despite you can't instantiate? In

    order to be able to be inherited
  34. 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
  35. 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
  36. rails/commands/server.rb module Rails class Server < ::Rack::Server def initialize(*) super

    ENV["RAILS_ENV"] ||= options[:environment] end def start super ennnd
  37. I'm not gonna do rack code reading here But method

    calls go on like start => wrapped_app => app => build_app_and_options_from_ con g => Rack::Builder.parse_ le => Rack::Builder.new_from_string
  38. Pro tip: `puts caller` Find it difficult to follow the

    method calls? Add `puts caller` to con g.ru le and start the server.
  39. 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
  40. `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>'
  41. Pro tip: `puts caller` Remember to use caller when you

    get lost in the method call hell.
  42. 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
  43. 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
  44. How the initializers are run? Calls #run method for each

    Initializer object in the initializers collection
  45. Initializer#run It simply instance_execs the stored Proc in some context

    stored in @context @context is actually the application instance
  46. 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
  47. 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
  48. 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.initial ize method call
  49. 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
  50. Rails.application as a Rack application (railties/lib/rails/applicaiton.rb) module Rails class Application

    ... def call(env) env["ORIGINAL_FULLPATH"] = build_original_fullpath(env) env["ORIGINAL_SCRIPT_NAME"] = env["SCRIPT_NAME"] super(env) ennnd
  51. Rails::Engine is also a Rack app (railties/lib/rails/engine.rb) module Rails class

    Engine ... def call(env) env.merge!(env_config) if env['SCRIPT_NAME'] env.merge! "ROUTES_#{routes.object_id}_SCRIPT_NAME" => env['SCRIPT_NAME'].dup end app.call(env) ennnd
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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"
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. "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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. end