Why do we need configuration? Creating and handling configuration files in Rust 📝
Tags: rust, programming, tutorial, beginners
Published: 2022-11-08 05:29:25

Configuration is nearly always a necessity when it comes to both public and private software.

Just imagine, you install a program that plays a sound every 30 seconds. Then suddenly your needs shift, and now you need it to play every 15 seconds. Oh no! This program doesn't allow you to change the rate of which the sound plays. You are now left with two choices:

1. Edit the program yourself, which could take a lot of time
2. Find another program which allows you to configure the interval of the sound

From a developer standpoint, if configuration were present in this situation, fewer users would be turned away from the lack of flexibility in our software.

### NOTE

Level: **Beginner-Intermediate**

In this project we'll use the [toml](https://crates.io/crates/toml) crate. When it comes to handling configurations, this is not your only option. Take a look at these popular crates:

- [serde_json](https://crates.io/crates/serde_json)
- [serde_yaml](https://crates.io/crates/serde_yaml)
- [quick-xml](https://crates.io/crates/quick-xml)

# Getting started

## Creating the Cargo project

In your terminal, run the following command:

```txt
cargo init <PROJECT_NAME>
```

## Adding dependencies

For our project we will need two dependencies:

- serde
- toml

Add the following to your `Cargo.toml`:

```toml
serde = { version = "1.0.147", features = ["derive"] }
toml = "0.5.9"
```

# Defining our config structure

## What will our program do?

For this example, our program will print a given word N amount of times.<br>
Our target config will look like this:

```toml
word = "Banana!"
repeat = 3
```

And the output:

```txt
Banana!
Banana!
Banana!
```

## Definition

First, import the `Serialize` and `Deserialize` traits from the `serde` crate.<br>
<sub>NOTE: This will only work if you have the `derive` feature enabled for `serde`. We added this feature earlier.</sub>

Create your struct and name it something along the lines of `AppConfig`. This struct will store the values for your config.

Above the struct definition, derive the `Serialize` and `Deserialize` traits we just imported.

```rust
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct AppConfig {
    word: String,
    repeat: usize,
}
```

But there's a problem with our code, we don't provide the user with default values. We can fix that easily by implementing `Default` for our struct.

```rust
impl Default for AppConfig {
    fn default() -> Self {
        Self {
            word: "Banana".to_string(),
            repeat: 5
        }
    }
}
```

Great! We've finished defining the structure of our configuration. It's **that easy**.

# Loading and initializing the configuration file

## Creating the `ConfigError` enum

When trying to load the configuration, two errors can occur:

- An IO error
- The configuration is invalid

We will create an enum which covers both of these error scenarios.<br>
This is necessary, because you want to inform the user of what's going wrong, instead of leaving them scratching their head.

```rust
use std::io;

enum ConfigError {
    IoError(io::Error),
    InvalidConfig(toml::de::Error)
}

// These implementations allow us to use the `?` operator on functions that
// don't necessarily return ConfigError.
impl From<io::Error> for ConfigError {
    fn from(value: io::Error) -> Self {
        Self::IoError(value)
    }
}

impl From<toml::de::Error> for ConfigError {
    fn from(value: toml::de::Error) -> Self {
        Self::InvalidConfig(value)
    }
}
```

## Definition

With the error enum defined, we're able to create a function to either load or initialize the configuration file.

The function will go as follows:

If the config file exists, read the content of the file, parse the TOML, then return the `AppConfig`.

If the file does not exist, save the default configuration to the file.

The `toml` crate provides two methods which provide us the functionality we need:

- `toml::from_str(&S)`
- `toml::to_string(&T)`

```rust
use std::path::Path;
use std::{fs, io};

fn load_or_initialize() -> Result<AppConfig, ConfigError> {
    let config_path = Path::new("Config.toml");

    if config_path.exists() {
        // The `?` operator tells Rust, if the value is an error, return that error.
        // You can also use the `?` operator on the Option enum.

        let content = fs::read_to_string(config_path)?;
        let config = toml::from_str(&content)?;

        return Ok(config);
    }

    // The config file does not exist, so we must initialize it with the default values.
    
    let config = AppConfig::default();
    let toml = toml::to_string(&config).unwrap();

    fs::write(config_path, toml)?;
    Ok(config)
}
```

# Implementing the program functionality

Implementing the program's actual functionality is pretty simple.<br>
First, load the configuration (or handle the errors), then continue with the program logic.

```rust
fn main() {
    let config = match load_or_initialize() {
        Ok(v) => v,
        Err(err) => {
            match err {
                ConfigError::IoError(err) => {
                    eprintln!("An error occurred while loading the config: {err}");
                }
                ConfigError::InvalidConfig(err) => {
                    eprintln!("An error occurred while parsing the config:");
                    eprintln!("{err}");
                }
            }
            return;
        }
    };

    for _ in 0..config.repeat {
        println!("{}", config.word);
    }
}
```

## Output

```txt
Hello readers!
Hello readers!
Hello readers!
Hello readers!
Hello readers!
```

## `Config.toml`

```toml
word = "Hello readers!"
repeat = 5
```

That's it! We've successfully implemented configuration for our program.

Note that this solution is extremely scalable. With the power of `serde`, we can make extremely complex configuration files.

### Conclusion

That's it for this article! Make sure to follow me for more well-written articles like this.