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::{pat::{LazyRegex, lazy_regex}, 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(
31//!         // validates that the name is not empty using a custom predicate function
32//!         validate(not_empty, "must not be empty"),
33//!         // validates that the name matches the provided regex
34//!         validate(lazy_regex!(ref r"^[a-z][-a-z0-9]*$")),
35//!     )]
36//!     app_name: String,
37//! }
38//!
39//! // We have to use `&String` rather than more idiomatic `&str` in order to
40//! // exactly match the validated type.
41//! fn not_empty(s: &String) -> bool {
42//!     !s.is_empty()
43//! }
44//!
45//! impl ValidatedConfig {
46//!     fn validate_secret_key(&self) -> Result<(), ErrorWithOrigin> {
47//!         if let Some(expected_len) = self.secret_key_len {
48//!             let actual_len = self.secret_key.expose_secret().len();
49//!             if expected_len != actual_len {
50//!                 return Err(ErrorWithOrigin::custom(format!(
51//!                     "unexpected `secret_key` length ({actual_len}); \
52//!                      expected {expected_len}"
53//!                 )));
54//!             }
55//!         }
56//!         Ok(())
57//!     }
58//! }
59//! ```
60//!
61//! ## Filtering
62//!
63//! Filtering reuses the `Validate` trait, but rather than failing, converts a value to `None`.
64//!
65//! ```
66//! use smart_config::validation;
67//! # use smart_config::{testing, DescribeConfig, DeserializeConfig, ErrorWithOrigin};
68//!
69//! #[derive(DescribeConfig, DeserializeConfig)]
70//! struct FilteringConfig {
71//!     /// Will convert `url: ''` to `None`.
72//!     #[config(deserialize_if(validation::NotEmpty))]
73//!     url: Option<String>,
74//!     /// Will convert either of `env: ''` or `env: 'unset'` to `None`.
75//!     #[config(deserialize_if(valid_env, "not empty or 'unset'"))]
76//!     env: Option<String>,
77//! }
78//!
79//! fn valid_env(s: &String) -> bool {
80//!     !s.is_empty() && s != "unset"
81//! }
82//!
83//! // Base case: no filtering.
84//! let env = smart_config::Environment::from_iter("", [
85//!     ("URL", "https://example.com"),
86//!     ("ENV", "prod"),
87//! ]);
88//! let config: FilteringConfig = testing::test_complete(env)?;
89//! assert_eq!(config.url.unwrap(), "https://example.com");
90//! assert_eq!(config.env.unwrap(), "prod");
91//!
92//! // Filtering applied to both params.
93//! let env = smart_config::Environment::from_iter("", [
94//!     ("URL", ""),
95//!     ("ENV", "unset"),
96//! ]);
97//! let config: FilteringConfig = testing::test_complete(env)?;
98//! assert_eq!(config.url, None);
99//! assert_eq!(config.env, None);
100//! # anyhow::Ok(())
101//! ```
102
103use std::{
104    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
105    fmt,
106    fmt::Formatter,
107    ops,
108    sync::Arc,
109};
110
111use regex::Regex;
112use serde::de;
113
114use crate::{
115    ErrorWithOrigin,
116    pat::{LazyRegex, RawStr},
117};
118
119#[doc(hidden)] // only used in proc macros
120pub mod _private;
121
122/// Generic post-validation for a configuration parameter or a config.
123///
124/// # Implementations
125///
126/// Validations are implemented for the following types:
127///
128/// - [`NotEmpty`]. Validates that a string or a collection, such as `Vec`, is not empty.
129/// - [`Range`](ops::Range), [`RangeInclusive`](ops::RangeInclusive) etc. Validates whether the type is within the provided bounds.
130pub trait Validate<T: ?Sized>: 'static + Send + Sync {
131    /// Describes this validation.
132    ///
133    /// # Errors
134    ///
135    /// Should propagate formatting errors.
136    fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result;
137
138    /// Validates a parameter / config.
139    ///
140    /// # Errors
141    ///
142    /// Should return an error if validation fails.
143    fn validate(&self, target: &T) -> Result<(), ErrorWithOrigin>;
144}
145
146impl<T: 'static + ?Sized> fmt::Debug for dyn Validate<T> {
147    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148        formatter
149            .debug_tuple("Validate")
150            .field(&self.to_string())
151            .finish()
152    }
153}
154
155impl<T: 'static + ?Sized> fmt::Display for dyn Validate<T> {
156    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157        self.describe(formatter)
158    }
159}
160
161/// Delegates via a reference. Useful for defining validation constants as `&'static dyn Validate<_>`.
162impl<T: ?Sized, V: Validate<T> + ?Sized> Validate<T> for &'static V {
163    fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        (**self).describe(formatter)
165    }
166
167    fn validate(&self, target: &T) -> Result<(), ErrorWithOrigin> {
168        (**self).validate(target)
169    }
170}
171
172macro_rules! impl_validate_for_range {
173    ($range:path) => {
174        impl<T> Validate<T> for $range
175        where
176            T: 'static + Send + Sync + PartialOrd + fmt::Debug,
177        {
178            fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179                write!(formatter, "must be in range {self:?}")
180            }
181
182            fn validate(&self, target: &T) -> Result<(), ErrorWithOrigin> {
183                if !self.contains(target) {
184                    let err = de::Error::invalid_value(
185                        de::Unexpected::Other(&format!("{target:?}")),
186                        &format!("value in range {self:?}").as_str(),
187                    );
188                    return Err(ErrorWithOrigin::json(err, Arc::default()));
189                }
190                Ok(())
191            }
192        }
193    };
194}
195
196impl_validate_for_range!(ops::Range<T>);
197impl_validate_for_range!(ops::RangeInclusive<T>);
198impl_validate_for_range!(ops::RangeTo<T>);
199impl_validate_for_range!(ops::RangeToInclusive<T>);
200impl_validate_for_range!(ops::RangeFrom<T>);
201
202/// Validates that a string or a data collection (e.g., [`Vec`]) is not empty.
203#[derive(Debug)]
204pub struct NotEmpty;
205
206macro_rules! impl_not_empty_validation {
207    ($ty:ident$(<$($arg:ident),+>)?) => {
208        impl$(<$($arg,)+>)? Validate<$ty$(<$($arg,)+>)?> for NotEmpty {
209            fn describe(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210                formatter.write_str("must not be empty")
211            }
212
213            fn validate(&self, target: &$ty$(<$($arg,)+>)?) -> Result<(), ErrorWithOrigin> {
214                if target.is_empty() {
215                    return Err(de::Error::custom("value is empty"));
216                }
217                Ok(())
218            }
219        }
220    };
221}
222
223impl_not_empty_validation!(String);
224impl_not_empty_validation!(Vec<T>);
225impl_not_empty_validation!(HashMap<K, V, S>);
226impl_not_empty_validation!(BTreeMap<K, V>);
227impl_not_empty_validation!(HashSet<K, S>);
228impl_not_empty_validation!(BTreeSet<K>);
229
230/// Validates that the string matches the provided regex.
231///
232/// Don't forget to surround the regex with `^$` if you want to match it completely.
233impl<T> Validate<String> for LazyRegex<T>
234where
235    T: ops::Deref<Target = Regex> + Send + Sync + 'static,
236{
237    fn describe(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
238        write!(formatter, "must match Regex({})", RawStr(self.0.as_str()))
239    }
240
241    fn validate(&self, target: &String) -> Result<(), ErrorWithOrigin> {
242        let target = target.as_ref();
243        if self.0.is_match(target) {
244            Ok(())
245        } else {
246            Err(de::Error::custom(format_args!(
247                "value does not match Regex({})",
248                RawStr(self.0.as_str())
249            )))
250        }
251    }
252}