REST: Haciendo posible URLs como /bugs/by_state/open

Hola a todos,

Tengo una aplicación REST donde quiero consultar un recurso filtrado por
cada uno de sus diferentes atributos.
Así que puedo tener las siguientes direcciones:

  • Lista de bugs: /bugs
  • El bug con id 1, y así sucesivamente: /bugs/1
  • Lista de valores de “loquesea”, donde “loquesea” puede ser cualquier
    atributo del bug, como “state”: /bugs/by_loquesea
  • Lista de bugs donde el atributo “loquesea” tiene el valor “valor”:
    /bugs/by_loquesea/valor

Las dos primeras direcciones las tengo funcionando (fácil). ¿Qué hago
con las otras? No sé qué poner en routes.rb ni qué hacer con mi
BugsController. Aquí algunas cosas que he pensado:

  • No puedo usar recursos anidados( si lo anido debería tener siempre al
    menos el id de un bug, así: /bugs/{id}/).
  • Empecé añadiendo un método llamado “by_state” en mi BugsController,
    siendo “state” uno de los atributos de “bug”, pero me da error al
    acceder a /bugs/by_state: “Couldn’t find Bug with ID=by_state”. Obvio.
  • Quizá debería añadir un controlador nuevo. Algo como
    BugsByController, que me permitiría crear direcciones como
    /bugs_by/{loquesea}.
    Pero de nuevo no sé cómo gestionar después la parte de la dirección de
    {valor}.
  • Para resolver esto podría poner un controlador por cada atributo, como
    BugsByStateController, BugsBy…, pero suena fatal.
  • Con “collections” tampoco podría. Puedo tener
    map.resources :bugs, :collection => { :by_state=> :get, by_dir=> :get,
    by_etc… }
    Obteniendo así las direcciones /bugs/by_state, /bugs/by_dir, etc… Pero
    de nuevo no sé dónde poner después el {valor} del atributo.

Y la otra posibilidad es hacerlo con map.connect y apañar algo como
/bugs_by/:att/:value, pero no sé yo…

Cualquier sugerencia será bien recibida.

On Jan 8, 2008, at 1:13 PM, Damaris F. wrote:

Tengo una aplicación REST donde quiero consultar un recurso filtrado
por
cada uno de sus diferentes atributos.
Así que puedo tener las siguientes direcciones:

  • Lista de bugs: /bugs
  • El bug con id 1, y así sucesivamente: /bugs/1
  • Lista de valores de “loquesea”, donde “loquesea” puede ser cualquier
    atributo del bug, como “state”: /bugs/by_loquesea
  • Lista de bugs donde el atributo “loquesea” tiene el valor “valor”:
    /bugs/by_loquesea/valor

Hola Damaris, para ese tipo de cosas Rails ofrece :collection
y :member, podriamos poner por ejemplo:

map.resources :bugs, :collection => {:by_loquesea => :get}

Si pones eso Rails enruta /bugs/by_loquesea/{id} directamente a la
accion BugsController#by_loquesea. Si viene el valor (que es opcional)
lo tendras en params[:id].

Por como planteas tu mail estoy seguro de que ya sabes que desde el
punto de vista REST esto esta bien, ya que publicas recursos
“logicos”, una sublista de bugs por cada posible valor, y los haces
accesibles por GET.

– fxn

Pero…tengo que hacerlo por cada atributo?

Es decir, tendría que tener algo así?:
map.resources :bugs, :collection => {:by_state => :get, by_otracosa =>
get, etc…}

Es que no sé hasta qué punto eso sería una buena solución, porque además
las tablas con las que yo trabajo tienen muchos atributos. Bien es
cierto que hay varios que tengo que darles un trato especial, así que
quizá tenerlos por separado no estaría mal, pero aún así…la mayoría
siguen un mismo patrón y tenía pensado tener un mismo método y los
atributos distintos descubrirlos con un if…

Una solución para generalizar sería así:
map.resources :bugs, :collection => {:by_attribute => :get}
Y que en params[:id] estuviera el atributo… pero entonces el valor…?

Muchísimas gracias.

Xavier N. wrote:

On Jan 8, 2008, at 1:13 PM, Damaris F. wrote:

Tengo una aplicaci�n REST donde quiero consultar un recurso filtrado
por
cada uno de sus diferentes atributos.
As� que puedo tener las siguientes direcciones:

  • Lista de bugs: /bugs
  • El bug con id 1, y as� sucesivamente: /bugs/1
  • Lista de valores de “loquesea”, donde “loquesea” puede ser cualquier
    atributo del bug, como “state”: /bugs/by_loquesea
  • Lista de bugs donde el atributo “loquesea” tiene el valor “valor”:
    /bugs/by_loquesea/valor

Hola Damaris, para ese tipo de cosas Rails ofrece :collection
y :member, podriamos poner por ejemplo:

map.resources :bugs, :collection => {:by_loquesea => :get}

Si pones eso Rails enruta /bugs/by_loquesea/{id} directamente a la
accion BugsController#by_loquesea. Si viene el valor (que es opcional)
lo tendras en params[:id].

Por como planteas tu mail estoy seguro de que ya sabes que desde el
punto de vista REST esto esta bien, ya que publicas recursos
“logicos”, una sublista de bugs por cada posible valor, y los haces
accesibles por GET.

– fxn

On Jan 8, 2008, at 2:01 PM, Damaris F. wrote:

atributos distintos descubrirlos con un if…
En tal caso puede que te sea mas adecuada una named route (es
compatible con las de map.resources):

map.bugs_by ‘bugs/by/:key/:value’, :controller => ‘bugs’, :action
=> ‘by’

con ello puedes hacer esto:

bugs_by_path(“loquesea”, “valor”)
=> “/bugs/by/loquesea/valor”

y trabajar con llaves de params :key y :value que tienen mas sentido.

– fxn

Una pregunta de última hora…
Esta solución de “named routes” me permite filtrar los bugs por atributo
pero dado un usuario?

Es decir, no sólo voy a obtener los bugs enteros de la aplicación (y
luego poder filtrarlos), sino que es posible que primero se busque un
usuario, luego sus bugs, y luego filtrarlos.

Damaris F. wrote:

Um, vale, es parecido a una de las soluciones que daba al principio con
map.connect bugs/:loquesea/:valor,

pero tu solución queda mejor (además, la otra no sería tampoco muy
factible sin el “by” delante), así que me la quedo :smiley:

Muchísimas gracias.

Xavier N. wrote:

On Jan 8, 2008, at 2:01 PM, Damaris F. wrote:

atributos distintos descubrirlos con un if…
En tal caso puede que te sea mas adecuada una named route (es
compatible con las de map.resources):

map.bugs_by ‘bugs/by/:key/:value’, :controller => ‘bugs’, :action
=> ‘by’

con ello puedes hacer esto:

bugs_by_path(“loquesea”, “valor”)
=> “/bugs/by/loquesea/valor”

y trabajar con llaves de params :key y :value que tienen mas sentido.

– fxn

Um, vale, es parecido a una de las soluciones que daba al principio con
map.connect bugs/:loquesea/:valor,

pero tu solución queda mejor (además, la otra no sería tampoco muy
factible sin el “by” delante), así que me la quedo :smiley:

Muchísimas gracias.

Xavier N. wrote:

On Jan 8, 2008, at 2:01 PM, Damaris F. wrote:

atributos distintos descubrirlos con un if…
En tal caso puede que te sea mas adecuada una named route (es
compatible con las de map.resources):

map.bugs_by ‘bugs/by/:key/:value’, :controller => ‘bugs’, :action
=> ‘by’

con ello puedes hacer esto:

bugs_by_path(“loquesea”, “valor”)
=> “/bugs/by/loquesea/valor”

y trabajar con llaves de params :key y :value que tienen mas sentido.

– fxn

Vaya! No sabía que podían anidarse los recursos y las named routes.

Muchas gracias!!

On Jan 8, 2008, at 5:26 PM, Damaris F. wrote:

Una pregunta de última hora…
Esta solución de “named routes” me permite filtrar los bugs por
atributo
pero dado un usuario?

Es decir, no sólo voy a obtener los bugs enteros de la aplicación (y
luego poder filtrarlos), sino que es posible que primero se busque un
usuario, luego sus bugs, y luego filtrarlos.

Creo que seguiriamos jugando con las mismas ideas. En este caso suena
a recurso anidado.

Las named routes aqui aportan que el codigo que genera enlaces queda
uniforme. Como bien decias es una solucion analoga al map.connect,
solo sucede que en una aplicacion RESTful ya rararamente
usas :controller, :action en link_to. Si te salen mal asunto, hay que
hacer el ejercicio que tu estas haciendo muy bien de modelar los
recursos y machambrar eso con sus rutas, ya que una cosa suele ir de
la mano de la otra.

Uno en RESTful Rails escribe idealmente solo cosas del estilo:

<%= link_to h(@user.name), user_path(@user) %>

por lo que si alguna ruta ad-hoc la vistes de ese modo queda majo:

<%= link_to ..., user_bugs_by_path(@user, type, value) %>

Para este caso podria servir entonces una nested named route:

map.resources :users do |users|
  users.bugs_by 'bugs/by/:key/:value',
                :controller => 'bugs',
                :action => 'by'
end

Por ejemplo:

>> user_bugs_by_path(@user, "key", "value")
=> "/users/1-fernandez-hermanos-sl/bugs/by/key/value"

En el controlador tendras llaves :user_id, :key, y :value en params
respectivamente cuando se haga esa peticion.

Fijate en el ejemplo que incluso funciona con un User#to_param propio
sin mas ni mas.

– fxn

Hola de nuevo, implementando lo que hablamos en este hilo me ha surgido
un problema.

En “routes.rb” tengo esto por ahora:
map.bugs_by “bugs/by/:att/:value”,
:controller => “bugs”, :action => “by”, :att => nil, :value =>nil
map.resources :bugs

Así que mi acción “by” hará lo siguiente:

  • Sin parámetros: Lista los posibles atributos de bugs.
  • Con el parámetro att: Lista los posibles valores del atributo att.
  • Con att y value: Lista los bugs cuyo atributo coincida con ese valor.

Implementado tengo lo siguiente:
def by
if params[:value] #params att and value

elsif params[:att]

else #No params
respond_to do |format|
format.html {render(:template=> “bugs/by_index”)}
format.xml {render(:xml => “bugs/by_index”)}
end

Pues bien, al acceder a “/bugs/by” efectivamente me coloca la plantilla
by_index.html.

Pero al acceder a /bugs/by.xml me dice que “Couldn’t find Bug with
ID=by”, y es que no llama a la acción “by”, sino a la de “show”.
Es como si la historia de los formatos con respond_to sólo valiera para
aquellas rutas definidas con map.resources.

Hola, (antes de seguir, darte las gracias por tu tiempo),

Si añado pues:
map.connect “sdrs/by.:format”, :controller => “sdrs”, :action => “by”

ya no me da error, pero sí al intentar generar el xml.
Lo estoy llamando así:
else #No params
respond_to do |format|
format.html {render(:template=> “bugs/by_index”)}
format.xml {render(:xml => “bugs/by_index”)}
end
Pero no le gusta.

¿Conocéis otra forma de mostrar una plantilla RXML concreta? (pensaba
ponerlo en otro post). He mirado en los foros y no he encontrado algo
parecido. No lo puedo dejar por defecto (es decir, que cogiera un
posible by.rxml) porque la acción “by” tiene una cláusula “if” que
generará una respuesta u otra dependiendo de los parámetros.

Xavier N. wrote:

On Jan 9, 2008, at 2:46 PM, Damaris F. wrote:

  • Sin par�metros: Lista los posibles atributos de bugs.
    else #No params
    Pero al acceder a /bugs/by.xml me dice que “Couldn’t find Bug with
    ID=by”, y es que no llama a la acci�n “by”, sino a la de “show”.
    Es como si la historia de los formatos con respond_to s�lo valiera
    para
    aquellas rutas definidas con map.resources.

Lo que ocurre en realidad es que map.resources genera dos conjuntos de
rutas:

* el pack sin formato: /users/:id and friends

* el pack paralelo con formato /users/:id.:format and friends

Fijate en las rutas ultimas de las default:

 # Install the default route as the lowest priority.
 map.connect ':controller/:action/:id.:format'
 map.connect ':controller/:action/:id'

map.resources genera algo analogo, por eso funciona .xml. Supongo que
conoces rake routes, pero si no fuera el caso echale un vistazo.

Ahi se ve bien que :format es un parametro que se pone explicito, y
tiene un tratamiento interno especial (como :controller y :action) que
lo enlaza a respond_to.

– fxn

On Jan 9, 2008, at 2:46 PM, Damaris F. wrote:

  • Sin parámetros: Lista los posibles atributos de bugs.
    else #No params
    Pero al acceder a /bugs/by.xml me dice que “Couldn’t find Bug with
    ID=by”, y es que no llama a la acción “by”, sino a la de “show”.
    Es como si la historia de los formatos con respond_to sólo valiera
    para
    aquellas rutas definidas con map.resources.

Lo que ocurre en realidad es que map.resources genera dos conjuntos de
rutas:

* el pack sin formato: /users/:id and friends

* el pack paralelo con formato /users/:id.:format and friends

Fijate en las rutas ultimas de las default:

 # Install the default route as the lowest priority.
 map.connect ':controller/:action/:id.:format'
 map.connect ':controller/:action/:id'

map.resources genera algo analogo, por eso funciona .xml. Supongo que
conoces rake routes, pero si no fuera el caso echale un vistazo.

Ahi se ve bien que :format es un parametro que se pone explicito, y
tiene un tratamiento interno especial (como :controller y :action) que
lo enlaza a respond_to.

– fxn

No, ya lo probé; con :template te añade al final el .rhtml y por tanto
nunca lo encuentra.

Xavier N. wrote:

On Jan 9, 2008, at 5:17 PM, Damaris F. wrote:

end
Pero no le gusta.

:xml quiere decir que le pasas una cadena con XML o algo que responde
a #to_xml (esto segundo desde Rails 2).

No lo he probado pero creo que en ese caso has de usar
render :template de nuevo, usando seguramente el nombre de archivo
completo incluyendo extension (eg, “bugs/by_index.xml.builder”). No
recuerdo de memoria si ademas has de poner el tipo MIME a mano
(response.content_type = Mime::XML).

– fxn

On Jan 9, 2008, at 5:17 PM, Damaris F. wrote:

end
Pero no le gusta.

:xml quiere decir que le pasas una cadena con XML o algo que responde
a #to_xml (esto segundo desde Rails 2).

No lo he probado pero creo que en ese caso has de usar
render :template de nuevo, usando seguramente el nombre de archivo
completo incluyendo extension (eg, “bugs/by_index.xml.builder”). No
recuerdo de memoria si ademas has de poner el tipo MIME a mano
(response.content_type = Mime::XML).

– fxn

On Jan 9, 2008, at 6:23 PM, Damaris F. wrote:

No, ya lo probé; con :template te añade al final el .rhtml y por tanto
nunca lo encuentra.

Hmmm… aqui funciona con Rails 2 (y el content type esta bien por
defecto):

class TestController < ApplicationController
def index
respond_to do |wants|
wants.html { render :template => ‘test/tmpl.rhtml’ }
wants.xml { render :template => ‘test/tmpl.rxml’ }
end
end
end

test/foo.rhtml

HTML

test/foo.rxml

xml.instruct!

fxn@feynman:~$ curl http://localhost:3000/test
HTML
fxn@feynman:~$ curl -H ‘Accept: text/xml’ http://localhost:3000/test

<?xml version="1.0" encoding="UTF-8"?>

Ves algo distinto?

– fxn

Um…
pues no sé lo que tenía yo de antes (porque lo había borrado), pero he
puesto tal y como lo tienes aquí y sí me funciona ya. No sé, es posible
que estuviera poniendo la extensión .xml en vez de rxml…

Gracias de nuevo.

Xavier N. wrote:

On Jan 9, 2008, at 6:23 PM, Damaris F. wrote:

No, ya lo prob�; con :template te a�ade al final el .rhtml y por tanto
nunca lo encuentra.

Hmmm… aqui funciona con Rails 2 (y el content type esta bien por
defecto):

class TestController < ApplicationController
def index
respond_to do |wants|
wants.html { render :template => ‘test/tmpl.rhtml’ }
wants.xml { render :template => ‘test/tmpl.rxml’ }
end
end
end

test/foo.rhtml

HTML

test/foo.rxml

xml.instruct!

fxn@feynman:~$ curl http://localhost:3000/test
HTML
fxn@feynman:~$ curl -H ‘Accept: text/xml’ http://localhost:3000/test

<?xml version="1.0" encoding="UTF-8"?>

Ves algo distinto?

– fxn

Hola, estoy implementado esto

Esta soluci�n de “named routes” me permite filtrar los bugs por
atributo
pero dado un usuario?

Uno en RESTful Rails escribe idealmente solo cosas del estilo:

<%= link_to h(@user.name), user_path(@user) %>

por lo que si alguna ruta ad-hoc la vistes de ese modo queda majo:

<%= link_to ..., user_bugs_by_path(@user, type, value) %>

Para este caso podria servir entonces una nested named route:

map.resources :users do |users|
  users.bugs_by 'bugs/by/:key/:value',
                :controller => 'bugs',
                :action => 'by'
end

Así que en mi routes.rb tengo:
map.bugs_by “bugs/by/:att/:value”,
:controller => “bugs”, :action => “by”, :att => nil, :value
=>nil

map.resources :clients do |clients|
clients.resources :bugs, :name_prefix=>“client_”
clients.client_bugs_by “bugs/by/:att/:value”,
:controller => “bugs”, :action => “by”, :att => nil, :value
=>nil
end

Sin embargo, cuando yo tengo un enlace del tipo:
<%= link_to ‘Filtrar’, client_bugs_by_path(@client) %>
Se va a la dirección:
http://localhost:3006/sdrs/by/19159

Si ejecuto “rake routes” tengo:
client_sdrs_by /sdrs/by/:att/:value
{:path_prefix=>“/clients/:client_id”, :action=>“by”,
:new_name_prefix=>“client_”, :controller=>“sdrs”}

Así que no sé muy bien cómo apañarlo para que me coja las cosas
correctamente.