use serde_json::Value;
use crate::error::{value_type_name, IssueCode, VldError};
use crate::schema::VldSchema;
#[derive(Clone)]
pub struct ZDate {
min: Option<(chrono::NaiveDate, String)>,
max: Option<(chrono::NaiveDate, String)>,
past: Option<String>,
future: Option<String>,
custom_type_error: Option<String>,
}
impl ZDate {
pub fn new() -> Self {
Self {
min: None,
max: None,
past: None,
future: None,
custom_type_error: None,
}
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
pub fn min(self, date: &str) -> Self {
let d = chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d")
.unwrap_or_else(|_| panic!("Invalid date literal: {}", date));
self.min_date(d)
}
pub fn min_date(mut self, date: chrono::NaiveDate) -> Self {
let msg = format!("Date must be on or after {}", date);
self.min = Some((date, msg));
self
}
pub fn max(self, date: &str) -> Self {
let d = chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d")
.unwrap_or_else(|_| panic!("Invalid date literal: {}", date));
self.max_date(d)
}
pub fn max_date(mut self, date: chrono::NaiveDate) -> Self {
let msg = format!("Date must be on or before {}", date);
self.max = Some((date, msg));
self
}
pub fn past(self) -> Self {
self.past_msg("Date must be in the past")
}
pub fn past_msg(mut self, msg: impl Into<String>) -> Self {
self.past = Some(msg.into());
self
}
pub fn future(self) -> Self {
self.future_msg("Date must be in the future")
}
pub fn future_msg(mut self, msg: impl Into<String>) -> Self {
self.future = Some(msg.into());
self
}
#[cfg(feature = "openapi")]
pub fn to_json_schema(&self) -> serde_json::Value {
serde_json::json!({"type": "string", "format": "date"})
}
}
impl Default for ZDate {
fn default() -> Self {
Self::new()
}
}
impl VldSchema for ZDate {
type Output = chrono::NaiveDate;
fn parse_value(&self, value: &Value) -> Result<chrono::NaiveDate, VldError> {
let s = value.as_str().ok_or_else(|| {
let msg = self.custom_type_error.clone().unwrap_or_else(|| {
format!(
"Expected date string (YYYY-MM-DD), received {}",
value_type_name(value)
)
});
VldError::single_with_value(
IssueCode::InvalidType {
expected: "string (date)".to_string(),
received: value_type_name(value),
},
msg,
value,
)
})?;
let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| {
VldError::single_with_value(
IssueCode::Custom {
code: "invalid_date".to_string(),
},
format!("Invalid date format: expected YYYY-MM-DD, got \"{}\"", s),
value,
)
})?;
let mut errors = VldError::new();
if let Some((min_date, msg)) = &self.min {
if date < *min_date {
errors.push_with_value(
IssueCode::TooSmall {
minimum: 0.0,
inclusive: true,
},
msg.clone(),
value,
);
}
}
if let Some((max_date, msg)) = &self.max {
if date > *max_date {
errors.push_with_value(
IssueCode::TooBig {
maximum: 0.0,
inclusive: true,
},
msg.clone(),
value,
);
}
}
let today = chrono::Utc::now().date_naive();
if let Some(msg) = &self.past {
if date >= today {
errors.push_with_value(
IssueCode::Custom {
code: "not_past_date".to_string(),
},
msg.clone(),
value,
);
}
}
if let Some(msg) = &self.future {
if date <= today {
errors.push_with_value(
IssueCode::Custom {
code: "not_future_date".to_string(),
},
msg.clone(),
value,
);
}
}
if errors.is_empty() {
Ok(date)
} else {
Err(errors)
}
}
}
#[derive(Clone)]
pub struct ZDateTime {
min: Option<(chrono::DateTime<chrono::Utc>, String)>,
max: Option<(chrono::DateTime<chrono::Utc>, String)>,
past: Option<String>,
future: Option<String>,
allow_naive: bool,
naive_offset: chrono::FixedOffset,
required_timezone_offset: Option<(chrono::FixedOffset, String)>,
custom_type_error: Option<String>,
}
impl ZDateTime {
pub fn new() -> Self {
Self {
min: None,
max: None,
past: None,
future: None,
allow_naive: true,
naive_offset: chrono::FixedOffset::east_opt(0).expect("0 offset must be valid"),
required_timezone_offset: None,
custom_type_error: None,
}
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
pub fn min(self, dt: &str) -> Self {
let parsed = chrono::DateTime::parse_from_rfc3339(dt)
.unwrap_or_else(|_| panic!("Invalid datetime literal: {}", dt))
.with_timezone(&chrono::Utc);
self.min_datetime(parsed)
}
pub fn min_datetime(mut self, dt: chrono::DateTime<chrono::Utc>) -> Self {
let msg = format!("Datetime must be on or after {}", dt.to_rfc3339());
self.min = Some((dt, msg));
self
}
pub fn max(self, dt: &str) -> Self {
let parsed = chrono::DateTime::parse_from_rfc3339(dt)
.unwrap_or_else(|_| panic!("Invalid datetime literal: {}", dt))
.with_timezone(&chrono::Utc);
self.max_datetime(parsed)
}
pub fn max_datetime(mut self, dt: chrono::DateTime<chrono::Utc>) -> Self {
let msg = format!("Datetime must be on or before {}", dt.to_rfc3339());
self.max = Some((dt, msg));
self
}
pub fn past(self) -> Self {
self.past_msg("Datetime must be in the past")
}
pub fn past_msg(mut self, msg: impl Into<String>) -> Self {
self.past = Some(msg.into());
self
}
pub fn future(self) -> Self {
self.future_msg("Datetime must be in the future")
}
pub fn future_msg(mut self, msg: impl Into<String>) -> Self {
self.future = Some(msg.into());
self
}
pub fn naive_allowed(mut self, allowed: bool) -> Self {
self.allow_naive = allowed;
self
}
pub fn with_timezone_only(self) -> Self {
self.naive_allowed(false)
}
pub fn naive_timezone_offset(mut self, offset_seconds: i32) -> Self {
let offset = chrono::FixedOffset::east_opt(offset_seconds).unwrap_or_else(|| {
panic!(
"Invalid timezone offset seconds: {} (expected range -86400..=86400)",
offset_seconds
)
});
self.naive_offset = offset;
self
}
pub fn timezone_offset_only(self, offset_seconds: i32) -> Self {
chrono::FixedOffset::east_opt(offset_seconds).unwrap_or_else(|| {
panic!(
"Invalid timezone offset seconds: {} (expected range -86400..=86400)",
offset_seconds
)
});
let sign = if offset_seconds >= 0 { '+' } else { '-' };
let abs = offset_seconds.unsigned_abs();
let hh = abs / 3600;
let mm = (abs % 3600) / 60;
let msg = format!("Timezone offset must be {}{:02}:{:02}", sign, hh, mm);
self.timezone_offset_only_msg(offset_seconds, msg)
}
pub fn timezone_offset_only_msg(mut self, offset_seconds: i32, msg: impl Into<String>) -> Self {
let offset = chrono::FixedOffset::east_opt(offset_seconds).unwrap_or_else(|| {
panic!(
"Invalid timezone offset seconds: {} (expected range -86400..=86400)",
offset_seconds
)
});
self.required_timezone_offset = Some((offset, msg.into()));
self
}
#[cfg(feature = "openapi")]
pub fn to_json_schema(&self) -> serde_json::Value {
serde_json::json!({"type": "string", "format": "date-time"})
}
}
impl Default for ZDateTime {
fn default() -> Self {
Self::new()
}
}
impl VldSchema for ZDateTime {
type Output = chrono::DateTime<chrono::Utc>;
fn parse_value(&self, value: &Value) -> Result<chrono::DateTime<chrono::Utc>, VldError> {
let s = value.as_str().ok_or_else(|| {
let msg = self.custom_type_error.clone().unwrap_or_else(|| {
format!(
"Expected datetime string, received {}",
value_type_name(value)
)
});
VldError::single_with_value(
IssueCode::InvalidType {
expected: "string (datetime)".to_string(),
received: value_type_name(value),
},
msg,
value,
)
})?;
use chrono::TimeZone;
let dt = if let Ok(dt_fixed) = chrono::DateTime::parse_from_rfc3339(s) {
if let Some((required, msg)) = &self.required_timezone_offset {
if dt_fixed.offset().local_minus_utc() != required.local_minus_utc() {
return Err(VldError::single_with_value(
IssueCode::Custom {
code: "invalid_timezone_offset".to_string(),
},
msg.clone(),
value,
));
}
}
dt_fixed.with_timezone(&chrono::Utc)
} else if self.allow_naive {
if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
self.naive_offset
.from_local_datetime(&ndt)
.single()
.expect("FixedOffset must map local datetime unambiguously")
.with_timezone(&chrono::Utc)
} else if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f")
{
self.naive_offset
.from_local_datetime(&ndt)
.single()
.expect("FixedOffset must map local datetime unambiguously")
.with_timezone(&chrono::Utc)
} else {
return Err(VldError::single_with_value(
IssueCode::Custom {
code: "invalid_datetime".to_string(),
},
format!("Invalid datetime format: \"{}\"", s),
value,
));
}
} else {
return Err(VldError::single_with_value(
IssueCode::Custom {
code: "invalid_datetime".to_string(),
},
format!("Invalid datetime format: \"{}\"", s),
value,
));
};
let mut errors = VldError::new();
if let Some((min_dt, msg)) = &self.min {
if dt < *min_dt {
errors.push_with_value(
IssueCode::TooSmall {
minimum: 0.0,
inclusive: true,
},
msg.clone(),
value,
);
}
}
if let Some((max_dt, msg)) = &self.max {
if dt > *max_dt {
errors.push_with_value(
IssueCode::TooBig {
maximum: 0.0,
inclusive: true,
},
msg.clone(),
value,
);
}
}
let now = chrono::Utc::now();
if let Some(msg) = &self.past {
if dt >= now {
errors.push_with_value(
IssueCode::Custom {
code: "not_past_datetime".to_string(),
},
msg.clone(),
value,
);
}
}
if let Some(msg) = &self.future {
if dt <= now {
errors.push_with_value(
IssueCode::Custom {
code: "not_future_datetime".to_string(),
},
msg.clone(),
value,
);
}
}
if errors.is_empty() {
Ok(dt)
} else {
Err(errors)
}
}
}