smart_config/validation/
mod.rs

1//! Parameter and config validation and filtering.
2//!
3//! # Overview
4//!
5//! The core validation functionality is encapsulated in the [`Validate`] trait.
6//!
7//! # Examples
8//!
9//! ## Validation
10//!
11//! ```
12//! use secrecy::{ExposeSecret, SecretString};
13//! use smart_config::validation;
14//! # use smart_config::{testing, DescribeConfig, DeserializeConfig, ErrorWithOrigin};
15//!
16//! #[derive(DescribeConfig, DeserializeConfig)]
17//! #[config(validate(
18//!     Self::validate_secret_key,
19//!     "secret key must have expected length"
20//! ))]
21//! struct ValidatedConfig {
22//!     secret_key: SecretString,
23//!     /// Reference key length. If specified, the secret key length
24//!     /// will be checked against it.
25//!     #[config(validate(..=100))]
26//!     // ^ Validates that the value is in the range. Note that validations
27//!     // handle `Option`s intelligently; if the value isn't specified
28//!     // (i.e., is `None`), it will pass validation.
29//!     secret_key_len: Option<usize>,
30//!     #[config(validate(not_empty, "must not be empty"))]
31//!     app_name: String,
32//! }
33//!
34//! // We have to use `&String` rather than more idiomatic `&str` in order to
35//! // exactly match the validated type.
36//! fn not_empty(s: &String) -> bool {
37//!     !s.is_empty()
38//! }
39//!
40//! impl ValidatedConfig {
41//!     fn validate_secret_key(&self) -> Result<(), ErrorWithOrigin> {
42//!         if let Some(expected_len) = self.secret_key_len {
43//!             let actual_len = self.secret_key.expose_secret().len();
44//!             if expected_len != actual_len {
45//!                 return Err(ErrorWithOrigin::custom(format!(
46//!                     "unexpected `secret_key` length ({actual_len}); \
47//!                      expected {expected_len}"
48//!                 )));
49//!             }
50//!         }
51//!         Ok(())
52//!     }
53//! }
54//! ```
55//!
56//! ## Filtering
57//!
58//! Filtering reuses the `Validate` trait, but rather than failing, converts a value to `None`.
59//!
60//! ```
61//! use smart_config::validation;
62//! # use smart_config::{testing, DescribeConfig, DeserializeConfig, ErrorWithOrigin};
63//!
64//! #[derive(DescribeConfig, DeserializeConfig)]
65//! struct FilteringConfig {
66//!     /// Will convert `url: ''` to `None`.
67//!     #[config(deserialize_if(validation::NotEmpty))]
68//!     url: Option<String>,
69//!     /// Will convert either of `env: ''` or `env: 'unset'` to `None`.
70//!     #[config(deserialize_if(valid_env, "not empty or 'unset'"))]
71//!     env: Option<String>,
72//! }
73//!
74//! fn valid_env(s: &String) -> bool {
75//!     !s.is_empty() && s != "unset"
76//! }
77//!
78//! // Base case: no filtering.
79//! let env = smart_config::Environment::from_iter("", [
80//!     ("URL", "https://example.com"),
81//!     ("ENV", "prod"),
82//! ]);
83//! let config: FilteringConfig = testing::test_complete(env)?;
84//! assert_eq!(config.url.unwrap(), "https://example.com");
85//! assert_eq!(config.env.unwrap(), "prod");
86//!
87//! // Filtering applied to both params.
88//! let env = smart_config::Environment::from_iter("", [
89//!     ("URL", ""),
90//!     ("ENV", "unset"),
91//! ]);
92//! let config: FilteringConfig = testing::test_complete(env)?;
93//! assert_eq!(config.url, None);
94//! assert_eq!(config.env, None);
95//! # anyhow::Ok(())
96//! ```
97
98use std::{
99    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
100    fmt, ops,
101    sync::Arc,
102};
103
104use serde::de;
105
106use crate::ErrorWithOrigin;
107
108#[doc(hidden)] // only used in proc macros
109pub mod _private;
110
111/// Generic post-validation for a configuration parameter or a config.
112///
113/// # Implementations
114///
115/// Validations are implemented for the following types:
116///
117/// - [`NotEmpty`]. Validates that a string or a collection, such as `Vec`, is not empty.
118/// - [`Range`](ops::Range), [`RangeInclusive`](ops::RangeInclusive) etc. Validates whether the type is within the provided bounds.
119pub trait Validate<T: ?Sized>: 'static + Send + Sync {
120    /// Describes this validation.
121    ///
122    /// # Errors
123    ///
124    /// Should propagate formatting errors.
125    fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result;
126
127    /// Validates a parameter / config.
128    ///
129    /// # Errors
130    ///
131    /// Should return an error if validation fails.
132    fn validate(&self, target: &T) -> Result<(), ErrorWithOrigin>;
133}
134
135impl<T: 'static + ?Sized> fmt::Debug for dyn Validate<T> {
136    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137        formatter
138            .debug_tuple("Validate")
139            .field(&self.to_string())
140            .finish()
141    }
142}
143
144impl<T: 'static + ?Sized> fmt::Display for dyn Validate<T> {
145    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146        self.describe(formatter)
147    }
148}
149
150/// Delegates via a reference. Useful for defining validation constants as `&'static dyn Validate<_>`.
151impl<T: ?Sized, V: Validate<T> + ?Sized> Validate<T> for &'static V {
152    fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153        (**self).describe(formatter)
154    }
155
156    fn validate(&self, target: &T) -> Result<(), ErrorWithOrigin> {
157        (**self).validate(target)
158    }
159}
160
161macro_rules! impl_validate_for_range {
162    ($range:path) => {
163        impl<T> Validate<T> for $range
164        where
165            T: 'static + Send + Sync + PartialOrd + fmt::Debug,
166        {
167            fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168                write!(formatter, "must be in range {self:?}")
169            }
170
171            fn validate(&self, target: &T) -> Result<(), ErrorWithOrigin> {
172                if !self.contains(target) {
173                    let err = de::Error::invalid_value(
174                        de::Unexpected::Other(&format!("{target:?}")),
175                        &format!("value in range {self:?}").as_str(),
176                    );
177                    return Err(ErrorWithOrigin::json(err, Arc::default()));
178                }
179                Ok(())
180            }
181        }
182    };
183}
184
185impl_validate_for_range!(ops::Range<T>);
186impl_validate_for_range!(ops::RangeInclusive<T>);
187impl_validate_for_range!(ops::RangeTo<T>);
188impl_validate_for_range!(ops::RangeToInclusive<T>);
189impl_validate_for_range!(ops::RangeFrom<T>);
190
191/// Validates that a string or a data collection (e.g., [`Vec`]) is not empty.
192#[derive(Debug)]
193pub struct NotEmpty;
194
195macro_rules! impl_not_empty_validation {
196    ($ty:ident$(<$($arg:ident),+>)?) => {
197        impl$(<$($arg,)+>)? Validate<$ty$(<$($arg,)+>)?> for NotEmpty {
198            fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
199                formatter.write_str("must not be empty")
200            }
201
202            fn validate(&self, target: &$ty$(<$($arg,)+>)?) -> Result<(), ErrorWithOrigin> {
203                if target.is_empty() {
204                    return Err(de::Error::custom("value is empty"));
205                }
206                Ok(())
207            }
208        }
209    };
210}
211
212impl_not_empty_validation!(String);
213impl_not_empty_validation!(Vec<T>);
214impl_not_empty_validation!(HashMap<K, V, S>);
215impl_not_empty_validation!(BTreeMap<K, V>);
216impl_not_empty_validation!(HashSet<K, S>);
217impl_not_empty_validation!(BTreeSet<K>);