use serde_json::Value;
use std::fmt::Display;
use std::marker::PhantomData;
use crate::error::{issue_error, DateKind, IssueCode, SizeOrigin, ValidationIssue, VldError};
use crate::primitives::iso::{is_valid_iso_date, is_valid_time_of_day, split_datetime};
use crate::schema::VldSchema;
mod sealed {
pub trait Sealed {}
}
pub(super) use sealed::Sealed;
pub trait DateBackend: Sealed {
type Date: Clone + PartialOrd + Display;
type DateTime: Clone + PartialOrd;
type Time: Clone + PartialOrd + Display;
fn parse_date(s: &str) -> Option<Self::Date>;
fn parse_time(s: &str) -> Option<Self::Time>;
fn naive_to_utc(naive: &str, offset_seconds: i32) -> Option<Self::DateTime>;
fn today() -> Self::Date;
fn now() -> Self::DateTime;
fn format_datetime(dt: &Self::DateTime) -> String;
fn format_time(t: &Self::Time) -> String {
t.to_string()
}
}
fn parse_date<B: DateBackend>(s: &str) -> Option<B::Date> {
is_valid_iso_date(s).then(|| B::parse_date(s)).flatten()
}
fn parse_time<B: DateBackend>(s: &str) -> Option<B::Time> {
is_valid_time_of_day(s).then(|| B::parse_time(s)).flatten()
}
fn parse_datetime<B: DateBackend>(
s: &str,
naive_offset: Option<i32>,
) -> Option<(B::DateTime, Option<i32>)> {
let (naive, offset) = split_datetime(s)?;
let at = offset.or(naive_offset)?;
if !in_common_range(&naive, at) {
return None;
}
let dt = B::naive_to_utc(&naive, at)?;
Some((dt, offset))
}
fn in_common_range(naive: &str, offset_seconds: i32) -> bool {
let num = |r: std::ops::Range<usize>| naive[r].parse::<i32>().unwrap_or(0);
let secs = num(11..13) * 3600 + num(14..16) * 60 + num(17..19) - offset_seconds;
match &naive[..10] {
"0000-01-01" => secs >= 0,
"9999-12-30" => secs <= 22 * 3600,
"9999-12-31" => secs <= 22 * 3600 - 86_400,
_ => true,
}
}
fn check_offset(offset_seconds: i32) -> i32 {
if offset_seconds.unsigned_abs() >= 86_400 {
panic!(
"Invalid timezone offset seconds: {} (expected range -86399..=86399)",
offset_seconds
);
}
offset_seconds
}
fn format_offset(offset_seconds: i32) -> String {
let sign = if offset_seconds >= 0 { '+' } else { '-' };
let abs = offset_seconds.unsigned_abs();
format!("{}{:02}:{:02}", sign, abs / 3600, (abs % 3600) / 60)
}
#[derive(Clone)]
struct Bound<T> {
value: T,
display: String,
msg: Option<String>,
}
#[derive(Clone)]
struct Bounds<T> {
min: Option<Bound<T>>,
max: Option<Bound<T>>,
past: Option<Option<String>>,
future: Option<Option<String>>,
}
impl<T: PartialOrd> Bounds<T> {
fn new() -> Self {
Self {
min: None,
max: None,
past: None,
future: None,
}
}
fn set_min(&mut self, value: T, display: String) {
self.min = Some(Bound {
value,
display,
msg: None,
});
}
fn set_max(&mut self, value: T, display: String) {
self.max = Some(Bound {
value,
display,
msg: None,
});
}
fn check(&self, v: &T, now: impl FnOnce() -> T, kind: DateKind, value: &Value) -> VldError {
let origin = match kind {
DateKind::Date => SizeOrigin::Date,
DateKind::DateTime => SizeOrigin::DateTime,
DateKind::Time => SizeOrigin::Time,
};
let mut errors = VldError::new();
let mut push = |code, msg: Option<&String>| {
errors
.issues
.push(ValidationIssue::with_override(code, msg).with_received(value))
};
if let Some(min) = self.min.as_ref().filter(|b| *v < b.value) {
push(
IssueCode::TooSmall {
origin,
minimum: Value::String(min.display.clone()),
inclusive: true,
},
min.msg.as_ref(),
);
}
if let Some(max) = self.max.as_ref().filter(|b| *v > b.value) {
push(
IssueCode::TooBig {
origin,
maximum: Value::String(max.display.clone()),
inclusive: true,
},
max.msg.as_ref(),
);
}
if self.past.is_some() || self.future.is_some() {
let now = now();
if let Some(msg) = self.past.as_ref().filter(|_| *v >= now) {
push(IssueCode::NotInPast { kind }, msg.as_ref());
}
if let Some(msg) = self.future.as_ref().filter(|_| *v <= now) {
push(IssueCode::NotInFuture { kind }, msg.as_ref());
}
}
errors
}
#[cfg(feature = "openapi")]
fn json_schema(&self, mut schema: Value) -> Value {
if let Some(b) = &self.min {
schema["formatMinimum"] = serde_json::json!(b.display);
}
if let Some(b) = &self.max {
schema["formatMaximum"] = serde_json::json!(b.display);
}
if self.past.is_some() {
schema["x-past"] = Value::Bool(true);
}
if self.future.is_some() {
schema["x-future"] = Value::Bool(true);
}
schema
}
}
#[derive(Clone)]
pub struct ZDate<B: DateBackend> {
bounds: Bounds<B::Date>,
custom_type_error: Option<String>,
_backend: PhantomData<fn() -> B>,
}
impl<B: DateBackend> ZDate<B> {
pub fn new() -> Self {
Self {
bounds: Bounds::new(),
custom_type_error: None,
_backend: PhantomData,
}
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
fn literal(date: &str) -> B::Date {
parse_date::<B>(date).unwrap_or_else(|| panic!("Invalid date literal: {}", date))
}
pub fn min(self, date: &str) -> Self {
self.min_date(Self::literal(date))
}
pub fn min_date(mut self, date: B::Date) -> Self {
let display = date.to_string();
self.bounds.set_min(date, display);
self
}
pub fn max(self, date: &str) -> Self {
self.max_date(Self::literal(date))
}
pub fn max_date(mut self, date: B::Date) -> Self {
let display = date.to_string();
self.bounds.set_max(date, display);
self
}
pub fn past(mut self) -> Self {
self.bounds.past = Some(None);
self
}
pub fn past_msg(mut self, msg: impl Into<String>) -> Self {
self.bounds.past = Some(Some(msg.into()));
self
}
pub fn future(mut self) -> Self {
self.bounds.future = Some(None);
self
}
pub fn future_msg(mut self, msg: impl Into<String>) -> Self {
self.bounds.future = Some(Some(msg.into()));
self
}
}
impl<B: DateBackend> Default for ZDate<B> {
fn default() -> Self {
Self::new()
}
}
impl<B: DateBackend> VldSchema for ZDate<B> {
type Output = B::Date;
fn output_json(&self, o: &B::Date) -> Option<Value> {
Some(Value::String(o.to_string()))
}
fn parse_value(&self, value: &Value) -> Result<B::Date, VldError> {
let s = value
.as_str()
.ok_or_else(|| crate::primitives::type_error(&self.custom_type_error, "date", value))?;
let date = parse_date::<B>(s).ok_or_else(|| {
VldError::from_code_with_value(
IssueCode::InvalidDate {
kind: DateKind::Date,
},
value,
)
})?;
let errors = self.bounds.check(&date, B::today, DateKind::Date, value);
if errors.is_empty() {
Ok(date)
} else {
Err(errors)
}
}
}
#[cfg(feature = "openapi")]
impl<B: DateBackend> crate::openapi::JsonSchema for ZDate<B> {
fn json_schema(&self) -> Value {
self.bounds
.json_schema(serde_json::json!({"type": "string", "format": "date"}))
}
}
#[cfg(feature = "openapi")]
impl<B: DateBackend> crate::openapi::CollectNestedSchemas for ZDate<B> {}
#[derive(Clone)]
struct RequiredOffset {
seconds: i32,
msg: Option<String>,
}
impl RequiredOffset {
fn error(&self, value: &Value) -> VldError {
issue_error(
IssueCode::InvalidTimezoneOffset {
expected: format_offset(self.seconds),
},
self.msg.as_ref(),
value,
)
}
}
#[derive(Clone)]
pub struct ZDateTime<B: DateBackend> {
bounds: Bounds<B::DateTime>,
allow_naive: bool,
naive_offset: i32,
required_timezone_offset: Option<RequiredOffset>,
custom_type_error: Option<String>,
_backend: PhantomData<fn() -> B>,
}
impl<B: DateBackend> ZDateTime<B> {
pub fn new() -> Self {
Self {
bounds: Bounds::new(),
allow_naive: true,
naive_offset: 0,
required_timezone_offset: None,
custom_type_error: None,
_backend: PhantomData,
}
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
fn literal(dt: &str) -> B::DateTime {
parse_datetime::<B>(dt, None)
.map(|(dt, _)| dt)
.unwrap_or_else(|| panic!("Invalid datetime literal: {}", dt))
}
pub fn min(self, dt: &str) -> Self {
self.min_datetime(Self::literal(dt))
}
pub fn min_datetime(mut self, dt: B::DateTime) -> Self {
let display = B::format_datetime(&dt);
self.bounds.set_min(dt, display);
self
}
pub fn max(self, dt: &str) -> Self {
self.max_datetime(Self::literal(dt))
}
pub fn max_datetime(mut self, dt: B::DateTime) -> Self {
let display = B::format_datetime(&dt);
self.bounds.set_max(dt, display);
self
}
pub fn past(mut self) -> Self {
self.bounds.past = Some(None);
self
}
pub fn past_msg(mut self, msg: impl Into<String>) -> Self {
self.bounds.past = Some(Some(msg.into()));
self
}
pub fn future(mut self) -> Self {
self.bounds.future = Some(None);
self
}
pub fn future_msg(mut self, msg: impl Into<String>) -> Self {
self.bounds.future = Some(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 {
self.naive_offset = check_offset(offset_seconds);
self
}
pub fn timezone_offset_only(mut self, offset_seconds: i32) -> Self {
self.required_timezone_offset = Some(RequiredOffset {
seconds: check_offset(offset_seconds),
msg: None,
});
self
}
pub fn timezone_offset_only_msg(mut self, offset_seconds: i32, msg: impl Into<String>) -> Self {
self.required_timezone_offset = Some(RequiredOffset {
seconds: check_offset(offset_seconds),
msg: Some(msg.into()),
});
self
}
}
impl<B: DateBackend> Default for ZDateTime<B> {
fn default() -> Self {
Self::new()
}
}
impl<B: DateBackend> VldSchema for ZDateTime<B> {
type Output = B::DateTime;
fn output_json(&self, o: &B::DateTime) -> Option<Value> {
Some(Value::String(B::format_datetime(o)))
}
fn parse_value(&self, value: &Value) -> Result<B::DateTime, VldError> {
let s = value.as_str().ok_or_else(|| {
crate::primitives::type_error(&self.custom_type_error, "datetime", value)
})?;
let naive_offset = (self.allow_naive && self.required_timezone_offset.is_none())
.then_some(self.naive_offset);
let (dt, offset) = parse_datetime::<B>(s, naive_offset).ok_or_else(|| {
if let (Some(required), Some((_, None))) =
(&self.required_timezone_offset, split_datetime(s))
{
return required.error(value);
}
VldError::from_code_with_value(
IssueCode::InvalidDate {
kind: DateKind::DateTime,
},
value,
)
})?;
if let Some(required) = &self.required_timezone_offset {
if offset != Some(required.seconds) {
return Err(required.error(value));
}
}
let errors = self.bounds.check(&dt, B::now, DateKind::DateTime, value);
if errors.is_empty() {
Ok(dt)
} else {
Err(errors)
}
}
}
#[cfg(feature = "openapi")]
impl<B: DateBackend> crate::openapi::JsonSchema for ZDateTime<B> {
fn json_schema(&self) -> Value {
let base = if self.allow_naive && self.required_timezone_offset.is_none() {
serde_json::json!({
"type": "string",
"anyOf": [
{"format": "date-time"},
{"format": "naive-date-time", "pattern": NAIVE_DATETIME_PATTERN},
],
})
} else {
serde_json::json!({"type": "string", "format": "date-time"})
};
self.bounds.json_schema(base)
}
}
#[cfg(feature = "openapi")]
const NAIVE_DATETIME_PATTERN: &str =
r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{1,9})?$";
#[cfg(feature = "openapi")]
const TIME_OF_DAY_PATTERN: &str = r"^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{1,9})?$";
#[cfg(feature = "openapi")]
impl<B: DateBackend> crate::openapi::CollectNestedSchemas for ZDateTime<B> {}
#[derive(Clone)]
pub struct ZTime<B: DateBackend> {
bounds: Bounds<B::Time>,
custom_type_error: Option<String>,
_backend: PhantomData<fn() -> B>,
}
impl<B: DateBackend> ZTime<B> {
pub fn new() -> Self {
Self {
bounds: Bounds::new(),
custom_type_error: None,
_backend: PhantomData,
}
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
fn literal(time: &str) -> B::Time {
parse_time::<B>(time).unwrap_or_else(|| panic!("Invalid time literal: {}", time))
}
pub fn min(mut self, time: &str) -> Self {
self.bounds.set_min(Self::literal(time), time.to_string());
self
}
pub fn min_time(mut self, time: B::Time) -> Self {
let display = B::format_time(&time);
self.bounds.set_min(time, display);
self
}
pub fn max(mut self, time: &str) -> Self {
self.bounds.set_max(Self::literal(time), time.to_string());
self
}
pub fn max_time(mut self, time: B::Time) -> Self {
let display = B::format_time(&time);
self.bounds.set_max(time, display);
self
}
}
impl<B: DateBackend> Default for ZTime<B> {
fn default() -> Self {
Self::new()
}
}
impl<B: DateBackend> VldSchema for ZTime<B> {
type Output = B::Time;
fn output_json(&self, o: &B::Time) -> Option<Value> {
Some(Value::String(B::format_time(o)))
}
fn parse_value(&self, value: &Value) -> Result<B::Time, VldError> {
let s = value
.as_str()
.ok_or_else(|| crate::primitives::type_error(&self.custom_type_error, "time", value))?;
let time = parse_time::<B>(s).ok_or_else(|| {
VldError::from_code_with_value(
IssueCode::InvalidDate {
kind: DateKind::Time,
},
value,
)
})?;
let errors = self
.bounds
.check(&time, || time.clone(), DateKind::Time, value);
if errors.is_empty() {
Ok(time)
} else {
Err(errors)
}
}
}
#[cfg(feature = "openapi")]
impl<B: DateBackend> crate::openapi::JsonSchema for ZTime<B> {
fn json_schema(&self) -> Value {
self.bounds.json_schema(serde_json::json!({
"type": "string",
"format": "partial-time",
"pattern": TIME_OF_DAY_PATTERN,
}))
}
}
#[cfg(feature = "openapi")]
impl<B: DateBackend> crate::openapi::CollectNestedSchemas for ZTime<B> {}