diff --git a/Cargo.toml b/Cargo.toml index 79097ac..62bfabd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ pin-project = "0.4.23" im = "15.0.0" chrono = { version = "0.4.15" } assert_matches = "1.3.0" +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.57" [dependencies.tracing-subscriber] version = "0.2.11" diff --git a/src/schedule.rs b/src/schedule.rs index 7468602..9cdbd9a 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -1,9 +1,10 @@ //! Scheduling for events to run at certain intervals in the future use chrono::{ - Date, DateTime, Datelike, Duration as CDuration, Local, NaiveDateTime, NaiveTime, - TimeZone, Weekday, + Date, DateTime, Datelike, Duration as CDuration, Local, NaiveDateTime, NaiveTime, TimeZone, + Weekday, }; +use serde::{Deserialize, Serialize}; use std::cmp; use std::iter::FromIterator; @@ -32,7 +33,7 @@ pub fn every_day() -> WeekdaySet { /// Represents the different types of date-time bounds that can be on a schedule #[allow(dead_code)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DateTimeBound { /// There is no bound (ie. the Schedule extends with no limit) None, @@ -73,14 +74,250 @@ impl DateTimeBound { } /// A schedule that determines when an event will occur. -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Schedule { + #[serde( + serialize_with = "ser::serialize_times", + deserialize_with = "ser::deserialize_times" + )] pub times: TimeSet, + #[serde( + serialize_with = "ser::serialize_weekdays", + deserialize_with = "ser::deserialize_weekdays" + )] pub weekdays: WeekdaySet, pub from: DateTimeBound, pub to: DateTimeBound, } +mod ser { + use super::{DateTimeBound, TimeSet, WeekdaySet}; + use chrono::{NaiveDate, NaiveTime, Weekday}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::{convert::TryInto, fmt}; + + #[derive(Default, Debug, Clone, Serialize, Deserialize)] + struct TimeOfDay { + hour: u32, + minute: u32, + second: u32, + #[serde(default)] + millisecond: u32, + } + + impl From<&NaiveTime> for TimeOfDay { + fn from(time: &NaiveTime) -> Self { + use chrono::Timelike; + Self { + hour: time.hour(), + minute: time.minute(), + second: time.second(), + millisecond: time.nanosecond() / 1_000_000_u32, + } + } + } + + #[derive(Copy, Clone, Debug)] + struct InvalidTimeOfDay; + + impl fmt::Display for InvalidTimeOfDay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "invalid time of day".fmt(f) + } + } + + impl TryInto for TimeOfDay { + type Error = InvalidTimeOfDay; + fn try_into(self) -> Result { + NaiveTime::from_hms_milli_opt(self.hour, self.minute, self.second, self.millisecond) + .ok_or(InvalidTimeOfDay) + } + } + + #[allow(clippy::ptr_arg)] + pub fn serialize_times(times: &TimeSet, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_seq(times.iter().map(TimeOfDay::from)) + } + + pub fn deserialize_times<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::{Error, SeqAccess, Visitor}; + + struct TimeSetVisitor; + + impl<'de> Visitor<'de> for TimeSetVisitor { + type Value = TimeSet; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a sequence of time of days") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut times = TimeSet::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(value) = seq.next_element::()? { + let time: NaiveTime = value.try_into().map_err(A::Error::custom)?; + times.push(time); + } + Ok(times) + } + } + + deserializer.deserialize_seq(TimeSetVisitor) + } + + #[allow(clippy::ptr_arg)] + pub fn serialize_weekdays(weekdays: &WeekdaySet, serializer: S) -> Result + where + S: Serializer, + { + let iter = weekdays + .iter() + .map(|weekday| weekday.num_days_from_sunday()); + serializer.collect_seq(iter) + } + + #[derive(Copy, Clone, Debug)] + struct InvalidWeekday; + + impl fmt::Display for InvalidWeekday { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "invalid weekday".fmt(f) + } + } + + fn weekday_from_days_from_sunday(days: u32) -> Result { + Ok(match days { + 0 => Weekday::Sun, + 1 => Weekday::Mon, + 2 => Weekday::Tue, + 3 => Weekday::Wed, + 4 => Weekday::Thu, + 5 => Weekday::Fri, + 6 => Weekday::Sat, + _ => return Err(InvalidWeekday), + }) + } + + pub fn deserialize_weekdays<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::{Error, SeqAccess, Visitor}; + + struct WeekdaySetVisitor; + + impl<'de> Visitor<'de> for WeekdaySetVisitor { + type Value = WeekdaySet; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a sequence of integers representing weekdays") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut weekdays = WeekdaySet::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(value) = seq.next_element::()? { + let weekday = weekday_from_days_from_sunday(value).map_err(A::Error::custom)?; + weekdays.push(weekday); + } + Ok(weekdays) + } + } + + deserializer.deserialize_seq(WeekdaySetVisitor) + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + struct DateAndYear { + year: i32, + month: u32, + day: u32, + } + + impl From for DateAndYear { + fn from(date: NaiveDate) -> Self { + use chrono::Datelike; + Self { + year: date.year(), + month: date.month(), + day: date.day(), + } + } + } + + #[derive(Copy, Clone, Debug)] + struct InvalidDateAndYear; + + impl fmt::Display for InvalidDateAndYear { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "invalid date or year".fmt(f) + } + } + + impl TryInto for DateAndYear { + type Error = InvalidDateAndYear; + fn try_into(self) -> Result { + NaiveDate::from_ymd_opt(self.year, self.month, self.day).ok_or(InvalidDateAndYear) + } + } + + impl Serialize for DateTimeBound { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + DateTimeBound::None => serializer.serialize_none(), + DateTimeBound::Yearly(date_time) => { + // Discard time + let mut date_of_year: DateAndYear = date_time.date().into(); + // Set year to 0 (since it is yearly) + date_of_year.year = 0; + + date_of_year.serialize(serializer) + } + DateTimeBound::Definite(date_time) => { + // Discard time + let date_of_year: DateAndYear = date_time.date().into(); + + date_of_year.serialize(serializer) + } + } + } + } + + impl<'de> Deserialize<'de> for DateTimeBound { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + let date_of_year: Option = Deserialize::deserialize(deserializer)?; + Ok(match date_of_year { + Some(date_of_year) => { + let year = date_of_year.year; + let date: NaiveDate = date_of_year.try_into().map_err(D::Error::custom)?; + let date_time = date.and_hms(0, 0, 0); + if year == 0 { + DateTimeBound::Yearly(date_time) + } else { + DateTimeBound::Definite(date_time) + } + } + None => DateTimeBound::None, + }) + } + } +} + /// Gets the next date matching the `weekday` after `date` fn next_weekday(mut date: Date, weekday: Weekday) -> Date { while date.weekday() != weekday { @@ -174,6 +411,7 @@ impl Schedule { #[cfg(test)] mod test { use super::*; + use assert_matches::assert_matches; use chrono::NaiveDate; #[test] @@ -360,4 +598,50 @@ mod test { assert_eq!(result, case.expected_result, "case failed: {:?}", case); } } + + #[test] + fn test_serialize() { + let sched = Schedule::new( + vec![ + NaiveTime::from_hms_milli(0, 0, 0, 0), + NaiveTime::from_hms_milli(23, 59, 59, 999), + ], + every_day(), + DateTimeBound::Yearly(NaiveDate::from_ymd(2020, 1, 1).and_hms(0, 0, 0)), + DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(23, 59, 59)), + ); + let ser = serde_json::to_string(&sched).unwrap(); + // Weekdays should match the order in `every_day()` but with sunday being 0 + assert_eq!( + &ser, + "{\ + \"times\":[\ + {\"hour\":0,\"minute\":0,\"second\":0,\"millisecond\":0},\ + {\"hour\":23,\"minute\":59,\"second\":59,\"millisecond\":999}\ + ],\ + \"weekdays\":[1,2,3,4,5,6,0],\ + \"from\":{\ + \"year\":0,\"month\":1,\"day\":1\ + },\ + \"to\":{\ + \"year\":9999,\"month\":12,\"day\":31\ + }\ + }" + ); + let sched_de: Schedule = serde_json::from_str(&ser).unwrap(); + assert_eq!(sched.times, sched_de.times); + assert_eq!(sched.weekdays, sched_de.weekdays); + // This serialization is lossy (year is discarded for yearly) + // assert_eq!(sched_de.from, sched.from); + assert_eq!( + sched_de.from, + DateTimeBound::Yearly(NaiveDate::from_ymd(0, 1, 1).and_hms(0, 0, 0)) + ); + // This serialization is also lossy (time is discarded) + // assert_eq!(sched_de.to, sched.to); + assert_eq!( + sched_de.to, + DateTimeBound::Definite(NaiveDate::from_ymd(9999, 12, 31).and_hms(0, 0, 0)) + ); + } }