Loading configuration files in Rust
📅 2025-05-11 · ✍️ Bas v.d. Wiel · 🏷 design
Loading and parsing configuration files is a key component of DOSContainer. The chosen file format for configuration is TOML, for simplicity. Rust’s community produced a crate specifically to read and write this format, so yay! But unfortunately for me, this is far from ideal. One of my project’s base principles is that I only want to write code if I absolutely have to. If I can borrow functionality from a community crate, that’s what I’ll do.
To get from a TOML file to a data structure we can use to build a disk image, there’s a number of steps that must be taken:
- Tell the program which file to read.
- Read the contents of the file into a String.
- Parse the String, assuming it’s valid TOML.
- Construct DOSContainer-internal data structures from the TOML.
Things can obviously go wrong at each of these steps, so we handle errors properly along the way. Unfortunately there’s preciously little we can really do if the input file is wrong in some unknown way, so an error usually means we quite with a message.
Tell the program which file to read
Since the first user interface to DOSContainer will be a command line utility, it will need
to accept parameters. I’ve been told that the clap
crate is overkill for that, but it does
seem to work just fine. Ok, so I’m getting a relatively big binary, but it’s what I’m working with
in this phase of the project.
#[derive(Subcommand)]
enum Commands {
/// Build a DOSContainer manifest into a disk image.
Build {
/// Filename of the HwSpec configuration file, this a TOML file.
hwspec: PathBuf,
/// Filename of the build manifest.
manifest: PathBuf,
},
/// Build a Collection of DOSContainer manifests into a library of disk images.
BuildCollection { startdir: PathBuf },
/// Download all dependencies required to host your own DOSContainer collections.
SelfHost {
/// Directory on your local machine that'll hold DOSContainer assets.
docroot: PathBuf,
},
}
This snippet of completely draft code serves to make the Build
command accept two positional
arguments that point to a file. The type PathBuf
ensures that the parameters get treated as
file system paths.
Commands::Build { hwspec, manifest: _ } => {
// Construct a HW Spec from a TOML file
let toml_string = fs::read_to_string(hwspec)?;
let hwspec = HwSpec::from_toml(&toml_string).expect("BOOM!");
println!("{}", hwspec);
Reading the file itself
This incomplete snippet serves to illustrate some of the steps taken. A TOML string toml_string
is
generated by reading the file as a string. If that fails, an error gets thrown by virtue of the ?
operator.
The next line instantiates an actual HwSpec
struct instance. The .expect("BOOM!");
bit is
there to save me the hassle of implementing error handling (this is all throwaway code for now). It will
go out with a boom if something goes wrong. If it succeeds, however, we’ll be the proud owners of a new
HwSpec
instance which tells DOSContainer what the properties of the target PC system are that we’re
building a collection for.
Parsing a string as if it were TOML
Lots of the proverbial magic happens in the from_toml()
method on the HwSpec
struct. Let’s look
a little closer at that.
pub fn from_toml(toml_string: &str) -> Result<Self, HwSpecError> {
let settings = Config::builder()
.add_source(File::from_str(toml_string, FileFormat::Toml))
.build()
.map_err(HwSpecError::ConfigBuild)?;
settings
.try_deserialize::<HwSpec>()
.map_err(HwSpecError::Deserialize)
}
You’ll notice that we’re calling Config::builder()
here. That part comes from the config
crate,
which does just about all of the heavy lifting for me together with serde
. The config
crate was
built specifically for the purpose of handling configuration settings from all sorts of sources. It can handle
files directly, so I’ll probably be refactoring that bit later on. For now, I’m passing in a string that contains
the contents of my file. The source is a File::from_str(toml_string, FileFormat::Toml))
, constructing what
the config
crate calls a ‘file’.
The config
crate constructs a ConfigBuilder
first. If that succeeds, we’ll know that we got a valid TOML
file that was usable to Rust. In the next step we try to ‘deserialize’ the ConfigBuilder
into a HwSpec
struct. Should that fail, we map the error into a HwSpecError::Deserialize
. That’s not exactly very descriptive,
but the error gets handled and we’ll know that something in the TOML file does not line up with what we expect to
see in a HwSpec
.
Proving that this worked
In the Build
command we’re seeing a last line:
println!("{}", hwspec);
This prints out the HwSpec
struct that we got from the TOML file in a human-readable form. If we make it this far,
we can be fully confident that this part of the configuration was ingested correctly.
Next up? The exact same trick, but for Manifest
files.