Use serde's serialize_with instead of a newtype

This is a significantly less horrible API than in the previous commit.
This commit is contained in:
Brandon W Maister 2017-02-24 11:58:21 -05:00 committed by Kang Seonghoon
parent bc879d705e
commit 44fc13d7df
6 changed files with 372 additions and 181 deletions

62
.travis.sh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/bash
# This is the script that's executed by travis, you can run it yourself to run
# the exact same suite
set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
channel() {
if [ -n "${TRAVIS}" ]; then
if [ "${TRAVIS_RUST_VERSION}" = "${CHANNEL}" ]; then
pwd
(set -x; cargo "$@")
fi
elif [ -n "${APPVEYOR}" ]; then
if [ "${APPVEYOR_RUST_CHANNEL}" = "${CHANNEL}" ]; then
pwd
(set -x; cargo "$@")
fi
else
pwd
(set -x; cargo "+${CHANNEL}" "$@")
fi
}
build_and_test() {
# interleave building and testing in hope that it saves time
# also vary the local time zone to (hopefully) catch tz-dependent bugs
# also avoid doc-testing multiple times---it takes a lot and rarely helps
cargo clean
channel build -v
TZ=ACST-9:30 channel test -v --lib
channel build -v --features rustc-serialize
TZ=EST4 channel test -v --features rustc-serialize --lib
channel build -v --features 'serde bincode'
TZ=UTC0 channel test -v --features 'serde bincode'
}
build_only() {
# Rust 1.13 doesn't support custom derive, so, to avoid doctests which
# validate that, we just build there.
cargo clean
channel build -v
channel build -v --features rustc-serialize
channel build -v --features 'serde bincode'
}
rustc --version
cargo --version
CHANNEL=nightly
build_and_test
CHANNEL=beta
build_and_test
CHANNEL=stable
build_and_test
CHANNEL=1.13.0
build_only

View File

@ -1,7 +1,7 @@
language: rust
sudo: false
rust:
# 1.13.0 is the earliest version that Serde 0.9 tests, so we follow suit
# 1.13.0 is the earliest version that Serde 1.0 tests, so we follow suit
- 1.13.0
- stable
- beta
@ -15,16 +15,7 @@ matrix:
env:
global:
- LD_LIBRARY_PATH: /usr/local/lib
script:
# interleave building and testing in hope that it saves time
# also vary the local time zone to (hopefully) catch tz-dependent bugs
# also avoid doc-testing multiple times---it takes a lot and rarely helps
- cargo build -v
- TZ=ACST-9:30 cargo test -v
- cargo build -v --features rustc-serialize
- TZ=EST4 cargo test -v --features rustc-serialize --lib
- cargo build -v --features 'serde bincode'
- TZ=UTC0 cargo test -v --features 'serde bincode' --lib
script: ./.travis.sh
notifications:
email: false
irc:

View File

@ -27,4 +27,5 @@ serde = { version = "1", optional = true }
[dev-dependencies]
serde_json = { version = "1" }
serde_derive = { version = "1" }
bincode = { version = "0.8.0", features = ["serde"], default-features = false }

View File

@ -14,8 +14,8 @@ install:
- ps: $env:PATH="$env:PATH;C:\rust\bin"
- rustc -vV
- cargo -vV
build_script:
# do not test all combinations, Travis will handle that
- cargo build -v --features "serde rustc-serialize"
build: false
test_script:
- cargo test -v --features "serde rustc-serialize"
- sh -c 'PATH=`rustc --print sysroot`/bin:$PATH ./.travis.sh'

View File

@ -5,7 +5,9 @@
use std::{str, fmt, hash};
use std::cmp::Ordering;
use std::ops::{Add, Sub, Deref};
use std::ops::{Add, Sub};
#[cfg(feature = "rustc-serialize")]
use std::ops::Deref;
use oldtime::Duration as OldDuration;
use {Weekday, Timelike, Datelike};
@ -33,8 +35,10 @@ pub struct DateTime<Tz: TimeZone> {
/// A DateTime that can be deserialized from a timestamp
///
/// A timestamp here is seconds since the epoch
#[cfg(feature = "rustc-serialize")]
pub struct TsSeconds<Tz: TimeZone>(DateTime<Tz>);
#[cfg(feature = "rustc-serialize")]
impl<Tz: TimeZone> From<TsSeconds<Tz>> for DateTime<Tz> {
/// Pull the inner DateTime<Tz> out
fn from(obj: TsSeconds<Tz>) -> DateTime<Tz> {
@ -42,6 +46,7 @@ impl<Tz: TimeZone> From<TsSeconds<Tz>> for DateTime<Tz> {
}
}
#[cfg(feature = "rustc-serialize")]
impl<Tz: TimeZone> Deref for TsSeconds<Tz> {
type Target = DateTime<Tz>;
@ -50,6 +55,7 @@ impl<Tz: TimeZone> Deref for TsSeconds<Tz> {
}
}
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.
@ -471,37 +477,6 @@ fn test_decodable_json<FUTC, FFixed, FLocal, E>(utc_from_str: FUTC,
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 std::fmt;
@ -518,7 +493,7 @@ mod rustc_serialize {
}
}
// try!-like function to convert a LocalResult into a serde-ish Result
// 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,
@ -590,23 +565,165 @@ mod rustc_serialize {
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);
}
}
/// Ser/de helpers
///
/// The various modules in here are intended to be used with serde's [`with`
/// annotation](https://serde.rs/attributes.html#field-attributes).
#[cfg(feature = "serde")]
mod serde {
pub mod serde {
use std::fmt;
use super::{DateTime, TsSeconds};
use super::DateTime;
use offset::{TimeZone, LocalResult};
use offset::utc::UTC;
use offset::local::Local;
use offset::fixed::FixedOffset;
use serde::{ser, de};
/// Ser/de to/from timestamps in seconds
///
/// Intended for use with `serde`'s `with` attribute.
///
/// # Example:
///
/// ```rust
/// # // We mark this ignored so that we can test on 1.13 (which does not
/// # // support custom derive), and run tests with --ignored on beta and
/// # // nightly to actually trigger these.
/// #
/// # #[macro_use] extern crate serde_derive;
/// # #[macro_use] extern crate serde_json;
/// # extern crate chrono;
/// # use chrono::{TimeZone, DateTime, UTC};
/// use chrono::datetime::serde::ts_seconds;
/// #[derive(Deserialize, Serialize)]
/// struct S {
/// #[serde(with = "ts_seconds")]
/// time: DateTime<UTC>
/// }
///
/// # fn example() -> Result<S, serde_json::Error> {
/// let time = UTC.ymd(2015, 5, 15).and_hms(10, 0, 0);
/// let my_s = S {
/// time: time.clone(),
/// };
///
/// let as_string = serde_json::to_string(&my_s)?;
/// assert_eq!(as_string, r#"{"time":1431684000}"#);
/// let my_s: S = serde_json::from_str(&as_string)?;
/// assert_eq!(my_s.time, time);
/// # Ok(my_s)
/// # }
/// # fn main() { example().unwrap(); }
/// ```
pub mod ts_seconds {
use std::fmt;
use serde::{ser, de};
use {DateTime, UTC, FixedOffset};
use offset::TimeZone;
use super::from;
/// Deserialize a DateTime from a seconds timestamp
///
/// Intended for use with `serde`s `deserialize_with` attribute.
///
/// # Example:
///
/// ```rust
/// # // We mark this ignored so that we can test on 1.13 (which does not
/// # // support custom derive), and run tests with --ignored on beta and
/// # // nightly to actually trigger these.
/// #
/// # #[macro_use] extern crate serde_derive;
/// # #[macro_use] extern crate serde_json;
/// # extern crate chrono;
/// # use chrono::{DateTime, UTC};
/// use chrono::datetime::serde::ts_seconds::deserialize as from_ts;
/// #[derive(Deserialize)]
/// struct S {
/// #[serde(deserialize_with = "from_ts")]
/// time: DateTime<UTC>
/// }
///
/// # fn example() -> Result<S, serde_json::Error> {
/// let my_s: S = serde_json::from_str(r#"{ "time": 1431684000 }"#)?;
/// # Ok(my_s)
/// # }
/// # fn main() { example().unwrap(); }
/// ```
pub fn deserialize<'de, D>(d: D) -> Result<DateTime<UTC>, D::Error>
where D: de::Deserializer<'de>
{
Ok(try!(d.deserialize_i64(SecondsTimestampVisitor).map(|dt| dt.with_timezone(&UTC))))
}
/// Serialize a UTC datetime into an integer number of seconds since the epoch
///
/// Intended for use with `serde`s `serialize_with` attribute.
///
/// # Example:
///
/// ```rust
/// # // We mark this ignored so that we can test on 1.13 (which does not
/// # // support custom derive), and run tests with --ignored on beta and
/// # // nightly to actually trigger these.
/// #
/// # #[macro_use] extern crate serde_derive;
/// # #[macro_use] extern crate serde_json;
/// # extern crate chrono;
/// # use chrono::{TimeZone, DateTime, UTC};
/// use chrono::datetime::serde::ts_seconds::serialize as to_ts;
/// #[derive(Serialize)]
/// struct S {
/// #[serde(serialize_with = "to_ts")]
/// time: DateTime<UTC>
/// }
///
/// # fn example() -> Result<String, serde_json::Error> {
/// let my_s = S {
/// time: UTC.ymd(2015, 5, 15).and_hms(10, 0, 0),
/// };
/// let as_string = serde_json::to_string(&my_s)?;
/// assert_eq!(as_string, r#"{"time":1431684000}"#);
/// # Ok(as_string)
/// # }
/// # fn main() { example().unwrap(); }
/// ```
pub fn serialize<S>(dt: &DateTime<UTC>, serializer: S) -> Result<S::Ok, S::Error>
where S: ser::Serializer
{
serializer.serialize_i64(dt.timestamp())
}
struct SecondsTimestampVisitor;
impl<'de> de::Visitor<'de> 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)
}
}
}
// TODO not very optimized for space (binary formats would want something better)
impl<Tz: TimeZone> ser::Serialize for DateTime<Tz> {
@ -699,65 +816,6 @@ 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;
@ -772,13 +830,6 @@ 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

@ -1443,24 +1443,6 @@ 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, TsSeconds};
@ -1498,20 +1480,19 @@ mod rustc_serialize {
super::test_decodable_json(json::decode);
}
#[test]
fn test_decodable_timestamps() {
super::test_decodable_json_timestamp(json::decode);
}
}
/// Tools to help serializing/deserializing NaiveDateTimes
#[cfg(feature = "serde")]
mod serde {
pub mod serde {
use std::fmt;
use super::{NaiveDateTime, TsSeconds};
use super::{NaiveDateTime};
use serde::{ser, de};
// TODO not very optimized for space (binary formats would want something better)
/// Serialize a NaiveDateTime as a string
///
/// See the [`ts_seconds`](./ts_seconds/index.html) module to serialize as
/// a timestamp.
impl ser::Serialize for NaiveDateTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: ser::Serializer
@ -1547,46 +1528,156 @@ mod serde {
}
}
impl de::Deserialize for NaiveDateTime {
impl<'de> de::Deserialize<'de> for NaiveDateTime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: de::Deserializer
where D: de::Deserializer<'de>
{
deserializer.deserialize_str(NaiveDateTimeVisitor)
}
}
struct NaiveDateTimeFromSecondsVisitor;
/// Used to serialize/deserialize from second-precision timestamps
///
/// # Example:
///
/// ```rust
/// # // We mark this ignored so that we can test on 1.13 (which does not
/// # // support custom derive), and run tests with --ignored on beta and
/// # // nightly to actually trigger these.
/// #
/// # #[macro_use] extern crate serde_derive;
/// # extern crate serde_json;
/// # extern crate serde;
/// # extern crate chrono;
/// # use chrono::{TimeZone, NaiveDate, NaiveDateTime, UTC};
/// use chrono::naive::datetime::serde::ts_seconds;
/// #[derive(Deserialize, Serialize)]
/// struct S {
/// #[serde(with = "ts_seconds")]
/// time: NaiveDateTime
/// }
///
/// # fn example() -> Result<S, serde_json::Error> {
/// let time = NaiveDate::from_ymd(2015, 5, 15).and_hms(10, 0, 0);
/// let my_s = S {
/// time: time.clone(),
/// };
///
/// let as_string = serde_json::to_string(&my_s)?;
/// assert_eq!(as_string, r#"{"time":1431684000}"#);
/// let my_s: S = serde_json::from_str(&as_string)?;
/// assert_eq!(my_s.time, time);
/// # Ok(my_s)
/// # }
/// # fn main() { example().unwrap(); }
/// ```
pub mod ts_seconds {
use std::fmt;
use serde::{ser, de};
impl de::Visitor for NaiveDateTimeFromSecondsVisitor {
type Value = NaiveDateTime;
use 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
{
NaiveDateTime::from_timestamp_opt(value, 0)
.ok_or_else(|| E::custom(format!("value is not a legal timestamp: {}", value)))
}
fn visit_u64<E>(self, value: u64) -> Result<NaiveDateTime, E>
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 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
/// Deserialize a DateTime from a seconds timestamp
///
/// Intended for use with `serde`s `deserialize_with` attribute.
///
/// # Example:
///
/// ```rust
/// # // We mark this ignored so that we can test on 1.13 (which does not
/// # // support custom derive), and run tests with --ignored on beta and
/// # // nightly to actually trigger these.
/// #
/// # #[macro_use] extern crate serde_derive;
/// # #[macro_use] extern crate serde_json;
/// # extern crate serde;
/// # extern crate chrono;
/// # use chrono::{NaiveDateTime, UTC};
/// # use serde::Deserialize;
/// use chrono::naive::datetime::serde::ts_seconds::deserialize as from_ts;
/// #[derive(Deserialize)]
/// struct S {
/// #[serde(deserialize_with = "from_ts")]
/// time: NaiveDateTime
/// }
///
/// # fn example() -> Result<S, serde_json::Error> {
/// let my_s: S = serde_json::from_str(r#"{ "time": 1431684000 }"#)?;
/// # Ok(my_s)
/// # }
/// # fn main() { example().unwrap(); }
/// ```
pub fn deserialize<'de, D>(d: D) -> Result<NaiveDateTime, D::Error>
where D: de::Deserializer<'de>
{
Ok(TsSeconds(try!(
deserializer.deserialize_str(NaiveDateTimeFromSecondsVisitor))))
Ok(try!(d.deserialize_i64(NaiveDateTimeFromSecondsVisitor)))
}
/// Serialize a UTC datetime into an integer number of seconds since the epoch
///
/// Intended for use with `serde`s `serialize_with` attribute.
///
/// # Example:
///
/// ```rust
/// # // We mark this ignored so that we can test on 1.13 (which does not
/// # // support custom derive), and run tests with --ignored on beta and
/// # // nightly to actually trigger these.
/// #
/// # #[macro_use] extern crate serde_derive;
/// # #[macro_use] extern crate serde_json;
/// # #[macro_use] extern crate serde;
/// # extern crate chrono;
/// # use chrono::{TimeZone, NaiveDate, NaiveDateTime, UTC};
/// # use serde::Serialize;
/// use chrono::naive::datetime::serde::ts_seconds::serialize as to_ts;
/// #[derive(Serialize)]
/// struct S {
/// #[serde(serialize_with = "to_ts")]
/// time: NaiveDateTime
/// }
///
/// # fn example() -> Result<String, serde_json::Error> {
/// let my_s = S {
/// time: NaiveDate::from_ymd(2015, 5, 15).and_hms(10, 0, 0),
/// };
/// let as_string = serde_json::to_string(&my_s)?;
/// assert_eq!(as_string, r#"{"time":1431684000}"#);
/// # Ok(as_string)
/// # }
/// # fn main() { example().unwrap(); }
/// ```
pub fn serialize<S>(dt: &NaiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
where S: ser::Serializer
{
serializer.serialize_i64(dt.timestamp())
}
struct NaiveDateTimeFromSecondsVisitor;
impl<'de> de::Visitor<'de> 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
{
NaiveDateTime::from_timestamp_opt(value, 0)
.ok_or_else(|| E::custom(format!("value is not a legal timestamp: {}", value)))
}
fn visit_u64<E>(self, value: u64) -> Result<NaiveDateTime, E>
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)))
}
}
}
#[cfg(test)] extern crate serde_json;
@ -1602,11 +1693,6 @@ 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