Create some scaffolding
Simply including and downloading a library is no fun: we want to see Rust in action. To achieve that, a sample program must be written – the scaffolding for which is the topic of the following steps.
Before we can really get coding, however, a small problem must be fixed. Rust's compiler does not allow for the use of advanced language features by default – if your application cannot be compiled due to feature use, you will need to fix the problem via the following command sequence:
tamhan@tamhan-thinkpad:~/rustspace/futuresample1$ rustup override set nightly
info: syncing channel updates for 'nightly-x86_64-unknown-linux-gnu'
. . .
Applying the 'set nightly' command in a folder containing a .toml file modifies it to mark its contents to be run using the latest version of rustc – with the flag set, the compile process should succeed. Next, open 'main.rs' and replace its contents with the following code:
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite().mount("/", routes![index]).launch();
}
Invoke 'cargo run' after saving the changes to see the output shown in the figure below. The package manager isn't limited to loading code, but can act as an advanced build tool.
3, 2, 1, lift off!
Rocket's developers, obviously, were inspired by the work of missile teams: make of this what you will. Like most other web frameworks, the actual applications are created as a collection of 'routes', which are assigned to a web server class. In this case, but one route is created – a 'get' call against '/' will yield the returning of the string 'Hello World'.
Incidentially, the main issue faced by developers coming to Rust from other languages is the somewhat odd syntax. Function return types are declared via an arrow following the header:
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
if rhs == 0 {
return false;
}
Careful onlookers will determine that the snippet above generates a function returning a Boolean value: Rust knows about a few dozen data types, which must be formally specified at declaration to prevent the passing of invalid types in a fashion similar to TypeScript.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
While the 'return' statement is supported by Rust; a special case occurs whenever the last line of a function is an expression. It is considered the 'return' value – a good example for this would look as per the following code:
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
. . .
lhs % rhs == 0
}
With that now out of the way, our next step involves the creation of a brand new route:
#[get("/world")]
fn world() -> &'static str {
"A new route!"
}
Rust's language design advocates the use of attributes: the elements inside the '#[]' construct are additional properties, which get applied to any element standing nearby.
In our particular case, the affected element is a function going by the name of 'world()'.
The next problem involves adding the new route to the above-mentioned web server element. This is easily accomplished as per the following:
fn main() {
rocket::ignite().mount("/", routes![index]).mount("/world", routes![world]).launch();
}
This code is interesting mainly because of the use of the code generator: 'mount' takes the 'routes!' macro, which generates code on the fly. With that out of the way, you can now perform another recompile, which will enable you to convince yourself of the correctness of our code – the Rocket handler will now detect a total of two routes.
In-depth analysis
Providing resources on request might make for a nice demo, but is lacking in practicability. A more interesting test involves accepting parameters from the client, and using them to modify the system behaviour as a whole.
The first step involves modifying the declaration of the route so it includes one or more parameters. Passing in a numeric and a string variable can be accomplished via a folder structure:
#[get("/world/<name>/<age>")]
fn world(name: String, age: u8) -> String {
format!("Hello, {} year old named {}!", age, name)
}
During compilation, the program will reveal a folder structure. Prove the correctness of the product by invoking http://localhost:8000/world/world/tam/40. The product also takes care of malformed requests – invoke http://localhost:8000/world/world/tam/tam to see a 404 error.
Understanding this behaviour requires a look at the routing infrastructure: like most other web frameworks, Rocket 'throws' incoming requests from route to route until one matches. Developers can also specify route rank via a numeric value:
#[get("/user/<id>")]
fn user(id: usize) -> T { ... }
#[get("/user/<id>", rank = 2)]
fn user_int(id: isize) -> T { ... }
#[get("/user/<id>", rank = 3)]
fn user_str(id: &RawStr) -> T { ... }
Do the JSON
Another aspect involves the creation of well-formed JSON. To use it, a set of supporting libraries must be added to the .toml file – a lot of advanced features are not domiciled in Rocket, but in 'rocket_contrib':
[dependencies]
. . .
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["json"]
Using the 'features' array lets us fine-tune the inclusion: you don't need to include all parts of the library. We furthermore load a group of helper libraries, which simplify serialisation.
Now we've edited the .toml file, it is time to return to the main Rust code. The newly-added elements must first be imported into the namespace:
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
use rocket_contrib::Json;
use rocket_contrib::Value;
A structure must be declared, which describes the format of the generated JSON object. We will limit ourselves to a numeric and a string value – be sure not to forget the attribute by mistake:
#[derive(Serialize, Deserialize)]
struct Message {
id: u8,
contents: String
}
One problem remains: a JSON object must be built and returned in response to an incoming query:
#[get("/world/<name>/<age>")]
fn world(name: String, age: u8) -> Json<Message> {
Json(Message {
contents: name,
id: age
})
}
Invoke the route we declared above, and feast your eyes on the output!
This article was originally published in issue 273 of creative web design magazine Web Designer. Buy issue 273 here or subscribe to Web Designer here.
Related articles:
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
Tam Hanna is a software consultant who specialises in the management of carrier and device manufacturer relationships, mobile application distribution and development, design and prototyping of process computers and sensors. He was a regular contributor to Web Designer magazine in previous years, and now occupies his time as the owner of Tamoggemon Software and Computer Software.