aven-core 0.1.21

Core library for the Aven local-first task manager
Documentation
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::ids::{TaskId, WorkspaceId, encode_crockford};

use super::schedule::{RecurrenceScheduleError, slot_values};
use super::{RecurrenceSchedule, RecurrenceSeriesId};

const TASK_ID_DOMAIN: &[u8] = b"aven recurrence task v1";
const TASK_CHANGE_DOMAIN: &[u8] = b"aven recurrence task change v1";
const OCCURRENCE_CHANGE_DOMAIN: &[u8] = b"aven recurrence occurrence change v1";
const TASK_FIELD_VERSION_DOMAIN: &[u8] = b"aven recurrence task field version v1";
const OCCURRENCE_FIELD_VERSION_DOMAIN: &[u8] = b"aven recurrence occurrence field version v1";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecurrenceOccurrenceIdentity {
    pub task_id: TaskId,
    pub task_change_id: String,
    pub occurrence_change_id: String,
    pub created_at: String,
    pub updated_at: String,
    pub occurrence_link: RecurrenceOccurrenceLink,
    pub field_version_seeds: RecurrenceFieldVersionSeeds,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecurrenceOccurrenceLink {
    pub workspace_id: WorkspaceId,
    pub series_id: RecurrenceSeriesId,
    pub slot_on: NaiveDate,
    pub task_id: TaskId,
    pub projected_at: String,
    pub change_id: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecurrenceFieldVersionSeeds {
    pub task: String,
    pub occurrence: String,
}

pub fn derive_occurrence_identity(
    workspace_id: &WorkspaceId,
    series_id: &RecurrenceSeriesId,
    schedule: &RecurrenceSchedule,
    slot_on: NaiveDate,
) -> Result<RecurrenceOccurrenceIdentity, RecurrenceScheduleError> {
    let slot = slot_values(schedule, slot_on)?;
    let slot_text = slot_on.format("%Y-%m-%d").to_string();
    let components = [
        workspace_id.as_str(),
        series_id.as_str(),
        slot_text.as_str(),
    ];
    let task_id = derive_id(TASK_ID_DOMAIN, &components)
        .parse::<TaskId>()
        .expect("derived recurrence task ID is valid");
    let task_change_id = derive_id(TASK_CHANGE_DOMAIN, &components);
    let occurrence_change_id = derive_id(OCCURRENCE_CHANGE_DOMAIN, &components);
    let field_version_seeds = RecurrenceFieldVersionSeeds {
        task: derive_id(TASK_FIELD_VERSION_DOMAIN, &components),
        occurrence: derive_id(OCCURRENCE_FIELD_VERSION_DOMAIN, &components),
    };
    let occurrence_link = RecurrenceOccurrenceLink {
        workspace_id: workspace_id.clone(),
        series_id: series_id.clone(),
        slot_on,
        task_id: task_id.clone(),
        projected_at: slot.boundary_at.clone(),
        change_id: occurrence_change_id.clone(),
    };
    Ok(RecurrenceOccurrenceIdentity {
        task_id,
        task_change_id,
        occurrence_change_id,
        created_at: slot.boundary_at.clone(),
        updated_at: slot.boundary_at,
        occurrence_link,
        field_version_seeds,
    })
}

fn derive_id(domain: &[u8], components: &[&str]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(domain);
    for component in components {
        hasher.update(component.as_bytes());
    }
    let digest = hasher.finalize();
    let mut bytes = [0u8; 10];
    bytes.copy_from_slice(&digest[..10]);
    encode_crockford(&bytes)
}