1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Choosing the engine and the reader — the two seams a load can swap.
//!
//! ```text
//! cargo run -p dynamic-config --example engines --features json,yaml
//! cargo run -p dynamic-config --example engines --features json,yaml,figment
//! ```
//!
//! The **engine** folds the collected layers into one configuration; the
//! **reader** turns a document's text into a tree. Everything else — which
//! files are looked for, what the environment contributes, precedence,
//! provenance, the snapshot, the reload — is this crate's either way.
//!
//! Run it with and without `--features figment`: the same numbers come out,
//! which is the point. The engines are held to one merge rule leaf by leaf
//! by the crate's own tests; the readers are *not* interchangeable down to
//! the corner, and this prints the one case where that shows.
use dynamic_config::{engine, load, reader, Format, LoadSpec, Source};
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Database {
host: String,
port: u16,
pool: Pool,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Pool {
max: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Three layers, in precedence order: later wins, tables descend into
// each other, everything else replaces.
let sources = [
Source::inline(
r#"{"db": {"host": "localhost", "port": 5432, "pool": {"max": 8}}}"#,
Format::Json,
),
Source::inline(r#"{"db": {"host": "db.internal"}}"#, Format::Json),
Source::inline(r#"{"db": {"pool": {"max": 32}}}"#, Format::Json),
];
// ---------------------------------------------------------------------
// The engine. `engine::all()` is the registry — one entry per engine
// compiled in, which is `config-rs` alone until the `figment` feature
// turns its adapter on.
// ---------------------------------------------------------------------
let mut answers = Vec::new();
for engine in engine::all() {
let database: Database = load(&LoadSpec::new("db", &sources).with_engine(engine))?;
println!("{:<10} {:?}", engine.name(), database);
answers.push(database);
}
// Not a hope: the crate's own agreement tests assert exactly this over
// three corpora, which is why the choice is a dependency question
// rather than a semantic one.
assert!(answers.windows(2).all(|pair| pair[0] == pair[1]));
// A load that names no engine gets the default, `config-rs`. Naming one
// per load is a `LoadSpec` field here and `Builder::engine` with the
// macro; `engine::set_engine` decides it once for the process.
// ---------------------------------------------------------------------
// The reader. Unlike the engines, these are dialects — so the crate
// defaults to its own and the difference is documented rather than
// smoothed over.
// ---------------------------------------------------------------------
let yaml = "db:\n host: db.internal\n port: 5432\n pool:\n max: 32\n";
for reader in reader::all() {
// figment's reader does not read `.properties`, this crate's own
// does not read RON — a reader that cannot read a format hands it
// on rather than failing the load.
let database: Database =
load(&LoadSpec::new("db", &[Source::inline(yaml, Format::Yaml)]).with_reader(reader))?;
println!("{:<10} {:?}", reader.name(), database);
}
// A format the chosen reader has no parser for is handed to one that
// has. `.properties` is the case that matters: neither backend crate
// ships a parser for it, and choosing one of them still reads it —
// through this crate's parser, so the answer is the same too.
let properties = "db.host = db.internal\ndb.port = 5432\ndb.pool.max = 32\n";
for reader in reader::all() {
let database: Database = load(
&LoadSpec::new("db", &[Source::inline(properties, Format::Properties)])
.with_reader(reader),
)?;
println!("{:<10} .properties {:?}", reader.name(), database);
}
// The reason to reach for a backend's reader is usually YAML: this
// crate's own is `serde_yaml`, which its author archived, and
// `config-rs` brings `yaml-rust2`, which is maintained.
let maintained: Database = load(
&LoadSpec::new("db", &[Source::inline(yaml, Format::Yaml)])
.with_reader(reader::config_rs()),
)?;
assert_eq!(maintained.pool.max, 32);
// Where they differ, and why the default is this crate's own: a
// top-level key with a dot in it is a *key* here, and a *path* to one
// of the backends — the same three bytes, two documents.
let dotted = r#"{"db": {"my.module": "debug"}}"#;
#[derive(Deserialize)]
struct Logging {
#[serde(rename = "my.module")]
module: Option<String>,
}
let native: Logging = load(
&LoadSpec::new("db", &[Source::inline(dotted, Format::Json)]).with_reader(reader::native()),
)?;
println!(
"\ndotted key, this crate's reader: {:?}",
native.module.as_deref()
);
Ok(())
}