aven_core/recurrence/
identity.rs1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5use crate::ids::{TaskId, WorkspaceId, encode_crockford};
6
7use super::schedule::{RecurrenceScheduleError, slot_values};
8use super::{RecurrenceSchedule, RecurrenceSeriesId};
9
10const TASK_ID_DOMAIN: &[u8] = b"aven recurrence task v1";
11const TASK_CHANGE_DOMAIN: &[u8] = b"aven recurrence task change v1";
12const OCCURRENCE_CHANGE_DOMAIN: &[u8] = b"aven recurrence occurrence change v1";
13const TASK_FIELD_VERSION_DOMAIN: &[u8] = b"aven recurrence task field version v1";
14const OCCURRENCE_FIELD_VERSION_DOMAIN: &[u8] = b"aven recurrence occurrence field version v1";
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct RecurrenceOccurrenceIdentity {
18 pub task_id: TaskId,
19 pub task_change_id: String,
20 pub occurrence_change_id: String,
21 pub created_at: String,
22 pub updated_at: String,
23 pub occurrence_link: RecurrenceOccurrenceLink,
24 pub field_version_seeds: RecurrenceFieldVersionSeeds,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct RecurrenceOccurrenceLink {
29 pub workspace_id: WorkspaceId,
30 pub series_id: RecurrenceSeriesId,
31 pub slot_on: NaiveDate,
32 pub task_id: TaskId,
33 pub projected_at: String,
34 pub change_id: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct RecurrenceFieldVersionSeeds {
39 pub task: String,
40 pub occurrence: String,
41}
42
43pub fn derive_occurrence_identity(
44 workspace_id: &WorkspaceId,
45 series_id: &RecurrenceSeriesId,
46 schedule: &RecurrenceSchedule,
47 slot_on: NaiveDate,
48) -> Result<RecurrenceOccurrenceIdentity, RecurrenceScheduleError> {
49 let slot = slot_values(schedule, slot_on)?;
50 let slot_text = slot_on.format("%Y-%m-%d").to_string();
51 let components = [
52 workspace_id.as_str(),
53 series_id.as_str(),
54 slot_text.as_str(),
55 ];
56 let task_id = derive_id(TASK_ID_DOMAIN, &components)
57 .parse::<TaskId>()
58 .expect("derived recurrence task ID is valid");
59 let task_change_id = derive_id(TASK_CHANGE_DOMAIN, &components);
60 let occurrence_change_id = derive_id(OCCURRENCE_CHANGE_DOMAIN, &components);
61 let field_version_seeds = RecurrenceFieldVersionSeeds {
62 task: derive_id(TASK_FIELD_VERSION_DOMAIN, &components),
63 occurrence: derive_id(OCCURRENCE_FIELD_VERSION_DOMAIN, &components),
64 };
65 let occurrence_link = RecurrenceOccurrenceLink {
66 workspace_id: workspace_id.clone(),
67 series_id: series_id.clone(),
68 slot_on,
69 task_id: task_id.clone(),
70 projected_at: slot.boundary_at.clone(),
71 change_id: occurrence_change_id.clone(),
72 };
73 Ok(RecurrenceOccurrenceIdentity {
74 task_id,
75 task_change_id,
76 occurrence_change_id,
77 created_at: slot.boundary_at.clone(),
78 updated_at: slot.boundary_at,
79 occurrence_link,
80 field_version_seeds,
81 })
82}
83
84fn derive_id(domain: &[u8], components: &[&str]) -> String {
85 let mut hasher = Sha256::new();
86 hasher.update(domain);
87 for component in components {
88 hasher.update(component.as_bytes());
89 }
90 let digest = hasher.finalize();
91 let mut bytes = [0u8; 10];
92 bytes.copy_from_slice(&digest[..10]);
93 encode_crockford(&bytes)
94}