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();
}
(This post originally appeared on rustit.be)