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
//! A JSON Schema for the config files, so an editor completes and checks them.
//!
//! ```text
//! cargo run -p dynamic-config --example schema --features schema,json
//! ```
//!
//! The point is what the schema describes: not a struct, but a *file*. Two
//! config types share `app.json` here, and one emitted schema covers both.
use dynamic_config::dynamic_config;
use schemars::JsonSchema;
use serde::Deserialize;
#[dynamic_config]
#[derive(Deserialize, JsonSchema)]
struct DbConfig {
/// Where the database lives.
#[allow(dead_code)]
host: String,
/// Kept out of logs, and marked `writeOnly` in the schema.
#[config(secret)]
#[serde(default)]
#[allow(dead_code)]
password: String,
}
#[dynamic_config]
#[derive(Deserialize, JsonSchema)]
struct ServerConfig {
host: String,
/// Doc comments become schema descriptions, which is what an editor shows
/// on hover — so they are worth writing on config types especially.
port: u16,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// The schema needs the same two facts the loader does — the file and the
// section key — so it comes from the same builder.
let db_sources = DbConfig::builder("db").file("dynamic-config/examples/app.json");
let server_sources = ServerConfig::builder("server").file("dynamic-config/examples/app.json");
// Loaded as well as described, so the example proves the schema belongs to
// a file this crate can actually read.
let server = server_sources.load()?;
println!("loaded: {}:{}\n", server.host, server.port);
// ---------------------------------------------------------------------
// One type: its schema describes the file it sits in, not the struct.
// ---------------------------------------------------------------------
let db = db_sources.schema();
println!("the db builder's schema() describes a file:");
println!(" top-level type = {}", db["type"]);
println!(" its only section = db");
println!(
" password writeOnly = {}",
db["properties"]["db"]["properties"]["password"]["writeOnly"]
);
println!(
" host writeOnly = {} (only the marked fields)",
db["properties"]["db"]["properties"]["host"]
.get("writeOnly")
.map_or("absent", |_| "true")
);
// ---------------------------------------------------------------------
// Two types over one file: one schema for the file they share.
// ---------------------------------------------------------------------
let whole_file = dynamic_config::schema::merge([db_sources.schema(), server_sources.schema()]);
println!("\nmerged, both sections are there:");
for section in whole_file["properties"]
.as_object()
.expect("the merged schema has properties")
.keys()
{
println!(" {section}");
}
// ---------------------------------------------------------------------
// Nothing is required, and that is deliberate.
// ---------------------------------------------------------------------
println!(
"\nrequired inside the db section = {:?}",
whole_file["properties"]["db"].get("required")
);
println!(
"required inside db.pool = {:?}",
whole_file["properties"]["db"]["properties"]
.get("pool")
.and_then(|pool| pool.get("required"))
);
println!(
"A field the file omits may still be supplied by the environment, a\n\
flag or a computed default — none of which an editor can see. Marking\n\
fields required would light up every 12-factor config file in red."
);
// ---------------------------------------------------------------------
// Writing it out, and wiring it up.
// ---------------------------------------------------------------------
let path = std::env::temp_dir().join("dynamic-config-example.schema.json");
std::fs::write(&path, serde_json::to_string_pretty(&whole_file)?)?;
println!("\nwrote {}", path.display());
println!(
"\nWire it up per format:\n\
\x20 JSON \"$schema\": \"./app.schema.json\" (a top-level key)\n\
\x20 YAML # yaml-language-server: $schema=./app.schema.json\n\
\x20 TOML #:schema ./app.schema.json"
);
println!(
"\n`$schema` is the one top-level key this crate does not read as a\n\
section — otherwise wiring the schema into the file it describes would\n\
stop the file from loading."
);
std::fs::remove_file(&path)?;
Ok(())
}