#203 Routing in Rails 3
- Download:
- source codeProject Files in Zip (89 KB)
- mp4Full Size H.264 Video (21.1 MB)
- m4vSmaller H.264 Video (14.3 MB)
- webmFull Size VP8 Video (41.3 MB)
- ogvFull Size Theora Video (28.9 MB)
Continuando nuestro repaso a las funcionalidades de Rails 3 esta vez nos vamos a centrar en las rutas: Rails 3 tiene una nueva API para la definición de rutas. Para ver cómo funcionan las nuevas rutas vamos a crear una aplicación llamada detour
y crearemos algunas rutas en ella.
rails detour
Una vez que hayamos creado la aplicación podemos abrir el fichero config/routes.rb
en un editor de texto. Merece la pena leer el contenido de este fichero por defecto porque contiene documentación y algunos ejemplos. A continuación, pasaremos a reemplazar los contenidos por defecto con varios ejemplos de rutas antiguas y mostraremos su equivalencia en Rails 3.
Detour::Application.routes.draw do |map| map.resources :products, :member => { :detailed => :get } map.resources :forums, :collection => { :sortable => :get, :sort => :put } do |forums| forums.resources :topics end map.root :controller => "home", :action => "index" map.about "/about", :controller => "info", :action => "about" end
Las rutas de estilo antiguo que vamos a convertir.
Comenzaremos con la primera de las rutas:
map.resources :products, :member = { :detailed => :get }
Esta ruta define un recurso products
y una acción específica de miembro llamada detailed
a la que se invoca con una petición GET.
El primer cambio que observamos en las rutas de Rails 3 es que ya no tenemos que tratar con el objeto map
, y en su lugar invocaremos resources
directamente en el bloque de routes.draw
. Los métodos :member
y :collection
se definen dentro de un bloque que pertenece a los recursos. Por tanto podemos reescribir la ruta así:
resources :products do get :detailed, :on => :member end
Veamos a continuación una ruta más complicada:
map.resources :forums, :collection => { :sortable => :get, :sort => :put } do |forums| forums.resources :topics end
En esta ruta tenemos un recurso forum
con dos métodos de colección y un recurso anidado llamado topics
. Con la nueva API podemos escribir así esta ruta:
resources :forums do collection do get :sortable put :sort end resources :topics end
Aquí también usamos resources
en lugar de map.resources
, pasándole un bloque. En esta ruta tenemos dos acciones sobre la colección y si bien podríamos definirlas de la misma manera que hicimos en la ruta anterior utilizando el argumento :on
, esta vez definiremos un bloque de collection
(las acciones sobre miembros se pueden definir igual con un bloque member
). En nuestro bloque hemos definidos dos nuevas acciones, una llamada sortable
(como petición GET) y otra llamada sort
(como PUT).
Para el recurso anidado tan sólo tenemos que invocar resources
otra vez, anidando el recurso topics
dentro de forums
.
La siguiente ruta que veremos es la que define el controlador y la acción a donde apunta la URL raíz:
map.root :controller => "home", :action => "index"
Aquí podemos utilizar root
y utilizar el argumento :to
al cual le podemos pasar una cadena que contiene el nombre del controlador y la acción separados por una almohadilla.
root :to => "home#index"
Esta posibilidad de especificar un controlador y una acción en una única cadena es nueva en Rails 3. Podemos hacer algo parecido en una ruta con nombre...
map.about "/about", :controller => "info", :action => "about"
... que se puede reescribir así en la API de Rails 3:
match "/about" => "info#about", :as => :about
Sin el argumento :as
la ruta anterior tan sólo sería una ruta genérica. Al añadirle :as
se convierte en una ruta con nombre de forma que en nuestra aplicación podremos utilizar about_path
o about_url
.
Nuevas Funcionalidades
No es difícil, como puede verse por los ejemplos anteriores, convertir las rutas de la antigua API pero lo realmente interesante en las rutas de Rails 3 son sus nuevas funcionalidades, que veremos en el resto del episodio.
Parámetros Opcionales
Las versiones previas de Rails permitían definir parámetros opcionales pero con una sintaxis más bien aparatosa. Veamos cómo se hace en Rails 3.
Es más fácil demostrar el uso de parámetros opcionales si nuestra aplicación tiene un controlador, así que crearemos un controlador info
con una acción about
. Nótese que en Rails 3 podemos utilizar rails g
como abreviatura de rails generate
.
rails g controller info about
Podemos arrancar nuestro servidor con este otro atajo:
rails s
Si ahora visitamos http://localhost:3000/about veremos la acción info#about
porque así lo indica la ruta raíz que escribimos anteriormente.
Una vez hecho esto supongamos que queremos proporcionar una versión en PDF de esta acción, pero si se visita http://localhost:3000/about.pdf recibiremos un error de rutas porque nuestra aplicación no puede encontrar una ruta que case con esa URL.
En el fichero de rutas podemos cambiar la ruta about
para recibir un parámetro de formato añadiendo un punto y :format
:
match "/about.:format" => "info#about", :as => :about
Si recargamos la URL con la vista de PDF la ruta será encontrada pero veremos un error de que falta la plantilla porque nuestra aplicación no sabe cómo mostrar dicha acción como un PDF.
Puede parecer que hemos corregido nuestro problema pero el formato ahora mismo no es opcional por lo que si volvemos a la vista por defecto veremos un error. Por suerte es fácil hacer que parte de una ruta sea opcional: todo lo que tenemos que hacer es rodear la parte opcional entre paréntesis, por ejemplo:
match "/about(.:format)" => "info#about", :as => :about
Ahora ya podemos visitar http://localhost:3000/about o http://localhost:3000/about.pdf y no tendremos errores por las rutas.
A continuación veremos cómo utilizar parámetros opcionales más complejos. Supongamos que nuestra aplicación tiene un número de artículos en un blog que queremos filtrar especificando un año con un mes opcional (o un mes y un día) en la URL.
Esto se puede hacer definiendo una ruta como la siguiente que tiene como destino nuestra acción info#about
.Nótese que podemos anidar los paréntesis cuando creemos parámetros opcionales.
match "/:year(/:month(/:day))" => "info#about"
En el código de la vista añadiremos código de depuración para poder ver los parámetros que se están recibiendo:
<h1>Info#about</h1> <p>Find me in app/views/info/about.html.erb</p> <%= debug params %>
Si ahora especificamos un año en la URL, éste se pasará a la acción about
. Igualmente ocurrirá si se pasan un año, mes y día.
Esta ruta es demasiado genérica: si, por ejemplo, quisiéramos visitar http://localhost:3000/foo/bar también se pasarían a la acción about
, cuando sólo queremos recibir parámetros que parezcan fechas. Podemos hacerlo definiendo restricciones.
Restricciones
En Rails 2 se podían definir restricciones, conocidas como requirements
. Podemos pasar la opción :constraints
a nuestra ruta con un hash de los valores y las restricciones que debe cumplir cada uno de ellos. Vamos a restringir la ruta de forma que sólo coincida si el parámetro del año es de 4 dígitos y el día y el año son de dos:
match "/:year(/:month(/:day))" => "info#about", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }
Con esta restricción cuando visitemos la ruta /foo/bar
de nuevo recibiremos un error de rutas porque la URL no cumple las restricciones que hemos definido.
Con los requirements de Rails 2 ya era posible hacer todo esto pero en Rails 3 podemos ir más lejos. Por ejemplo podemos utilizar un parámetro user_agent
para que una ruta quede restringida a cierto navegador, en este caso Firefox.
match "/secret" => "info#about", :constraints => { :user_agent => /Firefox/ }
Si visitamos http:/localhost:3000/secret en Safari, Chrome o Opera veremos un error de rutas pero si la visitamos con Firefox (o configuramos algún otro navegador para que se identifique como tal) la cadena de user agent enviada por el navegador coincidirá con la restricción y veremos la página
También podemos añadir una restricción un poco más útil, por ejemplo el host:
match "/secret" => "info#about", :constraints => { :host => /localhost/ }
Esta restricción quiere decir que podemos visitar http://localhost:3000/secret y veremos la página pero si por el contrario utilizamos la dirección IP ( http://127.0.0.1/secret ) recibiremos un error aunque las dos direcciones son equivalentes. Esto se puede utilizar para restringir ciertas rutas a un subdominio dado (en el futuro esta funcionalidad será extendida para poder fijar este tipo de restricciones con la opción :subdomain
)
Si tenemos varias rutas que definen una misma restricción podemos acabar con mucho código duplicado en nuestro fichero de rutas. Esto se puede compactar utilizando el método constraints:
y poniéndo las rutas dentro de un bloque:
constraints :host => /localhost/ do match "/secret" => "info#about" match "/topsecret" => "info#about" end
Rutas con Rack
En futuros episodios volveremos a acercarnos a las rutas en Rails 3 (hay mucho más que no hemos tenido tiempo de cubrir). Lo último que veremos es cómo las rutas de Rails 3 se pueden combinar con las de Rack. Por lo general una ruta recibe como destino un controlador y una acción pero también se puede pasar una aplicación Rack, lo cual es algo bastante potente. Para ilustrarlo crearemos una ruta que apunta a una aplicación Rack sencilla:
match "/hello" => proc { |env| [200, {}, "Hello Rack!"] }
Si no estamos familiarizados con Rack podemos ver o leer el episodio 151. Lo que estamos haciendo arriba es pasar un código de retorno, un hash (vacío) con las cabeceras y una respuesta sencilla.
Si visitamos /hello
en el navegador ahora veremos “Hello Rack!” por lo que ya sabemos que nuestra aplicación Rack funciona correctamente. Esto amplía mucho nuestras posibilidades a la hora de definir rutas y encaminarlas hacia diferentes aplicaciones. Por ejemplo ahora es muy sencillo definir rutas hacia aplicaciones Sinatra:
Las nuevas funcionalidades de rutas en Rails ofrecen interesantes posibilidades: aunque hoy sólo hemos hecho un somero repaso, en un futuro volveremos a examinar ciertas partes con más detalle.
Hay más información sobre las rutas en Rails 3 en el blog de Yehuda Katz y en las RailsGuides.