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}