confy_rs/lib.rs
1//! Simple, efficient configuration loading for Rust.
2//!
3//! `confy-rs` loads configuration from multiple file formats, merges layered
4//! sources and deserializes the result into your own types via `serde`. With
5//! the `watch` feature it also hot-reloads the configuration when source
6//! files change on disk.
7//!
8//! # Highlights
9//!
10//! - **Multi-format**: TOML / YAML / JSON, detected from the file extension
11//! - **Layered**: defaults + any number of files, deep-merged in order
12//! - **Hot reload**: debounced cross-platform file watching; readers are
13//! lock-free and the previous config is kept when a reload fails
14//! - **Lean**: format parsers and the watcher are feature-gated
15//!
16//! # Quick start
17//!
18//! ```
19//! use serde::Deserialize;
20//!
21//! #[derive(Debug, Deserialize)]
22//! struct AppConfig {
23//! name: String,
24//! port: u16,
25//! }
26//!
27//! # fn main() -> confy_rs::Result<()> {
28//! let config: AppConfig = confy_rs::load_str(
29//! r#"
30//! name = "demo"
31//! port = 8080
32//! "#,
33//! confy_rs::Format::Toml,
34//! )?;
35//! assert_eq!(config.port, 8080);
36//! # Ok(())
37//! # }
38//! ```
39//!
40//! Loading from files works the same way — the format is detected from the
41//! extension, and later sources override earlier ones:
42//!
43//! ```no_run
44//! # use serde::Deserialize;
45//! # #[derive(Deserialize)]
46//! # struct AppConfig { port: u16 }
47//! # fn main() -> confy_rs::Result<()> {
48//! // One-liner for the common case
49//! let config: AppConfig = confy_rs::load("config.toml")?;
50//!
51//! // Layered loading
52//! let config: AppConfig = confy_rs::builder()
53//! .file("config/default.toml")
54//! .file_optional("config/local.json")
55//! .build()?;
56//! # Ok(())
57//! # }
58//! ```
59//!
60//! # Hot reloading
61//!
62//! ```no_run
63//! # use serde::Deserialize;
64//! # #[derive(Deserialize)]
65//! # struct AppConfig { port: u16 }
66//! # fn main() -> confy_rs::Result<()> {
67//! let watcher = confy_rs::builder()
68//! .file("config.toml")
69//! .watch::<AppConfig>()?;
70//!
71//! watcher.on_change(|config| println!("port is now {}", config.port));
72//! let current = watcher.get(); // lock-free snapshot, callable anywhere
73//! # Ok(())
74//! # }
75//! ```
76//!
77//! Reloads are debounced, editors that save via atomic rename are handled,
78//! and when a reload fails the previous configuration stays active while the
79//! error is reported through [`ConfigWatcher::on_error`].
80//!
81//! # Cargo features
82//!
83//! | Feature | Default | Effect |
84//! |---------|---------|--------|
85//! | `toml` | yes | TOML support via the `toml` crate |
86//! | `yaml` | no | YAML support via `serde_yaml_ng` |
87//! | `watch` | yes | hot reloading via `notify` + `arc-swap` |
88//!
89//! JSON support is always built in.
90
91mod builder;
92mod error;
93mod source;
94#[cfg(feature = "watch")]
95mod watch;
96
97use std::path::Path;
98
99use serde::de::DeserializeOwned;
100
101pub use crate::builder::ConfigBuilder;
102pub use crate::error::Error;
103pub use crate::source::Format;
104#[cfg(feature = "watch")]
105pub use crate::watch::ConfigWatcher;
106
107/// Convenience alias for `std::result::Result<T, confy_rs::Error>`.
108pub type Result<T> = std::result::Result<T, Error>;
109
110/// Creates a new empty [`ConfigBuilder`].
111#[must_use]
112pub fn builder() -> ConfigBuilder {
113 ConfigBuilder::new()
114}
115
116/// Loads a single configuration file and deserializes it into `T`.
117///
118/// The format is detected from the file extension. This is shorthand for
119/// `confy_rs::builder().file(path).build()`.
120///
121/// # Errors
122///
123/// Returns an error if the file is missing or unreadable, its format is
124/// unknown or disabled, its content is invalid, or the parsed tree does not
125/// match `T`.
126///
127/// # Example
128///
129/// ```no_run
130/// # fn main() -> confy_rs::Result<()> {
131/// let config: serde_json::Value = confy_rs::load("config.toml")?;
132/// # Ok(())
133/// # }
134/// ```
135pub fn load<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T> {
136 builder().file(path.as_ref()).build()
137}
138
139/// Parses configuration from an in-memory string in the given format.
140///
141/// Handy for tests and for configuration that is embedded or fetched from
142/// somewhere other than the file system.
143///
144/// # Errors
145///
146/// Returns an error if the format is disabled, the content is invalid, or
147/// the parsed tree does not match `T`.
148pub fn load_str<T: DeserializeOwned>(text: &str, format: Format) -> Result<T> {
149 let value = format.parse(text, None)?;
150 serde_json::from_value(value).map_err(Error::Deserialize)
151}
152
153#[cfg(test)]
154mod tests {
155 use serde::Deserialize;
156
157 use super::*;
158
159 #[derive(Debug, PartialEq, Deserialize)]
160 struct Sample {
161 name: String,
162 port: u16,
163 }
164
165 #[test]
166 fn load_str_json() {
167 let config: Sample = load_str(r#"{ "name": "demo", "port": 8080 }"#, Format::Json).unwrap();
168 assert_eq!(
169 config,
170 Sample {
171 name: "demo".into(),
172 port: 8080
173 }
174 );
175 }
176
177 /// load_str: TOML
178 #[cfg(feature = "toml")]
179 #[test]
180 fn load_str_toml() {
181 let config: Sample = load_str("name = \"demo\"\nport = 8080\n", Format::Toml).unwrap();
182 assert_eq!(config.port, 8080);
183 }
184
185 /// load_str: YAML
186 #[cfg(feature = "yaml")]
187 #[test]
188 fn load_str_yaml() {
189 let config: Sample = load_str("name: demo\nport: 8080\n", Format::Yaml).unwrap();
190 assert_eq!(config.port, 8080);
191 }
192
193 #[cfg(not(feature = "yaml"))]
194 #[test]
195 fn load_str_yaml_disabled() {
196 let err = load_str::<Sample>("name: demo", Format::Yaml).unwrap_err();
197 assert!(matches!(err, Error::FeatureDisabled { .. }), "{err}");
198 }
199
200 #[test]
201 fn load_from_file() {
202 let dir = tempfile::tempdir().unwrap();
203 let path = dir.path().join("app.json");
204 std::fs::write(&path, r#"{ "name": "demo", "port": 1 }"#).unwrap();
205 let config: Sample = load(&path).unwrap();
206 assert_eq!(config.port, 1);
207 }
208}