Skip to main content

facet_validate/
lib.rs

1//! Validation attributes for facet.
2//!
3//! This crate provides validation attributes that can be used with the `#[facet(...)]` syntax.
4//! Validators are run during deserialization, providing errors with spans that point to the
5//! problematic JSON location.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use facet::Facet;
11//!
12//! #[derive(Facet)]
13//! pub struct Product {
14//!     #[facet(validate::min_length = 1, validate::max_length = 100)]
15//!     pub title: String,
16//!
17//!     #[facet(validate::min = 0)]
18//!     pub price: i64,
19//!
20//!     #[facet(validate::email)]
21//!     pub contact_email: String,
22//!
23//!     #[facet(validate::custom = validate_currency)]
24//!     pub currency: String,
25//! }
26//!
27//! fn validate_currency(s: &str) -> Result<(), String> {
28//!     match s {
29//!         "USD" | "EUR" | "GBP" => Ok(()),
30//!         _ => Err(format!("invalid currency code: {}", s)),
31//!     }
32//! }
33//! ```
34//!
35//! # Built-in Validators
36//!
37//! | Validator | Syntax | Applies To |
38//! |-----------|--------|------------|
39//! | `min` | `validate::min = 0` | numbers |
40//! | `max` | `validate::max = 100` | numbers |
41//! | `min_length` | `validate::min_length = 1` | String, Vec, slices |
42//! | `max_length` | `validate::max_length = 100` | String, Vec, slices |
43//! | `email` | `validate::email` | String |
44//! | `url` | `validate::url` | String |
45//! | `regex` | `validate::regex = r"..."` | String |
46//! | `contains` | `validate::contains = "foo"` | String |
47//! | `custom` | `validate::custom = fn_name` | any |
48
49#![warn(missing_docs)]
50
51use regex::Regex;
52use std::sync::LazyLock;
53
54// Re-export the validator function type for use in custom validators
55pub use facet_core::ValidatorFn;
56
57/// Validates that a string is a valid email address.
58///
59/// Uses a simple regex pattern that catches most common cases.
60pub fn is_valid_email(s: &str) -> bool {
61    static EMAIL_REGEX: LazyLock<Regex> =
62        LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap());
63    EMAIL_REGEX.is_match(s)
64}
65
66/// Validates that a string is a valid URL.
67///
68/// Uses a simple regex pattern that catches most common cases.
69pub fn is_valid_url(s: &str) -> bool {
70    // Use character classes instead of \s for portability
71    static URL_REGEX: LazyLock<Regex> =
72        LazyLock::new(|| Regex::new(r"^https?://[^ \t\r\n/$.?#][^ \t\r\n]*$").unwrap());
73    URL_REGEX.is_match(s)
74}
75
76/// Validates that a string matches a regex pattern.
77pub fn matches_pattern(s: &str, pattern: &str) -> bool {
78    match Regex::new(pattern) {
79        Ok(re) => re.is_match(s),
80        Err(_) => false,
81    }
82}
83
84// Define the validation attribute grammar
85facet::define_attr_grammar! {
86    ns "validate";
87    crate_path ::facet_validate;
88
89    /// Validation attributes for facet fields.
90    ///
91    /// These attributes can be used with `#[facet(validate::...)]` syntax.
92    pub enum Attr {
93        /// Minimum numeric value constraint.
94        ///
95        /// Usage: `#[facet(validate::min = 0)]`
96        #[target(field)]
97        Min(i64),
98
99        /// Maximum numeric value constraint.
100        ///
101        /// Usage: `#[facet(validate::max = 100)]`
102        #[target(field)]
103        Max(i64),
104
105        /// Minimum length constraint for strings and collections.
106        ///
107        /// Usage: `#[facet(validate::min_length = 1)]`
108        #[target(field)]
109        MinLength(usize),
110
111        /// Maximum length constraint for strings and collections.
112        ///
113        /// Usage: `#[facet(validate::max_length = 100)]`
114        #[target(field)]
115        MaxLength(usize),
116
117        /// Email format validation.
118        ///
119        /// Usage: `#[facet(validate::email)]`
120        #[target(field)]
121        Email,
122
123        /// URL format validation.
124        ///
125        /// Usage: `#[facet(validate::url)]`
126        #[target(field)]
127        Url,
128
129        /// Regex pattern validation.
130        ///
131        /// Usage: `#[facet(validate::regex = r"^[A-Z]{2}$")]`
132        #[target(field)]
133        Regex(&'static str),
134
135        /// String contains validation.
136        ///
137        /// Usage: `#[facet(validate::contains = "foo")]`
138        #[target(field)]
139        Contains(&'static str),
140
141        /// Custom validator function.
142        ///
143        /// The function must have signature `fn(&T) -> Result<(), String>`.
144        ///
145        /// Usage: `#[facet(validate::custom = my_validator)]`
146        #[target(field)]
147        Custom(validator ValidatorFn),
148    }
149}