Tutorial de Rocket, echa a volar tus webapps con Rust

Previamente ya hemos hablado de Iron como un web framework para Rust. Sin embargo desde que escribí ese post ha surgido otra librería que ha ganado mucha popularidad en poco tiempo. Se trata de Rocket. Un web framework que propone usar el rendimiento que ofrece Rust sin sacrificar la facilidad de uso de otros lenguajes.

Rocket lleva las pilas cargadas

A diferencia de Iron, Rocket incluye bastantes prestaciones por defecto con soporte para:

  • Plantillas
  • Cookies
  • Formularios
  • JSON
  • Soporte para rutas dinámicas

Un “Hola Mundo”

Rocket necesita una versión nightly del compilador de Rust. Una vez lo tengas creamos una aplicación con Cargo.

cargo new --bin rocket_app

Ahora modificamos el fichero Cargo.toml generado en la carpeta rocket_app para añadir las siguientes dependencias:

rocket = "0.2.4"
rocket_codegen = "0.2.4"
rocket_contrib = "*"

Editamos el archivo src/main.rs para que se parezca algo a esto:

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "El cohete ha despegado"
}

fn main() {
    rocket::ignite().mount("/",routes![index]).launch();
}

Con esto iniciamos Rocket y dejamos a la función index que gestione las peticiones GET encaminadas a /. El servidor devolverá El cohete ha despegado.

Ahora si ejecutamos cargo run veremos algo similar a esto:

Vemos que el servidor ya está escuchando en el puerto 8000 y está usando todos los cores (en mi caso 4) del ordenador.

Configurar Rocket

Rocket dispone de varias configuraciones predeterminadas que afectan a su funcionamiento. Para alternar entre las configuraciones debemos usar variables de entorno y para modificar las configuraciones en sí debemos usar un fichero llamado Rocket.toml.

Las configuraciones por defecto son: dev (development), stage (staging) y prod (production). Si no indicamos nada, Rocket se inicia con la configuración dev. Para arrancar con la configuración de producción modificamos el valor de ROCKET_ENV.

ROCKET_ENV=prod cargo run --release

Sería el comando para arrancar Rocket en modo producción. En el archivo Rocket.toml se puede modificar cada configuración, estableciendo el puerto, el número de workers y parámetros extra pero no vamos a entrar en ello.

Rutas dinámicas

Rocket soporta rutas dinámicas. Por ejemplo, si hacemos GET  /pelicula/Intocable podemos definir que la parte del nombre de la película sea un parámetro. Esto hará que la función encargada de /pelicula/Intocable y de /pelicula/Ratatouille sea la misma.

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/pelicula/<pelicula>")]
fn pelicula(pelicula: &str) -> String {
    format!("Veo que te gusta {}, a mi también!",pelicula)
}

#[get("/")]
fn index() -> &'static str {
    "El cohete ha despegado"
}

fn main() {
    rocket::ignite().mount("/",routes![index,pelicula]).launch();
}

Los argumentos de la función son los parámetros de la petición GET. ¿Qué pasa si no concuerda el tipo de la función con lo que se pasa por HTTP? Nada. Sencillamente Rocket ignora esa petición, busca otra ruta (puede haber sobrecarga de rutas) y si encuentra otra que si satisfaga los parámetros será esa la escogida. Para especificar el orden en el que se hace la sobrecarga de rutas puede usarse rank. En caso de no encontrarse nada, se devuelve un error 404.

POST, subir JSON y formularios

Rocket se integra con Serde para lograr una serialización/deserialización con JSON inocua. Si añadimos las dependencias serde, serde_json y serde_derive al fichero Cargo.toml podemos tener un método que acepete una petición POST solo para mensajes del tipo application/json con deserialización incorporada.

#![feature(plugin)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
extern crate rocket;

use rocket_contrib::{JSON, Value};

#[derive(Serialize,Deserialize)]
struct User{
    name: String,
    email: String
}

#[post("/upload", format="application/json", data="<user>")]
fn upload_user(user: JSON<User>) -> JSON<Value> {
    JSON(json!({
        "status" : 200,
        "message" : format!("Usuario {} registrado con éxito",user.email)
    }))
}

fn main() {
    rocket::ignite().mount("/",routes![upload_user]).launch();
}

Si el JSON no se ajusta a la estructura User simplemente se descarta devolviendo un error 400.

Lo mismo que es posible hacer con JSON puede hacerse con formularios usando el trait FromForm.


#![feature(plugin,custom_derive)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
extern crate rocket;

use rocket_contrib::{JSON, Value};
use rocket::request::{FromForm, Form};

#[derive(FromForm)]
struct User{
    name: String,
    email: String
}

#[post("/upload", data="<user>")]
fn upload_user(user: Form<User>) -> String {
    format!("Hola {}",user.get().name)
}

fn main() {
    rocket::ignite().mount("/",routes![upload_user]).launch();
}

Errores

En Rocket, como es lógico, es posible crear páginas personalizadas para cada error.

#![feature(plugin,custom_derive)]
#![plugin(rocket_codegen)]

#[get("/")]
fn index() -> &'static str {
    "El cohete ha despegado"
}

#[error(404)]
fn not_found() -> &'static str {
    "La página no ha podido ser encontrada"
}

fn main() {
    rocket::ignite().mount("/",routes![index]).catch(errors![not_found]).launch();
}

La lista de métodos que manejan errores hay que pasarla en el método catch de rocket::ignite

Respuestas

Rocket nos permite devolver cualquier cosa que implemente el trait Responder. Algunos tipos ya lo llevan como String, File, JSON, Option y Result. Pero nada nos impide que nuestros propios tipos implementen Responder. Con Responder tenemos el contenido y el código de error (que en la mayoría de casos será 200). En el caso de Result es muy interesante, pues si Err contiene algo que implementa Responder, se devolverá la salida que implemente también, pudiendo así hacer mejores respuestas de error, mientras que si no lo hacen se llamará al método que implemente el error 500 de forma genérica. Con Option, si el valor es Some se devolverá el contenido, si es None se generará un error 404.

#![feature(plugin,custom_derive)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate rocket_contrib;
extern crate rocket;

use rocket::response::{self, Responder, Response};
use std::io::Cursor;
use rocket::http::ContentType;

struct Pelicula{
    nombre: &'static str,
    pais: &'static str
}

impl<'r> Responder<'r> for Pelicula{
    fn respond(self) -> response::Result<'r> {
        Response::build()
        .sized_body(Cursor::new(format!("La película {} se hizo en {}",self.nombre,self.pais)))
        .header(ContentType::new("text","plain"))
        .ok()
    }
}

#[get("/pelicula/<pelicula>")]
fn pelicula(pelicula: &str) -> Result<Pelicula,String> {
    let intocable = Pelicula{
        nombre: "Intocable",
        pais: "Francia"
    };
    let madMax = Pelicula{
        nombre: "Mad Max",
        pais: "Estados Unidos"
    };
    match pelicula {
        "Intocable" => Ok(intocable),
        "Mad Max" => Ok(madMax),
        _ => Err(format!("No existe esa película en nuestra base de datos"))
    }
}

#[get("/")]
fn index() -> Result<String,String> {
    Err(format!("No implementado"))
}

#[error(404)]
fn not_found() -> &'static str {
    "La página no ha podido ser encontrada"
}

fn main() {
    rocket::ignite().mount("/",routes![index,pelicula]).catch(errors![not_found]).launch();
}

Este ejemplo para /pelicula/Intocable devolverá: La película Intocable se hizo en Francia mientras que para /pelicula/Ratatouille dirá No existe esa película en nuestra base de datos.

También es posible devolver plantillas. Rocket se integra por defecto con Handlebars y Tera, aunque no es muy costoso añadir cualquier otra como Maud.

Conclusión

Rocket es un prometedor web framework para Rust, bastante idiomático, que se integra muy bien con el lenguaje. Espero con ansia las nuevas veriones. Es posible que la API cambie bastante hasta que salga la versión 1.0, no obstante así es como ahora mismo funciona.

loading...

Usando Iron, un web framework para Rust

Rust cada día atrae a más desarrolladores. Es eficiente y es robusto. Mozilla ha sido la principal impulsora de este lenguaje para ser usado en entornos tan complejos como el propio Firefox.

Hoy vamos a introducirnos en el mundo del desarrollo web con Rust. Cuando la gente oye desarrollo web normalmente se piensa en lenguajes como PHP, Python, Ruby o JavaScript. Estos son lenguajes con los que es rápido desarrollar algo, aunque son mucho menos eficientes y es más fácil cometer errores debido a que son interpretados directamente. Un paso por encima tenemos a Java y C#, que cubren en parte las carencias de los otros lenguajes mencionados. Ha llegado la hora de hablar de Rust. Si bien es cierto que hay web frameworks en C++, nunca han sido muy populares. ¿Será Rust la opción que finalmente nos permita tener aplicaciones web con una eficiencia nativa?

Existen varios web frameworks en Rust, para este tutorial vamos a usar Iron, el más popular según Crates.io.

ironframework

Crear proyecto e instalar Iron

Lo primero que hay que hacer es crear un nuevo proyecto en Rust, lo hacemos gracias a Cargo.

cargo new --bin MundoRust

cd MundoRust

Ahora editamos el fichero Cargo.toml para añadir las dependencias que vamos a usar.

[package]
name = "MundoRust"
version = "0.1.0"
authors = ["Adrián Arroyo Calle"]

[dependencies]
iron = "0.4.0"
router = "0.4.0"
staticfile = "0.3.1"
mount = "0.2.1"

Ahora obtenemos las dependencias especificadas con Cargo.

cargo run

Hola Mundo con Iron

Vamos a empezar a programar en Rust. Vamos a hacer una simple aplicación que devuelva “Hola Rustáceos” por HTTP.

Editamos el archivo src/main.rs

extern crate iron;

use iron::prelude::*;

fn hola(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Hola Rustaceos")))
}

fn main() {
    Iron::new(hola).http("0.0.0.0:80").unwrap();
}

holarustaceos

Usando router para el enrutamiento

Hemos hecho un pequeño servidor HTTP con Iron. Pero nos falta algo, que sea capaz de manejar rutas. Que miweb.com/hola no sea lo mismo que miweb.com/adios. Iron por defecto no trae enrutador, pero es muy habitual usar Router, que ya hemos instalado antes por conveniencia.

extern crate iron;
extern crate router;

use iron::prelude::*;
use router::Router;

fn get_page(r: &mut Request) -> IronResult<Response>{
    let path = r.url.path();
    Ok(Response::with((iron::status::Ok, format!("Hola, peticion GET {}",path[0]))))
}

fn submit(_: &mut Request) -> IronResult<Response>{
    Ok(Response::with((iron::status::Ok, "Peticion POST")))
}

fn main() {
    let mut router = Router::new();

    router.get("/:page", get_page, "page");
    router.post("/submit", submit, "subdmit");

    Iron::new(router).http("0.0.0.0:80").unwrap();

}

getiron

Archivos estáticos

Para gestionar los ficheros estáticos vamos a usar staticfile y mount, otras dos librerías para Iron.

extern crate iron;
extern crate router;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use router::Router;
use staticfile::Static;
use mount::Mount;

fn get_page(r: &mut Request) -> IronResult<Response>{
    let path = r.url.path();
    Ok(Response::with((iron::status::Ok, format!("Hola, peticion GET {}",path[0]))))
}

fn main() {
    let mut router = Router::new();

    router.get("/:page", get_page, "page");

    let mut mount = Mount::new();
    mount.mount("/public",Static::new("static/"));
    mount.mount("/",router);

    Iron::new(mount).http("0.0.0.0:80").unwrap();
}

ironstaticfile

 

Hemos dado nuestros primeros pasos en Iron, un web framework para Rust. Iron es completamente modular y soporta muchas más cosas que aquí no hemos visto. Gran parte de su funcionalidad se implementa a través de middleware, como en otros web frameworks populares.

 

 

 

Crear credenciales OAuth2 para Google

Google dispone de muchas APIs a nuestra disposición de forma gratuita. Para poder usarla es necesario que hayamos iniciado sesión. Desde un navegador como Firefox o Chrome es muy sencillo, introducimos nuestro usuario y contraseña y ya podemos acceder a Gmail, YouTube, etc. Si queremos hacerlo desde nuestro propio programa también es posible, pero el procedimiento es distinto. Una vez hagamos esto podremos hacer lo mismo que haría un usuario, lo único que programándolo (leer los contactos, leer el correo, modificar suscripciones de YouTube, publicar en Blogger, etc).

Crea un proyecto de API

Crea un proyecto de Google y crea unos credenciales nuevos en la consola de APIs de Google.

screenshot-console developers google com 2016-07-26 14-52-43

Los credenciales pueden ser de varios tipos: clave de API, ID de OAuth y cuenta de servicio. La clave de API solo funciona en algunas API, el ID de OAuth es el sistema universal y la cuenta de servicio es para APIs de desarrollo. Creamos un ID de OAuth, pues es el método más universal.

screenshot-console developers google com 2016-07-26 16-29-16

Dependiendo de dónde opere nuestra aplicación tendremos un método u otro. El método Web requiere disponer de un servidor web y es el sistema óptimo si tenemos una aplicación Web (por ejemplo, tenemos una red social y queremos una opción de leer contactos de Gmail, usaríamos Web, pues es el procedimiento más cómodo). Si tu programa no tiene un servidor web detrás (es un programa normal de Windows, una herramienta de línea de comandos, un plugin de Firefox,…) usa Otro. Voy a explicar como funciona Otro.

screenshot-console developers google com 2016-07-26 16-30-31

Obtendremos un ID de cliente y un secreto de cliente. Guardamos esas dos claves, son muy importantes. Ahora vamos a activar las APIs. A la izquierda accedemos a Biblioteca. Buscamos la APIs que queramos usar y las activamos.

screenshot-console developers google com 2016-07-26 16-36-27

Obteniendo un access_token y un refresh_token

Para poder llamar a la API de Google es necesario tener un access_token. Los access_token caducan a las pocas horas. Para poder seguir realizando operaciones en la API pasado ese tiempo necesitamos un refresh_token. El refresh_token sirve exclusivamente para pedir un access_token nuevo. Este no caduca, pero no garantiza que siempre nos devuelva un access_token pues puede que el usuario haya decidido eliminar el permiso a nuestro programa.

El procedimiento de OAuth2 es el siguiente:

  1. Nuestra aplicación quiere autenticar al usuario en Google
  2. Nuestra aplicación genera una URL especial que debe abrir el usuario en el navegador
  3. En esa URL inicia sesión en Google el usuario, igual que si fuera a iniciar sesión en Gmail
  4. Se le informa al usuario de los permisos/APIs que nuestro programa quiere usar
  5. El usuario acepta o rechaza
  6. Si acepta ocurren varias cosas dependiendo del método. Si elegiste Otro se mostrará un código que deberás copiar en la aplicación.
  7. Ese código la aplicación lo envía a Google junto a nuestros datos de client ID y client secret.
  8. Google nos envía el access_token y el refresh_token.

Elaborar la URL de inicio de sesión

En la URL de inicio de sesión figuran varios datos.

  • El tipo de petición
  • Los permisos o APIs a las que queremos acceder (scopes)
  • Nuestro ID de cliente
  • Y la URL de redirección, que en nuestro caso, es especial.

En este ejemplo vemos una petición, con dos APIs o permisos, Chrome WebStore y Google Play.

https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore+https://www.googleapis.com/auth/androidpublisher&client_id=AQUI EL ID DE CLIENTE&redirect_uri=urn:ietf:wg:oauth:2.0:oob

Validar el código

El usuario nos dará el código que le haya dado Google. Nosotros ahora para obtener el access_token y el refresh_token tenemos que enviarlo de vuelta. En Curl el comando sería el siguiente:

curl "https://accounts.google.com/o/oauth2/tken" -d "client_id=AQUI EL ID DE CLIENTE&client_secret=AQUI EL CLIENT SECRET&code=ESTE ES EL CÓDIGO QUE NOS DA EL USUARIO&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob"

La respuesta, en un fichero JSON, contendrá el access_token y el refresh_token. En las librerías de APIs de Google muchas veces basta con especificar el client ID, el client secret y el refresh_token, pues se encargan de pedir automáticamente el access_token.