homeASCIIcasts

232: Esploriamo il routing di Rails 3 (parte 2) 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Andrea Salicetti

In questo episodio continueremo da dove eravamo rimasti la settimana scorsa e riprenderemo a esaminare il codice interno di Rails 3 deputato al routing. Al termine dello scorso episodio, il file di route appariva più o meno così:

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => ProductsController.action("index")
  match 'products/recent'
end

L’ultima volta abbiamo visto il codice interno del metodo match e abbiamo scoperto cosa accade quando si invoca il match nel file di route, tuttavia ci sono diversi altri metodi che possiamo richiamare che vedremo proprio in questo episodio.

Esaminando il codice sorgente di Rails 3.0, ritroviamo le logiche di routing nella cartella actionpack/lib/actiondispatch/routing. Ci focalizzeremo sulla classe Mapper presente in tale cartella poichè, come già discusso la volta scorsa, il blocco presente all’interno del file di route è inquadrato nell’ambito di questa classe. In altre parole, ogni metodo richiamato all’interno del blocco è chiamato su di un’istanza di Mapper, ragion per cui possiamo richiamare un qualsiasi metodo di istanza della classe Mapper all’interno del nostro file di route.

Il codice della classe Mapper può apparire un po’ destabilizzante. Il codice è tanto, quasi 1000 linee, e complesso, ma la buona notizia è che si tratta in effetti del file più verboso per quel che concerne il routing di Rails, per cui se riuscirete ad afferrare le logiche che intercorrono in questo file e a capirle, vi sarete fatti un’idea piuttosto buona del funzionamento interno complessivo del routing in Rails.

Al fine di ottenere una buona vista di insieme del codice, collasseremo il corpo dei metodi. In TextMate la pressione dei tasti Command-Option-0 causerà appunto il collassamento di tutto quanto è collassabile all’interno del file. Fatto ciò espandiamo il module di radice ActionDispatch, il suo sottomodulo Routing ed infine la classe Mapping stessa, per avere un quadro della sua struttura:

La struttura della classe Mapper.

I primi due elementi nella classe Mapper sono la definizione delle classi Constraints e Mapping. Le abbiamo viste entrambe nell’ultimo episodio, ma ciò che è significativo notare qui è come queste siano innestate sotto alla classe Mapper. Tutto ciò potrebbe apparire strano se vi siete appena avvicinati a Ruby e vi potreste giustamente domandare perchè dovreste aver bisogno di innestare le classi in un modo simile. Nulla di magico: la classe Constraints è completamente separata dalla classe Mapper. La ragione per cui è stata realizzata una simile struttura è che l’innestamento delle classi definisce il namespace per le classi Constraints e Mapping in modo tale che queste appaiano sotto il namespace di Mapper. Non c’è alcuna ereditarietà o condivisione di comportamento quando si realizzano simili innestamenti di classi in Ruby.

Spostandoci più in basso, troviamo due metodi di classe, self.normalize_path e self.normalize_name. Si tratta di metodi di utilità che sono utilizzati frequentemente all’interno della classe. Sotto di questi, c’è un insieme di module:

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

module Base...

module HttpHelpers...

module Scoping...

module Resources...

module Shorthand...

include Base
include HttpHelpers
include Scoping
include Resources
include Shorthand

Questi cinque module sono inclusi nella classe Mapper. Il codice al loro interno è stato confinato in module semplicemente per pulizia.

Base

Abbiamo visto il primo module, Base, nell’ultimo episodio. Contiene il metodo match, il metodo root che utilizza match e anche un metodo mount che fornisce un altro modo di mappare un’applicazione Rack ad un URL:

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

module Base
  def initialize(set) #:nodoc:

  def root(options = {})
    match '/', options.reverse_merge(:as => :root)
  end

  def match(path, options=nil)...

  def mount(app, options = nil)...

  def default_url_options=(options)...
  alias_method :default_url_options, :default_url_options=
end

HttpHelpers

Il module successivo è HttpHelpers, all’interno del quale sono definiti i metodi get, post, put e delete. Questi metodi sono usati per mappare i route a determinati tipi di richieste:

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

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

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

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

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

  def redirect(*args, &block)...

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

Tutti questi metodi al loro interno chiamano il metodo privato map_method. Questo metodo imposta l’opzione :via a seconda del metodo passatogli e infine chiama il metodo match. Noterete nel vostro codice di routing che molti dei metodi delegano al metodo match, passandogli e personalizzando a priori determinate opzioni. Per cui, se vogliamo che un certo route risponda solo a una richiesta GET, potremmo scrivere, usando l’opzione via:

match 'products/recent', :via => :get

All’atto pratico, facciamo la stessa cosa usando il metodo più conciso get, che crea proprio un route con la stessa opzione:

get 'products/recent'

I metodi post, put e delete funzionano in modo analogo al get per gli altri tipi di richieste HTTP. Il metodo redirect, invece, si differenzia dagli altri ed è interessante perchè restituisce un’applicazione Rack:

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

def redirect(*args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}

  path      = args.shift || block
  path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
  status    = options[:status] || 301

  lambda do |env|
    req = Request.new(env)

    params = [req.symbolized_path_parameters]
    params << req if path_proc.arity > 1

    uri = URI.parse(path_proc.call(*params))
    uri.scheme ||= req.scheme
    uri.host   ||= req.host
    uri.port   ||= req.port unless req.standard_port?

    body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)

    headers = {
      'Location' => uri.to_s,
      'Content-Type' => 'text/html',
      'Content-Length' => body.length.to_s
    }

    [ status, headers, [body] ]
  end
end

Il metodo restituisce un’applicazione Rack, restituendo un array comprendente uno stato, alcuni elementi di header e un body. Lo stato è impostato per default a 301 che impone al browser un semplice redirect 301 Moved Permanently. Potremmo usare questo metodo redirect direttamente all’interno del nostro file di route se volessimo che un URL ridirigesse ad un altro. Nel nostro file di route abbiamo già un route che utilizza il parametro :to e questo parametro accetta un’applicazione Rack:

match 'products', :to => ProductsController.action("index")

Dato che il metodo redirect restituisce un’applicazione Rack, lo possiamo usare in questo caso per fare un redirect a un altro URL in questo modo:

match 'products', :to => redirect("/items")

Questa funzionalità diventa davvero utile nel momento in cui si decide di modificare gli URL dell’applicazione, ma si desidera continuare a fornire supporto agli URL vecchi. Si può usare redirect per ridirigere questi URL ai nuovi ad essi corrispondenti.

Shorthand

I moduli successivi sarebbero Scoping e Resources, ma li vediamo fra poco. Per ora, focalizziamoci sul module Shorthand. E’ un module interessante che ridefinisce il metodo match, che era stato precedentemente definito nel module Base. Questo metodo match supporta una sintassi diversa per le opzioni che è possibile passargli. Il metodo shorthand rappresenta un modo alternativo per scrivere l’opzione :to in un route tipo il redirect che abbiamo scritto poc’anzi:

match 'products', :to => redirect('/items')

Si tratta di una sintassi comune nei file di route. Il metodo shorthand ci permette di scrivere il route con un semplice hash fatto dal route e da qualunque cosa a cui il route debba puntare. Come si può fare con la sintassi estesa, è possibile aggiungere parametri in coda al route:

match 'products' => redirect('/items')

Il metodo match ridefinito in Shorthand imposta il parametro :to se non è ancora stato impostato. Poi chiama il super, ma dal momento che Mapper non eredita da un’altra classe, cosa comporta, in questo caso, la chiamata a super?

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

module Shorthand
  def match(*args)
    if args.size == 1 && args.last.is_a?(Hash)
      options  = args.pop
      path, to = options.find { |name, value| name.is_a?(String) }
      options.merge!(:to => to).delete(path)
      super(path, options)
    else
      super
    end
  end
end

Ogni volta che si usa super in questo modo, Ruby cerca un metodo con lo stesso nome che sia stato definito in un module a monte. Il module Shorthand è definito come ultimo nella lista di module inclusi nel Mapper, per cui Ruby controllerà nei module precedenti alla ricerca di un metodo match e delegherà ad esso. In questo caso chiamerà il match presente nel module Base.

Questa tecnica è usata spesso nel codice sorgente di Rails 3. Le prime versioni di Rails usavano l’alias_method_chain per ridefinire comportamenti specifici, ma ora, in Rails 3, possiamo più semplicemente usare il super.

Resources

Chiuso il discorso sul module Shorthand, ci occuperemo ora del Resources. Come ci si potrebbe aspettare, questo module contiene il metodo resources e tutti i metodi ad esso associati. Usiamo il metodo resources nel nostro file di route per creare route RESTful:

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

def resources(*resources, &block)
  options = resources.extract_options!

  if apply_common_behavior_for(:resources, resources, options, &block)
    return self
  end

  resource_scope(Resource.new(resources.pop, options)) do
    yield if block_given?

    collection_scope do
      get  :index if parent_resource.actions.include?(:index)
      post :create if parent_resource.actions.include?(:create)
    end

    new_scope do
      get :new
    end if parent_resource.actions.include?(:new)

    member_scope  do
      get    :edit if parent_resource.actions.include?(:edit)
      get    :show if parent_resource.actions.include?(:show)
      put    :update if parent_resource.actions.include?(:update)
      delete :destroy if parent_resource.actions.include?(:destroy)
    end
  end

  self
end

Questo metodo è piuttosto complesso, ma osservandone la struttura generale assume un certo significato. Ci sono un paio di metodi di collezione, get :index e post :create; c’è un metodo get :new e infine get :edit, get :show, put :update e delete :destroy. Dovreste riconoscere questi come le famose sette action RESTful che sono create per un controller quando si dichiara resources per esso nel file di route.

Si noti la prima linea di codice nel blocco del metodo resource_scope. Se viene passato un blocco al metodo, di conseguenza il metodo farà yield a tale blocco prima di creare le action RESTful. Tutto ciò ci consente di creare le nostre action personalizzate nel file di route. Per esempio, potremmo aggiungere un route di collection che restituisce i prodotti in sconto:

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => redirect('/items')
  get 'products/recent'
  resources :products do
    collection do:
      get :discounted
    end
  end
end

Il codice dentro al blocco passato a resources nel route riportato qui sopra verrà eseguito dalla chiamata a yield in resource_scope e successivamente verranno definite le action RESTful standard. Nel blocco riportato qui sopra possiamo usare codice simile a quello presente nel metodo resources dei sorgenti di Rails, per definire le nostre action personalizzate.

Guardando i blocchi, nel file di route di sopra riportato, potreste essere indotti a pensare che l’oggetto cambi ogni volta che si crea un nuovo blocco, ma non è così. Stiamo ancora lavorando con lo stesso oggetto Mapper con cui abbiamo lavorato all’inizio, per cui chiamare get nel blocco più innestato esegue esattamente la stessa cosa che eseguirebbe richiamandolo dal più esterno. La sola cosa che cambia è che siamo in uno scope differente, ma nel parleremo a breve di questo.

Se tornate a esaminare il metodo resources del codice sorgente di Rails, vedrete che il codice utilizza una chiamata collection_scope quando definisce le action index e create, mentre all’interno dei nostri file di route utilizziamo semplicemente collection. Che differenza c’è? Beh, in realtà non molta. Se diamo un’occhiata al metodo collection nella classe Mapper, vedremo che questi delega proprio a collection_scope:

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

def collection
  unless @scope[:scope_level] == :resources
    raise ArgumentError, "can't use collection outside resources scope"
  end

  collection_scope do
    yield
  end
end

Riguardiamo rapidamente il file di route:

/config/routes.rb

Store::Application.routes.draw do
  match 'products', :to => redirect('/items')
  get 'products/recent'
  resources :products do
    collection do:
      get :discounted
    end
  end
end

Entrambe le chiamate a get nel codice di sopra invocano lo stesso metodo, ma quello che invoca da dentro il blocco collection assumerà un po’ di comportamento aggiuntivo a seconda dello scope in cui si trova all’interno dei blocchi resources e collection.

Se torniamo a vedere il module Resources, vedremo un metodo familiare: match. Questo metodo ridefinisce il metodo match, aggiungendo un po’ di comportamento aggiuntivo in base a resources:

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

def match(*args)
  options = args.extract_options!.dup
  options[:anchor] = true unless options.key?(:anchor)

  if args.length > 1
    args.each { |path| match(path, options.dup) }
    return self
  end

  on = options.delete(:on)
  if VALID_ON_OPTIONS.include?(on)
    args.push(options)
    return send(on){ match(*args) }
  elsif on
    raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
  end

  if @scope[:scope_level] == :resources
    args.push(options)
    return nested { match(*args) }
  elsif @scope[:scope_level] == :resource
    args.push(options)
    return member { match(*args) }
  end

  action = args.first
  path = path_for_action(action, options.delete(:path))

  if action.to_s =~ /^[\w\/]+$/
    options[:action] ||= action unless action.to_s.include?("/")
    options[:as] = name_for_action(action, options[:as])
  else
    options[:as] = name_for_action(options[:as])
  end

  super(path, options)
end

Se osserviamo, circa a metà del codice qui sopra riportato, vedremo la linea di codice che verifica lo scope attuale per vedere se è resources. Se lo è, viene aggiunto un po’ di comportamento differente. La logica è piuttosto complessa; tutto quello che dovete sapere è che il module Resources ridefinisce il metodo match. Si noti che alla fine chiama super affinchè sia invocato il metodo match del module Base. Si tenga presente che il metodo get invoca il match ed è qui che si trova la logica addizionale per gestire la get e gli altri metodi che sono definiti in resources.

Scoping

Siamo ora giuti all’ultimo metodo della classe Mapping: Scoping. Ovunque vi sia un blocco, all’interno dei vostri file di route, dietro le quinte c’è anche una chiamata al metodo scope di Scoping. Ciò significa che questi definirà del comportamento addizionale per il codice presente all’interno del blocco.

Oltre al metodo scope, ci sono una serie di altri metodi, che delegano tutti a scope:

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

def initialize(*args) #:nodoc:
  @scope = {}
  super
end

def controller(controller, options={})
  options[:controller] = controller
  scope(options) { yield }
end

def namespace(path, options = {})
  path = path.to_s
  options = { :path => path, :as => path, :module => path,
              :shallow_path => path, :shallow_prefix => 			path }.merge!(options)
  scope(options) { yield }
end

def constraints(constraints = {})
  scope(:constraints => constraints) { yield }
end

def defaults(defaults = {})
  scope(:defaults => defaults) { yield }
end

Questi metodi sono tutti abbastanza semplici e delegano tutti ad un metodo più generico, previa impostazione di alcune opzioni. Per esempio, defaults chiama scope dopo aver settato alcune opzioni defaults. Analogamente, constraints invoca scope impostando alcune opzioni constraints. Il metodo namespace è un po’ più complesso, ma fa sostanzialmente la stessa cosa. Il module ha anche un metodo initialize che crea semplicemente una variabile di istanza @scope e la imposta per essere un hash vuoto. Vi potreste ancora domandare cosa ci fa in un module un metodo initialize. Anche in questo caso, nel module si sta facendo l’override di un metodo che sarà in realtà definito nella classe che include tale module. Quando il module Scoping viene incluso nella classe Mapper questo metodo initialize ridefinirà quello definito nel metodo initialize, aggiungendo la variabile @scope e poi richiamando il super.

Infine abbiamo il metodo scope, dove viene fatto il lavoro sporco. C’è molta complessità in questo metodo, ma sostanzialmente tutto ciò che fa è riempire la variabile @scope con alcune informazioni, basandosi sulle opzioni che sono state passate nello scope. Il metodo unisce le opzioni usando una serie di metodi privati nel module. Tutto ciò che fa è di immagazzinare l’informazione di scope affinchè possa essere utilizzata successivamente all’interno di qualsiasi chiamata match abbiata. Sostanzialmente aggiunge utleriori funzionalità in base allo scope corrente.

Ecco come funzionano, fondamentalmente, i blocchi definiti all’interno dei file di route. Se definiamo un route del genere:

/config/routes.rb

Store::Application.routes.draw do
  controller :products do
    match #...
  end  
end

Ogni volta che chiamiamo match nel blocco controller (e si ricordi che questi delega allo scope) le opzioni del controller saranno automaticamente fornite all’interno di questi.

E’ tutto per questo episodio. Spero che tutto ciò vi abbia chiarito meglio di come siano trattati i metodi all’interno del file di route. Anche se esistono molti metodi fra cui scegliere all’interno del file di route, molti di questi sono semplici deleganti o al metodo match o allo scope, che passano in più solo alcune opzioni.