Sinatra como servidor rest, tutorial¶
Hagamos un ejemplo simple de cómo implementar las operaciones CRUD con un servidor basado en Sinatra con la forma REST como API. No vamos aquí adentrarnos en discusiones teóricas acerca de si REST+CRUD es o no una buena idea, pragmáticamente hablando es una idea muy útil y funcional y que concentra los esfuerzos del programador en profundizar acerca de la programación Web (HTTP).
Entonces lo que queremo es (requerimientos):
- Definir un modelo/estructura de datos con la cual trabajar.
- Definir sus operaciones CRUD
- Mapear dichas operaciones a los verbos HTTP:
- POST - Create
- GET - Retrieve
- PUT - Update
- DELETE - Delete
Trabajemos con AR¶
Para detalles de cómo trabajar con Active Record sin Rails, mire en ActiveRecord sin Rails.
1require 'rubygems' if RUBY_VERSION < '1.9'
2require 'sinatra'
3require 'sqlite3'
4require 'active_record'
5Dir["models/*rb"].each { |f| require f }
6
7ActiveRecord::Base.establish_connection(
8 :adapter => "sqlite3",
9 :database => "db/db.sqlite3"
10)
11
12get '/xytlines/1' do
13 # Recuperemos de la tabla xytlines el primer registro
14 # y devolvamos algo para empezar
15 Xytline.first.x
16end
Corrida y primera salida¶
Lado servidor
$ ruby modelo-simple-con-ar.rb == Sinatra/1.2.6 has taken the stage on 4567 for development with backup from Mongrel 127.0.0.1 - - [14/Dec/2011 11:37:12] "GET /xytlines/1 HTTP/1.0" 200 19 0.0171 ...
Lado cliente
$ curl http://localhost:4567/xytlines/1 Xytline.first.x = 1
Recuperación del objeto¶
Primero debemos ajustar nuestras rutas para que se acomoden a nuestro esquema:
1get %r{/xytlines/([\d]+)} do
2 # Ahora con el regex arriba, podemos hacer la búsqueda en la BD
3 # y devolver el x del registro encontrado
4 "Xytline.find(#{id}).x = #{Xytline.find(id).x}"
5end
Con el regex especificado entonces sólo servimos las solicitudes GET que especifiquen algún número entero para la colección de Xytlines en nuestra base de datos, miremos nuestra nueva salida, reiniciando el lado servidor por supuesto:
$ curl http://localhost:4567/xytlines/1 Xytline.find(1).x = 1
¿Qué pasa si pedimos un registro inexistente?
Atrapemos el error de una forma simple, así:
1get %r{/xytlines/([\d]+)} do |id|
2 begin
3 "Xytline.find(#{id}).x = #{Xytline.find(id).x}"
4 rescue
5 "No existe Xytline(#{id})"
6 end
7end
Ejemplo de salida de registro inexistente:
$ curl http://localhost:4567/xytlines/2 No existe Xytline(2)
Eliminando un objeto¶
En resumen, ya sabemos obtener un objeto de la colección Xytlines, es decir hemos cubierto la R(etrieve) del CRUD (básicamente) con el GET, ahora toca ir al otro extremo de la ecuación, eliminemos un registro (ajustaremos los formatos de salida luego).
Veamos el código, se muestra solamente lo relevante.
1delete %r{/xytlines/([\d]+)} do |id|
2 begin
3 Xytline.destroy("#{id}")
4 "Xytline(#{id}) eliminado"
5 rescue
6 "No existe el Xytline(#{id})"
7 end
8end
$ curl -X DELETE http://localhost:4567/xytlines/2 Xytline(2) eliminado
$ curl -X DELETE http://localhost:4567/xytlines/2 No existe el Xytline(2)
Representación de datos¶
Hasta ahora hemos visto que es muy fácil obtener un objeto y eliminarlo, ahora toca lidiar con los dos aspectos más complicados, la creación de un objeto nuevo, y la actualización de dichos objetos en la base de datos. En ambos casos debemos pasar datos al lado servidor desde el cliente, entonces toca definir la representación de los datos del objeto para que estos puedan viajar desde el cliente hasta el servidor y en consecuencia se puedan entender.
De mis preferencias tengo JSON y YAML ya que con ellos se simplifica mucho el pase de datos entre ambos extremos, entonces vamos a querer representar nuestros objetos en formatos JSON y/o YAML y a usar los métodos HTTP POST y HTTP PUT para pasar dichos datos.
Primero recordemos como lidiar con el POST por ejemplo:
1post %r{/xytlines/([\d]+)} do |id|
2 # los datos vienen en el arreglo params
3 "Usted dijo '#{params[:mensaje]}'"
4end
$ curl -X POST -d "mensaje=soy_tu_amo" http://localhost:4567/xytlines/1 Usted dijo 'soy_tu_amo'
Bien, como lo que nos interesa es tener un servicio tipo RESTfull API, no nos vamos a complicar en la gestión de formulario Web/HTML ni por el estilo, sino a concentrarnos en representaciones JSON y YAML. Le será terriblemente fácil generar y manipular dichas representaciones con por ejemplo con curl, y desarrollar su servicio mucho más rápido.
Vamos a volver a visitar el método GET para primero que nada, obtener una representación JSON para poder visualizar su simplicidad.
Xytline es un modelo ActiveRecord que simplemente tiene cuatro valores enteros.
1class XytlinesMigration < ActiveRecord::Migration
2 def self.up
3 create_table :xytlines do |t|
4 t.integer :x
5 t.integer :y
6 t.integer :t
7 t.integer :q
8 end
9 end
10
11 def self.down
12 drop_table :xytlines
13 end
14end
Si el JSON o YAML correspondiente ha de representar este objeto, lo más simple que se me ocurre por ahora es representarlo como un Hash:
1Xtyline = { :x => x, :y=> y, :t => t , :q => q}
Veamos cómo nos ayuda Ruby YAML al respecto:
1>> require 'yaml'
2=> true
3>> xytline = { :x => 1, :y => 2 , :t => 3 , :q => 100 }
4=> {:x=>1, :t=>3, :y=>2, :q=>100}
5>> xytline.to_yaml
6=> "--- \n:x: 1\n:t: 3\n:y: 2\n:q: 100\n"
Lo importante de notar aquí es que esto es una representación simple en texto que nos permite comunicar los datos entre ambos extremos o serializar a un archivo y luego recuperar dichos datos y reconstruir el Hash. Hagamos entonces el ejemplo.
Creando un objeto enviando un YAML¶
$ curl -X POST -F "yaml=<hash.yml;type=text/yaml" http://localhost:4567/xytlines/new Se creado exitosamente un nuevo registro xytline(10)
donde:
$ cat hash.yml --- :x: 1 :t: 3 :y: 2 :q: 200
Esto entonces simula un formulario rellenado por el usuario y enviado al servidor como text/yaml, estamos digamos literalmente subiendo el archivo yaml al servidor. Veamos el código:
1# CREATE xytline/new
2require 'yaml'
3post %r{/xytlines/new} do
4 datos = YAML.load(params[:yaml])
5 begin
6 xytlines = Xytline.create([datos])
7 "Se creado exitosamente un nuevo registro xytline(#{xytlines.first.id})"
8 rescue
9 "No se ha podido crear un nuevo registro xytline"
10 end
11end
Salida en el servidor:
$ ruby modelo-simple-con-ar.rb == Sinatra/1.2.6 has taken the stage on 4567 for development with backup from Mongrel 127.0.0.1 - - [14/Dec/2011 18:20:47] "POST /xytlines/new HTTP/1.1" 200 52 0.1140 ...
Actualizando un objeto enviando un YAML¶
La técnica es idéntica excepción que se opera sobre un registro existente, es decir, el YAML debe representar un registro existente. Un nuestro caso simple el indice lo tomamos del mismo URL.
Lado cliente
$ curl -X PUT -F "yaml=<hash.yml;type=text/yaml" http://localhost:4567/xytlines/1 Se ha modificado exitosamente el registro xytline(1)
Lado servidor
$ ruby modelo-simple-con-ar.rb == Sinatra/1.2.6 has taken the stage on 4567 for development with backup from Mongrel 127.0.0.1 - - [14/Dec/2011 18:32:55] "PUT /xytlines/1 HTTP/1.1" 200 52 0.1076 ...
Código servidor
1# PUT xytlines/:id
2put %r{/xytlines/([\d]+)} do |id|
3 begin
4 xytline = Xytline.find("#{id}")
5 rescue
6 "No existe dicho xytline(#{id})"
7 end
8
9 cambios = YAML.load(params[:yaml])
10
11 begin
12 Xytline.update(id,cambios)
13 "Se ha modificado exitosamente el registro xytline(#{id})"
14 rescue
15 "No se ha podido actualizar el registro xytline(#{id})"
16 end
17end
Especificando el tipo¶
El uso de la opción -H nos permite agregar encabezados arbitrarios permitiendo así especificar el tipo de datos requeridos/aceptados por el cliente.
$ curl -H "Accept: text/yaml" http://localhost:4567/xytlines/7
Referencias: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14
Ahora, para lidiar con el tipo y escoger el tipo de salida, podemos usar el objeto request:
1# RETRIEVE xytline/:id
2get %r{/xytlines/([\d]+)} do |id|
3 # request.accept es Array
4 begin
5 if request.accept.include?("text/yaml")
6 result = Xytline.find("#{id}").to_yaml
7 else
8 # No es realmente correcto, deberíamos responder
9 # que no manejamos ese tipo de datos
10 raise Sinatra::NotFound
11 end
12
13 rescue
14 raise Sinatra::NotFound
15 end
16
17 result
18end
Depurando tipos¶
Volviendo sobre una versión algo más completa del GET:
1# RETRIEVE xytline/:id
2get %r{/xytlines/([\d]+)} do |id|
3 # request.accept es Array
4 request.accept.each {|rh| puts rh}
5 begin
6 case
7 when request.accept.include?("text/yaml")
8 # Salida yaml
9 result = Xytline.find("#{id}").to_yaml
10 when request.accept.include?("text/html")
11 # Salida HTML
12 result = Xytline.find("#{id}").to_s
13 when request.accept.include?("text/plain")
14 # Datos del yaml pero enviando como texto plano
15 result = Xytline.find("#{id}").to_yaml
16 else
17 # Solicitud de tipo de contenido no aceptable
18 status 406
19 end
20 rescue
21 # No encuentro dicho registro
22 status 404
23 end
24
25 result
26end
Depuremos con curl (agregando -v):
$ curl -v -H "Accept: text/yaml" http://localhost:4567/xytlines/3
* About to connect() to localhost port 4567 (#0)
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /xytlines/3 HTTP/1.1
> User-Agent: curl/7.21.7 (x86_64-redhat-linux-gnu) libcurl/7.21.7 NSS/3.12.10.0 zlib/1.2.5 libidn/1.22 libssh2/1.2.7
> Host: localhost:4567
> Accept: text/yaml
>
< HTTP/1.1 200 OK
< Connection: close
< Date: Sat, 17 Dec 2011 15:43:41 GMT
< Content-Type: text/html;charset=utf-8
< Content-Length: 99
<
--- !ruby/object:Xytline
attributes:
x: 1
y: 2
id: 3
q: 100
t: 3
attributes_cache: {}
* Closing connection #0
Note por favor que la respuesta NO concuerda con lo solicitado por el cliente, text/yaml versus text/html. Para corregir, debemos configurar y definir:
1configure do
2 mime_type :yaml, 'text/yaml'
3 mime_type :plain, 'text/yaml'
4end
5
6# RETRIEVE xytline/:id
7get %r{/xytlines/([\d]+)} do |id|
8 # request.accept es Array
9 request.accept.each {|rh| puts rh}
10 begin
11 case
12 when request.accept.include?("text/yaml")
13 # Salida yaml
14 content_type :yaml
15 result = Xytline.find("#{id}").to_yaml
16 when request.accept.include?("text/html")
17 # Salida HTML
18 result = Xytline.find("#{id}").to_s
19 when request.accept.include?("text/plain")
20 # Datos del yaml pero enviando como texto plano
21 content_type :plain
22 result = Xytline.find("#{id}").to_yaml
23 else
24 # Solicitud de tipo de contenido no aceptable
25 status 406
26 end
27 rescue
28 # No encuentro dicho registro
29 status 404
30 end
31
32 result
33end
Confirmemos ahora que la salida es adecuada:
$ curl -v -H "Accept: text/yaml" http://localhost:4567/xytlines/3
* About to connect() to localhost port 4567 (#0)
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /xytlines/3 HTTP/1.1
> User-Agent: curl/7.21.7 (x86_64-redhat-linux-gnu) libcurl/7.21.7 NSS/3.12.10.0 zlib/1.2.5 libidn/1.22 libssh2/1.2.7
> Host: localhost:4567
> Accept: text/yaml
>
< HTTP/1.1 200 OK
< Connection: close
< Date: Sat, 17 Dec 2011 15:55:50 GMT
< Content-Type: text/yaml;charset=utf-8
< Content-Length: 99
<
--- !ruby/object:Xytline
attributes:
x: 1
y: 2
id: 3
q: 100
t: 3
attributes_cache: {}
* Closing connection #0