From c0c61b5bfa6cb2553f41e8b9697355ae65488b43 Mon Sep 17 00:00:00 2001 From: Brandon W Maister Date: Sat, 4 Feb 2017 16:51:19 -0500 Subject: [PATCH] Add the ability for serde to deserialize timestamps Timestamps are defined in terms of UTC, so what this does is, if we encounter an integer instead of a str, create a FixedOffset timestamp with an offset of zero and create the timestamp from that. --- src/datetime.rs | 65 ++++++++++++++++++++++++++++++++++++++++--- src/naive/datetime.rs | 28 +++++++++++++++++-- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/datetime.rs b/src/datetime.rs index 6f6f71d..eb4166c 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -432,11 +432,20 @@ fn test_decodable_json(utc_from_str: FUTC, norm(&Some(UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)))); assert_eq!(norm(&utc_from_str(r#""2014-07-24T13:57:06+01:23""#).ok()), norm(&Some(UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)))); + assert_eq!(norm(&utc_from_str("0").ok()), + norm(&Some(UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)))); + assert_eq!(norm(&utc_from_str("-1").unwrap()), + norm(&UTC.ymd(1969, 12, 31).and_hms(23, 59, 59))); + assert_eq!(norm(&fixed_from_str(r#""2014-07-24T12:34:06Z""#).ok()), norm(&Some(FixedOffset::east(0).ymd(2014, 7, 24).and_hms(12, 34, 6)))); assert_eq!(norm(&fixed_from_str(r#""2014-07-24T13:57:06+01:23""#).ok()), norm(&Some(FixedOffset::east(60*60 + 23*60).ymd(2014, 7, 24).and_hms(13, 57, 6)))); + assert_eq!(norm(&fixed_from_str("0").ok()), + norm(&Some(UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)))); + assert_eq!(norm(&fixed_from_str("-1").unwrap()), + norm(&UTC.ymd(1969, 12, 31).and_hms(23, 59, 59))); // we don't know the exact local offset but we can check that // the conversion didn't change the instant itself @@ -444,6 +453,10 @@ fn test_decodable_json(utc_from_str: FUTC, UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)); assert_eq!(local_from_str(r#""2014-07-24T13:57:06+01:23""#).unwrap(), UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)); + assert_eq!(fixed_from_str("0").unwrap(), + UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)); + assert_eq!(local_from_str("-1").unwrap(), + &UTC.ymd(1969, 12, 31).and_hms(23, 59, 59)); assert!(utc_from_str(r#""2014-07-32T12:34:06Z""#).is_err()); assert!(fixed_from_str(r#""2014-07-32T12:34:06Z""#).is_err()); @@ -508,7 +521,7 @@ mod rustc_serialize { mod serde { use std::fmt; use super::DateTime; - use offset::TimeZone; + use offset::{TimeZone, LocalResult}; use offset::utc::UTC; use offset::local::Local; use offset::fixed::FixedOffset; @@ -535,14 +548,30 @@ mod serde { } } + // try!-like function to convert a LocalResult into a serde-ish Result + fn from(me: LocalResult, ts: V) -> Result + where E: de::Error, + V: Display, + T: Display, + { + match me { + LocalResult::None => Err(E::custom( + format!("value is not a legal timestamp: {}", ts))), + LocalResult::Ambiguous(min, max) => Err(E::custom( + format!("value is an ambiguous timestamp: {}, could be either of {}, {}", + ts, min, max))), + LocalResult::Single(val) => Ok(val) + } + } + struct DateTimeVisitor; impl<'de> de::Visitor<'de> for DateTimeVisitor { type Value = DateTime; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a formatted date and time string") + write!(formatter, "a formatted date and time string or a unix timestamp") } fn visit_str(self, value: &str) -> Result, E> @@ -550,8 +579,28 @@ mod serde { { value.parse().map_err(|err| E::custom(format!("{}", err))) } + + // Deserialize a timestamp in seconds since the epoch + fn visit_i64(self, value: i64) -> Result, E> + where E: de::Error + { + from(FixedOffset::east(0).timestamp_opt(value, 0), value) + } + + // Deserialize a timestamp in seconds since the epoch + fn visit_u64(self, value: u64) -> Result, E> + where E: de::Error + { + from(FixedOffset::east(0).timestamp_opt(value as i64, 0), value) + } + } + /// Deserialize a value that optionally includes a timezone offset in its + /// string representation + /// + /// The serialized value can be either a string representation or a unix + /// timestamp impl<'de> de::Deserialize<'de> for DateTime { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de> @@ -560,6 +609,10 @@ mod serde { } } + /// Deserialize into a UTC value + /// + /// The serialized value can be either a string representation or a unix + /// timestamp impl<'de> de::Deserialize<'de> for DateTime { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de> @@ -568,6 +621,11 @@ mod serde { } } + /// Deserialize a value that includes no timezone in its string + /// representation + /// + /// The serialized value can be either a string representation or a unix + /// timestamp impl<'de> de::Deserialize<'de> for DateTime { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de> @@ -785,4 +843,3 @@ mod tests { assert_eq!(1234567, datetime.timestamp_subsec_nanos()); } } - diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index 0271218..9d00c2f 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -1402,6 +1402,16 @@ fn test_decodable_json(from_str: F) assert_eq!( from_str(r#""+262143-12-31T23:59:60.9999999999997""#).ok(), // excess digits are ignored Some(date::MAX.and_hms_nano(23, 59, 59, 1_999_999_999))); + assert_eq!( + from_str("0").unwrap(), + NaiveDate::from_ymd(1970, 1, 1).and_hms(0, 0, 0), + "should parse integers as timestamps" + ); + assert_eq!( + from_str("-1").unwrap(), + NaiveDate::from_ymd(1969, 12, 31).and_hms(23, 59, 59), + "should parse integers as timestamps" + ); // bad formats assert!(from_str(r#""""#).is_err()); @@ -1487,9 +1497,9 @@ mod serde { impl<'de> de::Visitor<'de> for NaiveDateTimeVisitor { type Value = NaiveDateTime; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a formatted date and time string") + write!(formatter, "a formatted date and time string or a unix timestamp") } fn visit_str(self, value: &str) -> Result @@ -1497,6 +1507,20 @@ mod serde { { value.parse().map_err(|err| E::custom(format!("{}", err))) } + + fn visit_i64(self, value: i64) -> Result + where E: de::Error + { + NaiveDateTime::from_timestamp_opt(value, 0) + .ok_or_else(|| E::custom(format!("value is not a legal timestamp: {}", value))) + } + + fn visit_u64(self, value: u64) -> Result + where E: de::Error + { + NaiveDateTime::from_timestamp_opt(value as i64, 0) + .ok_or_else(|| E::custom(format!("value is not a legal timestamp: {}", value))) + } } impl<'de> de::Deserialize<'de> for NaiveDateTime {