Le fonctionnement interne de Rails

https://slides.com/guireccorbel/parisrb2015/

Guirec Corbel

@GuirecCorbel

guirec@optik360.com

Pourquoi comprendre Rails ?

  • Devenir un expert
  • Se faire connaître
  • Expérience incroyable

Pourquoi apprendre Rails ?

Fils conducteur

Décrire le chemin fait par une requête de sa réception par l'application jusqu'au retour de la réponse

Rack

Interface pour développer des applications web en Ruby

 

Intercepte les requêtes HTTP et retourne un résultat

Rack

class ParisRb
  def call(env)
    [200, {}, ["Bonjour ParisRb"]]
  end
end

run ParisRb.new
➜  parisrb  rackup
Thin web server (v1.6.3 codename Protein Powder)
Maximum connections set to 1024
Listening on localhost:9292, CTRL+C to stop
➜  parisrb  curl http://localhost:9292/
Bonjour ParisRb% 

Rack

{"SERVER_SOFTWARE"=>"thin 1.6.3 codename Protein Powder", 
 "SERVER_NAME"=>"localhost", 
 "rack.input"=>#<Rack::Lint::InputWrapper:0x007f0c3fa46918 @input=#<StringIO:0x007f0c3fb9a2b0>>, 
 "rack.version"=>[1, 0], 
 "rack.errors"=>#<Rack::Lint::ErrorWrapper:0x007f0c3fa46710 @error=#<IO:<STDERR>>>, 
 "rack.multithread"=>false, 
 "rack.multiprocess"=>false, 
 "rack.run_once"=>false, 
 "REQUEST_METHOD"=>"GET", 
 "REQUEST_PATH"=>"/", 
 "PATH_INFO"=>"/", 
 "REQUEST_URI"=>"/", 
 "HTTP_VERSION"=>"HTTP/1.1", 
 "HTTP_USER_AGENT"=>"curl/7.35.0", 
 "HTTP_HOST"=>"localhost:9292", 
 "HTTP_ACCEPT"=>"*/*", 
 "GATEWAY_INTERFACE"=>"CGI/1.2", 
 "SERVER_PORT"=>"9292", 
 "QUERY_STRING"=>"", 
 "SERVER_PROTOCOL"=>"HTTP/1.1", 
 "rack.url_scheme"=>"http", 
 "SCRIPT_NAME"=>"", 
 "REMOTE_ADDR"=>"127.0.0.1", 
 "async.callback"=>#<Method: Thin::Connection#post_process>, 
 "async.close"=>#<EventMachine::DefaultDeferrable:0x007f0c3fad4ad8>, 
 "rack.tempfiles"=>[]}

Rack

class ParisRb
  def call(env)
    if env["REQUEST_PATH"] == "/bonsoir"
      [200, {}, ["Bonsoir ParisRb"]]
    elsif env["REQUEST_PATH"] == "/bonjour"
      [200, {}, ["Bonjour ParisRb"]]
    else
      [404, {}, ["Not Found"]]
    end
  end
end

run ParisRb.new
➜  parisrb  curl http://localhost:9292/bonsoir
Bonsoir ParisRb%
➜  parisrb  curl http://localhost:9292/bonjour
Bonjour ParisRb%
➜  parisrb  curl http://localhost:9292/salut
Not Found% 

Rack

#config.ru
# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment', __FILE__)
run Rails.application
➜  parisrb  rackup
[2015-06-07 03:08:29] INFO  WEBrick 1.3.1
[2015-06-07 03:08:29] INFO  ruby 2.1.5 (2014-11-13) [x86_64-linux]
[2015-06-07 03:08:29] INFO  WEBrick::HTTPServer#start: pid=7646 port=9292
127.0.0.1 - - [07/Jun/2015:03:08:31 -0400] "GET / HTTP/1.1" 200 - 0.2457
127.0.0.1 - - [07/Jun/2015:03:08:39 -0400] "GET /products HTTP/1.1" 200 - 0.7318

Rack

Besoin d'un application simple ?

Utilisez Sinatra plutôt que Rack

Résumé

  • Réception d'un requête HTTP
  • ...
  • ...
  • ...
  • Retour de la réponse

Middleware

require 'benchmark'

class TimeMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    puts Benchmark.measure { @response = @app.call(env) }
    @response
  end
end

Middleware

use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f2f84717ba8>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Parisrb::Application.routes

Point d'entrée

module Parisrb
  class Application < Rails::Application
    # ...
  end
end
# railties/lib/rails/application.rb
module Rails
  class Application < Engine
    # ...
  end
end
# railties/lib/rails/engine.rb
module Rails
  class Engine < Railtie
    # ...
    def routes
      @routes ||= ActionDispatch::Routing::RouteSet.new
      @routes.append(&Proc.new) if block_given?
      @routes
    end
    # ...
  end
end

Résumé

  • Réception d'un requête HTTP
  • Transformation par un stack de middleware
  • ...
  • ...
  • Retour de la réponse

#resources

Rails.application.routes.draw do
  resources :products
end
# actionpack/lib/action_dispatch/routing/route_set.rb

def draw(&block)
  clear! unless @disable_clear_and_finalize
  eval_block(block)
  finalize! unless @disable_clear_and_finalize
  nil
end

def eval_block(block)
  if block.arity == 1
    raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
      "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
  end
  mapper = Mapper.new(self)
  if default_scope
    mapper.with_default_scope(default_scope, &block)
  else
    mapper.instance_exec(&block)
  end
end

#resources

#actionpack/lib/action_dispatch/routing/mapper.rb

def resource(*resources, &block)
  # ...
  resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
    # ...
    set_member_mappings_for_resource
  end
  self
end

def set_member_mappings_for_resource
  member do
    get :edit if parent_resource.actions.include?(:edit)
    get :show if parent_resource.actions.include?(:show)
    if parent_resource.actions.include?(:update)
      patch :update
      put   :update
    end
    delete :destroy if parent_resource.actions.include?(:destroy)
  end
end

Bref :

  #resources = [#get, #post, #delete, #patch, #put]

#resources

#actionpack/lib/action_dispatch/routing/mapper.rb

def get(*args, &block)
  map_method(:get, args, &block)
end

def map_method(method, args, &block)
  options = args.extract_options!
  options[:via] = method
  match(*args, options, &block)
  self
end

Bref :

  [#get, #post, #delete, #patch, #put] = #match

Ajouter une route

match 'products', to: 'products#index', via: :get

match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
match 'photos/:id', to: PeoductRackApp, via: :get
# Yes, controller actions are just rack endpoints
match 'photos/:id', to: ProductsController.action(:show), via: :get
def match(path, *rest)
  # ... manipule les options
  paths.each do |_path|
    # ... continue de manipuler les options
    decomposed_match(_path, route_options)
  end
end
def decomposed_match(path, options) # :nodoc:
  if on = options.delete(:on)
    send(on) { decomposed_match(path, options) }
  else
    case @scope.scope_level
    when :resources
      nested { decomposed_match(path, options) }
    when :resource
      member { decomposed_match(path, options) }
    else
      add_route(path, options)
    end
  end
end
def add_route(action, options) # :nodoc:
  # ... manipule les options

  mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
  # app.class == ActionDispatch::Routing::RouteSet::Dispatcher
  # set.class == ActionDispatch::Routing::RouteSet
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
end

Ajouter une route

# actionpack/lib/action_dispatch/journey/routes.rb
# Add a route to the routing table.
def add_route(app, path, conditions, defaults, name = nil)
  route = Route.new(name, app, path, conditions, defaults)

  route.precedence = routes.length
  routes << route
  named_routes[name] = route if name && !named_routes[name]
  clear_cache!
  route
end

Ajouter une route

require ::File.expand_path('../config/environment', __FILE__)
run ProductsController.action(:index)

Point d'entrée

module Parisrb
  class Application < Rails::Application
    # ...
  end
end
# railties/lib/rails/application.rb
module Rails
  class Application < Engine
    # ...
  end
end
# railties/lib/rails/engine.rb
module Rails
  class Engine < Railtie
    # ...
    def routes
      @routes ||= ActionDispatch::Routing::RouteSet.new
      @routes.append(&Proc.new) if block_given?
      @routes
    end
    # ...
  end
end

Trouver les routes

# actionpack/lib/action_dispatch/routing/route_set.rb
def call(env)
  req = request_class.new(env)
  req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
  @router.serve(req)
end
# actionpack/lib/action_dispatch/journey/router.rb 
def serve(req)
  find_routes(req).each do |match, parameters, route|
    # ... manipule la requête
    status, headers, body = route.app.serve(req)
    # ... manipule la requête
    return [status, headers, body]
  end
  return [404, {'X-Cascade' => 'pass'}, ['Not Found']] 
end

Trouver les routes

# actionpack/lib/action_dispatch/journey/router.rb
def find_routes req
  routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
    r.path.match(req.path_info)
  }

  # ... renseigne les informations sur le chemin et les paramêtres
end
# actionpack/lib/action_dispatch/journey/routes.rb
module ActionDispatch
  module Journey # :nodoc:
    # The Routing table. Contains all routes for a system. Routes can be
    # added to the table by calling Routes#add_route.
    class Routes # :nodoc:
      include Enumerable
      # ...
    end
  end
end

Trouver les routes

# actionpack/lib/action_dispatch/journey/router.rb 
def serve(req)
  find_routes(req).each do |match, parameters, route|
    # ... manipule la requête
    status, headers, body = route.app.serve(req)
    # ... manipule la requête
    return [status, headers, body]
  end
  return [404, {'X-Cascade' => 'pass'}, ['Not Found']] 
end
module ActionDispatch
  module Routing
    class RouteSet #:nodoc:
      class Dispatcher < Routing::Endpoint #:nodoc:
        def serve(req)
          req.check_path_parameters!
          params = req.path_parameters

          prepare_params!(params)
          # {:controller=>"products", :action=>"index"}

          # Just raise undefined constant errors if a controller was specified as default.
          unless controller = controller(params, @defaults.key?(:controller))
            return [404, {'X-Cascade' => 'pass'}, []]
          end

          dispatch(controller, params[:action], req.env)
        end

        def dispatch(controller, action, env)
          controller.action(action).call(env)
        end
      end
    end
  end
end

Trouver les routes

Résumé

  • Réception d'un requête HTTP
  • Transformation par un stack de middleware
  • Recherche de la route
  • ...
  • Retour de la réponse

À retenir

  • Rails est basé sur Rack
  • Rails est compliqué
  • Rails n'est pas magique
  • Rails est très compliqué (mais avec du temps on y arrive)

Sources

  • Rebuilding Rails - Noah Gibbs
  • Crafting Rails Applications - José Valim
  • Railscasts Pro - Ryan Bates
  • Code source de Rails

Questions ???

ParisRb2015

By Guirec Corbel

ParisRb2015

  • 2,095