Book Image

Rust Web Programming

By : Maxwell Flitton
Book Image

Rust Web Programming

By: Maxwell Flitton

Overview of this book

Are safety and high performance a big concern for you while developing web applications? While most programming languages have a safety or speed trade-off, Rust provides memory safety without using a garbage collector. This means that with its low memory footprint, you can build high-performance and secure web apps with relative ease. This book will take you through each stage of the web development process, showing you how to combine Rust and modern web development principles to build supercharged web apps. You'll start with an introduction to Rust and understand how to avoid common pitfalls when migrating from traditional dynamic programming languages. The book will show you how to structure Rust code for a project that spans multiple pages and modules. Next, you'll explore the Actix Web framework and get a basic web server up and running. As you advance, you'll learn how to process JSON requests and display data from the web app via HTML, CSS, and JavaScript. You'll also be able to persist data and create RESTful services in Rust. Later, you'll build an automated deployment process for the app on an AWS EC2 instance and Docker Hub. Finally, you'll play around with some popular web frameworks in Rust and compare them. By the end of this Rust book, you'll be able to confidently create scalable and fast web applications with Rust.
Table of Contents (19 chapters)
1
Section 1:Setting Up the Web App Structure
4
Section 2:Processing Data and Managing Displays
8
Section 3:Data Persistence
12
Section 4:Testing and Deployment

Metaprogramming with macros

Metaprogramming can generally be described as a way in which the program can manipulate itself based on certain instructions. Considering the strong typing Rust has, one of the simplest ways in which we can meta program is by using generics. A classic example of demonstrating generics is through coordinates:

struct Coordinate <T> {
    x: T, 
    y: T
}
fn main() {
     let one = Coordinate{x: 50, y: 50};
     let two = Coordinate{x: 500, y: 500};
     let three = Coordinate{x: 5.6, y: 5.6};
}

Here, the compiler is looking for all the times where the coordinate struct is called and creates structs with the types that were used when compiling. The main mechanism of metaprogramming in Rust is done with macros. Macros enable us to abstract code. We've already been using macros in our print functions. The ! notation at the end of the function denotes that this is a macro that's being called. Defining our own macros is a blend of defining a function and using a lifetime notation within a match statement in the function. In order to demonstrate this, we will define a macro that capitalizes a string:

macro_rules! capitalize {
    ($a: expr) => {
        let mut v: Vec<char> = $a.chars().collect();
        v[0] = v[0].to_uppercase().nth(0).unwrap();
        $a = v.into_iter().collect();
    }
}
fn main() {
    let mut x = String::from("test");
    capitalize!(x);
    println!("{}", x);
}

Instead of using the term fn, we use the macro_rules! definition. We then say that $a is the expression that's passed into the macro. We get the expression, convert it into a vector of chars, uppercase the first char, and then convert it back into a string.

Note that we don't return anything in the capitalize macro and that when we call the macro, we don't assign a variable to it. However, when we print the x variable at the end, we can see that it is capitalized. This does not behave like an ordinary function. We also have to note that we didn't define a type. Instead, we just said it was an expression; the macro still does checks via traits. Passing an integer into the macro results in the following error:

|     capitalize!(32);
|     ---------------- in this macro invocation
|
= help: the trait `std::iter::FromIterator<char>` is not implemented for `{integer}`

Lifetimes, blocks, literals, paths, meta, and more can also be passed instead of an expression. While it's important to have a brief understanding of what's under the hood of a basic macro for debugging and further reading, diving more into developing complex macros will not help us when it comes to developing web apps.

We must remember that macros are a last resort and should be used sparingly. Errors that are thrown in macros can be hard to debug. In web development, a lot of the macros are already defined in third-party packages. Because of this, we do not need to write macros ourselves to get a web app up and running. Instead, we will mainly be using derive macros out of the box.

Derive macros can be analogous to decorators in JavaScript and Python. They sit on top of a function or struct and change its functionality. A good way to demonstrate this in action is by revisiting our coordinate struct. Here, we will put it through a print function we define, and then try and print it again with the built-in print macro:

struct Coordinate {
    x: i8, 
    y: i8
}
fn print(point: Coordinate) {
    println!("{} {}", point.x, point.y);
}
fn main() {
    let test = Coordinate{x: 1, y:2};
    print(test);
    println!("{}", test.x)
}

Unsurprisingly, we get the following error when compiling:

|     let test = Coordinate{x: 1, y:2};
|         ---- move occurs because `test` has type           `Coordinate`, which does not implement the `Copy`           trait
|     print(test);
|           ---- value moved here
|     println!("{}", test.x)
|                         ^^^^^^ value borrowed here after move

Here, we can see that we're getting the error that the coordinate was moved into our function and was then borrowed later. We can solve this with the & notation. However, it's also worth noting the second line in the error, stating that our struct does not have a copy trait. Instead of trying to build a copy trait ourselves, we can use a derive macro to give our struct a copy trait:

#[derive(Clone, Copy)]
struct Coordinate {
    x: i8, 
    y: i8
}

Now, the code will run. The copy trait is fired when we move the coordinate into our print function. We can stack these traits. By merely adding the debug trait to the derive macro, we can print out the whole struct using the :? operator in the print macro:

#[derive(Debug, Clone, Copy)]
struct Coordinate {
    x: i8, 
    y: i8
}
fn main() {
    let test = Coordinate{x: 1, y:2};
    println!("{:?}", test)
}

This gives us a lot of powerful functionality in web development. For instance, we will be using them in JSON serialization using the serde crate:

use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Coordinate {
    x: i8, 
    y: i8
}

With this, we can pass the coordinate into the crate's functions to serialize into JSON, and then deserialize. We can create our own derive macros, but the code behind our own derive macros has to be packaged in its own crate. While we will go over cargo and file structure in the next chapter, we will not be building our own derive macros.