Skip to main content

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}