use crate::{ActorId, ActorKind, Dependency, LagBasis, Plan, WorkItem};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
mod clock;
mod working;
pub use clock::{ClockTime, WorkingPeriod};
pub use working::{BeyondCalendar, MAX_WINDOW_HOURS, WorkingTime};
pub(crate) mod validate;
pub use validate::MAX_CALENDAR_HOURS;
pub const ALWAYS: &str = "always";
pub const STANDARD: &str = "standard";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Calendars {
pub time_zone: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub definitions: BTreeMap<String, CalendarDefinition>,
#[serde(default, skip_serializing_if = "KindCalendars::is_default")]
pub kinds: KindCalendars,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub actors: BTreeMap<String, String>,
#[serde(default = "human", skip_serializing_if = "is_human")]
pub default_executor: ActorKind,
#[serde(default = "human", skip_serializing_if = "is_human")]
pub verifier: ActorKind,
}
fn human() -> ActorKind {
ActorKind::Human
}
fn is_human(kind: &ActorKind) -> bool {
*kind == ActorKind::Human
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KindCalendars {
#[serde(default = "standard_name")]
pub human: String,
#[serde(default = "always_name")]
pub agent: String,
#[serde(default = "always_name")]
pub service: String,
}
fn standard_name() -> String {
STANDARD.into()
}
fn always_name() -> String {
ALWAYS.into()
}
impl Default for KindCalendars {
fn default() -> Self {
Self {
human: standard_name(),
agent: always_name(),
service: always_name(),
}
}
}
impl KindCalendars {
#[must_use]
pub fn is_default(&self) -> bool {
*self == Self::default()
}
#[must_use]
pub fn of(&self, kind: ActorKind) -> &str {
match kind {
ActorKind::Human => &self.human,
ActorKind::Agent => &self.agent,
ActorKind::Service => &self.service,
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CalendarDefinition {
pub week: WeekPattern,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exceptions: Vec<CalendarException>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WeekPattern {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mon: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tue: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub wed: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub thu: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fri: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sat: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sun: Vec<WorkingPeriod>,
}
impl WeekPattern {
#[must_use]
pub fn day(&self, day: chrono::Weekday) -> &[WorkingPeriod] {
use chrono::Weekday::{Fri, Mon, Sat, Sun, Thu, Tue, Wed};
match day {
Mon => &self.mon,
Tue => &self.tue,
Wed => &self.wed,
Thu => &self.thu,
Fri => &self.fri,
Sat => &self.sat,
Sun => &self.sun,
}
}
pub fn days(&self) -> impl Iterator<Item = (chrono::Weekday, &[WorkingPeriod])> {
use chrono::Weekday::{Fri, Mon, Sat, Sun, Thu, Tue, Wed};
[Mon, Tue, Wed, Thu, Fri, Sat, Sun]
.into_iter()
.map(|day| (day, self.day(day)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CalendarException {
pub from: chrono::NaiveDate,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to: Option<chrono::NaiveDate>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hours: Vec<WorkingPeriod>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl CalendarException {
#[must_use]
pub fn last(&self) -> chrono::NaiveDate {
self.to.unwrap_or(self.from)
}
#[must_use]
pub fn covers(&self, date: chrono::NaiveDate) -> bool {
self.from <= date && date <= self.last()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CalendarSource {
Task,
Actor,
Kind,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedCalendar {
pub calendar: String,
pub executor: ActorKind,
pub source: CalendarSource,
}
#[derive(Debug, Clone, Copy)]
pub enum CalendarRules<'a> {
Always,
Standard,
Weekly(&'a CalendarDefinition),
}
#[must_use]
pub fn standard_definition() -> CalendarDefinition {
let day = vec![
WorkingPeriod::new(ClockTime::hours(8), ClockTime::hours(12)),
WorkingPeriod::new(ClockTime::hours(13), ClockTime::hours(17)),
];
CalendarDefinition {
week: WeekPattern {
mon: day.clone(),
tue: day.clone(),
wed: day.clone(),
thu: day.clone(),
fri: day,
sat: Vec::new(),
sun: Vec::new(),
},
exceptions: Vec::new(),
}
}
impl Calendars {
#[must_use]
pub fn in_zone(time_zone: impl Into<String>) -> Self {
Self {
time_zone: time_zone.into(),
definitions: BTreeMap::new(),
kinds: KindCalendars::default(),
actors: BTreeMap::new(),
default_executor: ActorKind::Human,
verifier: ActorKind::Human,
}
}
#[must_use]
pub fn rules(&self, name: &str) -> Option<CalendarRules<'_>> {
if name == ALWAYS {
return Some(CalendarRules::Always);
}
match self.definitions.get(name) {
Some(definition) => Some(CalendarRules::Weekly(definition)),
None if name == STANDARD => Some(CalendarRules::Standard),
None => None,
}
}
pub fn zone(&self) -> Result<chrono_tz::Tz, BeyondCalendar> {
self.time_zone.parse().map_err(|_| BeyondCalendar)
}
#[must_use]
pub fn resolve(&self, work: &WorkItem) -> ResolvedCalendar {
let owner = work.execution.owner.as_ref();
let executor = owner
.map(|o| o.kind)
.or(work.schedule.executor)
.unwrap_or(self.default_executor);
let actor = owner.and_then(|o| self.actors.get(&o.to_string()));
let (calendar, source) = match (&work.schedule.calendar, actor) {
(Some(own), _) => (own.clone(), CalendarSource::Task),
(None, Some(available)) => (available.clone(), CalendarSource::Actor),
(None, None) => (self.kinds.of(executor).to_owned(), CalendarSource::Kind),
};
ResolvedCalendar {
calendar,
executor,
source,
}
}
#[must_use]
pub fn verifier_calendar(&self) -> &str {
self.kinds.of(self.verifier)
}
#[must_use]
pub fn after_working(
&self,
calendar: &str,
from: chrono::DateTime<chrono::Utc>,
hours: f64,
) -> Option<chrono::DateTime<chrono::Utc>> {
let rules = self.rules(calendar)?;
let zone = self.zone().ok()?;
let offset = WorkingTime::offset_after(rules, zone, from, hours)?;
crate::events::hours_after(from, offset)
}
}
impl Plan {
#[must_use]
pub fn work_calendar(&self, work: &WorkItem) -> Option<ResolvedCalendar> {
self.calendars.as_ref().map(|c| c.resolve(work))
}
#[must_use]
pub fn lag_end(
&self,
edge: &Dependency,
event_at: chrono::DateTime<chrono::Utc>,
hours: f64,
) -> Option<chrono::DateTime<chrono::Utc>> {
let calendars = self.calendars.as_ref();
let successor = self.work_items.get(&edge.successor);
match (edge.lag_basis, calendars, successor) {
(LagBasis::Working, Some(calendars), Some(work)) => {
calendars.after_working(&calendars.resolve(work).calendar, event_at, hours)
}
_ => crate::events::hours_after(event_at, hours),
}
}
}
pub(crate) fn actor_key(key: &str) -> Option<ActorId> {
key.parse().ok()
}
#[cfg(test)]
mod tests;