Skip to main content

vld_config/
lib.rs

1//! # vld-config — Validate configuration files with `vld`
2//!
3//! Load configuration from TOML, YAML, JSON, or environment variables and
4//! validate it against `vld` schemas at load time. Supports both
5//! [config-rs](https://docs.rs/config) and [figment](https://docs.rs/figment)
6//! as backends.
7//!
8//! # Quick Start (config-rs)
9//!
10//! ```rust,no_run
11//! use vld::prelude::*;
12//! use vld_config::from_config;
13//!
14//! vld::schema! {
15//!     #[derive(Debug)]
16//!     pub struct AppSettings {
17//!         pub host: String => vld::string().min(1),
18//!         pub port: i64    => vld::number().int().min(1).max(65535),
19//!     }
20//! }
21//!
22//! let config = config::Config::builder()
23//!     .add_source(config::File::with_name("config"))
24//!     .add_source(config::Environment::with_prefix("APP"))
25//!     .build()
26//!     .unwrap();
27//!
28//! let settings: AppSettings = from_config(&config).unwrap();
29//! println!("Listening on {}:{}", settings.host, settings.port);
30//! ```
31//!
32//! # Quick Start (figment)
33//!
34//! ```rust,ignore
35//! use vld::prelude::*;
36//! use vld_config::from_figment;
37//!
38//! vld::schema! {
39//!     #[derive(Debug)]
40//!     pub struct AppSettings {
41//!         pub host: String => vld::string().min(1),
42//!         pub port: i64    => vld::number().int().min(1).max(65535),
43//!     }
44//! }
45//!
46//! let figment = figment::Figment::new()
47//!     .merge(figment::providers::Serialized::defaults(
48//!         serde_json::json!({"host": "0.0.0.0", "port": 3000}),
49//!     ))
50//!     .merge(figment::providers::Env::prefixed("APP_"));
51//!
52//! let settings: AppSettings = from_figment(&figment).unwrap();
53//! ```
54
55use std::fmt;
56use vld::schema::VldParse;
57
58/// Error type for config validation.
59#[derive(Debug)]
60pub enum VldConfigError {
61    /// Failed to extract/deserialize config into JSON.
62    Source(String),
63    /// Validation failed — contains all vld validation issues.
64    Validation(vld::error::VldError),
65}
66
67impl fmt::Display for VldConfigError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            VldConfigError::Source(msg) => write!(f, "config error: {msg}"),
71            VldConfigError::Validation(err) => write!(f, "validation error: {err}"),
72        }
73    }
74}
75
76impl std::error::Error for VldConfigError {}
77
78impl From<vld::error::VldError> for VldConfigError {
79    fn from(err: vld::error::VldError) -> Self {
80        VldConfigError::Validation(err)
81    }
82}
83
84// ---------------------------------------------------------------------------
85// config-rs backend
86// ---------------------------------------------------------------------------
87
88/// Load and validate configuration from a [`config::Config`] instance.
89///
90/// The config is first deserialized into a `serde_json::Value`, then
91/// validated and parsed using the `vld` schema via [`VldParse`].
92///
93/// # Errors
94///
95/// Returns [`VldConfigError::Source`] if the config cannot be deserialized
96/// into JSON, or [`VldConfigError::Validation`] if validation fails.
97///
98/// # Example
99///
100/// ```rust,no_run
101/// use vld::prelude::*;
102///
103/// vld::schema! {
104///     #[derive(Debug)]
105///     pub struct Settings {
106///         pub host: String => vld::string().min(1),
107///         pub port: i64    => vld::number().int().min(1).max(65535),
108///     }
109/// }
110///
111/// let config = config::Config::builder()
112///     .add_source(config::File::with_name("config"))
113///     .build()
114///     .unwrap();
115///
116/// let settings: Settings = vld_config::from_config(&config).unwrap();
117/// ```
118#[cfg(feature = "config-rs")]
119pub fn from_config<T: VldParse>(config: &config::Config) -> Result<T, VldConfigError> {
120    let value: serde_json::Value = config
121        .clone()
122        .try_deserialize()
123        .map_err(|e| VldConfigError::Source(e.to_string()))?;
124    T::vld_parse_value(&value).map_err(VldConfigError::Validation)
125}
126
127/// Load and validate configuration from a [`config::ConfigBuilder`].
128///
129/// Convenience wrapper that builds the config and validates in one step.
130///
131/// # Example
132///
133/// ```rust,no_run
134/// use vld::prelude::*;
135///
136/// vld::schema! {
137///     #[derive(Debug)]
138///     pub struct Settings {
139///         pub host: String => vld::string().min(1),
140///         pub port: i64    => vld::number().int().min(1).max(65535),
141///     }
142/// }
143///
144/// let settings: Settings = vld_config::from_builder(
145///     config::Config::builder()
146///         .add_source(config::File::with_name("config"))
147/// ).unwrap();
148/// ```
149#[cfg(feature = "config-rs")]
150pub fn from_builder<T: VldParse>(
151    builder: config::ConfigBuilder<config::builder::DefaultState>,
152) -> Result<T, VldConfigError> {
153    let config = builder
154        .build()
155        .map_err(|e| VldConfigError::Source(e.to_string()))?;
156    from_config(&config)
157}
158
159/// Validate a raw `serde_json::Value` as if it came from a config source.
160///
161/// Useful when you've already loaded the config data into a JSON value
162/// (e.g. from a custom source) and just want `vld` validation.
163pub fn from_value<T: VldParse>(value: &serde_json::Value) -> Result<T, VldConfigError> {
164    T::vld_parse_value(value).map_err(VldConfigError::Validation)
165}
166
167// ---------------------------------------------------------------------------
168// figment backend
169// ---------------------------------------------------------------------------
170
171/// Load and validate configuration from a [`figment::Figment`] instance.
172///
173/// The figment data is extracted into a `serde_json::Value`, then
174/// validated and parsed using the `vld` schema.
175///
176/// # Errors
177///
178/// Returns [`VldConfigError::Source`] if figment extraction fails, or
179/// [`VldConfigError::Validation`] if validation fails.
180///
181/// # Example
182///
183/// ```rust,no_run
184/// use vld::prelude::*;
185///
186/// vld::schema! {
187///     #[derive(Debug)]
188///     pub struct Settings {
189///         pub host: String => vld::string().min(1),
190///         pub port: i64    => vld::number().int().min(1).max(65535),
191///     }
192/// }
193///
194/// let figment = figment::Figment::new()
195///     .merge(figment::providers::Serialized::defaults(
196///         serde_json::json!({"host": "localhost", "port": 8080}),
197///     ));
198///
199/// let settings: Settings = vld_config::from_figment(&figment).unwrap();
200/// ```
201#[cfg(feature = "figment")]
202pub fn from_figment<T: VldParse>(figment: &figment::Figment) -> Result<T, VldConfigError> {
203    let value: serde_json::Value = figment
204        .extract()
205        .map_err(|e| VldConfigError::Source(e.to_string()))?;
206    T::vld_parse_value(&value).map_err(VldConfigError::Validation)
207}
208
209/// Prelude — re-exports everything you need.
210pub mod prelude {
211    #[cfg(feature = "figment")]
212    pub use crate::from_figment;
213    pub use crate::from_value;
214    pub use crate::VldConfigError;
215    #[cfg(feature = "config-rs")]
216    pub use crate::{from_builder, from_config};
217    pub use vld::prelude::*;
218}