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