use std::{fmt, ops::Deref};
use chrono::{NaiveDateTime, TimeZone as _, Timelike as _, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{
into_caveat, json,
warning::{self, GatherWarnings as _},
IntoCaveat, Verdict,
};
pub type ChronoDateTime = chrono::DateTime<Utc>;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
ContainsEscapeCodes,
Decode(json::decode::WarningKind),
Invalid(Error),
InvalidType,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::ContainsEscapeCodes => write!(f, "contains_escape_codes"),
WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
WarningKind::Invalid(_) => write!(f, "invalid"),
WarningKind::InvalidType => write!(f, "invalid_type"),
}
}
}
impl warning::Kind for WarningKind {
fn id(&self) -> std::borrow::Cow<'static, str> {
match self {
WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
WarningKind::Decode(kind) => format!("decode.{}", kind.id()).into(),
WarningKind::Invalid(_) => "invalid".into(),
WarningKind::InvalidType => "invalid_type".into(),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Error(chrono::ParseError);
impl Ord for Error {
fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
std::cmp::Ordering::Equal
}
}
impl PartialOrd for Error {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
into_caveat!(chrono::Duration);
impl json::FromJson<'_, '_> for chrono::Duration {
type WarningKind = duration::WarningKind;
fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
let mut warnings = warning::Set::new();
let Some(s) = elem.as_number_str() else {
warnings.with_elem(duration::WarningKind::InvalidType, elem);
return Err(warnings);
};
let seconds = match s.parse::<u64>() {
Ok(n) => n,
Err(err) => {
warnings.with_elem(duration::WarningKind::Invalid(err.to_string()), elem);
return Err(warnings);
}
};
let Ok(seconds) = i64::try_from(seconds) else {
warnings.with_elem(
duration::WarningKind::Invalid(
"The duration value is larger than an i64 can represent.".into(),
),
elem,
);
return Err(warnings);
};
let duration = chrono::Duration::seconds(seconds);
Ok(duration.into_caveat(warnings))
}
}
pub mod duration {
use std::{borrow::Cow, fmt};
use crate::warning;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
Invalid(String),
InvalidType,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::Invalid(err) => write!(f, "Unable to parse the duration: {err}"),
WarningKind::InvalidType => write!(f, "The value should be a string."),
}
}
}
impl warning::Kind for WarningKind {
fn id(&self) -> Cow<'static, str> {
match self {
WarningKind::Invalid(_) => "invalid".into(),
WarningKind::InvalidType => "invalid_type".into(),
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DateTime(ChronoDateTime);
into_caveat!(DateTime);
impl DateTime {
#[must_use]
pub fn to_second_precision(self) -> DateTime {
let naive_date_time = self
.naive_local()
.with_nanosecond(0)
.expect("Resetting nanos to zero is always Some");
DateTime(self.timezone().from_utc_datetime(&naive_date_time))
}
pub fn into_inner(self) -> ChronoDateTime {
self.0
}
pub fn to_rfc3399(self) -> String {
self.0.to_rfc3339()
}
}
impl From<DateTime> for ChronoDateTime {
fn from(value: DateTime) -> Self {
value.0
}
}
impl Deref for DateTime {
type Target = ChronoDateTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<ChronoDateTime> for DateTime {
fn from(value: ChronoDateTime) -> Self {
DateTime(value)
}
}
impl From<json::decode::WarningKind> for WarningKind {
fn from(warn_kind: json::decode::WarningKind) -> Self {
Self::Decode(warn_kind)
}
}
impl json::FromJson<'_, '_> for DateTime {
type WarningKind = WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
let mut warnings = warning::Set::new();
let Some(s) = elem.as_raw_str() else {
warnings.with_elem(WarningKind::InvalidType, elem);
return Err(warnings);
};
let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
let s = match pending_str {
json::decode::PendingStr::NoEscapes(s) => s,
json::decode::PendingStr::HasEscapes(_) => {
warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
return Err(warnings);
}
};
let err = match s.parse::<ChronoDateTime>() {
Ok(date) => return Ok(Self(date).into_caveat(warnings)),
Err(err) => err,
};
let Ok(date) = s.parse::<NaiveDateTime>() else {
warnings.with_elem(WarningKind::Invalid(Error(err)), elem);
return Err(warnings);
};
let datetime = Self(Utc.from_utc_datetime(&date));
Ok(datetime.into_caveat(warnings))
}
}
impl<'de> Deserialize<'de> for DateTime {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let date_string = String::deserialize(deserializer)?;
let err = match date_string.parse::<ChronoDateTime>() {
Ok(date) => return Ok(date.into()),
Err(err) => err,
};
if let Ok(date) = date_string.parse::<NaiveDateTime>() {
Ok(Utc.from_utc_datetime(&date).into())
} else {
Err(serde::de::Error::custom(err))
}
}
}
impl Serialize for DateTime {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.format("%Y-%m-%dT%H:%M:%SZ").to_string())
}
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format("%Y-%m-%dT%H:%M:%SZ"))
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)]
pub struct Date(chrono::NaiveDate);
into_caveat!(Date);
into_caveat!(chrono::NaiveDate);
impl json::FromJson<'_, '_> for chrono::NaiveDate {
type WarningKind = WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
let mut warnings = warning::Set::new();
let Some(s) = elem.as_raw_str() else {
warnings.with_elem(WarningKind::InvalidType, elem);
return Err(warnings);
};
let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
let s = match pending_str {
json::decode::PendingStr::NoEscapes(s) => s,
json::decode::PendingStr::HasEscapes(_) => {
warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
return Err(warnings);
}
};
let date = match s.parse::<chrono::NaiveDate>() {
Ok(v) => v,
Err(err) => {
warnings.with_elem(WarningKind::Invalid(Error(err)), elem);
return Err(warnings);
}
};
Ok(date.into_caveat(warnings))
}
}
impl json::FromJson<'_, '_> for Date {
type WarningKind = WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
Ok(chrono::NaiveDate::from_json(elem)?.map(Self))
}
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = <String as Deserialize>::deserialize(deserializer)?;
let date = chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(D::Error::custom)?;
Ok(Self(date))
}
}
impl From<Date> for chrono::NaiveDate {
fn from(value: Date) -> Self {
value.0
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Serialize)]
pub struct Time(chrono::NaiveTime);
impl<'de> Deserialize<'de> for Time {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = <String as Deserialize>::deserialize(deserializer)?;
let date = chrono::NaiveTime::parse_from_str(&s, "%H:%M").map_err(D::Error::custom)?;
Ok(Self(date))
}
}
impl From<Time> for chrono::NaiveTime {
fn from(value: Time) -> Self {
value.0
}
}
into_caveat!(Time);
into_caveat!(chrono::NaiveTime);
impl json::FromJson<'_, '_> for chrono::NaiveTime {
type WarningKind = time::WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
let mut warnings = warning::Set::new();
let value = elem.as_value();
let Some(s) = value.as_raw_str() else {
warnings.with_elem(time::WarningKind::InvalidType, elem);
return Err(warnings);
};
let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
let s = match pending_str {
json::decode::PendingStr::NoEscapes(s) => s,
json::decode::PendingStr::HasEscapes(_) => {
warnings.with_elem(time::WarningKind::ContainsEscapeCodes, elem);
return Err(warnings);
}
};
let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
Ok(v) => v,
Err(err) => {
warnings.with_elem(time::WarningKind::Invalid(err.to_string()), elem);
return Err(warnings);
}
};
Ok(date.into_caveat(warnings))
}
}
impl json::FromJson<'_, '_> for Time {
type WarningKind = time::WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
Ok(chrono::NaiveTime::from_json(elem)?.map(Self))
}
}
pub mod time {
use std::{borrow::Cow, fmt};
use crate::{json, warning};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
ContainsEscapeCodes,
Decode(json::decode::WarningKind),
Invalid(String),
InvalidType,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::ContainsEscapeCodes => write!(
f,
"The value contains escape codes but it does not need them."
),
WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
WarningKind::Invalid(msg) => write!(f, "Unable to parse the time: {msg}"),
WarningKind::InvalidType => write!(f, "The value should be a string."),
}
}
}
impl warning::Kind for WarningKind {
fn id(&self) -> Cow<'static, str> {
match self {
WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
WarningKind::Decode(kind) => format!("decode.{}", kind.id()).into(),
WarningKind::Invalid(_) => "invalid".into(),
WarningKind::InvalidType => "invalid_type".into(),
}
}
}
impl From<json::decode::WarningKind> for WarningKind {
fn from(warn_kind: json::decode::WarningKind) -> Self {
Self::Decode(warn_kind)
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use crate::datetime::ChronoDateTime;
use super::{DateTime, Error};
#[test]
const fn error_should_be_send_and_sync() {
const fn f<T: Send + Sync>() {}
f::<Error>();
}
impl FromStr for DateTime {
type Err = <ChronoDateTime as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let dt = s.parse()?;
Ok(Self(dt))
}
}
}
#[cfg(test)]
mod test_datetime_serde_deser {
use chrono::{TimeZone, Timelike, Utc};
use super::*;
fn parse_timestamp(timestamp: &str) -> ChronoDateTime {
serde_json::from_str::<DateTime>(timestamp).unwrap().into()
}
#[test]
fn should_parse_utc_datetime() {
assert_eq!(
parse_timestamp(r#""2015-06-29T22:39:09Z""#),
Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
);
}
#[test]
fn should_parse_timezone_to_utc() {
assert_eq!(
parse_timestamp(r#""2015-06-29T22:39:09+02:00""#),
Utc.with_ymd_and_hms(2015, 6, 29, 20, 39, 9).unwrap()
);
}
#[test]
fn should_parse_timezone_naive_to_utc() {
assert_eq!(
parse_timestamp(r#""2015-06-29T22:39:09""#),
Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
);
}
#[test]
fn should_format_as_utc() {
let test_datetime: DateTime = Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap().into();
assert_eq!(
serde_json::to_string(&test_datetime).unwrap(),
r#""2015-06-29T22:39:09Z""#
);
}
#[test]
fn should_format_without_partial_seconds() {
let test_datetime: DateTime = Utc
.with_ymd_and_hms(2015, 6, 29, 22, 39, 9)
.unwrap()
.with_nanosecond(12_345_678)
.unwrap()
.into();
assert_eq!(
serde_json::to_string(&test_datetime).unwrap(),
r#""2015-06-29T22:39:09Z""#
);
}
}
#[cfg(test)]
mod test_datetime_from_json {
use assert_matches::assert_matches;
use chrono::{TimeZone, Utc};
use crate::{
json::{self, FromJson as _},
Verdict,
};
use super::{DateTime, WarningKind};
#[track_caller]
fn parse_timestamp_from_json(json: &'static str) -> Verdict<DateTime, WarningKind> {
let elem = json::parse(json).unwrap();
let date_time_time = elem.find_field("start_date_time").unwrap();
DateTime::from_json(date_time_time.element())
}
#[test]
fn should_parse_utc_datetime() {
const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09Z" }"#;
let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
assert_matches!(*warnings, []);
assert_eq!(
datetime.into_inner(),
Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
);
}
#[test]
fn should_parse_timezone_to_utc() {
const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09+02:00" }"#;
let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
assert_matches!(*warnings, []);
assert_eq!(
datetime.into_inner(),
Utc.with_ymd_and_hms(2015, 6, 29, 20, 39, 9).unwrap()
);
}
#[test]
fn should_parse_timezone_naive_to_utc() {
const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09" }"#;
let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
assert_matches!(*warnings, []);
assert_eq!(
datetime.into_inner(),
Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
);
}
}