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}