#231 Routing Walkthrough Part 1
L’episodio di questa settimana sarà un po’ diverso. Andremo infatti ad esplorare le parti interne di Rails 3 e a spulciare fra il suo codice, focalizzandoci su quello che gestisce il routing. Qui sotto è riportato il file di route dell’applicazione del negozio virtuale dell’ultimo episodio. Per poter sapere ciò che fa di fatto questo codice di routing, daremo un’occhiata al codice Rails che viene richiamato quando lo si lancia:
Store::Application.routes.draw do resources :products do resources :reviews end root :to => "products#index" end
Potreste domandarvi quale sia il punto di tutto ciò e se serva a qualcosa andare a vedere del codice non nostro, ma secondo me leggere codice Ruby scritto da altri è un eccellente modo di migliorare le proprie competenze in Ruby. Vedrete trucchi e tecniche che altri usano e che potrete in seguito riciclare per il vostro codice. Leggere il codice sorgente di Rails è anche un buon modo per imparare ad usare meglio Rails e proprio in questa occasione potremo scoprire modi migliori di scrivere il file di routing. Se avete intenzione di provare a debuggare un problema o di ottimizzare del codice in uno dei vostri progetti, o se state persino per considerare seriamente l’idea di contribuire a Rails, allora leggere le sue parti interne rappresenta un ottimo modo per cominciare.
Partiamo
Questo episodio tratterà alcuni aspetti avanzati, per cui si assume che conosciate già il funzionamento del routing in Rails 3. Se così non fosse, o se voleste rinfrescare le vostre conoscenze in merito, potreste guardare o leggere l’episodio 203 prima di questo, considerato anche che la sintassi di Rails 3 differisce abbastanza rispetto a Rails 2.
Il codice sorgente di Rails è conservato su Github ed è semplice clonare il repository per consultare il codice contenutovi. Dobbiamo solo lanciare:
$ git clone git://github.com/rails/rails.git
Il branch master del repository che abbiamo scaricato è per Rails 3.1, che è la versione attualemente in sviluppo, ma noi vogliamo consultare i sorgenti della stessa versione su cui gira la nostra applicazione. Possiamo spostarci alla versione taggata 3.0.0 lanciando:
$ git checkout v3.0.0
Aprendo la cartella di Rails notiamo subito che Rails è composto da tante parti differenti. Tutto ciò che concerne i controller, le viste o i route, si trova sotto la cartella actionpack
, per cui è questa la parte del codice che ci interessa:
Sotto actionpack
la maggior parte del codice riguardante il routing è sotto la cartella lib/action_dispatch
:
I metodi routes
e draw
Prima di cominciare a guardare il codice sorgente di Rails, ritorniamo alla nostra applicazione e guardiamo nuovamente il file di route:
Store::Application.routes.draw do resources :products do resources :reviews end root :to => "products#index" end
La prima linea del codice riportato di sopra chiama un metodo chiamato routes sulla classe Store::Application
, dove Store
è il nome della nostra applicazione. Se guardiamo al file application.rb
, vedremo che tale nome dell’applicazione è definito proprio là:
require File.expand_path('../boot', __FILE__) require 'rails/all' # If you have a Gemfile, require the gems listed there, including any gems # you've limited to :test, :development, or :production. Bundler.require(:default, Rails.env) if defined?(Bundler) module Store class Application < Rails::Application # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] end end
La classe Store::Application
è dove molte delle configurazioni dell’applicazione avvengono e specializza da Rails::Application
. Ogni cosa prefissata dal namespace Rails
è tipicamente definita nella cartella railties
nel codice sorgente Rails; il codice della classe per Rails::Application
è definito in rails/railties/lib/rails/application.rb
e in questo file troveremo il metodo routes
:
def routes @routes ||= ActionDispatch::Routing::RouteSet.new end
Questo è il metodo che viene richiamato nel file di route della nostra applicazione e tutto ciò che fa è di creare una nuova ActionDispatch::Routing::RouteSet
. Il metodo route restituisce questo nuovo RouteSet
e nella prima riga del nostro file di route chiamiamo proprio la draw
su questo oggetto. Vediamo ciò che fa il metodo draw
.
Nel codice sorgente di Rails una classe la si può spesso trovare in un file omonimo o dal nome analogo, infatti la classe RouteSet
non fa eccezione. All’interno della classe troviamo il metodo draw
:
def draw(&block) clear! unless @disable_clear_and_finalize mapper = Mapper.new(self) if block.arity == 1 mapper.instance_exec(DeprecatedMapper.new(self), &block) else mapper.instance_exec(&block) end finalize! unless @disable_clear_and_finalize nil end
Il metodo draw
accetta un blocco per argomento. Per prima cosa ripulisce qualsiasi eventuale route già presente, poi crea un nuovo oggetto Mapper
, passando ad esso self
(il RouteSet
). (Approfondiremo la classe Mapper
fra poco.) Dopodichè il metodo controlla l’arity
del blocco, ossia quanti argomenti sono stati passati ad esso. Se gli è stato passato un unico argomento, allora significa che il file di routing sta usando una sintassi Rails 2. Tutto ciò viene fatto ovviamente per avere retrocompatibilità con le versioni precedenti a Rails 3, che passano una variabile map
al blocco, in questo modo:
Store::Application.routes.draw do |map| # Instaradamenti a la Rails 2... end
A prescindere poi che l’applicazione usi una sintassi di routing stile Rails 2 piuttosto che Rails 3, viene chiamata una instance_exec
. Nel caso di sintassi Rails 3, si passa direttamente il blocco a tale metodo, mentre invece se si tratta di sintassi Rails 2, viene creato un nuovo oggetto DeprecatedMapper
che viene passato come primo argomento oltre al blocco. In entrambi i casi, ciò farà sì che il blocco sia eseguito come se fosse stato definito all’interno dell’istanza del mapper. La conseguenza di questa azione implica che tutto ciò che è definito all’interno del blocco nel file di route, viene invocato sull’istanza di un oggetto Mapper
. Questo trucco ci rende disponibile quella potente sintassi domain-specific dei file di route in Rails 3, nei quali è possibile chiamare semplicemente dei metodi tipo resources
senza la necessità di dover esplicitamente farlo su istanze specifiche di una determinata classe, come invece si faceva in Rails 2, quando si usava la map.resources
.
L’ultima cosa che viene fatta dal metodo draw è la chiamata al finalize!
, che è definito nella classe RouteSet
e che "congela" l’insieme dei route.
Come avviene il mapping di un route
Ora che sappiamo che tutto quanto si trova dentro al file di route in Rails 3 viene invocato su di un’istanza di Mapper
, usiamo un semplice route come esempio e vediamo come viene processato:
Store::Application.routes.draw do match 'products', :to => 'products#index' end
Diamo un’occhiata alla classe Mapper
per vedere cosa fa il metodo match
. Ci sono molti metodi denominati match
all’interno di quella classe; quello che ci interessa è nel module Base
:
module Base def initialize(set) #:nodoc: @set = set end def root(options = {}) match '/', options.reverse_merge(:as => :root) end def match(path, options=nil) mapping = Mapping.new(@set, @scope, path, options || {}).to_route @set.add_route(*mapping) self end # other methods end
Per creare un nuovo oggetto Mapper
, dobbiamo passare al costruttore un oggetto RouteSet
. Infatti, quando viene invocato il metodo match
nel file di route per creare un nuovo route, questo nuovo route viene aggiunto all’insieme passato. Tutto ciò avviene mediante la creazione di un nuovo oggetto di tipo Mapping
e la conseguente invocazione su di esso del metodo to_route
. Si noti anche il metodo root
, che è molto semplice. Tutto ciò che fa è chiamare match
, passandogli l’URL di root e aggiungendo l’opzione :as => :root
, affinchè diventi un named route. Ogni volta che si definisce nella propria applicazione Rails un URL di root, dietro le quinte viene dunque semplicemente invocato il metodo match
.
Ora vediamo più in dettaglio la classe Mapping
, definita nel file mapper.rb
:
class Mapping #:nodoc: IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] def initialize(set, scope, path, options) @set, @scope = set, scope @options = (@scope[:options] || {}).merge(options) @path = normalize_path(path) normalize_options! end def to_route [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ] end private def normalize_options! path_without_format = @path.sub(/\(\.:format\)$/, '') if using_match_shorthand?(path_without_format, @options) to_shorthand = @options[:to].blank? @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') @options[:as] ||= Mapper.normalize_name(path_without_format) end @options.merge!(default_controller_and_action(to_shorthand)) end # other private methods omitted. end
All’atto dell’inizializzazione del Mapper
, sono impostate alcune variabili di istanza in base ai parametri passati, dopodichè viene invocato il normalize_options!
. Il metodo normalize_options!
esegue un controllo per vedere se si sta utilizzando un determinato tipo di sintassi "abbreviata" nel file di route e nel metodo using_match_shorthand?
che viene chiamato, osserviamo un interessante escamotage. Esiste un modo rapido per definire le action di un controller in cui si separa il controller e i nomi delle action con uno slash:
# match "account/overview" def using_match_shorthand?(path, options) path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$} end
Se abbiamo definito il nostro route in questo modo, allora le opzioni :to
e :as
saranno impostate per noi, a seconda del nome dell’URL. Verifichiamolo tornando al file di route della nostra applicazione, aggiungendo un altro route che utilizzi la sintassi smart:
Store::Application.routes.draw do match 'products', :to => 'products#index' match 'products/recent', :to => 'products#recent' end
Abbiamo fornito un parametro :to
a questo nuovo route, ma verrebbe comunque aggiunto automaticamente se non lo avessimo specificato. Il metodo di shortcut crea automaticamente per noi anche un parametro :as
, come se avessimo aggiunto :as => :products_recent
.
Come viene usato Rack nel routing
Tornando al metodo match
, una volta creato un nuovo oggetto Mapping
, richiamiamo il metodo to_route
su di esso. Questo metodo restituisce un array di opzioni che sono usate per creare un nuovo route:
def to_route [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ] end
I primi quattro elementi nell’array di sopra sono i valori restituiti dalle chiamate ai metodi presenti nella classe Mapper
:
def app Constraints.new( to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults), blocks, @set.request_class ) end def conditions { :path_info => @path }.merge(constraints).merge(request_method_condition) end def requirements @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements| requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints] @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) } end end def defaults @defaults ||= (@options[:defaults] || {}).tap do |defaults| defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults] @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) } end end
La prima opzione è app
; ogniqualvolta vediate un qualcosa chiamato app
nel codice sorgente di Rails, è molto probabile che si faccia riferimento a un’applicazione Rack. Questo metodo app
restituisce un nuovo oggetto Constraints
, per cui vediamo se si tratta effettivamente, in questo caso, di un’applicazione Rack.
La classe Constraints
è definita nello stesso file mapper.rb
. Accetta una serie di metodi, uno dei quali è chiamato call
e prende in ingresso come parametro un environment:
def call(env) req = @request.new(env) @constraints.each { |constraint| if constraint.respond_to?(:matches?) && !constraint.matches?(req) return [ 404, {'X-Cascade' => 'pass'}, [] ] elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req)) return [ 404, {'X-Cascade' => 'pass'}, [] ] end } @app.call(env) end
La classe Constraints
sembra proprio un’applicazione Rack. Una cosa interessante sulla classe Constraints
è che fa l’override del metodo self.new
. Potreste domandarvi il perchè, dal momento che esiste già anche un metodo initialize
:
def self.new(app, constraints, request = Rack::Request) if constraints.any? super(app, constraints, request) else app end end attr_reader :app def initialize(app, constraints, request) @app, @constraints, @request = app, constraints, request end
La ragione dietro a questa scelta è solo di performance. Constraints
è un pezzo del middleware di Rack; in altri termini, racchiude un’altra applicazione Rack. Il primo argomento passato al self.new
è app
, che rappresenta un’applicazione Rack; quando viene invocato il metodo call
, se viene scatenato uno dei constraints, restituisce un errore 404
, altrimenti scatenerà l’applicazione Rack che sta wrappando. Il metodo self.new
fa parte di logiche di ottimizzazione di performance e serve affinchè, quando viene chiamato, Rails non allochi un altro oggetto in memoria, wrappandolo e usandolo, ma piuttosto restituisca semplicemente l’applicazione Rack iniziale stessa.
Torniamo ora al codice che richiama tutto ciò. Si noti che i primi due argomenti sono una applicazione Rack e un array di vincoli. Il tutto viene richiamato nel metodo app
della classe Mapping
. Quando qui si crea un nuovo vincolo, controlliamo per vedere se il metodo to risponde al call
. (Il metodo to
restituisce semplicemente l’opzione :to
che era stata definita per il route.) In caso affermativo, si tratta di un’applicazione Rack e viene passata questa; altrimenti si passa un nuovo oggetto RouteSet::Dispatcher
con un paio di opzioni di default:
def app Constraints.new( to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults), blocks, @set.request_class ) end
E’ questo codice a darci la possibilità di passare un’applicazione Rack ad un route in questa maniera:
root :to => proc { |env| [200, {}, ["Welcome"]] }
Ci sono più approfondimenti sull’uso di Rack nei route nell’episodio 222 [guardalo, leggilo]. Poterlo usare nei route ci da molta flessibilità.
Se non passiamo un’applicazione Rack all’opzione :to
, allora viene creato un nuovo oggetto RouteSet::Dispatcher
. Vediamo ora come viene gestito quest’ultimo.
La classe Dispatcher
gestisce lo smistamento di una richiesta al controller corretto che dovrà servirla. Nel suo metodo controller_reference
si può vedere il codice dove viene identificato l’opportuno controller:
def controller_reference(controller_param) unless controller = @controllers[controller_param] controller_name = "#{controller_param.camelize}Controller" controller = @controllers[controller_param] = ActiveSupport::Dependencies.ref(controller_name) end controller.get end
Questa classe ha anche dei metodi per fare cose tipo impostare l’action di default a index
se non ne è stata definita una e un metodo dispatch
che chiama la action stessa e che restituisce un’applicazione Rack. Tutto ciò significa che possiamo prendere uno qualsiasi dei nostri controller, chiamare action
su di esso, passandogli il nome della action e ottenendo un’applicazione Rack in risposta:
ruby-1.9.2-p0 > ProductsController.action("index") => #<Proc:0x00000100ec56c0@/Users/asalicetti/.rvm/gems/ruby-1.9.2-p0/gems/actionpack-3.0.0/lib/action_controller/metal.rb:172>
Questo è quel che succede dietro le quinte quando passiamo un file di route tipo questo:
match 'products', :to => 'products#index'
La stringa products#index
è convertita in ProductsController.action("index")
e questa restituisce un’applicazione Rack. La sintassi della stringa è una banale scorciatoia per fare la stessa cosa.
C’è molto altro in merito al routing che potremmo dire in questo episodio, ma questo sembra anche un buon momento per fermarsi. Ci sono i metodi resources
che generano una serie di route, ci sono i vari metodi che permettono di limitare le condizioni a determinati scope e passare blocchi a queste affinchè sia possibile realizzare un cascade di scope, ma in effetti fin qui c’è abbastanza per incoraggiarvi ad esplorare per conto vostro il codice di routing.
Il routing è una delle aree più complicata di Rails, per cui se siete un po’ intimoriti dalla complessità del codice che vi troverete di fronte, non vi preoccupate: è davvero complesso. Partite con alcune altre parti del codice di Rails prima e fateci il callo prima di affrontare parti più complesse.