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

Building structs

In dynamic languages, classes have been the bedrock of developing data structures with custom functionality. In terms of Rust, structs enable us to define data structures with functionality. To mimic a class, we can define a Human struct:

struct Human {
    name: String,
    age: i8,
    current_thought: String
}
impl Human { 
    fn new(input_name: &str, input_age: i8) -> Human {
          return Human {
          name: input_name.to_string(), 
          age: input_age, 
          current_thought: String::from("nothing")
      }
    }    
    fn with_thought(mut self, thought: &str ) -> Human {
        self.current_thought = thought;
        return self
    }
    
    fn speak(&self) -> () {
        println!("Hello my name is {} and I'm {} years             old.", &self.name, &self.age);
    }
}
fn main() {
    let developer = Human::new("Maxwell Flitton", 31);
    developer.speak();
    println!("currently I'm thinking {}",              developer.current_thought);
    
    let new_developer = Human::new("Grace", 30).with_thought(
        String::from("I'm Hungry"));
    new_developer.speak();
    println!("currently I'm thinking {}",             new_developer.current_thought);
}

This looks very familiar. Here, we have a Human struct that has name and age attributes. The impl block is associated with the Human struct. The new function inside the impl block is essentially a constructor for the Human struct. The constructor states that current_thought is a string that's been initialized with nothing because we want it to be an optional field.

We can define the optional current_thought field by calling the with_thought function directly after calling the new function, which we can see in action when we define new_developer. Self is much like self in Python, and also like this in JavaScript as it's a reference to the Human struct.

Now that we understand structs and their functionality, we can revisit hash maps to make them more functional. Here, we will exploit enums to allow the hash map to accept an integer or a string:

use std::collections::HashMap;
enum AllowedData {
          S(String),
          I(i8)
}
struct CustomMap {
          body: HashMap<String, AllowedData>
}

Now that the hash map has been hosted as a body attribute, we can define our own constructor, get, insert, and display functions:

impl CustomMap {
          fn new() -> CustomMap {
               return CustomMap{body: HashMap::new()}
     }
     fn get(&self, key: &str) -> &AllowedData {
          return self.body.get(key).unwrap()
     }
     fn insert(&mut self, key: &str, value: AllowedData) -> () {
          self.body.insert(key.to_string(), value);
     }
     fn display(&self, key: &str) -> () {
          match self.get(key) {
               AllowedData::I(value) => println!("{}",                   value),
               AllowedData::S(value) => println!("{}",                   value)
          }
     }
}
fn main() {
    // defining a new hash map
    let mut map = CustomMap::new();
    // inserting two different types of data 
    map.insert("test", AllowedData::I(8));
    map.insert("testing", AllowedData::S(        "test value".to_string()));
    // displaying the data
    map.display("test");
    map.display("testing");
}

Now that we can build structs and exploit enums to handle multiple data types, we can tackle more complex problems in Rust. However, as the problem's complexity increases, the chance of repeating code also increases. This is where traits come in.

Verifying with traits

As we can see, enums can empower our structs so that they can handle multiple types. This can also be translated for any type of function or data structure. However, this can lead to a lot of repetition. Take, for instance, a User Struct. Users have a core set of values, such as a username and password. However, they could also have extra functionality based on roles. With users, we have to check roles before firing certain processes.

We also want to add the same functionality to a number of different user types. We can do this with traits. In this sense, we're going to use traits like a mixin. Here, we will create three traits for a user struct: a trait for editing data, another for creating data, and a final one for deleting data:

trait CanEdit {
     fn edit(&self) {
          println!("user is editing");
     }
}
trait CanCreate {
     fn create(&self) {
          println!("user is creating");
     }
}
trait CanDelete {
     fn delete(&self) {
          println!("user is deleting");
     }
}

Here, if a struct implements a trait, then it can use and overwrite the functions defined in the trait block. Next, we can define an admin user struct that implements all three traits:

struct AdminUser {
     name: String,
     password: String,
}
impl CanDelete for AdminUser {}
impl CanCreate for AdminUser {}
impl CanEdit for AdminUser {}

Now that our user struct has implemented all three traits, we can create a function that only allows users inside that have the CanDelete trait implemented:

fn delete<T: CanDelete>(user: T) -> () {
     user.delete();
}

Similar to the lifetime annotation, we use angle brackets before the input definitions to define T as a CanDelete trait. If we create a general user struct and we don't implement the CanDelete trait for it, Rust will fail to compile if we try to pass the general user through the delete function; it will complain, stating that it does not implement the CanDelete trait.

Now, with what we know, we can develop a user struct that inherits from a base user struct and has traits that can allow us to use the user struct in different functions. Rust does not directly support inheritance. However, we can combine structs with basic composition:

struct BaseUser {
     name: String,
     password: String
}
struct GeneralUser {
     super_struct: BaseUser,
     team: String
}
impl GeneralUser {
     fn new(name: String, password: String, team: String) ->      GeneralUser {
          return GeneralUser{super_struct: BaseUser{name,              password}, team: team}
     }
}
impl CanEdit for GeneralUser {}
impl CanCreate for GeneralUser {
     fn create(&self) -> () {
          println!("{} is creating under a {} team",              self.super_struct.name, self.team);
     }
}

Here, we defined what attributes are needed by a user in the base user struct. We then housed that under the super_struct attribute for the general user struct. Once we did this, we performed the composition in the constructor function, which is defined as new, and then we implemented two traits for this general user. In the CanCreate trait, we overwrote the create function and utilized the team attribute that was given to the general user.

As we can see, building structs that inherit from base structs is fairly straightforward. These traits enable us to slot in functionality such as mixins, and they go one step further by enabling typing of the struct in functions. Traits get even more powerful than this, and it's advised that you read more about them to enhance your ability to solve problems in Rust.

With what we know about traits, we can reduce code complexity and repetition when solving problems. However, a deeper dive into traits at this point will have diminishing returns when it comes to developing web apps. Another widely used method for structs and processes is macros.