Skip to main content

validity/
lib.rs

1//! Type safe validation of arbitrary data
2//!
3//! Provides the `Valid<T>` struct which wraps some data, after verifiying that it meets some
4//! criteria:
5//! ```
6//! # use validity::*;
7//! #[derive(Debug)]
8//! struct PhoneNumber(String);
9//!
10//! enum InvalidPhoneNumber {
11//!   NonDigit,
12//!   WrongLength,
13//! }
14//!
15//! impl Validate for PhoneNumber {
16//!   type Context<'a> = ();
17//!   type Error = InvalidPhoneNumber;
18//!
19//!   fn is_valid(&self, _ctx: Self::Context<'_>) -> Result<(), Self::Error> {
20//!     if self.0.len() == 11 {
21//!         return Err(InvalidPhoneNumber::WrongLength);
22//!     }
23//!
24//!     if self.0.chars().any(|c| !c.is_digit(10)) {
25//!         return Err(InvalidPhoneNumber::NonDigit);
26//!     }
27//!
28//!     Ok(())
29//!   }
30//! }
31//!
32//! fn main() {
33//!   let number = PhoneNumber("01234567890".to_string());
34//!   if let Ok(number) = number.validate() {
35//!     handle_phone_number(number);
36//!   } else {
37//!     println!("error!");
38//!   }
39//! }
40//!
41//! fn handle_phone_number(number: Valid<PhoneNumber>) {
42//!   println!("This is a definitely valid phone number: {:?}", number.into_inner());
43//! }
44//! ```
45//!
46//! Some validation requires access to some context. For example, you may want to validate that an
47//! email address exists in your database. For that, you can pass this context via the `Context`
48//! associated type.
49//!
50//! When validating, you can call `foo.validate_with(context)`:
51//! ```
52//! # use validity::*;
53//! # struct Database {}
54//! # impl Database {
55//! #   fn check_phone_exists(&self, _: &PhoneNumber) -> bool { true }
56//! #   fn new() -> Self { Self {} }
57//! # }
58//! #[derive(Debug)]
59//! struct PhoneNumber(String);
60//!
61//! enum InvalidPhoneNumber {
62//!   NonDigit,
63//!   WrongLength,
64//!   NotInDatabase,
65//! }
66//!
67//! impl Validate for PhoneNumber {
68//!   type Context<'a> = Database;
69//!   type Error = InvalidPhoneNumber;
70//!
71//!   fn is_valid(&self, db: Self::Context<'_>) -> Result<(), Self::Error> {
72//!     if self.0.len() == 11 {
73//!         return Err(InvalidPhoneNumber::WrongLength);
74//!     }
75//!
76//!     if self.0.chars().any(|c| !c.is_digit(10)) {
77//!         return Err(InvalidPhoneNumber::NonDigit);
78//!     }
79//!
80//!     if !db.check_phone_exists(self) {
81//!         return Err(InvalidPhoneNumber::NotInDatabase);
82//!     }
83//!
84//!     Ok(())
85//!   }
86//! }
87//! ```
88//! You can then call this with:
89//! ```rust
90//! # use validity::*;
91//! # struct Database {}
92//! # impl Database {
93//! #   fn check_phone_exists(&self, _: &PhoneNumber) -> bool { true }
94//! #   fn new() -> Self { Self {} }
95//! # }
96//! # #[derive(Debug)]
97//! # struct PhoneNumber(String);
98//! # enum InvalidPhoneNumber {
99//! #   NonDigit,
100//! #   WrongLength,
101//! #   NotInDatabase,
102//! # }
103//! # impl Validate for PhoneNumber {
104//! #   type Context<'a> = Database;
105//! #   type Error = InvalidPhoneNumber;
106//! #   fn is_valid(&self, db: Self::Context<'_>) -> Result<(), Self::Error> {
107//! #     if self.0.len() == 11 {
108//! #         return Err(InvalidPhoneNumber::WrongLength);
109//! #     }
110//! #     if self.0.chars().any(|c| !c.is_digit(10)) {
111//! #         return Err(InvalidPhoneNumber::NonDigit);
112//! #     }
113//! #     if !db.check_phone_exists(self) {
114//! #         return Err(InvalidPhoneNumber::NotInDatabase);
115//! #     }
116//! #     Ok(())
117//! #   }
118//! # }
119//! let db = Database::new();
120//! let phone = PhoneNumber("01234567890".to_string());
121//! phone.validate_with(db);
122//! ```
123
124#![no_std]
125
126use core::{
127    fmt::{Debug, Formatter},
128    ops::Deref,
129};
130
131/// A thin wrapper around a value that guarantees that it is "valid"
132///
133/// A `Valid<T>` can only be constructed by calling [`Validate::validate`] and then handling the
134/// possible error
135///
136/// Note, `Valid<T>` is not `repr(transparent)`, so using `transmute` to forcibly convert is
137/// undefined behaviour.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub struct Valid<T>(T);
140
141impl<T> Valid<T> {
142    /// Consume self and return the inner value
143    pub fn into_inner(self) -> T {
144        self.0
145    }
146
147    /// Create a `Valid<T>` without validating
148    ///
149    /// This is only available with the `test-mock` feature enabled.
150    ///
151    /// It goes without saying that this function invalidates all compile-time guarantees. It's
152    /// provided as an "escape hatch", intended for testing. While
153    #[cfg(feature = "test-mock")]
154    pub fn danger_new_unvalidated(t: T) -> Self {
155        Self(t)
156    }
157}
158
159impl<T> Deref for Valid<T> {
160    type Target = T;
161    fn deref(&self) -> &Self::Target {
162        &self.0
163    }
164}
165
166/// A trait which defines what it means for a type to be "valid"
167///
168/// Because validity is defined by a trait, each type has a single definition of "valid", so
169/// newtype wrappers are recommended to give additional meaning to each type.
170///
171/// For example:
172/// ```
173/// # use validity::*;
174/// #[derive(Debug)]
175/// struct PhoneNumber(String);
176///
177/// enum InvalidPhoneNumber {
178///   NonDigit,
179///   WrongLength,
180/// }
181///
182/// impl Validate for PhoneNumber {
183///   type Context<'a> = ();
184///   type Error = InvalidPhoneNumber;
185///
186///   fn is_valid(&self, _ctx: Self::Context<'_>) -> Result<(), Self::Error> {
187///     if self.0.len() == 11 {
188///         return Err(InvalidPhoneNumber::WrongLength);
189///     }
190///
191///     if self.0.chars().any(|c| !c.is_digit(10)) {
192///         return Err(InvalidPhoneNumber::NonDigit);
193///     }
194///
195///     Ok(())
196///   }
197/// }
198///
199/// fn main() {
200///   let number = PhoneNumber("01234567890".to_string());
201///   if let Ok(number) = number.validate() {
202///     handle_phone_number(number);
203///   } else {
204///     println!("error!");
205///   }
206/// }
207///
208/// fn handle_phone_number(number: Valid<PhoneNumber>) {
209///   println!("This is a definitely valid phone number: {:?}", number.into_inner());
210/// }
211/// ```
212pub trait Validate {
213    /// Context required for validation
214    type Context<'a>;
215
216    /// The error returned by validation operations
217    type Error;
218
219    /// Perform the validation on this object
220    ///
221    /// Valid data should return `Ok(())`, and invalid data should return `Err(Self::Error)` which
222    /// indicates the reason why validation failed
223    fn is_valid(&self, ctx: Self::Context<'_>) -> Result<(), Self::Error>;
224
225    /// Validate with the given context
226    fn validate_with(self, ctx: Self::Context<'_>) -> Result<Valid<Self>, Failure<Self>>
227    where
228        Self: Sized,
229    {
230        match self.is_valid(ctx) {
231            Ok(()) => Ok(Valid(self)),
232            Err(error) => Err(Failure { value: self, error }),
233        }
234    }
235
236    /// Validate this object, and if successful return a `Valid<Self>` which acts as a "proof of
237    /// validity"
238    ///
239    /// If validation fails,
240    fn validate(self) -> Result<Valid<Self>, Failure<Self>>
241    where
242        Self: for<'a> Validate<Context<'a> = ()>,
243        Self: Sized,
244    {
245        self.validate_with(())
246    }
247}
248
249/// A struct representing a failure to validate a value
250///
251/// It contains both the value that failed validation, as well as the error that caused that error
252pub struct Failure<T: Validate> {
253    /// The value that caused validation to fail
254    pub value: T,
255    /// The error that was generated
256    pub error: <T as Validate>::Error,
257}
258
259impl<T> Debug for Failure<T>
260where
261    T: Validate + Debug,
262    T::Error: Debug,
263{
264    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
265        f.debug_struct("Failure")
266            .field("value", &self.value)
267            .field("error", &self.error)
268            .finish()
269    }
270}