Introduction
more-config is a crate containing all of the fundamental abstractions for configuration in Rust.
Features
This crate provides the following features:
- default - Abstractions for configuration, including the cmd, env, and mem features
- all - Includes all features
- binder - Bind a configuration to strongly-typed values and structs
- chained - Chain multiple configuration providers
- cmd - Configuration provided by command-line arguments
- env - Configuration provided by environment variables
- derive - Enables full and partial configuration deserialization using serde
- ini - Configuration provided by an *.ini file
- json - Configuration provided by a *.json file
- mem - Configuration provided by in-memory data
- typed - Configuration provided by strongly-typed, in-memory data
- xml - Configuration provided by a *.xml file
- yaml - Configuration provided by a *.yaml file
Contributing
more-config is free and open source. You can find the source code on GitHub
and issues and feature requests can be posted on the GitHub issue tracker.
more-config relies on the community to fix bugs and add features: if you’d like to contribute, please read the
CONTRIBUTING guide and consider opening
a pull request.
License
This project is licensed under the MIT license.
Getting Started
The simplest way to get started is to install the crate using all features.
cargo add more-config --features all
Once you know which configuration sources you want to support, you can limit the features to only the ones you need.
Example
Configuration is a common requirement of virtually any application and can be performed using one or more configuration providers. Configuration providers read configuration data from key-value pairs using a variety of configuration sources:
- Settings files, such as
appsettings.json - Environment variables
- Command-line arguments
- In-memory data structures
- Custom providers
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_in_memory(&[("MyKey", "MyValue")])
.add_json_file("appsettings.json".is().optional())
.add_env_vars()
.add_command_line()
.build()?;
println!("MyKey = {}", config.get("MyKey").unwrap().as_str());
Ok(())
}
Configuration providers that are added later have higher precedence and override previous key settings. For example, if
MyKey is set in both appsettings.json and an environment variable, then the environment variable value is used. If
appsettings.json does not exist or contain MyKey and there is no environment variable for MyKey, then the
in-memory value of MyValue is used. Finally, if command-line argument --MyKey is provided, it overrides all other
values.
Abstractions
The configuration framework contains a common set of traits and behaviors for numerous scenarios.
Configuration
The Configuration struct is the pinnacle of the entire framework. It defines the behaviors to retrieve a configured value or iterate over all key-value pairs, access or traverse child sections, and react to a reload triggered by the underlying configuration source.
pub struct Configuration {
pub fn get(&self, key: &str) -> Option<&str>;
pub fn section(&self, key: impl Into<String>) -> Section<'_>;
pub fn sections(&self) -> Vec<Section<'_>>;
pub fn reload_token(&self) -> impl ChangeToken;
}
The entire configuration can be enumerated as tuples of key/value pairs similar to a HashMap.
Configuration Section
Hierarchical configurations are divided into sections. A configuration Section has its own key and, possibly, a value. A configuration section which does not have a value will always yield an empty string.
pub struct Section<'a> {
pub fn key(&self) -> &str;
pub fn value(&self) -> &str;
pub fn path(&self) -> &str;
pub fn exists(&self) -> bool;
pub fn get(&self, key: &str) -> Option<&str>;
pub fn section(&self, key: &str) -> Section<'a>;
pub fn sections(&self) -> Vec<Section<'a>>
}
A configuration section can also be enumerated as tuples of key/value pairs similar to a HashMap.
Configuration Provider
A configuration Provider is responsible for loading configuration key/value pairs as a collection of Settings. A provider might support automatic reloading and can advertise when a reload has occurred via a reload ChangeToken.
pub trait Provider {
fn name(&self) -> &str;
fn reload_token(&self) -> Box<dyn ChangeToken>;
fn load(&self, settings: &mut Settings) -> config::Result;
}
Configuration Builder
A configuration builder accumulates one or more configuration providers and then builds a Configuration.
pub struct Builder {
pub fn providers(&self) -> impl Iterator<Item = &dyn Provider>;
pub fn add(&mut self, provider: impl Provider + 'static);
pub fn build(&self) -> config::Result<Configuration>;
}
Working With Configuration Data
There are several different ways to work with configuration data. Configuration providers are normalized to a generic key-value pair format, which can then be merged and consumed universally; regardless of the original format.
Hierarchical Configuration Data
The Configuration API reads hierarchical configuration data by flattening the hierarchical data with the use of a delimiter in the configuration keys.
Consider the following appsettings.json file:
{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
},
"MyKey": "My appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information",
"App": "Warning",
"App.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
The following code displays several of the configurations settings:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_json_file("appsettings.json").build()?;
let my_key_value = config.get("MyKey").unwrap();
let title = config.get("Position:Title").unwrap();
let name = config.section("Position").get("Name").unwrap();
let default_log_level = config.get("Logging:LogLevel:Default").unwrap();
println!("MyKey value: {my_key_value}\n\
Title: {title}\n\
Name: {name}\n\
Default Log Level: {default_log_level}");
Ok(())
}
The preferred way to read hierarchical configuration data is using the Options pattern provided by the more-options crate. The section and sections methods are available to isolate sections and children of a section in the configuration data.
Configuration Keys and Values
Configuration keys:
- Are case-insensitive; for example,
ConnectionStringandconnectionstringare treated as equivalent keys - If a key and value is set in more than one configuration providers, the value from the last provider added is used
- Hierarchical keys
- Within the Configuration API, a colon separator (
:) works on all platforms - In environment variables, a colon separator may not work on all platforms. A double underscore,
__, is supported by all platforms and is automatically converted into a colon:
- Within the Configuration API, a colon separator (
- The Binder supports binding arrays to objects using array indices in configuration keys
Configuration values:
- Are strings
- Null values can’t be stored in configuration or bound to objects
Get Value
The get_value and get_value_or_default methods extract a single value from configuration with a specified key and converts it to the specified type.
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_json_file("settings.json").build()?;
let number: Option<u8> = config.get_value("NumberKey").unwrap().unwrap_or(99);
let flag: bool = config.get_value_or_default("Enabled").unwrap();
println!("Number = {number}");
println!("Flag = {flag}");
Ok(())
}
In the preceding code, if NumberKey isn’t found in the configuration, the default value of 99 is used. If Enabled
isn’t found in the configuration, it will default to false, which is the Default::default() for bool.
Section, Sections, and Exists
For the examples that follow, consider the following MySubsection.json file:
{
"section0": {
"key0": "value00",
"key1": "value01"
},
"section1": {
"key0": "value10",
"key1": "value11"
},
"section2": {
"subsection0": {
"key0": "value200",
"key1": "value201"
},
"subsection1": {
"key0": "value210",
"key1": "value211"
}
}
}
Section
section returns a configuration subsection with the specified subsection key.
The following code returns values for section1:
let section = config.section("section1");
println!("section1:key0: {}\n\
section1:key1: {}",
section.get("key0").unwrap(),
section.get("key1").unwrap());
The following code returns values for section2:subsection0:
let section = config.section("section2:subsection0");
println!("section2:subsection0:key0: {}\n\
section2:subsection0:key0: {}",
section.get("key0").unwrap(),
section.get("key1").unwrap());
If a matching section isn’t found, an empty Section is returned.
Sections and Exists
The following code calls sections and returns values for section2:subsection0:
let section = config.section("section2");
if section.exists() {
for subsection in section.sections() {
let key1 = format!("{}:key0", section.key());
let key2 = format!("{}:key1", section.key());
println!("{key1} value: {}\n\
{key2} value: {}",
section.get(&key1).unwrap(),
section.get(&key2).unwrap());
}
} else {
println!("section2 does not exist.");
}
The preceding code uses the exists extension to verify the section exists.
Working With Files
A Provider that is based on a file should support a FileSource:
pub struct FileSource {
pub path: PathBuf,
pub optional: bool,
pub reload_on_change: bool,
}
An optional file means that the path does not need to exist. When reload_on_change is specified, the provider will watch for changes to path and trigger a notification via Provider::reload_token. A file change might trigger before a file has been completely written, which is operating system dependent.
All of the built-in, file-based configuration providers support accepting a FileSource. A file source is most commonly just a file path, but it may include additional configuration features. The FileSourceBuilder struct and FileSourceBuilderExt trait provide several methods of specifying a FileSource and its options in a fluent manner.
use config::{FileSource, prelude::*};
use std::error::Error;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let xml = PathBuf::from("settings.xml");
let config = config::builder()
.add_ini_file(FileSource::new(PathBuf::("prod.cfg.ini"), false, false))
.add_ini_file(FileSource::optional(PathBuf::("dev.cfg.ini")))
.add_xml_file(xml.is().optional())
.add_json_file("settings.json".is().optional().reloadable())
.build()?;
for (key, value) in &config {
println!("{key} = {value}");
}
Ok(())
}
In-Memory Configuration Provider
These features are only available if the mem feature is activated
The mem::Provider uses an in-memory collection as configuration key-value pairs. This is most useful as a default configuration or when providing test values.
The following code adds a memory collection to the configuration system and displays the settings:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_in_memory(&[
("MyKey", "Dictionary MyKey Value"),
("Position:Title", "Dictionary_Title"),
("Position:Name", "Dictionary_Name"),
("Logging:LogLevel:Default", "Warning"),
])
.build()?;
let my_key_value = config.get("MyKey").unwrap();
let title = config.get("Position:Title").unwrap();
let name = config.section("Position").get("Name").unwrap();
let default_log_level = config.get("Logging:LogLevel:Default").unwrap();
println!("MyKey value: {my_key_value}\n\
Title: {title}\n\
Name: {name}\n\
Default Log Level: {default_log_level}");
Ok(())
}
Environment Variable Configuration Provider
These features are only available if the env feature is activated
The env::Provider loads configuration from environment variable key-value pairs.
The : separator doesn’t work with environment variable hierarchical keys on all platforms. __, the double underscore, is:
- Supported by all platforms; for example, the
:separator is not supported by Bash, but__is. - Automatically replaced by a
:
export MyKey="My key from Environment"
export Position__Title=Console
export Position__Name="John Doe"
Call add_env_vars to add environment variables or add_env_vars_with_prefix with a string to specify a prefix for environment variables:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_env_vars_with_prefix("MyCustomPrefix_")
.build()?;
for (key, value) in &config {
println!("{key} = {value}");
}
Ok(())
}
Environment variables set with the MyCustomPrefix_ prefix override the default configuration providers. This includes
environment variables without the prefix. The prefix is stripped off when the configuration key-value pairs are read.
export MyCustomPrefix_MyKey="My key with MyCustomPrefix_ Environment"
export MyCustomPrefix_Position__Title="Custom Editor"
export MyCustomPrefix_Position__Name="Jane Doe"
Naming of Environment Variables
Environment variable names reflect the structure of an appsettings.json file. Each element in the hierarchy is
separated by a double underscore. When the element structure includes an array, the array index should be treated as an
additional element name in this path. Consider the following appsettings.json file and its equivalent values
represented as environment variables.
{
"SmtpServer": "smtp.example.com",
"Logging":
[
{
"Name": "ToEmail",
"Level": "Critical",
"Args":
{
"FromAddress": "MySystem@example.com",
"ToAddress": "SRE@example.com"
}
},
{
"Name": "ToConsole",
"Level": "Information"
}
]
}
export SmtpServer=smtp.example.com
export Logging__0__Name=ToEmail
export Logging__0__Level=Critical
export Logging__0__Args__FromAddress=MySystem@example.com
export Logging__0__Args__ToAddress=SRE@example.com
export Logging__1__Name=ToConsole
export Logging__1__Level=Information
Names can also be all uppercase letters:
export SMTP_SERVER=smtp.example.com
export LOGGING__0__NAME=ToEmail
will transform to StmpServer and Logging:0:Name respectively.
Command-Line Configuration Provider
These features are only available if the cmd feature is activated
The cmd::Provider loads configuration from command-line argument key-value pairs. Configuration values set on the command-line can be used to override configuration values set with all the other configuration providers. When used, it is recommended that this is the last configuration provider added.
Command-line Arguments
The following command sets keys and values using =:
myapp MyKey="Using =" Position:Title=Cmd Position:Name=Cmd_Joe
The following command sets keys and values using /:
myapp /MyKey "Using /" /Position:Title=Cmd /Position:Name=Cmd_Joe
The following command sets keys and values using --:
myapp --MyKey "Using --" --Position:Title=Cmd --Position:Name=Cmd_Joe
The key value:
- Must follow
=, or the key must have a prefix of--or/when the value follows a space - Isn’t required if
=is used; for example,MySetting=
Within the same command, don’t mix command-line argument key-value pairs that use = with key-value pairs that use a space.
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_command_line().build()?;
println!("Name = {}", config.section("Position").get("Name").unwrap());
Ok(())
}
Switch Mappings
Switch mappings allow key name replacement logic. Provide a hash map of switch replacements to the add_command_line_map method.
When the switch mappings hash map is used, the hash map is checked for a key that matches the key provided by a
command-line argument. If the command-line key is found in the hash map, the hash map value is passed back to set the
key-value pair into the application’s configuration. A switch mapping is required for any command-line key prefixed with
a single dash (-).
Switch mappings hash map key rules:
- Switches must start with
-or--. - The switch mappings hash map must not contain duplicate keys.
To use a switch mappings hash map, pass it into the call to add_command_line_map:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let switch_mappings = [
("-k1", "key1"),
("-k2", "key2"),
("--alt3", "key3"),
("--alt4", "key4"),
("--alt5", "key5"),
("--alt6", "key6"),
];
let config = config::builder().add_command_line_map(&switch_mappings).build()?;
for (key, value) in &config {
println!("{key} = {value}");
}
Ok(())
}
Run the following command works to test key replacement:
myapp -k1 value1 -k2 value2 --alt3=value2 /alt4=value3 --alt5 value5 /alt6 value6
The following code shows the key values for the replaced keys:
println!("Key1: {}\n\
Key2: {}\n\
Key3: {}\n\
Key4: {}\n\
Key5: {}\n\
Key6: {}",
config.get("Key1").unwrap(),
config.get("Key2").unwrap(),
config.get("Key3").unwrap(),
config.get("Key4").unwrap(),
config.get("Key5").unwrap(),
config.get("Key6").unwrap());
Filtering
By default, the sequence of arguments provided by std::env::args() is supplied to the cmd::Provider. If custom
filtering is required, no extension method is provided to do so. The setup is still trivial albeit more verbose.
use config::{cmd, prelude::*};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let mut args: Vec<_> = std::env::args().collect();
// TODO: apply filtering
let mut builder = config::builder();
builder.add(cmd::Provider::from(args));
let config = builder.build()?;
for (key, value) in &config {
println!("{key} = {value}");
}
Ok(())
}
JSON Configuration Provider
These features are only available if the json feature is activated
The json::Provider supports loading configuration from a *.json file.
Consider the following appsettings.json file:
{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
},
"MyKey": "My appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information",
"App": "Warning",
"App.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
The following code displays several of the preceding configurations settings:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_json_file("appsettings.json").build()?;
let my_key_value = config.get("MyKey").unwrap();
let title = config.get("Position:Title").unwrap();
let name = config.section("Position").get("Name").unwrap();
let default_log_level = config.get("Logging:LogLevel:Default").unwrap();
println!("MyKey value: {my_key_value}\n\
Title: {title}\n\
Name: {name}\n\
Default Log Level: {default_log_level}");
Ok(())
}
XML Configuration Provider
These features are only available if the xml feature is activated
The xml::Provider supports loading configuration from a *.xml file.
The following code adds several configuration providers, including a couple of *.xml files:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let name = std::env::var("ENVIRONMENT").or_else("production");
let config = config::builder()
.add_xml_file("MyXmlConfig.xml".is().optional())
.add_xml_file(format!("MyXmlConfig.{name}.xml").is().optional())
.add_env_vars()
.add_command_line()
.build()?;
Ok(())
}
The XML configuration files have a few special rules that are different from other configuration providers:
- XML namespaces are not supported on elements or attributes
- The
Nameattribute (case-insensitive) is considered as a surrogate key in lieu of the element it is applied to - Duplicate key-value combinations are ambiguous and not allowed
- Repeating elements with different values are considered array-like
Consider the following configuration file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<MyKey>MyXMLFile Value</MyKey>
<Position>
<Title>Title from MyXMLFile</Title>
<Name>Name from MyXMLFile</Name>
</Position>
<Logging>
<LogLevel>
<Default>Information</Default>
<App>Warning</App>
</LogLevel>
</Logging>
</configuration>
The following code displays several of the preceding configuration settings:
let my_key_value = config.get("MyKey").unwrap();
let title = config.get("Position:Title").unwrap();
let name = config.section("Position").get("Name").unwrap();
let default_log_level = config.get("Logging:LogLevel:Default").unwrap();
println!("MyKey value: {my_key_value}\n\
Title: {title}\n\
Name: {name}\n\
Default Log Level: {default_log_level}");
Repeating elements that use the same element name work if the name attribute is used to distinguish the elements:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<section name="section0">
<key name="key0">value 00</key>
<key name="key1">value 01</key>
</section>
<section name="section1">
<key name="key0">value 10</key>
<key name="key1">value 11</key>
</section>
</configuration>
The following code reads the previous configuration file and displays the keys and values:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_xml_file("MyXmlConfig2.xml").build()?;
let val00 = config.get("section:section0:key:key0").unwrap();
let val01 = config.get("section:section0:key:key1").unwrap();
let val10 = config.get("section:section1:key:key0").unwrap();
let val11 = config.get("section:section1:key:key1").unwrap();
println!("section:section0:key:key0 value: {val00}\n\
section:section0:key:key1 value: {val01}\n\
section:section1:key:key0 value: {val10}\n\
section:section1:key:key1 value: {val11}");
Ok(())
}
If the name attribute were not used, then the elements would be treated as array-like:
section:0:key:0section:0:key:1section:1:key:0section:1:key:1
Attributes can also be used to supply values:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<key attribute="value" />
<section>
<key attribute="value" />
</section>
</configuration>
The previous configuration file loads the following keys with value of value:
key:attributesection:key:attribute
YAML Configuration Provider
These features are only available if the yaml feature is activated
The yaml::Provider supports loading configuration from a *.yaml file.
Consider the following appsettings.yaml file:
Position:
Title: Editor
Name: Joe Smith
MyKey: My appsettings.yaml Value
Logging:
LogLevel:
Default: Information
App: Warning
App.Hosting.Lifetime: Information
AllowedHosts: "*"
The following code displays several of the preceding configurations settings:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_yaml_file("appsettings.yaml").build()?;
let my_key_value = config.get("MyKey");
let title = config.get("Position:Title");
let name = config.section("Position").get("Name");
let default_log_level = config.get("Logging:LogLevel:Default");
println!("MyKey value: {my_key_value}\n\
Title: {title}\n\
Name: {name}\n\
Default Log Level: {default_log_level}");
Ok(())
}
INI Configuration Provider
These features are only available if the ini feature is activated
The ini::Provider supports loading configuration from an *.ini file.
The following code adds several configuration providers, including a couple of *.ini files:
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let name = std::env::var("ENVIRONMENT").or_else("production");
let config = config::builder()
.add_ini_file("MyIniConfig.ini".is().optional())
.add_ini_file(format!("MyIniConfig.{name}.ini").is().optional())
.add_env_vars()
.add_command_line()
.build()?;
Ok(())
}
In the preceding code, settings in the MyIniConfig.ini and MyIniConfig.{Environment}.ini files are overridden by
settings in the:
- Environment variables configuration provider
- Command-line configuration provider
Assume the MyIniConfig.ini file contains:
MyKey="MyIniConfig.ini Value"
[Position]
Title="My INI Config title"
Name="My INI Config name"
[Logging:LogLevel]
Default=Information
App=Warning
The following code displays several of the preceding configurations settings:
let my_key_value = config.get("MyKey").unwrap();
let title = config.get("Position:Title").unwrap();
let name = config.section("Position").get("Name").unwrap();
let default_log_level = config.get("Logging:LogLevel:Default").unwrap();
println!("MyKey value: {my_key_value}\n\
Title: {title}\n\
Name: {name}\n\
Default Log Level: {default_log_level}");
Typed Configuration Provider
These features are only available if the typed feature is activated
The typed::Provider uses a typed data structure as the source input for configuration values. This is most useful as an alternative default configuration to in-memory configuration, which uses loosely defined key/value pairs. This is also useful when providing test values.
The following code adds a data structure to the configuration system and displays the settings:
use config::prelude::*;
use serde::{Deserialize, Serialize};
use std::error::Error;
#[derive(Clone, Default, Deserialize, Serialize)]
struct PerfSettings {
cores: u8,
}
#[derive(Clone, Default, Deserialize, Serialize)]
struct AppOptions {
title: String,
perf: PerfSettings,
}
fn main() -> Result<(), Box<dyn Error + 'static>> {
let default = AppOptions {
title: String::from("Banana processor"),
perf: SubOptions{ cores: 7 },
};
let config = config::builder().add_typed(default).build()?;
let title = config.get("Title").unwrap();
let cores = config.get("Perf:Cores").unwrap();
println!("Title: {title}\n\
Cores: {cores}");
Ok(())
}
Although bare primitives and collections of primitives are serializable, they are not supported because there is no
corresponding key which would otherwise be derived from an associated complex structure. This limitation only applies
to the root value. Nested data structures can be primitives, tuples, and so on. The one exception is a typed
HashMap<String, _> because keys are present; however, this is only slightly better than using the
in-memory provider.
Chained Configuration Provider
These features are only available if the chained feature is activated
Although it is not a very common usage scenario, you may encounter a scenario where you need to chain multiple configurations from different sources into a unified configuration. A practical example would be distinct configurations defined by different crates.
Let’s assume that crate1 defines its default configuration as:
use config::{Configuration, Result, prelude::*};
fn default_config() -> Result<Configuration> {
config::builder()
.add_in_memory(&[("Mem1:KeyInMem1", "ValueInMem1")])
.add_in_memory(&[("Mem2:KeyInMem2", "ValueInMem2")])
.build()
}
Let’s assume that crate2 defines its default configuration as:
use config::{Configuration, Result, prelude::*};
fn default_config() -> Result<Configuration> {
config::builder()
.add_in_memory(&[("Mem3:KeyInMem3", "ValueInMem3")])
.build()
}
An application can now compose the crate1 and crate2 configurations into its own configuration.
use config::prelude::*;
use crate1;
use crate2;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_configuration(crate1::default_config()?)
.add_configuration(crate2::default_config()?)
.add_env_vars()
.add_command_line()
.build()?;
println!("Mem1:KeyInMem1 = {}", config.get("mem1:keyinmem1").unwrap());
println!("Mem2:KeyInMem2 = {}", config.get("Mem2:KeyInMem2").unwrap());
println!("Mem3:KeyInMem3 = {}", config.get("MEM3:KEYINMEM3").unwrap());
Ok(())
}
This configuration would output:
Mem1:KeyInMem1 = ValueInMem1
Mem2:KeyInMem2 = ValueInMem2
Mem3:KeyInMem3 = ValueInMem3
Data Binding
Data binding requires the binder feature, which will also trigger activation of the optional serde dependency and is required for deserialization.
Data binding leverages the serde crate to enable deserializing configurations in part, or in whole, into strongly-typed structures. It is also possible to retrieve strongly-typed scalar values.
A Configuration is deserialized through the Binder trait:
pub trait Binder {
fn reify<T: DeserializeOwned>(&self) -> config::Result<T>;
fn reify_unchecked<T: DeserializeOwned>(&self) -> T;
fn bind<T: DeserializeOwned>(&self, instance: &mut T) -> config::Result;
fn bind_unchecked<T: DeserializeOwned>(&self, instance: &mut T);
fn bind_at<T>(&self, key: impl AsRef<str>, instance: &mut T) -> config::Result
where
T: DeserializeOwned;
fn bind_at_unchecked<T>(&self, key: impl AsRef<str>, instance: &mut T)
where
T: DeserializeOwned;
fn get_value<T: FromStr>(&self, key: impl AsRef<str>) -> Result<Option<T>, T::Err>;
fn get_value_or_default<T>(&self, key: impl AsRef<str>) -> Result<T, T::Err>
where
T: FromStr + Default;
}
Consider the following struct:
use serde::Deserialize;
#[derive(Default, Deserialize)]
struct ContactOptions {
name: String,
primary: bool,
phones: Vec<String>,
}
Configuration keys are normalized or expected to otherwise be Pascal Case for consistency.
The following demonstrates how to load a configuration and then reify the configuration into the struct that was defined above. This example used the in-memory configuration provider, but any configuration provider or multiple configuration providers can be used.
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_in_memory(&[
("name", "John Doe"),
("primary", "true"),
("phones:0", "+44 1234567"),
("phones:1", "+44 2345678"),
])
.build()?;
let primary: bool = config.get_value_or_default("primary")?;
let options: ContactOptions = config.reify()?;
println!("Is Primary: {primary}");
println!("{}", &options.name);
println!("Phones:");
for phone in &contact.phones {
println!("\n {phone}");
}
Ok(())
}
It is also possible to bind an existing structure to an entire configuration or bind at a specific configuration section.
use config::prelude::*;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_in_memory(&[
("name", "John Doe"),
("primary", "true"),
("phones:0", "+44 1234567"),
("phones:1", "+44 2345678"),
])
.build()?;
let mut options = ContactOptions::default();
config.bind(&mut options)?;
println!("{}", &options.name);
println!("Phones:");
for phone in &contact.phones {
println!("\n {phone}");
}
Ok(())
}
Note: The bound struct must implement
Deserialize::deserialize_in_placeto perform a true, in-place update. The default implementation creates a new struct and binds to it, which is essentially the same as mutating the struct to the result of reify.
Partial Updates
In order to support partial updates, a drop-in replace is provided using the config::Deserialize derive macro. The
implementation has parity with most the serde deserialization capabilities with added support for
Deserialize::deserialize_in_place.
This feature is a meant to support configuration and is not an all purpose deserialization mechanism. If you have
additional deserialization requirements consider providing an implementation for Deserialize::deserialize_in_place or
use another technique such as an ephemeral struct that can accept the deserialized configuration values and then merge
that to your general purpose struct.
use config::{Deserialize, prelude::*};
use std::error::Error;
#[derive(Default, Deserialize)] // ← config::Deserialize instead of serde::Deserialize
struct ContactOptions {
name: String,
primary: bool,
phones: Vec<String>,
}
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder()
.add_in_memory(&[
("name", "John Doe"),
("phones:0", "+44 1234567"),
])
.build()?;
let mut options = ContactOptions::default();
let before = &mut options as *mut _;
config.bind(&mut options)?;
let after = &mut options as *mut _;
// verify the pointer wasn't replaced
assert_eq!(before, after, "options was replaced");
println!("{}", &options.name);
println!("Phones:");
for phone in &contact.phones {
println!("\n {phone}");
}
Ok(())
}
Partial data binding requires the derive feature, which will also trigger activation of the optional binder feature.
Bind an Array
bind supports binding arrays to objects using array indices in configuration keys.
Consider MyArray.json:
{
"array": {
"entries": {
"0": "value00",
"1": "value10",
"2": "value20",
"4": "value40",
"5": "value50"
}
}
}
The following code reads the configuration and displays the values:
use config::prelude::*;
use serde::Deserialize;
use std::error::Error;
#[derive(Default, Deserialize)]
struct ArrayExample {
entries: Vec<String>,
}
fn main() -> Result<(), Box<dyn Error + 'static>> {
let config = config::builder().add_json_file("MyArray.json").build()?;
let array: ArrayExample = config.reify()?;
for (i, item) in array.entries.iter().enumerate() {
println!("Index: {i}, Value: {item}");
}
Ok(())
}
The preceding code returns the following output. Note that index 3 has the value value40, which corresponds to
"4": "value40" in MyArray.json. The bound array indices are continuous and not bound to the configuration key index.
The configuration binder isn’t capable of binding null values or creating null entries in bound objects; however, a
missing value can be mapped to Option.
Index: 0 Value: value00
Index: 1 Value: value10
Index: 2 Value: value20
Index: 3 Value: value40
Index: 4 Value: value50
Troubleshooting
Multiple sources of configuration information can be difficult and confusing to track down why a particular configured value is the value that it is. Several diagnostic capabilities are provided to help you trace how a value was configured.
Source tracing is supported for up to 8 configuration providers. The limit of 8 is a design choice to keep the cost of tracing low. It’s unlikely an application would have more than 8 configuration providers at a time.
Tracing
The configuration system provides limited support for the tracing crate.
Configuration Reload
Whenever a configuration reloads, it will log a Level::TRACE message indicating a reload has
occurred. If the reload fails, a Level::ERROR message will be recorded with the error details so that background
failures can be observed and triaged.
Whenever an existing configuration value is overridden by another configuration provider, a Level::TRACE message will
log the key, old value, new value, who provided the old value, and who override the key with a new value. This can
help identify unexpected configuration resolution in an application.
Formatting
A configuration can also be inspected by simply printing it out. The implementation of the Display
trait will output the configuration hierarchy section by section. The implementation also supports an
alternate output form that includes the corresponding providers associated with a configured value. The default
alternate behavior expands all providers that set the configuration value. The number of providers that are output can
be controlled by the formatting width. The configuration providers that are output are ranked most recent to oldest. A
value of 0, indicates the last effective provider for a configuration value.
Default Output
The default output using format!("{config}") has the hierarchical format:
Section1:
Key1 = Value1
Section2:
Key2 = Value2
Show Providers
The alternate output using format!("{config:#}") has the format:
Section1:
Key1 = Value1 (Memory)
Section2:
Key2 = Value2 (Memory → overrides.json)
Show Effective Provider Only
The alternate and width output using format!("{config:#1}") has the format:
Section1:
Key1 = Value1 (Memory)
Section2:
Key2 = Value2 (overrides.json)