diff --git a/.travis.sh b/.travis.sh new file mode 100755 index 0000000..cba5909 --- /dev/null +++ b/.travis.sh @@ -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 diff --git a/.travis.yml b/.travis.yml index 69920fd..74a29cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/Cargo.toml b/Cargo.toml index 018469a..5bfb6a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/appveyor.yml b/appveyor.yml index 241d049..9831503 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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' diff --git a/src/datetime.rs b/src/datetime.rs index 4679e38..fcacc65 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -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 { /// A DateTime that can be deserialized from a timestamp /// /// A timestamp here is seconds since the epoch +#[cfg(feature = "rustc-serialize")] pub struct TsSeconds(DateTime); +#[cfg(feature = "rustc-serialize")] impl From> for DateTime { /// Pull the inner DateTime out fn from(obj: TsSeconds) -> DateTime { @@ -42,6 +46,7 @@ impl From> for DateTime { } } +#[cfg(feature = "rustc-serialize")] impl Deref for TsSeconds { type Target = DateTime; @@ -50,6 +55,7 @@ impl Deref for TsSeconds { } } + impl DateTime { /// 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(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(utc_from_str: FUTC, - fixed_from_str: FFixed, - local_from_str: FLocal) - where FUTC: Fn(&str) -> Result, E>, - FFixed: Fn(&str) -> Result, E>, - FLocal: Fn(&str) -> Result, E>, - E: ::std::fmt::Debug -{ - fn norm(dt: &Option>) -> Option<(&DateTime, &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(me: LocalResult, d: &mut D) -> Result 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 + /// } + /// + /// # fn example() -> Result { + /// 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 + /// } + /// + /// # fn example() -> Result { + /// 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, 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 + /// } + /// + /// # fn example() -> Result { + /// 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(dt: &DateTime, serializer: S) -> Result + where S: ser::Serializer + { + serializer.serialize_i64(dt.timestamp()) + } + + struct SecondsTimestampVisitor; + + impl<'de> de::Visitor<'de> for SecondsTimestampVisitor { + type Value = DateTime; + + 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(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) + } + } + + } + // TODO not very optimized for space (binary formats would want something better) impl ser::Serialize for DateTime { @@ -699,65 +816,6 @@ mod serde { } } - struct SecondsTimestampVisitor; - - impl de::Visitor for SecondsTimestampVisitor { - type Value = DateTime; - - 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(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) - } - } - - impl de::Deserialize for TsSeconds { - fn deserialize(deserializer: D) -> Result - 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 { - fn deserialize(deserializer: D) -> Result - where D: de::Deserializer - { - Ok(TsSeconds(try!( - deserializer.deserialize_str(SecondsTimestampVisitor)))) - } - } - - /// Deserialize into a UTC value - impl de::Deserialize for TsSeconds { - fn deserialize(deserializer: D) -> Result - 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 diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index a6f7604..d200635 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -1443,24 +1443,6 @@ fn test_decodable_json(from_str: F) assert!(from_str(r#"null"#).is_err()); } - -#[cfg(all(test, any(feature = "rustc-serialize", feature = "serde")))] -fn test_decodable_json_timestamp(from_str: F) - where F: Fn(&str) -> Result, 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(&self, serializer: S) -> Result where S: ser::Serializer @@ -1547,46 +1528,156 @@ mod serde { } } - impl de::Deserialize for NaiveDateTime { + impl<'de> de::Deserialize<'de> for NaiveDateTime { fn deserialize(deserializer: D) -> Result - 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 { + /// 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(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 { - fn deserialize(deserializer: D) -> Result + /// 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 { + /// 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 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 { + /// 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(dt: &NaiveDateTime, serializer: S) -> Result + 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(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))) + } + } + } #[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