use chrono::{DateTime, Duration, NaiveDate, NaiveTime, SecondsFormat, Utc};
use crate::core::validators::ValidationError;
use super::FieldType;
pub fn validate_temporal_flags(auto_now: bool, auto_now_add: bool) -> Result<(), ValidationError> {
if auto_now && auto_now_add {
return Err(ValidationError::new(
"auto_now and auto_now_add are mutually exclusive.",
"invalid",
));
}
Ok(())
}
pub fn parse_datetime_field(value: &str) -> Result<DateTime<Utc>, ValidationError> {
DateTime::parse_from_rfc3339(value)
.map(|value| value.with_timezone(&Utc))
.map_err(|_| {
ValidationError::new("Enter a valid RFC 3339 datetime.", "invalid")
.with_param("value", value)
})
}
pub fn parse_date_field(value: &str) -> Result<NaiveDate, ValidationError> {
NaiveDate::parse_from_str(value, "%Y-%m-%d").map_err(|_| {
ValidationError::new("Enter a valid ISO date.", "invalid").with_param("value", value)
})
}
pub fn parse_time_field(value: &str) -> Result<NaiveTime, ValidationError> {
NaiveTime::parse_from_str(value, "%H:%M:%S")
.or_else(|_| NaiveTime::parse_from_str(value, "%H:%M"))
.map_err(|_| {
ValidationError::new("Enter a valid ISO time.", "invalid").with_param("value", value)
})
}
fn parse_duration_field(value: &str) -> Result<Duration, ValidationError> {
if let Ok(microseconds) = value.trim().parse::<i64>() {
return Ok(Duration::microseconds(microseconds));
}
let (days, time_part) = if let Some((days_part, rest)) = value.split_once(' ') {
(
days_part.parse::<i64>().map_err(|_| {
ValidationError::new("Enter a valid duration.", "invalid")
.with_param("value", value)
})?,
rest,
)
} else {
(0, value)
};
let parts = time_part.split(':').collect::<Vec<_>>();
let (hours, minutes, seconds) = match parts.as_slice() {
[hours, minutes, seconds] => (
hours.parse::<i64>().ok(),
minutes.parse::<i64>().ok(),
seconds.parse::<i64>().ok(),
),
_ => (None, None, None),
};
match (hours, minutes, seconds) {
(Some(hours), Some(minutes), Some(seconds)) => Ok(Duration::days(days)
+ Duration::hours(hours)
+ Duration::minutes(minutes)
+ Duration::seconds(seconds)),
_ => {
Err(ValidationError::new("Enter a valid duration.", "invalid")
.with_param("value", value))
}
}
}
#[must_use]
pub fn db_type(field: &FieldType, vendor: &str) -> Option<String> {
let _ = vendor;
match field {
FieldType::DateTime { .. } => Some("timestamp with time zone".to_string()),
FieldType::Date { .. } => Some("date".to_string()),
FieldType::Time => Some("time".to_string()),
FieldType::Duration => Some("bigint".to_string()),
_ => None,
}
}
pub fn get_prep_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
match field {
FieldType::DateTime { .. } => {
Ok(parse_datetime_field(value)?.to_rfc3339_opts(SecondsFormat::AutoSi, true))
}
FieldType::Date { .. } => Ok(parse_date_field(value)?.format("%Y-%m-%d").to_string()),
FieldType::Time => Ok(parse_time_field(value)?.format("%H:%M:%S").to_string()),
FieldType::Duration => Ok(parse_duration_field(value)?
.num_microseconds()
.expect("duration microseconds should fit into i64")
.to_string()),
_ => Err(ValidationError::new(
"Field type is not handled by datetime preparation.",
"invalid",
)),
}
}
pub fn from_db_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
match field {
FieldType::DateTime { .. } => {
Ok(parse_datetime_field(value)?.to_rfc3339_opts(SecondsFormat::AutoSi, true))
}
FieldType::Date { .. } => Ok(parse_date_field(value)?.format("%Y-%m-%d").to_string()),
FieldType::Time => Ok(parse_time_field(value)?.format("%H:%M:%S").to_string()),
FieldType::Duration => Ok(parse_duration_field(value)?
.num_microseconds()
.expect("duration microseconds should fit into i64")
.to_string()),
_ => Err(ValidationError::new(
"Field type is not handled by datetime conversion.",
"invalid",
)),
}
}
#[must_use]
pub fn formfield(field: &FieldType) -> Option<&'static str> {
match field {
FieldType::DateTime { .. } => Some("DateTimeField"),
FieldType::Date { .. } => Some("DateField"),
FieldType::Time => Some("TimeField"),
FieldType::Duration => Some("DurationField"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{FieldType, db_type, formfield, from_db_value, get_prep_value};
#[test]
fn db_type_for_postgres_uses_timestamp_with_timezone() {
let field = FieldType::DateTime {
auto_now: false,
auto_now_add: false,
};
assert_eq!(
db_type(&field, "postgres").as_deref(),
Some("timestamp with time zone")
);
}
#[test]
fn get_prep_value_normalizes_datetime() {
let field = FieldType::DateTime {
auto_now: false,
auto_now_add: false,
};
let prepared = get_prep_value(&field, "2024-01-02T03:04:05Z")
.expect("datetime preparation should parse RFC3339 values");
assert_eq!(prepared, "2024-01-02T03:04:05Z");
}
#[test]
fn from_db_value_parses_date() {
let field = FieldType::Date {
auto_now: false,
auto_now_add: false,
};
let parsed = from_db_value(&field, "2024-05-06").expect("database date should parse");
assert_eq!(parsed, "2024-05-06");
}
#[test]
fn formfield_returns_correct_type() {
assert_eq!(formfield(&FieldType::Time), Some("TimeField"));
}
}