waterui_form/
valid.rs

1//! Validation utilities for form components.
2
3use core::{
4    error::Error,
5    fmt::{Debug, Display},
6    ops::Range,
7};
8
9use alloc::string::{String, ToString};
10use nami::{Binding, SignalExt};
11use regex::Regex;
12use waterui_core::View;
13use waterui_layout::stack::vstack;
14use waterui_text::text;
15
16macro_rules! impl_error {
17    ($ident:ident,$message:expr) => {
18        #[derive(Debug, Clone, Copy)]
19        #[doc = $message]
20        pub struct $ident;
21
22        impl core::fmt::Display for $ident {
23            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24                write!(f, $message)
25            }
26        }
27
28        impl core::error::Error for $ident {}
29    };
30}
31
32/// Trait for views that can be validated.
33/// This trait allows a view to be associated with a validator that can
34/// check the validity of the view's value.
35pub trait Validatable: View + Sized {
36    /// The type of value that this view holds and can be validated.
37    type Value;
38    /// Returns a mutable reference to the binding of the view's value.
39    /// This binding can be used to get or set the value,
40    /// as well as to observe changes
41    fn validable(&mut self) -> &mut Binding<Self::Value>;
42}
43
44/// Trait for validating values of type `T`.
45///
46/// Implementors of this trait provide a method to validate values
47/// and return either success or a reason for validation failure.
48pub trait Validator<T>: Clone + 'static {
49    /// The error type returned when validation fails.
50    type Err: Error;
51    /// Validates the given value.
52    ///
53    /// # Arguments
54    ///
55    /// * `value` - The value to validate.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error of type `Self::Err` if validation fails.
60    fn validate(&self, value: T) -> Result<(), Self::Err>;
61
62    /// Combines this validator with another using logical AND.
63    /// # Arguments
64    /// * `other` - The other validator to combine with.
65    ///
66    /// # Returns
67    /// A new validator that succeeds only if both validators succeed.
68    fn and<V>(self, other: V) -> And<Self, V>
69    where
70        Self: Sized,
71        V: Validator<T>,
72    {
73        And(self, other)
74    }
75    /// Combines this validator with another using logical OR.
76    /// # Arguments
77    /// * `other` - The other validator to combine with.
78    /// # Returns
79    /// A new validator that succeeds if either validator succeeds.
80    fn or<V>(self, other: V) -> Or<Self, V>
81    where
82        Self: Sized,
83        V: Validator<T>,
84    {
85        Or(self, other)
86    }
87}
88
89/// A view that combines a view with a validator.
90/// This struct holds a view and a validator, allowing the view's value
91/// to be validated
92#[derive(Debug, Clone)]
93pub struct ValidatableView<V, T> {
94    view: V,
95    validator: T,
96}
97
98impl<V, T> View for ValidatableView<V, T>
99where
100    T: Validator<V::Value>,
101    V: Validatable<Value: Clone>,
102{
103    fn body(mut self, _env: &waterui_core::Environment) -> impl View {
104        let value = {
105            let value = self.view.validable();
106            let validator = self.validator.clone();
107            let new_binding = value.filter(move |v| validator.validate(v.clone()).is_ok());
108
109            *value = new_binding;
110            value.clone()
111        };
112        vstack((
113            self.view,
114            text(value.map(move |v| {
115                if let Err(reason) = self.validator.validate(v) {
116                    reason.to_string()
117                } else {
118                    String::new()
119                }
120            })),
121        ))
122    }
123}
124
125/// An error indicating that a value is out of a specified range.
126#[derive(Debug, Clone)]
127pub struct OutOfRange<T>(pub Range<T>);
128
129impl<T: Display> Display for OutOfRange<T> {
130    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131        write!(
132            f,
133            "Value is out of range: {} - {}.",
134            self.0.start, self.0.end
135        )
136    }
137}
138
139impl<T: Display + Debug> Error for OutOfRange<T> {}
140
141impl<T: Display + Debug + Ord + Clone + 'static> Validator<T> for Range<T> {
142    type Err = OutOfRange<T>;
143    fn validate(&self, value: T) -> Result<(), Self::Err> {
144        self.contains(&value)
145            .then_some(())
146            .ok_or(OutOfRange(self.clone()))
147    }
148}
149
150impl<V: Validatable, T> ValidatableView<V, T> {
151    /// Creates a new `ValidatableView`.
152    ///
153    /// # Arguments
154    /// * `view` - The view to be validated.
155    /// * `validator` - The validator
156    pub const fn new(view: V, validator: T) -> Self {
157        Self { view, validator }
158    }
159}
160impl_error!(NotMatch, "Value does not match the required pattern.");
161
162impl<T> Validator<T> for Regex
163where
164    T: AsRef<str>,
165{
166    type Err = NotMatch;
167    fn validate(&self, value: T) -> Result<(), Self::Err> {
168        self.is_match(value.as_ref()).then_some(()).ok_or(NotMatch)
169    }
170}
171
172/// A validator that combines two validators with logical AND.
173/// Short-circuits on the first failure.
174#[derive(Debug, Clone)]
175pub struct And<A, B>(A, B);
176
177/// An error type for the `And` validator, representing which validator failed.
178#[derive(Debug, Clone)]
179pub enum AndError<A, B> {
180    /// The first validator failed.
181    A(A),
182    /// The second validator failed.
183    B(B),
184}
185
186impl<A, B> Display for AndError<A, B>
187where
188    A: Display,
189    B: Display,
190{
191    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
192        match self {
193            Self::A(a) => write!(f, "{a}"),
194            Self::B(b) => write!(f, "{b}"),
195        }
196    }
197}
198
199impl<A, B> Error for AndError<A, B>
200where
201    A: Error,
202    B: Error,
203{
204}
205
206impl<T, A, B> Validator<T> for And<A, B>
207where
208    T: Clone,
209    A: Validator<T>,
210    B: Validator<T>,
211{
212    type Err = AndError<A::Err, B::Err>;
213    fn validate(&self, value: T) -> Result<(), Self::Err> {
214        self.0.validate(value.clone()).map_err(AndError::A)?;
215        self.1.validate(value).map_err(AndError::B)
216    }
217}
218
219/// A validator that combines two validators with logical OR.
220/// Succeeds if at least one validator succeeds.
221/// Short-circuits on the first success.
222#[derive(Debug, Clone)]
223pub struct Or<A, B>(A, B);
224
225/// An error type for the `Or` validator, representing both validation failures.
226#[derive(Debug, Clone)]
227pub struct OrError<A, B>(pub A, pub B);
228
229impl<A, B> Display for OrError<A, B>
230where
231    A: Display,
232    B: Display,
233{
234    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
235        write!(
236            f,
237            "At least one of the following errors occurred:\n1. {}\n2. {}",
238            self.0, self.1
239        )
240    }
241}
242
243impl<A, B> Error for OrError<A, B>
244where
245    A: Error,
246    B: Error,
247{
248}
249
250impl<T, A, B> Validator<T> for Or<A, B>
251where
252    T: Clone,
253    A: Validator<T>,
254    B: Validator<T>,
255{
256    type Err = OrError<A::Err, B::Err>;
257    fn validate(&self, value: T) -> Result<(), Self::Err> {
258        self.0
259            .validate(value.clone())
260            .or_else(|e1| self.1.validate(value).map_err(|e2| OrError(e1, e2)))
261    }
262}
263
264/// A validator that checks if a value is present (not None or not empty).
265#[derive(Debug, Clone, Copy)]
266pub struct Required;
267
268impl_error!(RequiredError, "Value is required.");
269
270impl<T> Validator<Option<T>> for Required {
271    type Err = RequiredError;
272    fn validate(&self, value: Option<T>) -> Result<(), Self::Err> {
273        value.is_some().then_some(()).ok_or(RequiredError)
274    }
275}
276
277impl<'a> Validator<&'a str> for Required {
278    type Err = RequiredError;
279
280    fn validate(&self, value: &'a str) -> Result<(), Self::Err> {
281        // we consider a string with only whitespace as empty
282        value.trim().is_empty().then_some(()).ok_or(RequiredError)
283    }
284}