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}