Book Image

Rust Web Development with Rocket

By : Karuna Murti
Book Image

Rust Web Development with Rocket

By: Karuna Murti

Overview of this book

Looking for a fast, powerful, and intuitive framework to build web applications? This Rust book will help you kickstart your web development journey and take your Rust programming skills to the next level as you uncover the power of Rocket - a fast, flexible, and fun framework powered by Rust. Rust Web Development with Rocket wastes no time in getting you up to speed with what Rust is and how to use it. You’ll discover what makes it so productive and reliable, eventually mastering all of the concepts you need to play with the Rocket framework while developing a wide set of web development skills. Throughout this book, you'll be able to walk through a hands-on project, covering everything that goes into making advanced web applications, and get to grips with the ins and outs of Rocket development, including error handling, Rust vectors, and wrappers. You'll also learn how to use synchronous and asynchronous programming to improve application performance and make processing user content easy. By the end of the book, you'll have answers to all your questions about creating a web application using the Rust language and the Rocket web framework.
Table of Contents (20 chapters)
1
Part 1: An Introduction to the Rust Programming Language and the Rocket Web Framework
7
Part 2: An In-Depth Look at Rocket Web Application Development
14
Part 3: Finishing the Rust Web Application Development

Writing Hello World!

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:

  1. Let's create a new folder, for example, 01HelloWorld.
  2. Create a new file inside the folder and give it the name main.rs.
  3. Let's write our first code in Rust:
    fn main() { 
        println!("Hello World!");
    }
  4. After that, save your file, and in the same folder, open your terminal, and compile the code using the rustc command:
    rustc main.rs
  5. You can see there's a file inside the folder called main; run that file from your terminal:
    ./main
  6. Congratulations! You just wrote your first 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.

Writing a more complex program

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:

  1. Let's make a new folder, for example, 02ComplexProgram. After that, create the main.rs file again and add the main function again:
    fn main() {}
  2. Then, use the 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:

  1. The first 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.
  2. The 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.
  3. The next piece of code, that is, the 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.
  4. Two Rust enums (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:

  1. There are two ways to create a module: create 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;
    }
  2. By default, a type or trait is private, but we want to use it in main.rs and implement the encryptor on a different file, so we should denote the trait as public by adding the pub keyword.
  3. That trait has one function, encrypt(), which has self-reference as a parameter and returns String.
  4. Now, we should define this new module in main.rs. Put this line before the fn main block:
    pub mod encryptor;
  5. Then, let's make a simple type that implements the 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;
  6. Let's make another file named rot13.rs inside the encryptor folder.
  7. We want to define a simple struct that only has one piece of data, a string, and tell the compiler that the struct is implementing the 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.

  1. Next, let's try compiling our program:
    > 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.

  1. We now can continue implementing our 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.

  1. On our first iteration, we want to allocate a new string, get the original 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
    }
  2. Let's try compiling that program. You will quickly find it is not working, with all errors being `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.

  1. Let's modify our code a little bit:
    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
    }
  2. There are two ways of returning; the first one is using the 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.
  3. The preceding code works just fine, but we can make it more idiomatic. First, let's process the iterator without the 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.

  1. Let's change the code again:
    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.

  1. Now that we have implemented our simple encryptor, let's use it in our main application:
    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.

  1. Put the following line above the main() function and the compiler should produce a binary without any error:
    use encryptor::Encryptable;
  2. Let's try running the executable:
    > ./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.