
Rust Web Development with Rocket
By :

In this section, we are going to write a very basic program, Hello World!. After we successfully compile that, we are going to write a more complex program to see the basic capabilities of the Rust language. Let's do it by following these instructions:
01HelloWorld
.main.rs
.fn main() { println!("Hello World!"); }
rustc
command:rustc main.rs
main
; run that file from your terminal:./main
Hello World
program in the Rust language. Next, we're going to step up our Rust language game; we will showcase basic Rust applications with control flow, modules, and other functionalities.
Of course, after making the Hello World
program, we should try to write a more complex program to see what we can do with the language. We want to make a program that captures what the user inputted, encrypts it with the selected algorithm, and returns the output to the terminal:
02ComplexProgram
. After that, create the main.rs
file again and add the main
function again:fn main() {}
std::io
module and write the part of the program to tell the user to input the string they want to encrypt:use std::io; fn main() { println!("Input the string you want to encrypt:"); let mut user_input = String::new(); io::stdin() .read_line(&mut user_input) .expect("Cannot read input"); println!("Your encrypted string: {}", user_input); }
Let's explore what we have written line by line:
use std::io;
, is telling our program that we are going to use the std::io
module in our program. std
should be included by default on a program unless we specifically say not to use it.let...
line is a variable declaration. When we define a variable in Rust, the variable is immutable by default, so we must add the mut
keyword to make it mutable. user_input
is the variable name, and the right hand of this statement is initializing a new empty String
instance. Notice how we initialize the variable directly. Rust allows the separation of declaration and initialization, but that form is not idiomatic, as a programmer might try to use an uninitialized variable and Rust disallows the use of uninitialized variables. As a result, the code will not compile.stdin()
function, initializes the std::io::Stdin
struct. It reads the input from the terminal and puts it in the user_input
variable. Notice that the signature for read_line()
accepts &mut String
. We have to explicitly tell the compiler we are passing a mutable reference because of the Rust borrow checker, which we will discuss later in Chapter 9, Displaying User's Post. The read_line()
output is std::result::Result
, an enum with two variants, Ok(T)
and Err(E)
. One of the Result
methods is expect()
, which returns a generic type T
, or if it's an Err
variant, then it will cause panic with a generic error E
combined with the passed message.std::result::Result
and std::option::Option
) are very ubiquitous and important in the Rust language, so by default, we can use them in the program without specifying use
.Next, we want to be able to encrypt the input, but right now, we don't know what encryption we want to use. The first thing we want to do is make a trait, a particular code in the Rust language that tells the compiler what functionality a type can have:
module_name.rs
or create a folder with module_name
and add a mod.rs
file inside that folder. Let's create a folder named encryptor
and create a new file named mod.rs
. Since we want to add a type and implementation later, let's use the second way. Let's write this in mod.rs
:pub trait Encryptable { fn encrypt(&self) -> String; }
main.rs
and implement the encryptor on a different file, so we should denote the trait as public by adding the pub
keyword.encrypt()
, which has self-reference as a parameter and returns String
.main.rs
. Put this line before the fn
main block:pub mod encryptor;
Encryptable
trait. Remember the Caesar cipher, where the cipher substitutes a letter with another letter? Let's implement the simplest one called ROT13
, where it converts 'a'
to 'n'
and 'n'
to 'a'
, 'b'
to 'o'
and 'o'
to 'b'
, and so on. Write the following in the mod.rs
file:pub mod rot13;
rot13.rs
inside the encryptor
folder.Encryptable
trait. Put this code inside the rot13.rs
file:pub struct Rot13(pub String); impl super::Encryptable for Rot13 {}
You might notice we put pub
in everything from the module declaration, to the trait declaration, struct declaration, and field declaration.
> rustc main.rs error[E0046]: not all trait items implemented, missing: `encrypt` --> encryptor/rot13.rs:3:1 | 3 | impl super::Encryptable for Rot13 {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `encrypt` in implementation | ::: encryptor/mod.rs:6:5 | 6 | fn encrypt(&self) -> String; | ---------------------------------------------- ------ `encrypt` from trait error: aborting due to previous error For more information about this error, try `rustc --explain E0046`.
What is going on here? Clearly, the compiler found an error in our code. One of Rust's strengths is helpful compiler messages. You can see the line where the error occurs, the reason why our code is wrong, and sometimes, it even suggests the fix for our code. We know that we have to implement the super::Encryptable
trait for the Rot13
type.
If you want to see more information, run the command shown in the preceding error, rustc --explain E0046
, and the compiler will show more information about that particular error.
Rot13
encryption. First, let's put the signature from the trait into our implementation:impl super::Encryptable for Rot13 { fn encrypt(&self) -> String { } }
The strategy for this encryption is to iterate each character in the string and add 13 to the char value if it has a character before 'n'
or 'N'
, and remove 13 if it has 'n'
or 'N'
or characters after it. The Rust language handles Unicode strings by default, so the program should have a restriction to operate only on the Latin alphabet.
String
length, start from the zeroeth index, apply a transformation, push to a new string, and repeat until the end:fn encrypt(&self) -> String { let mut new_string = String::new(); let len = self.0.len(); for i in 0..len { if (self.0[i] >= 'a' && self.0[i] < 'n') || (self.0[i] >= 'A' && self.0[i] < 'N') { new_string.push((self.0[i] as u8 + 13) as char); } else if (self.0[i] >= 'n' && self.0[i] < 'z') || (self.0[i] >= 'N' && self.0[i] < 'Z') { new_string.push((self.0[i] as u8 - 13) as char); } else { new_string.push(self.0[i]); } } new_string }
`String` cannot be indexed by `usize`
. Remember that Rust handles Unicode by default? Indexing a string will create all sorts of complications, as Unicode characters have different sizes: some are 1 byte but others can be 2, 3, or 4 bytes. With regard to index, what exactly are we saying? Is index means the byte position in a String
, grapheme, or Unicode scalar values?In the Rust language, we have primitive types such as u8
, char
, fn
, str
, and many more. In addition to those primitive types, Rust also defines a lot of modules in the standard library, such as string
, io
, os
, fmt
, and thread
. These modules contain many building blocks for programming. For example, the std::string::String
struct deals with String
. Important programming concepts such as comparison and iteration are also defined in these modules, for example, std::cmp::Eq
to compare an instance of a type with another instance. The Rust language also has std::iter::Iterator
to make a type iterable. Fortunately, for String
, we already have a method to do iteration.
fn encrypt(&self) -> String { let mut new_string = String::new(); for ch in self.0.chars() { if (ch >= 'a' && ch < 'n') || (ch >= 'A' && ch < 'N') { new_string.push((ch as u8 + 13) as char); } else if (ch >= 'n' && ch < 'z') || (ch >= 'N' && ch < 'Z') { new_string.push((ch as u8 - 13) as char); } else { new_string.push(ch); } } new_string }
return
keyword such as return new_string;
, or we can write just the variable without a semicolon in the last line of a function. You will see that it's more common to use the second form.for
loop. Let's remove the new string initialization and use the map()
method instead. Any type implementing std::iter::Iterator
will have a map()
method that accepts a closure as the parameter and returns std::iter::Map
. We can then use the collect()
method to collect the result of the closure into its own String
:fn encrypt(&self) -> Result<String, Box<dyn Error>> { self.0 .chars() .map(|ch| { if (ch >= 'a' && ch < 'n') || (ch >= 'A' && ch < 'N') { (ch as u8 + 13) as char } else if (ch >= 'n' && ch < 'z') || ( ch >= 'N' && ch < 'Z') { (ch as u8 - 13) as char } else { ch } }) .collect() }
The map()
method accepts a closure in the form of |x|...
. We then use the captured individual items that we get from chars()
and process them.
If you look at the closure, you'll see we don't use the return
keyword either. If we don't put the semicolon in a branch and it's the last item, it will be considered as a return
value.
Using the if
block is good, but we can also make it more idiomatic. One of the Rust language's strengths is the powerful match
control flow.
fn encrypt(&self) -> String { self.0 .chars() .map(|ch| match ch { 'a'..='m' | 'A'..='M' => (ch as u8 + 13) as char, 'n'..='z' | 'N'..='Z' => (ch as u8 - 13) as char, _ => ch, }) .collect() }
That looks a lot cleaner. The pipe (|
) operator is a separator to match items in an arm. The Rust matcher is exhaustive, which means that the compiler will check whether all possible values of the matcher are included in the matcher or not. In this case, it means all characters in Unicode. Try removing the last arm and compiling it to see what happens if you don't include an item in a collection.
You can define a range by using ..
or ..=
. The former means we are excluding the last element, and the latter means we are including the last element.
fn main() { ... io::stdin() .read_line(&mut user_input) .expect("Cannot read input"); println!( "Your encrypted string: {}", encryptor::rot13::Rot13(user_input).encrypt() ); }
Right now, when we try to compile it, the compiler will show an error. Basically, the compiler is saying you cannot use a trait function if the trait is not in the scope, and the help from the compiler is showing what we need to do.
main()
function and the compiler should produce a binary without any error:use encryptor::Encryptable;
> ./main Input the string you want to encrypt: asdf123 Your encrypted string: nfqs123 > ./main Input the string you want to encrypt: nfqs123 Your encrypted string: asdf123
We have finished our program and we improved it with real-world encryption. In the next section, we're going to learn how to search for and use third-party libraries and incorporate them into our application.
Change the font size
Change margin width
Change background colour