DEV Community

loading...
Cover image for Rust 4 - Modules, crates, testing, documentation

Rust 4 - Modules, crates, testing, documentation

petr7555 profile image Petr Janik Updated on ・6 min read

Rust 4 - Modules, crates, testing, documentation

Modules

We need to explicitly build the module tree in Rust - there’s no implicit mapping between file system tree to module tree.

Consider we want to create the following module structure (the leaves of this tree are functions):

crate
 └── front_of_house
     ├── hosting
        ├── add_to_waitlist
        └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
Enter fullscreen mode Exit fullscreen mode

To create this module structure we can add the following to the main.rs file.

// main.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

fn main() {}
Enter fullscreen mode Exit fullscreen mode

Or we create the following files structure:

src
 ├── front_of_house
    ├── mod.rs
    ├── hosting.rs
    └── serving.rs
 └── main.rs
Enter fullscreen mode Exit fullscreen mode

The hosting.rs and serving.rs files are modules and contain the functions mentioned above.

mod.rs file must declare the submodules, such as:

// mod.rs
mod hosting;
mod serving;
Enter fullscreen mode Exit fullscreen mode

In main.rs, we must also declare the front_of_house submodule.

// main.rs
mod front_of_house;

fn main() {} 
Enter fullscreen mode Exit fullscreen mode

To be able to call the front_of_house::serving::take_order(); function from main.rs, the function has to be public. Also the modules leading to that function have to be public.

Make the function public.

// serving.rs
pub fn take_order(){}

fn serve_order(){}

fn take_payment(){}
Enter fullscreen mode Exit fullscreen mode

Make the serving module in mod.rs public.

// mod.rs
mod hosting;
pub mod serving;
Enter fullscreen mode Exit fullscreen mode

Now we can call it from main().

mod front_of_house;

fn main() {
    // Absolute path
    crate::front_of_house::serving::take_order();

    // Relative path
    front_of_house::serving::take_order();
}
Enter fullscreen mode Exit fullscreen mode

For more thorough explanation of modules, I suggest reading the following posts:

Clear explanation of Rust's module system

Rust modules explained

Public structures

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}
Enter fullscreen mode Exit fullscreen mode

Public enums

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}
Enter fullscreen mode Exit fullscreen mode

use with relative and absolute path

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant_rel() {
        // relative
        use self::front_of_house::hosting;
    hosting::add_to_waitlist();
}

pub fn eat_at_restaurant_abs() {
        // absolute 
    use crate::front_of_house::hosting;
    hosting::add_to_waitlist();
}

pub fn eat_at_restaurant_full() {
        // full path
        use crate::front_of_house::hosting::add_to_waitlist;
    add_to_waitlist();
}
Enter fullscreen mode Exit fullscreen mode

use with multiple modules

use std::io::{self, Write};
// Write does not need to be prefixed but you’d still need to do io::BufReader and so on.
Enter fullscreen mode Exit fullscreen mode
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}

        pub fn seat_at_table() {}
    }
}

fn eat_at_restaurant() {
    use front_of_house::hosting::{self, add_to_waitlist};
    hosting::seat_at_table();
    hosting::add_to_waitlist();
    add_to_waitlist();
}
Enter fullscreen mode Exit fullscreen mode

Glob operator - avoid

use std::collections::*;
Enter fullscreen mode Exit fullscreen mode

super and self

fn function() {
    println!("called `function()`");
}

mod cool {
    pub fn function() {
        println!("called `cool::function()`");
    }
}

mod my {
    fn function() {
        println!("called `my::function()`");
    }

    mod cool {
        pub fn function() {
            println!("called `my::cool::function()`");
        }
    }

    pub fn indirect_call() {
        // Let's access all the functions named `function` from this scope!
        print!("called `my::indirect_call()`, that\n");

        // The `self` keyword refers to the current module scope - in this case `my`.
        // Calling `self::function()` and calling `function()` directly both give
        // the same result, because they refer to the same function.
        self::function();
        function();

        // We can also use `self` to access another module inside `my`:
        self::cool::function();

        // The `super` keyword refers to the parent scope (outside the `my` module).
        super::function();

        // This will bind to the `cool::function` in the *crate* scope.
        // In this case the crate scope is the outermost scope.
        {
            use crate::cool::function as root_function;
            root_function();
        }
    }
}

fn main() {
    my::indirect_call();
    // prints:
    // called `my::indirect_call()`, that
    // called `my::function()`
    // called `my::function()`
    // called `my::cool::function()`
    // called `function()`
    // called `cool::function()`
}
Enter fullscreen mode Exit fullscreen mode

Dependencies

On crates.io

// Cargo.toml
// ...
[dependencies]
time = "0.2.16"
Enter fullscreen mode Exit fullscreen mode
// main.rs
use time;

fn main() {
    println!("2020 has {} days", time::days_in_year(2020));
}
Enter fullscreen mode Exit fullscreen mode

Local

projects
 ├── hello_utils
    └── src
        └── lib.rs
 └── my_project
     └── src
         └── main.rs

Enter fullscreen mode Exit fullscreen mode
// projects/my_project/Cargo.toml
// ...
[dependencies]
hello_utils = { path = "../hello_utils" }
Enter fullscreen mode Exit fullscreen mode
// lib.rs
pub fn hello(){
    println!("Hello!");
}
Enter fullscreen mode Exit fullscreen mode
// main.rs
use hello_utils;

fn main() {
    hello_utils::hello();
}
Enter fullscreen mode Exit fullscreen mode

Workspace

Create add folder.

Inside add folder, run cargo new adder and cargo new add-one --lib.

This creates the following structure:

add
 ├── Cargo.lock
 ├── Cargo.toml
 ├── add-one
    ├── Cargo.toml
    └── src
        └── lib.rs
 ├── adder
    ├── Cargo.toml
    └── src
        └── main.rs
 └── target
Enter fullscreen mode Exit fullscreen mode
// add/Cargo.toml (note that [package] section is missing)
[workspace]

members = [
    "adder",
    "add-one",
]
Enter fullscreen mode Exit fullscreen mode
// add/add-one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
    x + 1
}
Enter fullscreen mode Exit fullscreen mode
// add/adder/src/main.rs
use add_one;

fn main() {
    let num = 10;
    println!(
        "Hello, world! {} plus one is {}!",
        num,
        add_one::add_one(num)
    );
}
Enter fullscreen mode Exit fullscreen mode
// add/adder/Cargo.toml
// ...
[dependencies]
add-one = { path = "../add-one" }
Enter fullscreen mode Exit fullscreen mode

Run with cargo run -p adder.

Output: Hello, world! 10 plus one is 11!.

Testing

We can write tests either in the same file or separately.

Unit tests

Put unit tests in the src directory in each file with the code that they’re testing. The convention is to create a module named tests in each file to contain the test functions and to annotate the module with #[cfg(test)].

// src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(4, 2 + 2);
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration tests

Integration test use your library in the same way any other code would, which means they can only call functions that are part of your library’s public API. To create integration tests, you need a tests directory next to your src directory.

// tests/integration_test.rs
use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}
Enter fullscreen mode Exit fullscreen mode

Run tests with:

# runs all tests
cargo test
# runs only library tests
cargo test --lib
# runs only documentation tests
cargo test --doc
Enter fullscreen mode Exit fullscreen mode

Test fails when the program panics.

#[test]
fn test_that_fails() {
        panic!("Make this test fail");
}
Enter fullscreen mode Exit fullscreen mode

Assertions

// when you want to ensure that some condition in a test evaluates to true
assert!(4 == 2 + 2);
// compare two arguments for equality
assert_eq!(4, 2 + 2);
// compare two arguments for inequality
assert_ne!(4, 2 + 1);
Enter fullscreen mode Exit fullscreen mode

Custom messages

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was `{}`",
            result
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing panic

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}
Enter fullscreen mode Exit fullscreen mode

Test can return Result

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Documentation

Uses CommonMark.

Tests in documentation

Test in documentation code snippet

Sections

Section in documentation code snippet

Generate documentation with:

# '--open' opens generated documentation in the browser
cargo doc --open
Enter fullscreen mode Exit fullscreen mode

Generated Rust documentation

Exercises

Password converter - part 2

Task

Refactor program from the previous week into a library.

Inside converters module, create a Converter trait.

Implement this trait for KasperskyPasswordManager inside converters::kaspersky module.

Write documentation, doc tests and unit tests for the KasperskyPasswordManager.

Make KasperskyPasswordManager generic.

Solution

Password converter lib

Adventure game - part 2

Task

Add fight scenes to the adventure game from the previous week.

There will be an enemy. The enemy has an attack damage range and health.

The player has an attack damage range and health (for each scene separate).

The player chooses an action:

  • attack - a random value in attack damage range is dealt to the enemy. If the enemy dies, the player proceeds to the next scene. Otherwise the enemy attacks back. If the player survives, the options are repeated with updated health. If the player dies, the game is over.
  • run away - go to another scene

Scenes will share code using trait.

Add documentation and tests.

Solution

Adventure game


Check out my learning Rust repo on GitHub!

Discussion

pic
Editor guide