Skip to main content

aven_core/
recurrence.rs

1pub mod identity;
2pub mod rule;
3pub mod schedule;
4
5use std::fmt;
6use std::str::FromStr;
7
8use chrono::{NaiveDate, NaiveTime};
9use getrandom::fill as fill_random;
10use serde::{Deserialize, Deserializer, Serialize};
11use sqlx::database::Database;
12use sqlx::decode::Decode;
13use sqlx::encode::{Encode, IsNull};
14use sqlx::error::BoxDynError;
15use sqlx::sqlite::{Sqlite, SqliteTypeInfo, SqliteValueRef};
16use sqlx::types::Type;
17
18use crate::ids::{BASE32, encode_crockford};
19
20pub use identity::{
21    RecurrenceFieldVersionSeeds, RecurrenceOccurrenceIdentity, RecurrenceOccurrenceLink,
22    RecurrenceProposalIds, derive_occurrence_identity, derive_proposal_ids,
23};
24pub use rule::{RecurrenceFrequency, RecurrenceRule, WeekdaySet};
25pub use schedule::{
26    RecurrenceScheduleError, RecurrenceSlotIter, is_slot, live_slot_on, next_slot_after,
27    projection_slot_at, slot_cutoff, slot_values,
28};
29
30#[cfg(test)]
31mod tests;
32
33#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
34#[serde(transparent)]
35pub struct RecurrenceSeriesId(String);
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct InvalidRecurrenceSeriesId;
39
40impl RecurrenceSeriesId {
41    pub fn new() -> Self {
42        let mut bytes = [0u8; 10];
43        fill_random(&mut bytes).expect("fill random bytes");
44        Self(encode_crockford(&bytes))
45    }
46
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52/// Prefix every recurrence series display ref carries, in place of the project
53/// prefix a task ref uses.
54pub(crate) const SERIES_REF_PREFIX: &str = "RCR";
55
56pub(crate) fn recurrence_series_display_ref(
57    series_id: &RecurrenceSeriesId,
58    ids: &[RecurrenceSeriesId],
59) -> String {
60    let id = series_id.as_str();
61    let shared = ids
62        .iter()
63        .filter(|candidate| candidate.as_str() != id)
64        .map(|candidate| {
65            id.bytes()
66                .zip(candidate.as_str().bytes())
67                .take_while(|(left, right)| left == right)
68                .count()
69        })
70        .max()
71        .unwrap_or(0);
72    let length = 4.max(shared.saturating_add(1)).min(id.len());
73    format!("{SERIES_REF_PREFIX}-{}", &id[..length])
74}
75
76impl Default for RecurrenceSeriesId {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl fmt::Display for RecurrenceSeriesId {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        self.0.fmt(formatter)
85    }
86}
87
88impl fmt::Display for InvalidRecurrenceSeriesId {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter.write_str("recurrence series ID must be 16 Crockford Base32 characters")
91    }
92}
93
94impl std::error::Error for InvalidRecurrenceSeriesId {}
95
96impl FromStr for RecurrenceSeriesId {
97    type Err = InvalidRecurrenceSeriesId;
98
99    fn from_str(value: &str) -> Result<Self, Self::Err> {
100        if value.len() == 16 && value.bytes().all(|byte| BASE32.contains(&byte)) {
101            Ok(Self(value.to_string()))
102        } else {
103            Err(InvalidRecurrenceSeriesId)
104        }
105    }
106}
107
108impl TryFrom<String> for RecurrenceSeriesId {
109    type Error = InvalidRecurrenceSeriesId;
110
111    fn try_from(value: String) -> Result<Self, Self::Error> {
112        value.parse()
113    }
114}
115
116impl<'de> Deserialize<'de> for RecurrenceSeriesId {
117    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118    where
119        D: Deserializer<'de>,
120    {
121        String::deserialize(deserializer)?
122            .parse()
123            .map_err(serde::de::Error::custom)
124    }
125}
126
127impl Type<Sqlite> for RecurrenceSeriesId {
128    fn type_info() -> SqliteTypeInfo {
129        <String as Type<Sqlite>>::type_info()
130    }
131}
132
133impl Encode<'_, Sqlite> for RecurrenceSeriesId {
134    fn encode_by_ref(
135        &self,
136        buffer: &mut <Sqlite as Database>::ArgumentBuffer,
137    ) -> Result<IsNull, BoxDynError> {
138        <String as Encode<Sqlite>>::encode_by_ref(&self.0, buffer)
139    }
140}
141
142impl<'row> Decode<'row, Sqlite> for RecurrenceSeriesId {
143    fn decode(value: SqliteValueRef<'row>) -> Result<Self, BoxDynError> {
144        String::decode(value)?.parse().map_err(Into::into)
145    }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum RecurrenceDuePolicy {
151    SameDay,
152    None,
153}
154
155impl RecurrenceDuePolicy {
156    pub const fn as_str(self) -> &'static str {
157        match self {
158            Self::SameDay => "same_day",
159            Self::None => "none",
160        }
161    }
162
163    pub fn parse(value: &str) -> Result<Self, InvalidRecurrenceValue> {
164        match value {
165            "same_day" => Ok(Self::SameDay),
166            "none" => Ok(Self::None),
167            _ => Err(InvalidRecurrenceValue::new("due policy", value)),
168        }
169    }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174pub enum RecurrenceSeriesState {
175    Active,
176    Paused,
177    Stopped,
178}
179
180impl RecurrenceSeriesState {
181    pub const fn as_str(self) -> &'static str {
182        match self {
183            Self::Active => "active",
184            Self::Paused => "paused",
185            Self::Stopped => "stopped",
186        }
187    }
188
189    pub fn parse(value: &str) -> Result<Self, InvalidRecurrenceValue> {
190        match value {
191            "active" => Ok(Self::Active),
192            "paused" => Ok(Self::Paused),
193            "stopped" => Ok(Self::Stopped),
194            _ => Err(InvalidRecurrenceValue::new("series state", value)),
195        }
196    }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum RecurrenceOutcome {
202    Completed,
203    Skipped,
204}
205
206impl RecurrenceOutcome {
207    pub const fn as_str(self) -> &'static str {
208        match self {
209            Self::Completed => "completed",
210            Self::Skipped => "skipped",
211        }
212    }
213
214    pub fn parse(value: &str) -> Result<Self, InvalidRecurrenceValue> {
215        match value {
216            "completed" => Ok(Self::Completed),
217            "skipped" => Ok(Self::Skipped),
218            _ => Err(InvalidRecurrenceValue::new("outcome", value)),
219        }
220    }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum RecurrenceProjectionState {
226    Projected,
227    Resolved,
228    Archived,
229}
230
231impl RecurrenceProjectionState {
232    pub const fn as_str(self) -> &'static str {
233        match self {
234            Self::Projected => "projected",
235            Self::Resolved => "resolved",
236            Self::Archived => "archived",
237        }
238    }
239
240    pub fn parse(value: &str) -> Result<Self, InvalidRecurrenceValue> {
241        match value {
242            "projected" => Ok(Self::Projected),
243            "resolved" => Ok(Self::Resolved),
244            "archived" => Ok(Self::Archived),
245            _ => Err(InvalidRecurrenceValue::new("projection state", value)),
246        }
247    }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct InvalidRecurrenceValue {
252    kind: &'static str,
253    value: String,
254}
255
256impl InvalidRecurrenceValue {
257    fn new(kind: &'static str, value: &str) -> Self {
258        Self {
259            kind,
260            value: value.to_string(),
261        }
262    }
263}
264
265impl fmt::Display for InvalidRecurrenceValue {
266    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
267        write!(
268            formatter,
269            "invalid recurrence {}: {}",
270            self.kind, self.value
271        )
272    }
273}
274
275impl std::error::Error for InvalidRecurrenceValue {}
276
277#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
278#[serde(transparent)]
279pub struct TimeZoneId(String);
280
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct InvalidTimeZoneId(String);
283
284impl TimeZoneId {
285    pub fn as_str(&self) -> &str {
286        &self.0
287    }
288
289    pub(crate) fn timezone(&self) -> chrono_tz::Tz {
290        self.0
291            .parse()
292            .expect("TimeZoneId always contains a chrono-tz zone")
293    }
294}
295
296impl fmt::Display for TimeZoneId {
297    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
298        self.0.fmt(formatter)
299    }
300}
301
302impl fmt::Display for InvalidTimeZoneId {
303    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(formatter, "invalid IANA time zone: {}", self.0)
305    }
306}
307
308impl std::error::Error for InvalidTimeZoneId {}
309
310impl FromStr for TimeZoneId {
311    type Err = InvalidTimeZoneId;
312
313    fn from_str(value: &str) -> Result<Self, Self::Err> {
314        value
315            .parse::<chrono_tz::Tz>()
316            .map(|_| Self(value.to_string()))
317            .map_err(|_| InvalidTimeZoneId(value.to_string()))
318    }
319}
320
321impl TryFrom<String> for TimeZoneId {
322    type Error = InvalidTimeZoneId;
323
324    fn try_from(value: String) -> Result<Self, Self::Error> {
325        value.parse()
326    }
327}
328
329impl<'de> Deserialize<'de> for TimeZoneId {
330    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
331    where
332        D: Deserializer<'de>,
333    {
334        String::deserialize(deserializer)?
335            .parse()
336            .map_err(serde::de::Error::custom)
337    }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct RecurrenceSchedule {
342    pub rule: RecurrenceRule,
343    pub timezone: TimeZoneId,
344    pub start_on: NaiveDate,
345    pub available_local_time: Option<NaiveTime>,
346    pub due_policy: RecurrenceDuePolicy,
347}
348
349impl RecurrenceSchedule {
350    pub fn new(
351        rule: RecurrenceRule,
352        timezone: TimeZoneId,
353        start_on: NaiveDate,
354        available_local_time: Option<NaiveTime>,
355        due_policy: RecurrenceDuePolicy,
356    ) -> Self {
357        Self {
358            rule,
359            timezone,
360            start_on,
361            available_local_time,
362            due_policy,
363        }
364    }
365
366    pub fn slots_on_or_after(&self, date: NaiveDate) -> RecurrenceSlotIter<'_> {
367        RecurrenceSlotIter::new(&self.rule, self.start_on, date)
368    }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct RecurrenceSlot {
373    pub scheduled_on: NaiveDate,
374    pub boundary_at: String,
375    pub available_at: String,
376    pub due_on: Option<String>,
377}