Skip to main content

renox_core/validation/
value.rs

1use crate::db::{DbValue, ToDbValue};
2
3/// What a rule sees of a field's value.
4#[derive(Debug, Clone, PartialEq)]
5#[non_exhaustive]
6pub enum Inspected {
7    /// `None`, or text that is empty after trimming.
8    Missing,
9    /// Non-empty text.
10    Text(String),
11    /// A number.
12    Number(f64),
13    /// A boolean, e.g. a checkbox.
14    Bool(bool),
15    /// A list (`Vec`) with this many items.
16    Items(usize),
17    /// An uploaded file: size in kilobytes, the extension its content (or
18    /// else its name) indicates, whether it really is an image, and its
19    /// size in pixels.
20    #[non_exhaustive]
21    File {
22        /// Size in kilobytes.
23        kilobytes: f64,
24        /// The extension its content indicates, else its name's.
25        extension: String,
26        /// Whether the content really is an image.
27        image: bool,
28        /// Width and height in pixels, for images.
29        dimensions: Option<(u32, u32)>,
30    },
31    /// A date or a date and time (a date alone is its midnight).
32    Date(chrono::NaiveDateTime),
33}
34
35/// A value that can be validated. Implemented for strings, numbers, `bool`,
36/// `Option<T>` and `Vec<T>`.
37pub trait FieldValue {
38    /// What rules see of the value.
39    fn inspect(&self) -> Inspected;
40    /// The value as a query parameter, for `unique` and `exists`.
41    fn db_value(&self) -> DbValue;
42}
43
44impl FieldValue for str {
45    fn inspect(&self) -> Inspected {
46        if self.trim().is_empty() {
47            Inspected::Missing
48        } else {
49            Inspected::Text(self.to_owned())
50        }
51    }
52
53    fn db_value(&self) -> DbValue {
54        self.to_db_value()
55    }
56}
57
58impl FieldValue for String {
59    fn inspect(&self) -> Inspected {
60        self.as_str().inspect()
61    }
62
63    fn db_value(&self) -> DbValue {
64        self.to_db_value()
65    }
66}
67
68macro_rules! number {
69    ($($t:ty),*) => {$(
70        impl FieldValue for $t {
71            fn inspect(&self) -> Inspected {
72                Inspected::Number(*self as f64)
73            }
74
75            fn db_value(&self) -> DbValue {
76                self.to_db_value()
77            }
78        }
79    )*};
80}
81
82number!(i8, i16, i32, i64, u8, u16, u32, f32, f64);
83
84impl FieldValue for bool {
85    fn inspect(&self) -> Inspected {
86        Inspected::Bool(*self)
87    }
88
89    fn db_value(&self) -> DbValue {
90        self.to_db_value()
91    }
92}
93
94impl FieldValue for chrono::NaiveDate {
95    fn inspect(&self) -> Inspected {
96        Inspected::Date(self.and_time(chrono::NaiveTime::MIN))
97    }
98
99    fn db_value(&self) -> DbValue {
100        self.to_db_value()
101    }
102}
103
104impl FieldValue for chrono::NaiveDateTime {
105    fn inspect(&self) -> Inspected {
106        Inspected::Date(*self)
107    }
108
109    fn db_value(&self) -> DbValue {
110        self.to_db_value()
111    }
112}
113
114impl FieldValue for chrono::DateTime<chrono::Utc> {
115    fn inspect(&self) -> Inspected {
116        Inspected::Date(self.naive_utc())
117    }
118
119    fn db_value(&self) -> DbValue {
120        self.to_db_value()
121    }
122}
123
124impl<T: FieldValue> FieldValue for Option<T> {
125    fn inspect(&self) -> Inspected {
126        self.as_ref()
127            .map_or(Inspected::Missing, FieldValue::inspect)
128    }
129
130    fn db_value(&self) -> DbValue {
131        self.as_ref().map_or(DbValue::Null, FieldValue::db_value)
132    }
133}
134
135impl<T> FieldValue for Vec<T> {
136    fn inspect(&self) -> Inspected {
137        if self.is_empty() {
138            Inspected::Missing
139        } else {
140            Inspected::Items(self.len())
141        }
142    }
143
144    fn db_value(&self) -> DbValue {
145        DbValue::Null
146    }
147}
148
149impl<T: FieldValue + ?Sized> FieldValue for &T {
150    fn inspect(&self) -> Inspected {
151        (**self).inspect()
152    }
153
154    fn db_value(&self) -> DbValue {
155        (**self).db_value()
156    }
157}
158
159impl FieldValue for crate::upload::Upload {
160    fn inspect(&self) -> Inspected {
161        // An empty file that was chosen is a file with no type: `image()`
162        // and `mimes()` refuse it. (No file chosen never gets here.)
163        if self.bytes().is_empty() {
164            return Inspected::File {
165                kilobytes: 0.0,
166                extension: String::new(),
167                image: false,
168                dimensions: None,
169            };
170        }
171        let extension = match self.sniffed_type() {
172            Some("image/png") => "png".to_owned(),
173            Some("image/jpeg") => "jpg".to_owned(),
174            Some("image/gif") => "gif".to_owned(),
175            Some("image/webp") => "webp".to_owned(),
176            Some("application/pdf") => "pdf".to_owned(),
177            _ => self.extension().unwrap_or_default(),
178        };
179        Inspected::File {
180            kilobytes: self.size() as f64 / 1024.0,
181            extension,
182            image: self.is_image(),
183            dimensions: self.dimensions(),
184        }
185    }
186
187    fn db_value(&self) -> DbValue {
188        DbValue::Null
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::upload::Upload;
196    use crate::validation::Validator;
197
198    #[test]
199    fn a_text_file_named_png_is_not_an_image() {
200        let fake = Some(Upload::new(
201            "fake.png",
202            "image/png",
203            axum::body::Bytes::from_static(b"not an image\n"),
204        ));
205        assert!(matches!(
206            FieldValue::inspect(&fake),
207            Inspected::File { image: false, .. }
208        ));
209        let mut v = Validator::new();
210        v.field("photo", &fake).label("picture").image().max(2048);
211        assert_eq!(
212            v.errors.first("photo"),
213            Some("The picture must be an image.")
214        );
215    }
216}