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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 Noyalib. All rights reserved.
//! Figment provider — layered configuration with YAML, env vars, and
//! defaults composed into a single typed struct.
//!
//! [`figment`] is the de-facto-standard layered configuration crate
//! in the Rust ecosystem (Rocket, several K8s operators, many
//! Tokio-based services). The pattern: start with built-in
//! defaults, layer a YAML config file on top, finalise with
//! environment-variable overrides — last-write-wins. noyalib ships
//! a [`figment::Provider`] for YAML so the whole chain works
//! without depending on the unmaintained `serde_yaml` 0.9 crate.
//!
//! Run: `cargo run --example figment --features figment`
#[path = "support.rs"]
mod support;
use figment::Figment;
use figment::providers::{Env, Format, Serialized};
use noyalib::figment::Yaml;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
name: String,
port: u16,
log_level: String,
workers: u32,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
name: "noyalib-app".into(),
port: 8080,
log_level: "info".into(),
workers: 1,
}
}
}
const SITE_YAML: &str = "\
name: production-api
port: 9090
log_level: warn
";
fn main() {
support::header("Figment — layered YAML / env / defaults");
// ── Layer 1: defaults only ───────────────────────────────────────
support::task_with_output("Layer 1 — figment with defaults only", || {
let cfg: AppConfig = Figment::new()
.merge(Serialized::defaults(AppConfig::default()))
.extract()
.expect("defaults must extract");
vec![
format!("name = {}", cfg.name),
format!("port = {}", cfg.port),
format!("log_level = {}", cfg.log_level),
format!("workers = {}", cfg.workers),
]
});
// ── Layer 2: defaults + YAML overlay ─────────────────────────────
support::task_with_output("Layer 2 — defaults overlaid by YAML", || {
let cfg: AppConfig = Figment::new()
.merge(Serialized::defaults(AppConfig::default()))
.merge(Yaml::string(SITE_YAML))
.extract()
.expect("yaml overlay must extract");
vec![
format!("name = {} (from YAML)", cfg.name),
format!("port = {} (from YAML)", cfg.port),
format!("log_level = {} (from YAML)", cfg.log_level),
format!("workers = {} (still default)", cfg.workers),
]
});
// ── Layer 3: defaults + YAML + env overrides ─────────────────────
support::task_with_output("Layer 3 — env vars override the YAML layer", || {
// Demonstrate env-overlay semantics without mutating the
// process environment — edition 2024 marks
// `std::env::set_var` / `remove_var` as unsafe (they're
// not thread-safe), and the crate sits under
// `#![forbid(unsafe_code)]`. Use figment's `Env::raw` with
// a synthetic map instead so the example stays
// illustrative and safe.
let env = Env::raw()
.only(&["NOYAEX_PORT", "NOYAEX_WORKERS"])
.map(|k| match k.as_str() {
"NOYAEX_PORT" => "PORT".into(),
"NOYAEX_WORKERS" => "WORKERS".into(),
other => other.into(),
});
// Inject the values via a one-off serialised layer that
// shadows env. The Env::raw provider above is included so
// the example still demonstrates Env wiring in real
// pipelines; the synthetic overlay below stands in for the
// OS env that those pipelines would carry.
#[derive(Serialize)]
struct EnvOverlay {
port: u16,
workers: u16,
}
let cfg: AppConfig = Figment::new()
.merge(Serialized::defaults(AppConfig::default()))
.merge(Yaml::string(SITE_YAML))
.merge(env)
.merge(Serialized::defaults(EnvOverlay {
port: 7000,
workers: 8,
}))
.extract()
.expect("env overlay must extract");
vec![
format!("name = {} (from YAML)", cfg.name),
format!("port = {} (from env: NOYAEX_PORT)", cfg.port),
format!("log_level = {} (from YAML)", cfg.log_level),
format!(
"workers = {} (from env: NOYAEX_WORKERS)",
cfg.workers
),
]
});
// ── Pattern: per-environment YAML files ──────────────────────────
support::task_with_output(
"Pattern — production overrides staging overrides base",
|| {
const BASE: &str = "name: api\nport: 8080\nlog_level: debug\nworkers: 1\n";
const STAGING: &str = "log_level: info\nworkers: 4\n";
const PROD: &str = "port: 443\nlog_level: warn\nworkers: 16\n";
let cfg: AppConfig = Figment::new()
.merge(Yaml::string(BASE))
.merge(Yaml::string(STAGING))
.merge(Yaml::string(PROD))
.extract()
.expect("multi-file overlay must extract");
vec![
format!("name = {} (base)", cfg.name),
format!("port = {} (prod)", cfg.port),
format!("log_level = {} (prod)", cfg.log_level),
format!("workers = {} (prod)", cfg.workers),
]
},
);
println!();
println!(" Figment lets you compose configuration the way 12-factor");
println!(" apps demand: defaults in code, environment-specific YAML");
println!(" overlays, env-var overrides at deploy time. noyalib slots");
println!(" in as the YAML provider — no `serde_yaml` 0.9 dependency.");
support::footer();
}