Rocket - access custom configuration in the routes

Rocket info! debug rocket::Config figment extract_inner

The Rocket web framework configuration system allows you to override the default values and provide additional keys and values in the Rocket.toml file located in the root directory of the project.

There is a whole page about Rocket configuration, At first I could not fully understand it, but I got help.

In this example we'll see how we can access custom configuration values in the routes.

Dependencies

examples/rocket/configuration/Cargo.toml

[package]
name = "configuration"
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"
serde = "1.0.196"


The Rocket.toml configuration file

examples/rocket/configuration/Rocket.toml

[default]
custom_in_default = "hi"

[debug]
custom_a = "Hello World!"

[release]
log_level = "normal"
custom_b = "Special value for b"

Logging the configuration value

In this example we use the logging facility of Rocket we have already seen.

Get the profile field of the configuration. This will be debug when we run cargo run (or cargo test) and it should be release when we run cargo run --release or cargo test --release. We compare it to the debug string, so this should print true in debug mode and false in release mode. This is how we can check in a given field contains a specific value we are expecting.

rocket::info!("profile is debug: {:?}", rocket::Config::default().profile == "debug");

Custom configuration fields

Besides the configuration fields that Rocket defined we can add our own fields to Rocket.toml and we can access them in the routes.

In the main route you will see the recommended way. I also included a route called /bad where I show the "brilliant", but apparently not too good solution I had in the original version of this post.

The recommended way

  1. Creat a struct that defines the fields we are expecting with the types of the fields and optionally add default values to each field.

  2. Make the struct Deserialize-able using serde.

We have this, the name of the struct does not matter.

use serde::Deserialize;

#[derive(Deserialize)]
struct MyConfig {
    #[serde(default = "get_default_custom_in_default_section")]
    custom_in_default: String,

    #[serde(default = "get_default_custom_a")]
    custom_a: String,

    #[serde(default = "get_default_custom_b")]
    custom_b: String,
}

fn get_default_custom_in_default_section() -> String {
    String::from("some other default")
}

fn get_default_custom_a() -> String {
    String::from("some default for a")
}

fn get_default_custom_b() -> String {
    String::from("some default for b")
}
  1. Tell Rocket to include the AdHoc configuration by attaching it:

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, bad, defaults])
        .attach(AdHoc::config::<MyConfig>())
}
  1. In the routes where we would like to access the configurations add it to the list of parameters:
#[get("/")]
fn index(config: &State<MyConfig>) -> &'static str {
  1. In the route access it via the function parameter

rocket::info!("custom_a {:?}", config.custom_a);

A bad implementation

Now look a the route handline the /bad path:

In the first example we define the type on the variable (and hence we really need a variable here). We also used unwrap_or to set a default value in case the field is not in Rocket.toml. There are several other unwrap-like methods for other ways to handle this situation.

let custom_a: String = rocket::Config::figment().extract_inner("custom_a").unwrap_or(String::from("some default"));
rocket::info!("home {:?}", custom_a);

In this example we used the Turbofish syntax to tell Rust what is the expected type of the field. As we did add this field to the Rocket.toml you will see that the value we get is the "some default".

let custom_b = rocket::Config::figment().extract_inner::<String>("custom_b").unwrap_or(String::from("some default"));
rocket::info!("home {:?}", custom_b);

Finally I just wanted to see all the configuration fields in one blob:

rocket::info!("default: {:#?}", rocket::Config::default());

Running the app seeing the logs

We can now run the app using

cargo run

or

cargo run --release

and as we access http://localhost:8000/ we can see the log messages on the console where we executed the cargo run ... command.

We can then also access http://localhost:8000/bad to see the results provided by the "bad" implementation and http://localhost:8000/defaults to see the defaults or Rocket.

Running the test seeing the logs

We don't even need to run the app to see the log messages. We can see them while running the tests.

By default if we run cargo test it will hide the log messages generated by Rocket. We can pass the --nocapture flag and then it will print all the logs as it was discussed when we wanted to know how to show the standard output and standard error in tests in Rust.

cargo test -- --nocapture

It is very colorful, but we are interested only what we printed.

The first 3 lines correspond to the first 3 calls to log::info! The first part of this whole output is the cull configurations this corresponds to our first call of log::info!.

profile is debug: true
custom_a "Hello World!"
custom_b "some default for b"
custom_in_default "hi"

The /defaults route will print this to the console.

default: Config {
    profile: Profile(
        Uncased {
            string: "debug",
        },
    ),
    address: 127.0.0.1,
    port: 8000,
    workers: 16,
    max_blocking: 512,
    ident: Ident(
        Some(
            "Rocket",
        ),
    ),
    ip_header: Some(
        Uncased {
            string: "X-Real-IP",
        },
    ),
    limits: Limits {
        limits: [
            (
                Uncased {
                    string: "bytes",
                },
                ByteUnit(
                    8192,
                ),
            ),
            (
                Uncased {
                    string: "data-form",
                },
                ByteUnit(
                    2097152,
                ),
            ),
            (
                Uncased {
                    string: "file",
                },
                ByteUnit(
                    1048576,
                ),
            ),
            (
                Uncased {
                    string: "form",
                },
                ByteUnit(
                    32768,
                ),
            ),
            (
                Uncased {
                    string: "json",
                },
                ByteUnit(
                    1048576,
                ),
            ),
            (
                Uncased {
                    string: "msgpack",
                },
                ByteUnit(
                    1048576,
                ),
            ),
            (
                Uncased {
                    string: "string",
                },
                ByteUnit(
                    8192,
                ),
            ),
        ],
    },
    temp_dir: RelativePathBuf {
        metadata_path: None,
        path: "/tmp",
    },
    keep_alive: 5,
    shutdown: Shutdown {
        ctrlc: true,
        signals: {
            Term,
        },
        grace: 2,
        mercy: 3,
        force: true,
        __non_exhaustive: (),
    },
    log_level: Normal,
    cli_colors: true,
    __non_exhaustive: (),
}

The code full code

examples/rocket/configuration/src/main.rs

#[macro_use]
extern crate rocket;

use rocket::{fairing::AdHoc, State};
use serde::Deserialize;

#[derive(Deserialize)]
struct MyConfig {
    #[serde(default = "get_default_custom_in_default_section")]
    custom_in_default: String,

    #[serde(default = "get_default_custom_a")]
    custom_a: String,

    #[serde(default = "get_default_custom_b")]
    custom_b: String,
}

fn get_default_custom_in_default_section() -> String {
    String::from("some other default")
}

fn get_default_custom_a() -> String {
    String::from("some default for a")
}

fn get_default_custom_b() -> String {
    String::from("some default for b")
}

#[get("/")]
fn index(config: &State<MyConfig>) -> &'static str {
    rocket::info!(
        "profile is debug: {:?}",
        rocket::Config::default().profile == "debug"
    );

    rocket::info!("custom_a {:?}", config.custom_a);

    rocket::info!("custom_b {:?}", config.custom_b);

    rocket::info!("custom_in_default {:?}", config.custom_in_default);

    "See the console"
}

#[get("/bad")]
fn bad() -> &'static str {
    rocket::info!(
        "profile is debug: {:?}",
        rocket::Config::default().profile == "debug"
    );

    let custom_a: String = rocket::Config::figment()
        .extract_inner("custom_a")
        .unwrap_or(String::from("some default in a"));
    rocket::info!("custom_a {:?}", custom_a);

    let custom_b = rocket::Config::figment()
        .extract_inner::<String>("custom_b")
        .unwrap_or(String::from("some default in b"));
    rocket::info!("custom_b {:?}", custom_b);

    let custom_in_default: String = rocket::Config::figment()
        .extract_inner("custom_in_default")
        .unwrap_or(String::from("some other default"));
    rocket::info!("custom_in_default {:?}", custom_in_default);

    "See the console"
}

#[get("/defaults")]
fn defaults() -> &'static str {
    rocket::info!("default: {:#?}", rocket::Config::default());

    "See the console"
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, bad, defaults])
        .attach(AdHoc::config::<MyConfig>())
}

#[cfg(test)]
mod test {
    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn home() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
    }
}

Related Pages

Rocket - web development with Rust

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