#322 RABL
- Download:
- source codeProject Files in Zip (100 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (12.2 MB)
- ogvFull Size Theora Video (23.6 MB)
Nuestras aplicaciones pueden devolver datos JSON de nuestros modelos de varias maneras. Se puede redefinir el método as_json
del modelo o utilizar la gema Jbuilder tal y como vimos en el episodio 320. Otra solución muy popular es la gema RABL. RABL significa Ruby API Builder Language y su funcionalidad algo más completa que el resto de soluciones.
En este episodio veremos cómo funciona RABL, utilizando la misma aplicación de ejemplo del episodio dedicado a Jbuilder, y así podremos ver mejor las diferencias entre ambas librerías. La aplicación es una aplicación de blog que tiene varios artículos y queremos ver los datos de cada artículo en formato JSON añadiendo .json
a la URL de dicho artículo. Si ahora mismo hacemos esto veremos un mensaje de error porque la aplicación no sabe cómo responder a peticiones JSON.
En marcha con RABL
Para hacer esto podríamos modificar ArticlesController
, asegurándonos de que la acción show
responde a las peticiones JSON y luego devolver una representación en JSON del artículo, pero en lugar de esto en este episodio vamos a usar RABL. Se trata de una gema que se instala de la forma habitual, añadiéndola al Gemfile
y ejecutando bundle
.
gem 'rabl'
De forma análoga a Jbuilder RABL incluye un gestor de plantillas por lo que podemos definir la respuesta en JSON en la capa de la vista. Creemos una plantilla para la representación en JSON de un artículo. Podemos hacerlo con código Ruby utilizando el DSL de RABL.
object @article attributes :id, :name, :published_at
Lo primero que aparece en una plantilla RABL por lo general es una llamada a object
, a la que se la pasa el objeto sobre el que queremos trabajar. Luego llamamos a attributes
para definir qué atributos queremos devolver. Ya podemos visitar la URL del artículo en JSON y podemos ver que se devuelve el JSON que hemos definido en la plantilla.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z"}}
Si se quieren especificar atributos más complejos podemos utilizar el método node
y pasarle un nombre y un bloque, de forma que se usará como valor lo que devuelva el bloque.
object @article attributes :id, :name, :published_at node(:edit_url) { "..." }
Con esto añadiremos un atributo llamado edit_url
al JSON.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"..."}}
En las plantillas de RABL tenemos acceso a los métodos helper por lo que podemos usar edit_article_url
para obtener la URl para editar un artículo y current_user
de forma que sólo mostremos esta URL si el usuario dispone de privilegios de administración (asumiendo que la solución de autenticación que usamos dispone de un método llamado current_user
)
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { edit_article_url(@article) } end
Pero esto tiene un problema, no es buena práctica usar la variable de instancia del artículo en edit_article_url
, deberíamos utilizar el objeto de artículo que se pasa al bloque.
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end
El objeto que se le pasa al bloque es el mismo objeto que se le pasa al método object
. Hay una razón importante que veremos más adelante por la que es mejor usar el objeto pasado al bloque. Si vemos el JSON del artículo veremos el atributo edit_url
con el valor correcto.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit"}}
Registros asociados
Es fácil incluir datos de los registros asociados, si queremos hacerlo. En nuestra aplicación un Article
pertenece a un Author
y tiene muchos Comments
. Podemos utilizar el método child
de RABL para incluir la información correspondiente a estos modelos asociados.
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end child :author do attributes :id, :name node(:url) { |author| author_url(author) } end
El código para generar el JSON del autor asociado es muy similar al código necesario para generar el JSON del artículo. La única diferencia es que al bloque del nodo se le pasa un autor en lugar de un artículo. Si ahora abrimos el JSON veremos, como es de esperar, los detalles del autor del artículo.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"}}}
Podemos hacer lo mismo con la asociación Comments
aunque como estamos tratando con una relación has_many
tenemos que usar el nombre en plural.
object @article attributes :id, :name, :published_at if current_user.admin? node(:edit_url) { |article| edit_article_url(article) } end child :author do attributes :id, :name node(:url) { |author| author_url(author) } end child :comments do attributes :id, :name, :content end
En la respuesta JSON los comentarios aparecerán anidados en un array, no como un registro sencillo como el autor.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}}
Reutilización de plantillas
Hemos adaptado los datos JSON que generamos a nuestro artículo pero, ¿y si queremos generar la misma salida en algún otro sitio de nuestra aplicación? Podríamos querer que la acción index
de ArticlesController
mostrase la misma salida en JSON pero para todos los artículos. Aquí es donde RABL resulta más brillante; lo único que tenemos que hacer es generar una plantilla RABL para la acción index
.
collection @articles extends "articles/show"
Al trabajar con múltiples artículos utilizaremos collection
en lugar de object
. Podríamos definir los atributos que queramos igual que en la plantilla anterior, pero como queremos generar el mismo JSON vamos a reutilizar la misma plantilla llamando a extends
y pasándole el nombre. Si ahora abrimos articles.json
veremos los datos en JSON de todos los artículos.
$ curl http://localhost:3000/articles.json [{"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}},{"article":{"id":2,"name":"Krypton","published_at":"2012-01-05T18:38:50Z","edit_url":"http://localhost:3000/articles/2/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"":[]}},{"article":{"id":3,"name":"Batman & Robin","published_at":"2012-01-26T18:38:50Z","edit_url":"http://localhost:3000/articles/3/edit","author":{"id":1,"name":"Bruce Wayne","url":"http://localhost:3000/authors/1"},"comments":[{"comment":{"id":3,"name":"The Joker","content":"Haha, Batman, you will see your bat signal tonight!"}},{"comment":{"id":4,"name":"Robin","content":"Enough with the games Joker."}},{"comment":{"id":5,"name":"Riddler","content":"Did someone say games?"}}]}}]
La posibilidad de reutilizar plantillas de esta manera es la razón por la que debemos minimizar el uso de variables de instancia en nuestras plantillas RABL. Como sólo usamos la variable @article
en la plantilla show
cuando definimos el objeto, resulta más fácil reutilizar esta plantilla porque en la llamada a node
estamos usando el artículo en lugar de la variable de instancia.
Nodos raíz
Puede comprobarse que el JSON generado por RABL incluye el nombre del modelo como nodo raíz.
$ curl http://localhost:3000/articles/1.json {"article":{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"comment":{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"}},{"comment":{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}}]}}
Esto a veces puede no interesar. En Rails 3.1 la salida por defecto en JSON no incluye el elemento raíz, y si queremos que RABL haga lo mismo tenemos que hacerlo por configuración. Podemos hacerlo en un nuevo fichero en el directorio config/initializers
, asignando a include_json_root
el valor false
.
Rabl.configure do |config| config.include_json_root = false end
Como se ha modificado un inicializador tendremos que reiniciar la aplicación para que estos cambios tengan efecto. Tras esto, veremos que el JSON del primer artículo ya no incluirá el nodo raíz.
$ curl http://localhost:3000/articles/1.json {"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"},{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}]}
Se dan más detalles sobre las opciones de configuración en la sección "Configuration" del README de RABL. Hay otras funcionalidades que no hemos visto, como por ejemplo la serialización a XML y Message Pack.
Cómo embeber JSON en una página web
A veces en lugar de llamar a una acción separada queremos incluir JSON directamente en un documento HTML. ¿Cómo podemos hacerlo con RABL? La plantilla HTML de la accion index
tiene el siguiente aspecto:
<h1>Articles</h1> <div id="articles"> <% @articles.each do |article| %> <h2> <%= link_to article.name, article %> <span class="comments">(<%= pluralize(article.comments.size, 'comment') %>)</span> </h2> <div class="info"> by <%= article.author.name %> on <%= article.published_at.strftime('%b %d, %Y') %> </div> <div class="content"><%= article.content %></div> <% end %> </div>
Añadiremos el JSON de los artículos en un atributo data-
del div
contenedor. Podríamos también invocar a @articles.to_json
pero no estaríamos usando la plantilla RABL. En vez de esto podemos llamar a render(:template)
y pasar el nombre de la plantilla.
<div id="articles" data-articles="<%= render(template: "articles/index.json.rabl") %>" >
Si recargamos la página y vemos el código fuente veremos el JSON embebido.
<div id="articles" data-articles="[{"id":1,"name":"Superman","published_at":"2012-01-19T18:38:50Z","edit_url":"http://localhost:3000/articles/1/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"comments":[{"id":1,"name":"Lois Lane","content":"Does anyone know where I can find Superman?"},{"id":2,"name":"Lex Luthor","content":"I have some Kryptonite for you Superman!"}]},{"id":2,"name":"Krypton","published_at":"2012-01-05T18:38:50Z","edit_url":"http://localhost:3000/articles/2/edit","author":{"id":2,"name":"Clark Kent","url":"http://localhost:3000/authors/2"},"":[]},{"id":3,"name":"Batman & Robin","published_at":"2012-01-26T18:38:50Z","edit_url":"http://localhost:3000/articles/3/edit","author":{"id":1,"name":"Bruce Wayne","url":"http://localhost:3000/authors/1"},"comments":[{"id":3,"name":"The Joker","content":"Haha, Batman, you will see your bat signal tonight!"},{"id":4,"name":"Robin","content":"Enough with the games Joker."},{"id":5,"name":"Riddler","content":"Did someone say games?"}]}]" >
Debemos tener en cuenta si utilizamos esta técnica que las variables de instancia que se utilicen en la plantilla deben ser establecidas desde la acción del controlador.