1use crate::{ActorId, ActorKind, Dependency, LagBasis, Plan, WorkItem};
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12mod clock;
13mod working;
14pub use clock::{ClockTime, WorkingPeriod};
15pub use working::{BeyondCalendar, MAX_WINDOW_HOURS, WorkingTime};
16pub(crate) mod validate;
17pub use validate::MAX_CALENDAR_HOURS;
18
19pub const ALWAYS: &str = "always";
21pub const STANDARD: &str = "standard";
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
28pub struct Calendars {
29 pub time_zone: String,
31 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33 pub definitions: BTreeMap<String, CalendarDefinition>,
34 #[serde(default, skip_serializing_if = "KindCalendars::is_default")]
36 pub kinds: KindCalendars,
37 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
39 pub actors: BTreeMap<String, String>,
40 #[serde(default = "human", skip_serializing_if = "is_human")]
42 pub default_executor: ActorKind,
43 #[serde(default = "human", skip_serializing_if = "is_human")]
45 pub verifier: ActorKind,
46}
47
48fn human() -> ActorKind {
49 ActorKind::Human
50}
51
52fn is_human(kind: &ActorKind) -> bool {
53 *kind == ActorKind::Human
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
59pub struct KindCalendars {
60 #[serde(default = "standard_name")]
62 pub human: String,
63 #[serde(default = "always_name")]
65 pub agent: String,
66 #[serde(default = "always_name")]
68 pub service: String,
69}
70
71fn standard_name() -> String {
72 STANDARD.into()
73}
74
75fn always_name() -> String {
76 ALWAYS.into()
77}
78
79impl Default for KindCalendars {
80 fn default() -> Self {
81 Self {
82 human: standard_name(),
83 agent: always_name(),
84 service: always_name(),
85 }
86 }
87}
88
89impl KindCalendars {
90 #[must_use]
92 pub fn is_default(&self) -> bool {
93 *self == Self::default()
94 }
95
96 #[must_use]
98 pub fn of(&self, kind: ActorKind) -> &str {
99 match kind {
100 ActorKind::Human => &self.human,
101 ActorKind::Agent => &self.agent,
102 ActorKind::Service => &self.service,
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
110pub struct CalendarDefinition {
111 pub week: WeekPattern,
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub exceptions: Vec<CalendarException>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
119#[serde(deny_unknown_fields)]
121pub struct WeekPattern {
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
124 pub mon: Vec<WorkingPeriod>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub tue: Vec<WorkingPeriod>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub wed: Vec<WorkingPeriod>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub thu: Vec<WorkingPeriod>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub fri: Vec<WorkingPeriod>,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub sat: Vec<WorkingPeriod>,
140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub sun: Vec<WorkingPeriod>,
143}
144
145impl WeekPattern {
146 #[must_use]
148 pub fn day(&self, day: chrono::Weekday) -> &[WorkingPeriod] {
149 use chrono::Weekday::{Fri, Mon, Sat, Sun, Thu, Tue, Wed};
150 match day {
151 Mon => &self.mon,
152 Tue => &self.tue,
153 Wed => &self.wed,
154 Thu => &self.thu,
155 Fri => &self.fri,
156 Sat => &self.sat,
157 Sun => &self.sun,
158 }
159 }
160
161 pub fn days(&self) -> impl Iterator<Item = (chrono::Weekday, &[WorkingPeriod])> {
163 use chrono::Weekday::{Fri, Mon, Sat, Sun, Thu, Tue, Wed};
164 [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
165 .into_iter()
166 .map(|day| (day, self.day(day)))
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(deny_unknown_fields)]
173pub struct CalendarException {
174 pub from: chrono::NaiveDate,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub to: Option<chrono::NaiveDate>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 pub hours: Vec<WorkingPeriod>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub name: Option<String>,
185}
186
187impl CalendarException {
188 #[must_use]
190 pub fn last(&self) -> chrono::NaiveDate {
191 self.to.unwrap_or(self.from)
192 }
193
194 #[must_use]
196 pub fn covers(&self, date: chrono::NaiveDate) -> bool {
197 self.from <= date && date <= self.last()
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum CalendarSource {
205 Task,
207 Actor,
209 Kind,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub struct ResolvedCalendar {
216 pub calendar: String,
218 pub executor: ActorKind,
220 pub source: CalendarSource,
222}
223
224#[derive(Debug, Clone, Copy)]
226pub enum CalendarRules<'a> {
227 Always,
229 Standard,
231 Weekly(&'a CalendarDefinition),
233}
234
235#[must_use]
237pub fn standard_definition() -> CalendarDefinition {
238 let day = vec![
239 WorkingPeriod::new(ClockTime::hours(8), ClockTime::hours(12)),
240 WorkingPeriod::new(ClockTime::hours(13), ClockTime::hours(17)),
241 ];
242 CalendarDefinition {
243 week: WeekPattern {
244 mon: day.clone(),
245 tue: day.clone(),
246 wed: day.clone(),
247 thu: day.clone(),
248 fri: day,
249 sat: Vec::new(),
250 sun: Vec::new(),
251 },
252 exceptions: Vec::new(),
253 }
254}
255
256impl Calendars {
257 #[must_use]
259 pub fn in_zone(time_zone: impl Into<String>) -> Self {
260 Self {
261 time_zone: time_zone.into(),
262 definitions: BTreeMap::new(),
263 kinds: KindCalendars::default(),
264 actors: BTreeMap::new(),
265 default_executor: ActorKind::Human,
266 verifier: ActorKind::Human,
267 }
268 }
269
270 #[must_use]
272 pub fn rules(&self, name: &str) -> Option<CalendarRules<'_>> {
273 if name == ALWAYS {
274 return Some(CalendarRules::Always);
275 }
276 match self.definitions.get(name) {
277 Some(definition) => Some(CalendarRules::Weekly(definition)),
278 None if name == STANDARD => Some(CalendarRules::Standard),
279 None => None,
280 }
281 }
282
283 pub fn zone(&self) -> Result<chrono_tz::Tz, BeyondCalendar> {
285 self.time_zone.parse().map_err(|_| BeyondCalendar)
286 }
287
288 #[must_use]
291 pub fn resolve(&self, work: &WorkItem) -> ResolvedCalendar {
292 let owner = work.execution.owner.as_ref();
293 let executor = owner
294 .map(|o| o.kind)
295 .or(work.schedule.executor)
296 .unwrap_or(self.default_executor);
297 let actor = owner.and_then(|o| self.actors.get(&o.to_string()));
298 let (calendar, source) = match (&work.schedule.calendar, actor) {
299 (Some(own), _) => (own.clone(), CalendarSource::Task),
300 (None, Some(available)) => (available.clone(), CalendarSource::Actor),
301 (None, None) => (self.kinds.of(executor).to_owned(), CalendarSource::Kind),
302 };
303 ResolvedCalendar {
304 calendar,
305 executor,
306 source,
307 }
308 }
309
310 #[must_use]
312 pub fn verifier_calendar(&self) -> &str {
313 self.kinds.of(self.verifier)
314 }
315
316 #[must_use]
320 pub fn after_working(
321 &self,
322 calendar: &str,
323 from: chrono::DateTime<chrono::Utc>,
324 hours: f64,
325 ) -> Option<chrono::DateTime<chrono::Utc>> {
326 let rules = self.rules(calendar)?;
327 let zone = self.zone().ok()?;
328 let offset = WorkingTime::offset_after(rules, zone, from, hours)?;
329 crate::events::hours_after(from, offset)
330 }
331}
332
333impl Plan {
334 #[must_use]
336 pub fn work_calendar(&self, work: &WorkItem) -> Option<ResolvedCalendar> {
337 self.calendars.as_ref().map(|c| c.resolve(work))
338 }
339
340 #[must_use]
343 pub fn lag_end(
344 &self,
345 edge: &Dependency,
346 event_at: chrono::DateTime<chrono::Utc>,
347 hours: f64,
348 ) -> Option<chrono::DateTime<chrono::Utc>> {
349 let calendars = self.calendars.as_ref();
350 let successor = self.work_items.get(&edge.successor);
351 match (edge.lag_basis, calendars, successor) {
352 (LagBasis::Working, Some(calendars), Some(work)) => {
353 calendars.after_working(&calendars.resolve(work).calendar, event_at, hours)
354 }
355 _ => crate::events::hours_after(event_at, hours),
356 }
357 }
358}
359
360pub(crate) fn actor_key(key: &str) -> Option<ActorId> {
362 key.parse().ok()
363}
364
365#[cfg(test)]
366mod tests;