implementing a simple language switch using rocket in rust

Let's look how easy it is to implement a simple cookie based language switch in the rocket web framework for the rust programming language. Defining the language type:

#[derive(PartialEq)]
enum Lang {
    Nl,
    En,
}

In this case there will be support for Dutch and English. PartialEq is derived to be able to compare Lang items with ==.

The Default trait is implemented to define the default language:

impl Default for Lang {
    fn default() -> Lang {
        Lang::Nl
    }
}

The FromStr trait is implemented to allow creating a Lang item from a string.

impl FromStr for Lang {
    type Err = Error;
    fn from_str(s:&str) -> Result<Self> {
        match s {
            "nl" => Ok(Lang::Nl),
            "en" => Ok(Lang::En),
            o => Err(format!("Unsupported language: {}", o).into()),
        }
    }
}

The Into<&'static str> trait is added to allow the conversion in the other direction.


impl Into<&'static str> for Lang {
    fn into(self) -> &'static str {
        match self {
            Lang::Nl => "nl",
            Lang::En => "en",
        }
    }
}

Finally the FromRequest trait is implemented to allow extracting the "lang" cookie from the request.

impl<'a, 'r> FromRequest<'a, 'r> for Lang {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Lang, ()> {
        match request.cookies().get_private("lang") {
            Some(r) => {
                match Lang::from_str(r.value()) {
                    Ok(l) => Success(l),
                    Err(_) => Success(Lang::default()),
                }
            }
            None => Success(Lang::default()),
        }
    }
}

It always succeeds and falls back to the default when no cookie or an unknown language is is found. How to use the Lang constraint on a request:

#[get("/page")]
fn page(lang: Lang) -> content::HTML<String> {
    let hello = if lang == Lang::Nl {
       "Hallo daar!"
    } else {
       "Hello there!"
    };
    content::HTML(format!(
"<html>
  <body>
   <h1>{}</h1>
   <a href='/lang/en'>English</a>
   <a href='/lang/nl'>Nederlands</a>
  </body>
</html>", hello))
}

And the language switch page:

#[get("/lang/<lang>")]
fn lang(mut cookies: Cookies, lang: String) -> Result<Redirect> {
    let lang:&'static str = Lang::from_str(&lang)?.into();
    info!("Setting language to: {}", lang);
    cookies.add_private(Cookie::new("lang", lang));
    Ok(Redirect::to("/page"))
}

And as a cherry on the pie, let's have the language switch page automatically redirect to the referrer. First let's implement a FromRequest trait for our own Referer type:

struct Referer {
    url:String
}

impl<'a, 'r> FromRequest<'a, 'r> for Referer {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Referer, ()> {
        match request.headers().get_one("Referer") {
            Some(r) => Success(Referer { url:r.into() }),
            None => Forward(()),
        }
    }
}

When it finds a Referer header it uses the content, else the request is forwarded to the next handler. This means that if the request has no Referer header it is not handled, and a 404 will be returned. Finally let's update the language switch request handler:


#[get("/lang/<lang>")]
fn lang(mut cookies: Cookies, referer: Referer, lang: String) -> Result<Redirect> {
    let lang:&'static str = Lang::from_str(&lang)?.into();
    info!("Setting language to: {}", lang);
    cookies.add_private(Cookie::new("lang", lang));
    Ok(Redirect::to(&referer.url))
}

Pretty elegant. A recap with all code combined and adding the missing glue:

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

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

use rocket::request::{Outcome, Request, FromRequest};
use rocket::outcome::Outcome::*;
use rocket::response::{NamedFile, Redirect, content};
use rocket::http::{Cookie, Cookies};

use std::str::FromStr;

#[derive(PartialEq)]
enum Lang {
    Nl,
    En,
}

impl Default for Lang {
    fn default() -> Lang {
        Lang::Nl
    }
}

impl FromStr for Lang {
    type Err = Error;
    fn from_str(s:&str) -> Result<Self> {
        match s {
            "nl" => Ok(Lang::Nl),
            "en" => Ok(Lang::En),
            o => Err(format!("Unsupported language: {}", o).into()),
        }
    }
}

impl Into<&'static str> for Lang {
    fn into(self) -> &'static str {
        match self {
            Lang::Nl => "nl",
            Lang::En => "en",
        }
    }
}

impl<'a, 'r> FromRequest<'a, 'r> for Lang {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Lang, ()> {
        match request.cookies().get_private("lang") {
            Some(r) => {
                match Lang::from_str(r.value()) {
                    Ok(l) => Success(l),
                    Err(_) => Success(Lang::default()),
                }
            }
            None => Success(Lang::default()),
        }
    }
}

struct Referer {
    url:String
}

impl<'a, 'r> FromRequest<'a, 'r> for Referer {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Referer, ()> {
        match request.headers().get_one("Referer") {
            Some(r) => Success(Referer { url:r.into() }),
            None => Forward(()),
        }
    }
}

#[get("/page")]
fn page(lang: Lang) -> content::HTML<String> {
    let hello = if lang == Lang::Nl {
       "Hallo daar!"
    } else {
       "Hello there!"
    };
    content::HTML(format!(
"<html>
  <body>
   <h1>{}</h1>
   <a href='/lang/en'>English</a>
   <a href='/lang/nl'>Nederlands</a>
  </body>
</html>", hello))
}

#[get("/lang/<lang>")]
fn lang(mut cookies: Cookies, referer: Referer, lang: String) -> Result<Redirect> {
    let lang:&'static str = Lang::from_str(&lang)?.into();
    info!("Setting language to: {}", lang);
    cookies.add_private(Cookie::new("lang", lang));
    Ok(Redirect::to(&referer.url))
}

fn main() {
    rocket::ignite()
        .attach(Template::fairing())
        .mount("/", routes![lang , page])
        .launch();
}