Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Rust Web Programming

You're reading from   Rust Web Programming A hands-on guide to developing fast and secure web apps with the Rust programming language

Arrow left icon
Product type Paperback
Published in Feb 2021
Publisher Packt
ISBN-13 9781800560819
Length 394 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Maxwell Flitton Maxwell Flitton
Author Profile Icon Maxwell Flitton
Maxwell Flitton
Arrow right icon
View More author details
Toc

Table of Contents (19) Chapters Close

Preface 1. Section 1:Setting Up the Web App Structure
2. Chapter 1: Quick Introduction to Rust FREE CHAPTER 3. Chapter 2: Designing Your Web Application in Rust 4. Section 2:Processing Data and Managing Displays
5. Chapter 3: Handling HTTP Requests 6. Chapter 4: Processing HTTP Requests 7. Chapter 5: Displaying Content in the Browser 8. Section 3:Data Persistence
9. Chapter 6: Data Persistence with PostgreSQL 10. Chapter 7: Managing User Sessions 11. Chapter 8: Building RESTful Services 12. Section 4:Testing and Deployment
13. Chapter 9: Testing Our Application Endpoints and Components 14. Chapter 10: Deploying Our Application on AWS 15. Chapter 11: Understanding Rocket Web Framework 16. Assessments 17. Other Books You May Enjoy Appendix A: Understanding the Warp Framework

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.

You have been reading a chapter from
Rust Web Programming
Published in: Feb 2021
Publisher: Packt
ISBN-13: 9781800560819
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime