configulator/lib.rs
1//! # Configulator
2//!
3//! A simple configuration manager for Rust applications with derive macro support.
4//!
5//! Supports configuration from multiple sources with clear precedence:
6//!
7//! 1. **Default values** (lowest priority)
8//! 2. **Config files** (any serde format via [`serde_loader`])
9//! 3. **Environment variables**
10//! 4. **CLI flags** (highest priority)
11//!
12//! ## Features
13//!
14//! - `#[derive(Config)]` macro for declarative configuration structs
15//! - Any serde-compatible file format - YAML, TOML, JSON, with a one-liner
16//! - Pluggable file format support - bring your own parser via [`FileLoader`]
17//! - Nested struct support
18//! - [`Vec<T>`](Vec) list fields
19//! - Custom types - anything implementing [`FromStr`](std::str::FromStr) + [`Default`]
20//! - Optional validation via the [`Validate`] trait
21//! - Boolean CLI flags (`--debug` sets true, `--debug false` sets false)
22//!
23//! ## Usage
24//!
25//! Add to your `Cargo.toml`:
26//!
27//! ```toml
28//! [dependencies]
29//! configulator-rs = "0.1"
30//! ```
31//!
32//! > **Note:** Because the configuration options are expressed as different cases
33//! > (i.e. `http.host` in a config file would be `HTTP__HOST` in environment
34//! > variables), this library cannot be used for configurations that contain the
35//! > same field name in different cases.
36//!
37//! ## Quick Start
38//!
39//! ```rust,no_run
40//! use configulator::{Config, Configulator, Validate};
41//!
42//! #[derive(Config, Default, Debug)]
43//! struct AppConfig {
44//! #[configulator(name = "host", default = "127.0.0.1", description = "Bind address")]
45//! host: String,
46//!
47//! #[configulator(name = "port", default = "8080", description = "Listen port")]
48//! port: u16,
49//!
50//! #[configulator(name = "debug", default = "false", description = "Enable debug mode")]
51//! debug: bool,
52//! }
53//!
54//! fn main() {
55//! let config: AppConfig = Configulator::new()
56//! .load_without_validation()
57//! .expect("failed to load config");
58//! println!("{config:?}");
59//! }
60//! ```
61//!
62//! ## Configuration Sources
63//!
64//! Enable as many or as few sources as you need via the builder:
65//!
66//! ```rust,no_run
67//! use configulator::{
68//! CLIFlagOptions, Config, Configulator,
69//! EnvironmentVariableOptions, FileOptions, Validate,
70//! serde_loader,
71//! };
72//!
73//! #[derive(Config, Default, Debug)]
74//! struct AppConfig {
75//! #[configulator(name = "host", default = "127.0.0.1", description = "Bind address")]
76//! host: String,
77//!
78//! #[configulator(name = "port", default = "8080", description = "Listen port")]
79//! port: u16,
80//!
81//! #[configulator(name = "debug", default = "false", description = "Enable debug mode")]
82//! debug: bool,
83//!
84//! #[configulator(name = "allowed-origins", default = "localhost,example.com")]
85//! allowed_origins: Vec<String>,
86//!
87//! #[configulator(name = "database")]
88//! database: DatabaseConfig,
89//! }
90//!
91//! #[derive(Config, Default, Debug)]
92//! struct DatabaseConfig {
93//! #[configulator(name = "url", default = "postgres://localhost/mydb")]
94//! url: String,
95//!
96//! #[configulator(name = "max-connections", default = "10")]
97//! max_connections: u32,
98//! }
99//!
100//! impl Validate for AppConfig {
101//! fn validate(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
102//! if self.port == 0 {
103//! return Err("port must be non-zero".into());
104//! }
105//! Ok(())
106//! }
107//! }
108//!
109//! fn main() -> Result<(), Box<dyn std::error::Error>> {
110//! let config = Configulator::<AppConfig>::new()
111//! .with_file(FileOptions {
112//! paths: vec!["config.yaml".into(), "/etc/myapp/config.yaml".into()],
113//! error_if_not_found: false,
114//! // Any serde-compatible format works: serde_json, toml, etc.
115//! loader: serde_loader(|s| serde_yaml_ng::from_str(s)),
116//! })
117//! // Env vars: MYAPP__HOST, MYAPP__DATABASE__MAX_CONNECTIONS, etc.
118//! .with_environment_variables(EnvironmentVariableOptions {
119//! prefix: "MYAPP".into(),
120//! separator: "__".into(),
121//! })
122//! // CLI flags: --host, --database.url, --debug, etc.
123//! .with_cli_flags(CLIFlagOptions {
124//! separator: ".".into(),
125//! })
126//! // .load() validates; use .load_without_validation() to skip
127//! .load()?;
128//!
129//! println!("Host: {}", config.host);
130//! println!("Database URL: {}", config.database.url);
131//! Ok(())
132//! }
133//! ```
134//!
135//! ## Derive Attributes
136//!
137//! Fields are annotated with `#[configulator(...)]` using these keys:
138//!
139//! | Key | Description |
140//! |---------------|------------------------------------------------|
141//! | `name` | Config key name (defaults to the field name) |
142//! | `default` | Default value as a string literal |
143//! | `description` | Help text shown in CLI `--help` output |
144//!
145//! ```rust,ignore
146//! // Field appears in config files, env vars, and CLI flags as "my-name".
147//! #[configulator(name = "my-name")]
148//! my_name: String,
149//!
150//! // Field has a description shown in CLI --help
151//! #[configulator(name = "my-name", description = "this text appears in --help")]
152//! my_name: String,
153//!
154//! // Field has a default value of 1
155//! #[configulator(name = "my-name", default = "1")]
156//! my_name: u32,
157//! ```
158//!
159//! ## Supported Types
160//!
161//! - All primitive scalars (`i8`–`i64`, `u8`–`u64`, `f32`, `f64`, `bool`, [`String`])
162//! - [`PathBuf`](std::path::PathBuf) and any other [`FromStr`](std::str::FromStr) + [`Default`] type
163//! - Custom enums (implement [`FromStr`](std::str::FromStr) + [`Default`])
164//! - [`Vec<T>`](Vec) for list values (comma-separated defaults, repeated CLI flags)
165//! - Nested structs (must also derive `Config`)
166//!
167//! ## Configuration Sources
168//!
169//! ### Config Files
170//!
171//! Configulator is format-agnostic, pass any serde-compatible deserializer via
172//! [`serde_loader`], or implement the [`FileLoader`] trait for full control.
173//! YAML, TOML, JSON, and any other serde format work out of the box.
174//!
175//! Provide a list of paths to search. The first file found is used.
176//!
177//! ```rust,ignore
178//! // YAML
179//! .with_file(FileOptions {
180//! paths: vec!["config.yaml".into()],
181//! error_if_not_found: false,
182//! loader: serde_loader(|s| serde_yaml_ng::from_str(s)),
183//! })
184//!
185//! // TOML
186//! .with_file(FileOptions {
187//! paths: vec!["config.toml".into()],
188//! error_if_not_found: false,
189//! loader: serde_loader(|s| toml::from_str(s)),
190//! })
191//!
192//! // JSON
193//! .with_file(FileOptions {
194//! paths: vec!["config.json".into()],
195//! error_if_not_found: false,
196//! loader: serde_loader(|s| serde_json::from_str(s)),
197//! })
198//! ```
199//!
200//! The CLI also accepts `--config` / `-c` to specify a config file path at runtime
201//! (requires calling [`.with_file()`](Configulator::with_file) first).
202//!
203//! ### Environment Variables
204//!
205//! Environment variables are formed as `PREFIX` + `SEPARATOR` + `FIELD_NAME`
206//! (uppercased, dashes become underscores).
207//!
208//! ```rust,ignore
209//! .with_environment_variables(EnvironmentVariableOptions {
210//! prefix: "MYAPP".into(),
211//! separator: "__".into(),
212//! })
213//! ```
214//!
215//! For example, a field named `max-connections` under a `database` parent with
216//! prefix `MYAPP` and separator `__` would be `MYAPP__DATABASE__MAX_CONNECTIONS`.
217//!
218//! ### CLI Flags
219//!
220//! Nested fields use the separator to form flag names (e.g. `--database.host`).
221//!
222//! ```rust,ignore
223//! .with_cli_flags(CLIFlagOptions {
224//! separator: ".".into(),
225//! })
226//! ```
227//!
228//! Boolean fields work as flags (`--debug` sets to true, `--debug false` sets
229//! to false). List fields can be repeated (`--ports 80 --ports 443`).
230//!
231//! You can also provide a custom `clap::Command` to set the app name, version,
232//! or add your own flags:
233//!
234//! ```rust,ignore
235//! .with_cli_command(clap::Command::new("myapp").version("1.0"))
236//! .with_cli_flags(CLIFlagOptions {
237//! separator: ".".into(),
238//! })
239//! ```
240//!
241//! ## Validation
242//!
243//! Implement the [`Validate`] trait and call [`.load()`](Configulator::load) to validate after loading.
244//! Use [`.load_without_validation()`](Configulator::load_without_validation) to skip validation.
245//!
246//! ## Feature Flags
247//!
248//! Configulator uses feature flags to keep dependencies minimal. All features
249//! are enabled by default.
250//!
251//! | Feature | Description | Dependencies |
252//! |---------|----------------------------------------------------------------------|--------------|
253//! | `file` | Config file loading (`FileOptions`, `serde_loader`, `--config` flag) | `serde` |
254//! | `cli` | CLI flag parsing via clap | `clap` |
255//! | `env` | Environment variable loading | - |
256//!
257//! To opt out of features you don't need:
258//!
259//! ```toml
260//! [dependencies]
261//! configulator-rs = { version = "0.1", default-features = false, features = ["env"] }
262//! ```
263
264#![warn(clippy::all)]
265#![forbid(unsafe_code)]
266#![cfg_attr(docsrs, feature(doc_cfg))]
267
268#[cfg(feature = "cli")]
269mod cli;
270mod configulator;
271mod defaults;
272mod derive_helpers;
273#[cfg(feature = "env")]
274mod environment;
275mod error;
276mod field_info;
277#[cfg(feature = "file")]
278mod file;
279mod options;
280mod value_map;
281
282// Re-export the derive macro
283pub use configulator_derive::Config;
284
285// Re-export public types
286pub use crate::configulator::Configulator;
287pub use crate::error::ConfigulatorError;
288#[cfg(feature = "cli")]
289pub use crate::options::CLIFlagOptions;
290#[cfg(feature = "env")]
291pub use crate::options::EnvironmentVariableOptions;
292#[cfg(feature = "file")]
293pub use crate::file::FileLoader;
294#[cfg(feature = "file")]
295pub use crate::file::serde_loader;
296#[cfg(feature = "file")]
297pub use crate::options::FileOptions;
298
299// Re-export derive-macro internals (used by generated code, not public API)
300#[doc(hidden)]
301pub use crate::derive_helpers::{
302 parse_list, parse_nested, parse_scalar, ConfigDetect, ConfiguratorScalar,
303};
304#[doc(hidden)]
305pub use crate::field_info::{FieldInfo, FieldType};
306#[doc(hidden)]
307pub use crate::value_map::{merge_value_maps, ConfigValue, ValueMap};
308
309/// Trait implemented by the `Config` derive macro. Provides field metadata.
310#[doc(hidden)]
311pub trait ConfigFields {
312 fn configulator_fields() -> Vec<FieldInfo>;
313}
314
315/// Trait implemented by the `Config` derive macro. Constructs a struct from a `ValueMap`.
316#[doc(hidden)]
317pub trait FromValueMap: Sized {
318 fn from_value_map(map: &ValueMap) -> Result<Self, ConfigulatorError>;
319}
320
321/// Trait for user-defined config validation.
322///
323/// Implement this on your config struct to add validation logic that runs
324/// after all sources are merged.
325pub trait Validate {
326 fn validate(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
327}