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