Rocket - Single counter in a plain text file

Rocket web get testing env env::set_var env::var

This counter example is part of the Rocket series.

Dependencies

Besides Rocker we also use the tempdir crate to create a temporary directory for our data file.

examples/rocket/single-counter-in-text-file/Cargo.toml

[package]
name = "single-counter-in-text-file"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5"
tempdir = "0.3"

The main code

The code from the example on the web all set the return value of the route function to be &'static str. In order to make this example work I had to change that to String.

We use a file called "counter.txt" in the current working directory, which, if we run the application using cargo run then is the same as the folder of the crate. We allow the user (who runs the application) to override this by setting an environment variable called COUNTER_PATH. This will be used in the tests to set the location of the "counter.txt" in some temporary folder.

examples/rocket/single-counter-in-text-file/src/main.rs

#[macro_use]
extern crate rocket;

use std::fs::File;
use std::io::Write;

#[get("/")]
fn index() -> String {
    let file = match std::env::var("COUNTER_PATH") {
        Ok(val) => std::path::PathBuf::from(val),
        Err(_) => {
            let current_dir = std::env::current_dir().unwrap();
            current_dir.join("counter.txt")
        }
    };

    let counter: u32 = if file.exists() {
        std::fs::read_to_string(&file)
            .unwrap()
            .trim_end()
            .parse()
            .unwrap()
    } else {
        0
    };
    let counter = counter + 1;

    let mut fh = File::create(file).unwrap();
    writeln!(&mut fh, "{}", counter).unwrap();

    format!("Counter is {}", counter)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod tests;

The test

In the test we create a temporary folder and we place the "counter.txt" file there. This will ensure that running the test does not interfere with the application. We set the environment variable COUNTER_PATH to this path that is consulted in the application itself.

We send two get requests to see the counter increasing.

examples/rocket/single-counter-in-text-file/src/tests.rs

use rocket::http::Status;
use rocket::local::blocking::Client;
use tempdir::TempDir;

#[test]
fn test_counte() {
    let tmp_dir = TempDir::new("counter").unwrap();
    std::env::set_var("COUNTER_PATH", tmp_dir.path().join("counter.txt"));

    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.into_string(), Some("Counter is 1".into()));

    let response = client.get("/").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.into_string(), Some("Counter is 2".into()));
}

Error handling - unwrap

In this example I did not deal much with error handling and called unwrap in many places. This was done to be able to focus on a working example without the extra code to deal with the - in this case - unlikely events. If you are interested in improving the error handling check out the article about unwrap.

No atomic file operation

Anothe issue with this solution is that the read and write operations on the file are not atomic. In a more real-world situation we would need to make sure that two requests can't update the counter at the same time.

Related Pages

Rocket - web development with Rust
Rocket - multi-counter using cookies

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo