Sign In Start Free Trial
Account

Add to playlist

Create a Playlist

Modal Close icon
You need to login to use this feature.
  • Book Overview & Buying Rust Web Programming
  • Table Of Contents Toc
Rust Web Programming

Rust Web Programming - Third Edition

By : Maxwell Flitton
4.3 (3)
close
close
Rust Web Programming

Rust Web Programming

4.3 (3)
By: Maxwell Flitton

Overview of this book

Rust is no longer just for systems programming. This book will show you why this safe and performant language is a crucial up-and-coming option for developing web applications, and get you on your way to building fully functional Rust web apps. You don’t need any experience with Rust to get started, and this new edition also comes with a shallower learning curve. You’ll get hands-on with emerging Rust web frameworks including Actix, Axum, Rocket, and Hyper. You’ll look at injecting Rust into the frontend with WebAssembly and HTTPS configuration with NGINX. Later, you’ll move on to more advanced async topics, exploring TCP and framing, and implementing async systems. As you work through the book, you’ll build a to-do application with authentication using a microservice architecture that compiles into one Rust binary, including the embedding of a frontend JavaScript application in the same binary. The application will have end-to-end atomic testing and a deployment pipeline. By the end of this book, you’ll fully understand the significance of Rust for web development. You’ll also have the confidence to build robust, functional, and scalable Rust web applications from scratch.
Table of Contents (24 chapters)
close
close
22
Other Books You May Enjoy
23
Index

Building Structs

In modern high-level dynamic languages, objects have been the bedrock for building big applications and solving complex problems, and for good reason. Objects enable us to encapsulate data, functionality, and behavior. In Rust, we do not have objects. However, we do have structs that can hold data in fields. We can then manage the functionality of these structs and group them together with traits. This is a powerful approach, and it gives us the benefits of objects without the high coupling, as highlighted in the following figure:

Diagram

Description automatically generated with medium confidence

Figure 1.9 – Difference between Rust structs and objects

We will start with something basic by creating a Human struct with the following code:

#[derive(Debug)]
struct Human<'a> {
    name: &'a str,
    age: i8,
    current_thought: &'a str
}

In the preceding code, we can see that our string literal fields have the same lifetime as the struct itself. We have also applied the Debug trait to the Human struct, so we can print it out and see everything. We can then create the Human struct and print it out, using the following code:

fn main() {
    let developer = Human{
        name: "Maxwell Flitton",
        age: 34,
        current_thought: "nothing"
    };
    println!("{:?}", developer);
    println!("{}", developer.name);
}

Running the preceding code will give us the following printout:

Human { name: "Maxwell Flitton", age: 34, current_thought: "nothing" }
Maxwell Flitton

We can see that our fields are what we expect. However, we can change our string slice fields to strings to get rid of lifetime parameters. We may also want to add another field where we can reference another Human struct under a friend field. However, we may also have no friends. We can account for this by creating an enum that is either a friend or not and assigning it to a friend field, as seen in the following code:

#[derive(Debug)]
enum Friend {
    HUMAN(Human),
    NIL
}
#[derive(Debug)]
struct Human {
    name: String,
    age: i8,
    current_thought: String,
    friend: Friend
}

We can then define the Human struct initially with no friends, just to see if it works with the following code:

    let developer = Human{
        name: "Maxwell Flitton".to_string(),
        age: 32,
        current_thought: "nothing".to_string(),
        friend: Friend::NIL
    };

However, when we run the compiler, it does not work. I would like to think this is because the compiler cannot believe that I have no friends. But alas, it has to do with the compiler not knowing how much memory to allocate for this declaration. This is shown through the following error code:

enum Friend {    HUMAN(Human),    NIL}#[derive(Debug)]
^^^^^^^^^^^            ----- recursive without indirection
|
recursive type has infinite size

Because of the enum, theoretically, the memory needed to store this variable could be infinite. One Human struct could reference another Human struct as a friend field, which could in turn reference another Human struct, resulting in a potentially infinite number of Human structs being linked together through the friend field. We can solve this problem with pointers. Instead of storing all the data of a Human struct in the friend field, we store a memory address that we know has a maximum value because it’s a standard integer. This memory address points to where another Human struct is stored in the memory. As a result, the program knows exactly how much memory to allocate when it crosses a Human struct, irrespective of whether the Human struct has a friend field or not. This can be achieved by using a Box struct, which is essentially a smart pointer for our enum, with the following code:

#[derive(Debug)]
enum Friend {
    HUMAN(Box<Human>),
    NIL
}

So now, our enum states whether the friend exists or not, and if so, it has a memory address if we need to extract information about this friend. We can achieve this with the following code:

fn main() {
    let another_developer = Human{
        name: "Caroline Morton".to_string(),
        age:30,
        current_thought: "I need to code!!".to_string(),
        friend: Friend::NIL
    };
    let developer = Human{
        name: "Maxwell Flitton".to_string(),
        age: 34,
        current_thought: "nothing".to_string(),
        friend: Friend::HUMAN(Box::new(another_developer))
    };
    match &developer.friend {
        Friend::HUMAN(data) => {
            println!("{}", data.name);
        },
        Friend::NIL => {}
    }
}

In the preceding code, we can see that we have created one Human struct, and then another Human struct with a reference to the first Human struct as a friend field. We then access the second Human struct’s friend through the friend field. Remember, we must handle both possibilities, as the friend field could be a nil value.

While it is exciting that friends can be made, if we take a step back, we can see that there is a lot of code written for each human we create. This is not helpful if we must create a lot of humans in a program. We can reduce this by implementing some functionality for our struct. We will essentially create a constructor for the struct with extra functions, so we can add optional values if we want. We will also make the thought field optional. So a basic struct with a constructor populating only the most essential fields can be achieved with the following code:

#[derive(Debug)]
struct Human {
    name: String,
    age: i8,
    current_thought: Option<String>,
    friend: Friend
}
impl Human {   
    fn new(name: &str, age: i8) -> Human {
        return Human{
            name: name.to_string(),
            age: age,
            current_thought: None,
            friend: Friend::NIL
        }
    }
}

Therefore, creating a new human now only takes the following line of code:

let developer = Human::new("Maxwell Flitton", 34);

This will have the following field values:

  • Name: "Maxwell Flitton"
  • Age: 34
  • Current Thought: None
  • Friend: NIL

We can add more functions to the implement block for adding friends and a current thought with the following code:

    fn with_thought(mut self, thought: &str) -> Human {
        self.current_thought = Some(thought.to_string());
        return self
    }
    fn with_friend(mut self, friend: Box<Human>) -> Human {
        self.friend = Friend::HUMAN(friend);
        return self
    }

In the preceding code, we can see that we pass in a mutable version of the struct that is calling these functions. These functions can be chained because they return the struct that called them. If we want to create a developer with a thought, we can do this with the following code:

let developer = Human::new("Maxwell Flitton", 34).with_thought(
                                             "I love Rust!");

Note that a function that does not require self as a parameter can be called with ::, while a function that does require self as a parameter can be called with a simple dot, .. If we want to create a developer with a friend, it can be done using the following code:

let developer_friend = Human::new("Caroline Morton", 30);
let developer = Human::new("Maxwell Flitton", 34)
                       .with_thought("I love Rust!")
                       .with_friend(Box::new(developer_friend));
println!("{:?}", developer);

Running the code will result in the following parameters for developer:

Name: "Maxwell Flitton"
Age: 34
Current Thought: Some("I love Rust!")
Friend: HUMAN(Human { name: "Caroline Morton", age: 30,
                     current_thought: None, friend: NIL })

We can see that structs combined with enums and functions that have been implemented with these structs can be powerful building blocks. We can define fields and functionality with only a small amount of code if we have defined our structs well. However, writing the same functionality for multiple structs can be time-consuming and result in a lot of repeated code. If you have worked with objects before, you may have utilized inheritance for that.

Rust goes one better. It has traits, which we will explore in the next chapter.

Building structs cheatsheet

  • No objects: Rust uses structs instead of objects to encapsulate data and functionality.
  • Struct definition: Structs hold data in fields (e.g., struct Human { name: String, age: i8 }).
  • String literals in structs: String literals in structs require lifetime parameters.
  • Box for recursive types: Use Box to handle recursive types, preventing infinite memory allocation.
  • Printing structs: Implement the Debug trait to print struct details.
  • Constructors in structs: Implement a new function in the impl block for struct initialization.
  • Function calling: Use :: for static methods and . for instance methods. Add methods to the impl block to chain function calls (e.g., with_thought).
  • Traits for reusability: Use traits to avoid repeated code and enhance functionality across multiple structs.
CONTINUE READING
83
Tech Concepts
36
Programming languages
73
Tech Tools
Icon Unlimited access to the largest independent learning library in tech of over 8,000 expert-authored tech books and videos.
Icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Icon 50+ new titles added per month and exclusive early access to books as they are being written.
Rust Web Programming
notes
bookmark Notes and Bookmarks search Search in title playlist Add to playlist download Download options font-size Font size

Change the font size

margin-width Margin width

Change margin width

day-mode Day/Sepia/Night Modes

Change background colour

Close icon Search
Country selected

Close icon Your notes and bookmarks

Confirmation

Modal Close icon
claim successful

Buy this book with your credits?

Modal Close icon
Are you sure you want to buy this book with one of your credits?
Close
YES, BUY

Submit Your Feedback

Modal Close icon
Modal Close icon
Modal Close icon