Skip to content

Commit

Permalink
Guaranteed stable serde representation
Browse files Browse the repository at this point in the history
This allows changing the internal representation of types without
breaking backwards compatibility.
  • Loading branch information
jhpratt committed Dec 12, 2019
1 parent 3139629 commit e53ca14
Show file tree
Hide file tree
Showing 17 changed files with 254 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ pub fn weeks_in_year(year: i32) -> u8 {
/// range, please [file an issue](https://github.com/time-rs/time/issues/new)
/// with your use case.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(try_from = "crate::serde::Date", into = "crate::serde::Date")
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Date {
#[allow(clippy::missing_docs_in_private_items)]
Expand Down
4 changes: 4 additions & 0 deletions src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ use core::{
/// This implementation allows for negative durations, unlike
/// [`core::time::Duration`].
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(from = "crate::serde::Duration", into = "crate::serde::Duration")
)]
#[derive(Clone, Copy, Debug, Default, Eq)]
pub struct Duration {
/// Is the `Duration` positive, negative, or zero?
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ mod numerical_traits;
mod offset_date_time;
/// The `PrimitiveDateTime` struct and its associated `impl`s.
mod primitive_date_time;
#[cfg(feature = "serde")]
mod serde;
/// Ensure certain methods are present on all types.
mod shim;
/// The `Sign` struct and its associated `impl`s.
Expand Down
7 changes: 7 additions & 0 deletions src/offset_date_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ use core::{
/// For equality, comparisons, and hashing, calculations are performed using the
/// [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(
try_from = "crate::serde::PrimitiveDateTime",
into = "crate::serde::PrimitiveDateTime"
)
)]
#[derive(Debug, Clone, Copy, Eq)]
pub struct OffsetDateTime {
/// The `PrimitiveDateTime`, which is _always_ UTC.
Expand Down
7 changes: 7 additions & 0 deletions src/primitive_date_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ use std::time::SystemTime;

/// Combined date and time.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(
try_from = "crate::serde::PrimitiveDateTime",
into = "crate::serde::PrimitiveDateTime"
)
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PrimitiveDateTime {
#[allow(clippy::missing_docs_in_private_items)]
Expand Down
21 changes: 21 additions & 0 deletions src/serde/date.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use core::convert::TryFrom;

// (year, ordinal)
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct Date(pub(crate) i32, pub(crate) u16);

impl From<crate::Date> for Date {
#[inline]
fn from(original: crate::Date) -> Self {
Self(original.year(), original.ordinal())
}
}

impl TryFrom<Date> for crate::Date {
type Error = &'static str;

#[inline]
fn try_from(original: Date) -> Result<Self, Self::Error> {
Self::try_from_yo(original.0, original.1).map_err(|_| "invalid value")
}
}
17 changes: 17 additions & 0 deletions src/serde/duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// (seconds, nanoseconds)
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct Duration(i64, u32);

impl From<crate::Duration> for Duration {
#[inline]
fn from(original: crate::Duration) -> Self {
Self(original.whole_seconds(), original.subsec_nanoseconds())
}
}

impl From<Duration> for crate::Duration {
#[inline]
fn from(original: Duration) -> Self {
Self::new(original.0, original.1)
}
}
27 changes: 27 additions & 0 deletions src/serde/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Types with guaranteed stable serde representations.
//!
//! This allows for the ability to change the internal structure of a type while
//! maintaining backwards compatibility.
//!
//! Strings are avoided where possible to allow for optimal representations in
//! various binary forms.

#![allow(clippy::missing_docs_in_private_items)]

// OffsetDateTime is in the primitive_date_time module.

mod date;
mod duration;
mod primitive_date_time;
mod sign;
mod time;
mod utc_offset;
mod weekday;

pub(crate) use self::time::Time;
pub(crate) use date::Date;
pub(crate) use duration::Duration;
pub(crate) use primitive_date_time::PrimitiveDateTime;
pub(crate) use sign::Sign;
pub(crate) use utc_offset::UtcOffset;
pub(crate) use weekday::Weekday;
48 changes: 48 additions & 0 deletions src/serde/primitive_date_time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use core::convert::{TryFrom, TryInto};

// Date followed by Time
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct PrimitiveDateTime(i32, u16, u32, u32);

impl From<crate::PrimitiveDateTime> for PrimitiveDateTime {
#[inline]
fn from(original: crate::PrimitiveDateTime) -> Self {
let date: crate::serde::Date = original.date().into();
let time: crate::serde::Time = original.time().into();
Self(date.0, date.1, time.0, time.1)
}
}

impl TryFrom<PrimitiveDateTime> for crate::PrimitiveDateTime {
type Error = &'static str;

#[inline]
fn try_from(original: PrimitiveDateTime) -> Result<Self, Self::Error> {
let date = crate::serde::Date(original.0, original.1);
let time = crate::serde::Time(original.2, original.3);
Ok(Self::new(date.try_into()?, time.into()))
}
}

impl From<crate::OffsetDateTime> for PrimitiveDateTime {
#[inline]
fn from(original: crate::OffsetDateTime) -> Self {
// Simplify handling by always using UTC.
let original = original.to_offset(crate::UtcOffset::UTC);
let date: crate::serde::Date = original.date().into();
let time: crate::serde::Time = original.time().into();
Self(date.0, date.1, time.0, time.1)
}
}

impl TryFrom<PrimitiveDateTime> for crate::OffsetDateTime {
type Error = &'static str;

#[inline]
fn try_from(original: PrimitiveDateTime) -> Result<Self, Self::Error> {
let date = crate::serde::Date(original.0, original.1);
let time = crate::serde::Time(original.2, original.3);
Ok(crate::PrimitiveDateTime::new(date.try_into()?, time.into())
.using_offset(crate::UtcOffset::UTC))
}
}
32 changes: 32 additions & 0 deletions src/serde/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use core::convert::TryFrom;

// positive => 1
// negative => -1
// zero => 0
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct Sign(i8);

impl From<crate::Sign> for Sign {
#[inline]
fn from(original: crate::Sign) -> Self {
match original {
crate::Sign::Positive => Self(1),
crate::Sign::Negative => Self(-1),
crate::Sign::Zero => Self(0),
}
}
}

impl TryFrom<Sign> for crate::Sign {
type Error = &'static str;

#[inline]
fn try_from(original: Sign) -> Result<Self, Self::Error> {
match original {
Sign(1) => Ok(Self::Positive),
Sign(-1) => Ok(Self::Negative),
Sign(0) => Ok(Self::Zero),
_ => Err("invalid value"),
}
}
}
22 changes: 22 additions & 0 deletions src/serde/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// (seconds since midnight, nanoseconds within second)
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct Time(pub(crate) u32, pub(crate) u32);

impl From<crate::Time> for Time {
#[inline]
fn from(original: crate::Time) -> Self {
Self(
original.hour() as u32 * 3_600
+ original.minute() as u32 * 60
+ original.second() as u32,
original.nanosecond(),
)
}
}

impl From<Time> for crate::Time {
#[inline]
fn from(original: Time) -> Self {
Self::from_nanoseconds_since_midnight(original.0 as u64 * 1_000_000_000 + original.1 as u64)
}
}
17 changes: 17 additions & 0 deletions src/serde/utc_offset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// seconds offset from UTC, positive is east
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct UtcOffset(i32);

impl From<crate::UtcOffset> for UtcOffset {
#[inline]
fn from(original: crate::UtcOffset) -> Self {
Self(original.as_seconds())
}
}

impl From<UtcOffset> for crate::UtcOffset {
#[inline]
fn from(original: UtcOffset) -> Self {
Self::seconds(original.0)
}
}
30 changes: 30 additions & 0 deletions src/serde/weekday.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use core::convert::TryFrom;

// 1-indexed day from Monday
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct Weekday(u8);

impl From<crate::Weekday> for Weekday {
#[inline]
fn from(original: crate::Weekday) -> Self {
Self(original.iso_weekday_number())
}
}

impl TryFrom<Weekday> for crate::Weekday {
type Error = &'static str;

#[inline]
fn try_from(original: Weekday) -> Result<Self, Self::Error> {
match original {
Weekday(1) => Ok(Self::Monday),
Weekday(2) => Ok(Self::Tuesday),
Weekday(3) => Ok(Self::Wednesday),
Weekday(4) => Ok(Self::Thursday),
Weekday(5) => Ok(Self::Friday),
Weekday(6) => Ok(Self::Saturday),
Weekday(7) => Ok(Self::Sunday),
_ => Err("invalid value"),
}
}
}
4 changes: 4 additions & 0 deletions src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use Sign::{Negative, Positive, Zero};
/// follows the same rules as real numbers.
#[repr(i8)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(try_from = "crate::serde::Sign", into = "crate::serde::Sign")
)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Sign {
/// A positive value.
Expand Down
4 changes: 4 additions & 0 deletions src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub(crate) const NANOS_PER_DAY: u64 = 24 * 60 * 60 * 1_000_000_000;
/// When comparing two `Time`s, they are assumed to be in the same calendar
/// date.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(from = "crate::serde::Time", into = "crate::serde::Time")
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Time {
#[allow(clippy::missing_docs_in_private_items)]
Expand Down
4 changes: 4 additions & 0 deletions src/utc_offset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ use crate::{
/// you need support outside this range, please file an issue with your use
/// case.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(from = "crate::serde::UtcOffset", into = "crate::serde::UtcOffset")
)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct UtcOffset {
/// The number of seconds offset from UTC. Positive is east, negative is
Expand Down
4 changes: 4 additions & 0 deletions src/weekday.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
/// two days after or five days before Friday), this type does not implement
/// `PartialOrd` or `Ord`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(try_from = "crate::serde::Weekday", into = "crate::serde::Weekday")
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Weekday {
#[allow(clippy::missing_docs_in_private_items)]
Expand Down

0 comments on commit e53ca14

Please sign in to comment.