One simple and maintainable way to store information in a filesystem is to use a text file. This is also very efficient for data spanning no more than 100 KB. However, there are several competing standards for storing information in text files, such as INI, CSV, JSON, XML, YAML, and others.
The one used by Cargo is TOML. This is a really powerful format that is used by many Rust developers to store the configuration data of their apps. It is designed to be written by hand, using a text editor, but it can also be written by an application very easily.
The toml_dynamic and toml_static projects (using the toml crate) load data from a TOML file. Reading a TOML file is useful when configuring a software application, and this is what we'll do. We will use the data/config.toml file, which contains all of the parameters for the projects of this chapter.
You can also create or modify a TOML file by using code, but we are not going to do that. Being able to modify a TOML file can be useful in some scenarios, such as to save user preferences.
It is important to consider that when a TOML file is changed by a program, it undergoes dramatic restructuring:
- It acquires specific formatting, which you may dislike.
- It loses all of its comments.
- Its items are sorted alphabetically.
So, if you want to use the TOML format both for manually edited parameters and for program-saved data, you would be better off using two distinct files:
- One edited only by humans
- One edited primarily by your software, but occasionally also by humans
This chapter describes two projects in which a TOML file is read using different techniques. These techniques are to be used in two different cases:
- In a situation where we are not sure which fields are contained in the file, and so we want to explore it. In this case, we use the toml_dynamic program.
- In another situation where, in our program, we describe exactly which fields should be contained in the file and we don't accept a different format. In this case, we use the toml_static program.
Using toml_dynamic
The purpose of this section is to read the config.toml file, located in the data folder, when we want to explore the content of that file. The first three lines of this file are as follows:
[input]
xml_file = "../data/sales.xml"
json_file = "../data/sales.json"
After these lines, the file contains other sections. Among them is the [postgresql] section, which contains the following line:
database = "Rust2018"
To run this project, enter the toml_dynamic folder and type in cargo run ../data/config.toml. A long output should be printed. It will begin with the following lines:
Original: Table(
{
"input": Table(
{
"json_file": String(
"../data/sales.json",
),
"xml_file": String(
"../data/sales.xml",
),
},
),
Notice that this is just a verbose representation of the first three lines of the config.toml file. This output proceeds with emitting a similar representation for the rest of the file. After having printed the whole data structure representing the file that is read, the following line is added to the output:
[Postgresql].Database: Rust2018
This is the result of a specific query on the data structure loaded when the file is read.
Let's look at the code of the toml_dynamic program:
- Declare a variable that will contain a description of the whole file. This variable is initialized in the next three statements:
let config_const_values =
- We add the pathname of the file from the first argument in the command line to config_path. Then, we load the contents of this file into the config_text string and we parse this string into a toml::Value structure. This is a recursive structure because it can have a Value property among its fields:
{
let config_path = std::env::args().nth(1).unwrap();
let config_text =
std::fs::read_to_string(&config_path).unwrap();
config_text.parse::<toml::Value>().unwrap()
};
- This structure is then printed using the debug structured formatting (:#?), and a value is retrieved from it:
println!("Original: {:#?}", config_const_values);
println!("[Postgresql].Database: {}",
config_const_values.get("postgresql").unwrap()
.get("database").unwrap()
.as_str().unwrap());
Notice that to get the value of the "database" item contained the "postgresql" section, a lot of code is required. The get function needs to look for a string, which may fail. That is the price of uncertainty.
Using toml_static
On the other hand, if we are quite sure of the organization of our TOML file, we should use another technique shown in the project, toml_static.
To run it, open the toml_static folder and type in cargo run ../data/config.toml. The program will only print the following line:
[postgresql].database: Rust2018
This project uses two additional crates:
- serde: This enables the use of the basic serialization/deserialization operations.
- serde_derive: This provides a powerful additional feature known as the custom-derive feature, which allows you to serialize/deserialize using a struct.
serde is the standard serialization/deserialization library. Serialization is the process of converting data structures of the program into a string (or a stream). Deserialization is the reverse process; it is the process of converting a string (or a stream) into some data structures of the program.
To read a TOML file, we need to use deserialization.
In these two projects, we don't need to use serialization as we are not going to write a TOML file.
In the code, first, a struct is defined for any section contained in the data/config.toml file. That file contains the Input, Redis, Sqlite, and Postgresql sections, and so we declare as many Rust structs as the sections of the file we want to read; then, the Config struct is defined to represent the whole file, having these sections as members.
For example, this is the structure for the Input section:
#[allow(unused)]
#[derive(Deserialize)]
struct Input {
xml_file: String,
json_file: String,
}
Notice that the preceding declaration is preceded by two attributes.
The allow(unused) attribute is used to prevent the compiler from warning us about unused fields in the following structure. It is convenient for us to avoid these noisy warnings. The derive(Deserialize) attribute is used to activate the automatic deserialization initiated by serde for the following structure.
After these declarations, it is possible to write the following line of code:
toml::from_str(&config_text).unwrap()
This invokes the from_str function, which parses the text of the file into a struct. The type of that struct is not specified in this expression, but its value is assigned to the variable declared in the first line of the main function:
let config_const_values: Config =
So, its type is Config.
Any discrepancies between the file's contents and the struct type will be considered an error in this operation. So, if this operation is successful, any other operation on the structure cannot fail.
While the previous program (toml_dynamic) had a kind of dynamic typing, such as that of Python or JavaScript, this program has a kind of static typing, similar to Rust or C++.
The advantage of static typing appears in the last statement, where the same behavior as the long statement of the previous project is obtained by simply writing config_const_values.postgresql.database.