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>);