#232 Routing Walkthrough Part 2
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ì:
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:
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:
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:
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:
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:
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:
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:
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
:
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:
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:
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
:
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:
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.