use crate::db::{DbValue, ToDbValue};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Inspected {
Missing,
Text(String),
Number(f64),
Bool(bool),
Items(usize),
#[non_exhaustive]
File {
kilobytes: f64,
extension: String,
image: bool,
dimensions: Option<(u32, u32)>,
},
Date(chrono::NaiveDateTime),
}
pub trait FieldValue {
fn inspect(&self) -> Inspected;
fn db_value(&self) -> DbValue;
}
impl FieldValue for str {
fn inspect(&self) -> Inspected {
if self.trim().is_empty() {
Inspected::Missing
} else {
Inspected::Text(self.to_owned())
}
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
impl FieldValue for String {
fn inspect(&self) -> Inspected {
self.as_str().inspect()
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
macro_rules! number {
($($t:ty),*) => {$(
impl FieldValue for $t {
fn inspect(&self) -> Inspected {
Inspected::Number(*self as f64)
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
)*};
}
number!(i8, i16, i32, i64, u8, u16, u32, f32, f64);
impl FieldValue for bool {
fn inspect(&self) -> Inspected {
Inspected::Bool(*self)
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
impl FieldValue for chrono::NaiveDate {
fn inspect(&self) -> Inspected {
Inspected::Date(self.and_time(chrono::NaiveTime::MIN))
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
impl FieldValue for chrono::NaiveDateTime {
fn inspect(&self) -> Inspected {
Inspected::Date(*self)
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
impl FieldValue for chrono::DateTime<chrono::Utc> {
fn inspect(&self) -> Inspected {
Inspected::Date(self.naive_utc())
}
fn db_value(&self) -> DbValue {
self.to_db_value()
}
}
impl<T: FieldValue> FieldValue for Option<T> {
fn inspect(&self) -> Inspected {
self.as_ref()
.map_or(Inspected::Missing, FieldValue::inspect)
}
fn db_value(&self) -> DbValue {
self.as_ref().map_or(DbValue::Null, FieldValue::db_value)
}
}
impl<T> FieldValue for Vec<T> {
fn inspect(&self) -> Inspected {
if self.is_empty() {
Inspected::Missing
} else {
Inspected::Items(self.len())
}
}
fn db_value(&self) -> DbValue {
DbValue::Null
}
}
impl<T: FieldValue + ?Sized> FieldValue for &T {
fn inspect(&self) -> Inspected {
(**self).inspect()
}
fn db_value(&self) -> DbValue {
(**self).db_value()
}
}
impl FieldValue for crate::upload::Upload {
fn inspect(&self) -> Inspected {
if self.bytes().is_empty() {
return Inspected::File {
kilobytes: 0.0,
extension: String::new(),
image: false,
dimensions: None,
};
}
let extension = match self.sniffed_type() {
Some("image/png") => "png".to_owned(),
Some("image/jpeg") => "jpg".to_owned(),
Some("image/gif") => "gif".to_owned(),
Some("image/webp") => "webp".to_owned(),
Some("application/pdf") => "pdf".to_owned(),
_ => self.extension().unwrap_or_default(),
};
Inspected::File {
kilobytes: self.size() as f64 / 1024.0,
extension,
image: self.is_image(),
dimensions: self.dimensions(),
}
}
fn db_value(&self) -> DbValue {
DbValue::Null
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::upload::Upload;
use crate::validation::Validator;
#[test]
fn a_text_file_named_png_is_not_an_image() {
let fake = Some(Upload::new(
"fake.png",
"image/png",
axum::body::Bytes::from_static(b"not an image\n"),
));
assert!(matches!(
FieldValue::inspect(&fake),
Inspected::File { image: false, .. }
));
let mut v = Validator::new();
v.field("photo", &fake).label("picture").image().max(2048);
assert_eq!(
v.errors.first("photo"),
Some("The picture must be an image.")
);
}
}