Deserialize both Serde and Serialize from timestamps

This introduces a newtype around DateTime and NaiveDateTime that deserlization
is implemented for.

There are two advantages to this over the previous implementation:

* It is expandable to other timestamp representations (e.g. millisecond and
  microsecond timestamps)
* It works with RustcSerialize::Decodable. AFAICT Decodable will error if you
  try to call more than one of the `read_*` functions in the same `decode`
  invocation. This is slightly annoying compared to serde which just calls the
  correct `visit_*` function for whatever type the deserializer encounters.

On the whole I think that I prefer this to the previous implementation of
deserializing timestamps (even though I don't care about RustcSerialize in the
post-1.15 world) because it is much more explicit.

On the other hand, this feels like it's introducing a lot of types, and
possibly making downstream crates introduce a variety of different structs for
ser/de and translating into different struct types.
This commit is contained in:
Brandon W Maister 2017-02-11 17:21:40 -05:00 committed by Kang Seonghoon
parent c0c61b5bfa
commit bc879d705e
2 changed files with 255 additions and 61 deletions

View File

@ -5,7 +5,7 @@
use std::{str, fmt, hash};
use std::cmp::Ordering;
use std::ops::{Add, Sub};
use std::ops::{Add, Sub, Deref};
use oldtime::Duration as OldDuration;
use {Weekday, Timelike, Datelike};
@ -30,6 +30,26 @@ pub struct DateTime<Tz: TimeZone> {
offset: Tz::Offset,
}
/// A DateTime that can be deserialized from a timestamp
///
/// A timestamp here is seconds since the epoch
pub struct TsSeconds<Tz: TimeZone>(DateTime<Tz>);
impl<Tz: TimeZone> From<TsSeconds<Tz>> for DateTime<Tz> {
/// Pull the inner DateTime<Tz> out
fn from(obj: TsSeconds<Tz>) -> DateTime<Tz> {
obj.0
}
}
impl<Tz: TimeZone> Deref for TsSeconds<Tz> {
type Target = DateTime<Tz>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Tz: TimeZone> DateTime<Tz> {
/// Makes a new `DateTime` with given *UTC* datetime and offset.
/// The local datetime should be constructed via the `TimeZone` trait.
@ -432,40 +452,61 @@ fn test_decodable_json<FUTC, FFixed, FLocal, E>(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
assert_eq!(local_from_str(r#""2014-07-24T12:34:06Z""#).unwrap(),
assert_eq!(local_from_str(r#""2014-07-24T12:34:06Z""#)
.expect("local shouuld parse"),
UTC.ymd(2014, 7, 24).and_hms(12, 34, 6));
assert_eq!(local_from_str(r#""2014-07-24T13:57:06+01:23""#).unwrap(),
assert_eq!(local_from_str(r#""2014-07-24T13:57:06+01:23""#)
.expect("local should parse with offset"),
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());
}
#[cfg(all(test, any(feature = "rustc-serialize", feature = "serde")))]
fn test_decodable_json_timestamps<FUTC, FFixed, FLocal, E>(utc_from_str: FUTC,
fixed_from_str: FFixed,
local_from_str: FLocal)
where FUTC: Fn(&str) -> Result<TsSeconds<UTC>, E>,
FFixed: Fn(&str) -> Result<TsSeconds<FixedOffset>, E>,
FLocal: Fn(&str) -> Result<TsSeconds<Local>, E>,
E: ::std::fmt::Debug
{
fn norm<Tz: TimeZone>(dt: &Option<DateTime<Tz>>) -> Option<(&DateTime<Tz>, &Tz::Offset)> {
dt.as_ref().map(|dt| (dt, dt.offset()))
}
assert_eq!(norm(&utc_from_str("0").ok().map(DateTime::from)),
norm(&Some(UTC.ymd(1970, 1, 1).and_hms(0, 0, 0))));
assert_eq!(norm(&utc_from_str("-1").ok().map(DateTime::from)),
norm(&Some(UTC.ymd(1969, 12, 31).and_hms(23, 59, 59))));
assert_eq!(norm(&fixed_from_str("0").ok().map(DateTime::from)),
norm(&Some(FixedOffset::east(0).ymd(1970, 1, 1).and_hms(0, 0, 0))));
assert_eq!(norm(&fixed_from_str("-1").ok().map(DateTime::from)),
norm(&Some(FixedOffset::east(0).ymd(1969, 12, 31).and_hms(23, 59, 59))));
assert_eq!(*fixed_from_str("0").expect("0 timestamp should parse"),
UTC.ymd(1970, 1, 1).and_hms(0, 0, 0));
assert_eq!(*local_from_str("-1").expect("-1 timestamp should parse"),
UTC.ymd(1969, 12, 31).and_hms(23, 59, 59));
}
#[cfg(feature = "rustc-serialize")]
mod rustc_serialize {
use super::DateTime;
use offset::TimeZone;
use std::fmt;
use super::{DateTime, TsSeconds};
use offset::{TimeZone, LocalResult};
use offset::utc::UTC;
use offset::local::Local;
use offset::fixed::FixedOffset;
@ -477,21 +518,47 @@ mod rustc_serialize {
}
}
// try!-like function to convert a LocalResult into a serde-ish Result
fn from<T, D>(me: LocalResult<T>, d: &mut D) -> Result<T, D::Error>
where D: Decoder,
T: fmt::Display,
{
match me {
LocalResult::None => Err(d.error(
"value is not a legal timestamp")),
LocalResult::Ambiguous(..) => Err(d.error(
"value is an ambiguous timestamp")),
LocalResult::Single(val) => Ok(val)
}
}
impl Decodable for DateTime<FixedOffset> {
fn decode<D: Decoder>(d: &mut D) -> Result<DateTime<FixedOffset>, D::Error> {
match d.read_str()?.parse::<DateTime<FixedOffset>>() {
Ok(dt) => Ok(dt),
Err(_) => Err(d.error("invalid date and time")),
}
d.read_str()?.parse::<DateTime<FixedOffset>>()
.map_err(|_| d.error("invalid date and time"))
}
}
impl Decodable for TsSeconds<FixedOffset> {
fn decode<D: Decoder>(d: &mut D) -> Result<TsSeconds<FixedOffset>, D::Error> {
from(FixedOffset::east(0).timestamp_opt(d.read_i64()?, 0), d)
.map(|dt| TsSeconds(dt))
}
}
impl Decodable for DateTime<UTC> {
fn decode<D: Decoder>(d: &mut D) -> Result<DateTime<UTC>, D::Error> {
match d.read_str()?.parse::<DateTime<FixedOffset>>() {
Ok(dt) => Ok(dt.with_timezone(&UTC)),
Err(_) => Err(d.error("invalid date and time")),
}
d.read_str()?
.parse::<DateTime<FixedOffset>>()
.map(|dt| dt.with_timezone(&UTC))
.map_err(|_| d.error("invalid date and time"))
}
}
impl Decodable for TsSeconds<UTC> {
fn decode<D: Decoder>(d: &mut D) -> Result<TsSeconds<UTC>, D::Error> {
from(UTC.timestamp_opt(d.read_i64()?, 0), d)
.map(|dt| TsSeconds(dt))
}
}
@ -504,6 +571,13 @@ mod rustc_serialize {
}
}
impl Decodable for TsSeconds<Local> {
fn decode<D: Decoder>(d: &mut D) -> Result<TsSeconds<Local>, D::Error> {
from(UTC.timestamp_opt(d.read_i64()?, 0), d)
.map(|dt| TsSeconds(dt.with_timezone(&Local)))
}
}
#[cfg(test)] use rustc_serialize::json;
#[test]
@ -515,12 +589,18 @@ mod rustc_serialize {
fn test_decodable() {
super::test_decodable_json(json::decode, json::decode, json::decode);
}
#[test]
fn test_decodable_timestamps() {
super::test_decodable_json_timestamps(json::decode, json::decode, json::decode);
}
}
#[cfg(feature = "serde")]
mod serde {
use std::fmt;
use super::DateTime;
use super::{DateTime, TsSeconds};
use offset::{TimeZone, LocalResult};
use offset::utc::UTC;
use offset::local::Local;
@ -551,8 +631,8 @@ mod serde {
// try!-like function to convert a LocalResult into a serde-ish Result
fn from<T, E, V>(me: LocalResult<T>, ts: V) -> Result<T, E>
where E: de::Error,
V: Display,
T: Display,
V: fmt::Display,
T: fmt::Display,
{
match me {
LocalResult::None => Err(E::custom(
@ -579,21 +659,6 @@ mod serde {
{
value.parse().map_err(|err| E::custom(format!("{}", err)))
}
// Deserialize a timestamp in seconds since the epoch
fn visit_i64<E>(self, value: i64) -> Result<DateTime<FixedOffset>, 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<E>(self, value: u64) -> Result<DateTime<FixedOffset>, 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
@ -634,6 +699,65 @@ mod serde {
}
}
struct SecondsTimestampVisitor;
impl de::Visitor for SecondsTimestampVisitor {
type Value = DateTime<FixedOffset>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result
{
write!(formatter, "a unix timestamp in seconds")
}
/// Deserialize a timestamp in seconds since the epoch
fn visit_i64<E>(self, value: i64) -> Result<DateTime<FixedOffset>, 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<E>(self, value: u64) -> Result<DateTime<FixedOffset>, E>
where E: de::Error
{
from(FixedOffset::east(0).timestamp_opt(value as i64, 0), value)
}
}
impl de::Deserialize for TsSeconds<Local> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer
{
Ok(TsSeconds(try!(
deserializer
.deserialize_str(SecondsTimestampVisitor)
.map(|dt| dt.with_timezone(&Local)))))
}
}
/// Can deserialize a timestamp into a FixedOffset
///
/// The offset will always be 0, because timestamps are defined as UTC.
impl de::Deserialize for TsSeconds<FixedOffset> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer
{
Ok(TsSeconds(try!(
deserializer.deserialize_str(SecondsTimestampVisitor))))
}
}
/// Deserialize into a UTC value
impl de::Deserialize for TsSeconds<UTC> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer
{
Ok(TsSeconds(try!(
deserializer.deserialize_str(SecondsTimestampVisitor)
.map(|dt| dt.with_timezone(&UTC)))))
}
}
#[cfg(test)] extern crate serde_json;
#[cfg(test)] extern crate bincode;
@ -648,6 +772,13 @@ mod serde {
|input| self::serde_json::from_str(&input));
}
#[test]
fn test_serde_deserialize_timestamps() {
super::test_decodable_json_timestamps(self::serde_json::from_str,
self::serde_json::from_str,
self::serde_json::from_str);
}
#[test]
fn test_serde_bincode() {
// Bincode is relevant to test separately from JSON because

View File

@ -4,7 +4,7 @@
//! ISO 8601 date and time without timezone.
use std::{str, fmt, hash};
use std::ops::{Add, Sub};
use std::ops::{Add, Sub, Deref};
use num::traits::ToPrimitive;
use oldtime::Duration as OldDuration;
@ -54,6 +54,24 @@ pub struct NaiveDateTime {
time: NaiveTime,
}
/// A DateTime that can be deserialized from a seconds-based timestamp
pub struct TsSeconds(NaiveDateTime);
impl From<TsSeconds> for NaiveDateTime {
/// Pull the internal NaiveDateTime out
fn from(obj: TsSeconds) -> NaiveDateTime {
obj.0
}
}
impl Deref for TsSeconds {
type Target = NaiveDateTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl NaiveDateTime {
/// Makes a new `NaiveDateTime` from date and time components.
/// Equivalent to [`date.and_time(time)`](../date/struct.NaiveDate.html#method.and_time)
@ -1402,16 +1420,6 @@ fn test_decodable_json<F, E>(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());
@ -1428,7 +1436,6 @@ fn test_decodable_json<F, E>(from_str: F)
assert!(from_str(r#""2016-07-08 09:10:48.090""#).is_err());
assert!(from_str(r#""2016-007-08T09:10:48.090""#).is_err());
assert!(from_str(r#""yyyy-mm-ddThh:mm:ss.fffffffff""#).is_err());
assert!(from_str(r#"0"#).is_err());
assert!(from_str(r#"20160708000000"#).is_err());
assert!(from_str(r#"{}"#).is_err());
// pre-0.3.0 rustc-serialize format is now invalid
@ -1436,9 +1443,27 @@ fn test_decodable_json<F, E>(from_str: F)
assert!(from_str(r#"null"#).is_err());
}
#[cfg(all(test, any(feature = "rustc-serialize", feature = "serde")))]
fn test_decodable_json_timestamp<F, E>(from_str: F)
where F: Fn(&str) -> Result<TsSeconds, E>, E: ::std::fmt::Debug
{
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"
);
}
#[cfg(feature = "rustc-serialize")]
mod rustc_serialize {
use super::NaiveDateTime;
use super::{NaiveDateTime, TsSeconds};
use rustc_serialize::{Encodable, Encoder, Decodable, Decoder};
impl Encodable for NaiveDateTime {
@ -1449,7 +1474,15 @@ mod rustc_serialize {
impl Decodable for NaiveDateTime {
fn decode<D: Decoder>(d: &mut D) -> Result<NaiveDateTime, D::Error> {
d.read_str()?.parse().map_err(|_| d.error("invalid date and time"))
d.read_str()?.parse().map_err(|_| d.error("invalid date time string"))
}
}
impl Decodable for TsSeconds {
fn decode<D: Decoder>(d: &mut D) -> Result<TsSeconds, D::Error> {
Ok(TsSeconds(
NaiveDateTime::from_timestamp_opt(d.read_i64()?, 0)
.ok_or_else(|| d.error("invalid timestamp"))?))
}
}
@ -1464,12 +1497,17 @@ mod rustc_serialize {
fn test_decodable() {
super::test_decodable_json(json::decode);
}
#[test]
fn test_decodable_timestamps() {
super::test_decodable_json_timestamp(json::decode);
}
}
#[cfg(feature = "serde")]
mod serde {
use std::fmt;
use super::NaiveDateTime;
use super::{NaiveDateTime, TsSeconds};
use serde::{ser, de};
// TODO not very optimized for space (binary formats would want something better)
@ -1499,7 +1537,7 @@ mod serde {
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result
{
write!(formatter, "a formatted date and time string or a unix timestamp")
write!(formatter, "a formatted date and time string")
}
fn visit_str<E>(self, value: &str) -> Result<NaiveDateTime, E>
@ -1507,6 +1545,25 @@ mod serde {
{
value.parse().map_err(|err| E::custom(format!("{}", err)))
}
}
impl de::Deserialize for NaiveDateTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer
{
deserializer.deserialize_str(NaiveDateTimeVisitor)
}
}
struct NaiveDateTimeFromSecondsVisitor;
impl de::Visitor for NaiveDateTimeFromSecondsVisitor {
type Value = NaiveDateTime;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result
{
write!(formatter, "a unix timestamp")
}
fn visit_i64<E>(self, value: i64) -> Result<NaiveDateTime, E>
where E: de::Error
@ -1527,7 +1584,8 @@ mod serde {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer<'de>
{
deserializer.deserialize_str(NaiveDateTimeVisitor)
Ok(TsSeconds(try!(
deserializer.deserialize_str(NaiveDateTimeFromSecondsVisitor))))
}
}
@ -1544,6 +1602,11 @@ mod serde {
super::test_decodable_json(|input| self::serde_json::from_str(&input));
}
#[test]
fn test_serde_deserialize_timestamp() {
super::test_decodable_json_timestamp(self::serde_json::from_str);
}
#[test]
fn test_serde_bincode() {
// Bincode is relevant to test separately from JSON because