Implement de/serialization of schedules
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #7
This commit is contained in:
parent
b6bcde020c
commit
9e9c1a353a
@ -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"
|
||||
|
292
src/schedule.rs
292
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<NaiveTime> for TimeOfDay {
|
||||
type Error = InvalidTimeOfDay;
|
||||
fn try_into(self) -> Result<NaiveTime, Self::Error> {
|
||||
NaiveTime::from_hms_milli_opt(self.hour, self.minute, self.second, self.millisecond)
|
||||
.ok_or(InvalidTimeOfDay)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::ptr_arg)]
|
||||
pub fn serialize_times<S>(times: &TimeSet, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_seq(times.iter().map(TimeOfDay::from))
|
||||
}
|
||||
|
||||
pub fn deserialize_times<'de, D>(deserializer: D) -> Result<TimeSet, D::Error>
|
||||
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut times = TimeSet::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(value) = seq.next_element::<TimeOfDay>()? {
|
||||
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<S>(weekdays: &WeekdaySet, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<Weekday, InvalidWeekday> {
|
||||
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<WeekdaySet, D::Error>
|
||||
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut weekdays = WeekdaySet::with_capacity(seq.size_hint().unwrap_or(0));
|
||||
while let Some(value) = seq.next_element::<u32>()? {
|
||||
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<NaiveDate> 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<NaiveDate> for DateAndYear {
|
||||
type Error = InvalidDateAndYear;
|
||||
fn try_into(self) -> Result<NaiveDate, Self::Error> {
|
||||
NaiveDate::from_ymd_opt(self.year, self.month, self.day).ok_or(InvalidDateAndYear)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for DateTimeBound {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
let date_of_year: Option<DateAndYear> = 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<Tz: TimeZone>(mut date: Date<Tz>, weekday: Weekday) -> Date<Tz> {
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user