note-to-self-lib 0.1.0

Shared data model, crypto, and sync types for note-to-self.
Documentation
use crate::crypto::checksum;
use crate::model::{Entry, Hlc, Manifest, ManifestEntry};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncBlob {
    pub id: Uuid,
    pub blob: String,
    pub version: u64,
    pub hlc: Hlc,
    pub checksum: String,
}

impl SyncBlob {
    pub fn from_raw(id: Uuid, raw: &[u8], version: u64, hlc: Hlc) -> Self {
        Self {
            id,
            blob: STANDARD.encode(raw),
            version,
            hlc,
            checksum: checksum(raw),
        }
    }

    pub fn raw(&self) -> Result<Vec<u8>, base64::DecodeError> {
        STANDARD.decode(&self.blob)
    }

    pub fn manifest_entry(&self) -> ManifestEntry {
        ManifestEntry {
            version: self.version,
            hlc: self.hlc,
            checksum: self.checksum.clone(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncRequest {
    pub device_id: Uuid,
    pub manifest: Manifest,
    pub upload: Vec<SyncBlob>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncResponse {
    pub download: Vec<SyncBlob>,
    pub ack_uploaded: Vec<Uuid>,
    pub server_manifest: Manifest,
}

pub fn dirty_entries<'a>(
    entries: impl Iterator<Item = &'a Entry>,
    manifest: &Manifest,
) -> Vec<&'a Entry> {
    entries
        .filter(|entry| match manifest.get(&entry.id) {
            None => true,
            Some(state) => entry.version != state.version || entry.hlc != state.hlc,
        })
        .collect()
}

pub fn manifest_entry_for_blob(blob: &[u8], entry: &Entry) -> ManifestEntry {
    ManifestEntry {
        version: entry.version,
        hlc: entry.hlc,
        checksum: checksum(blob),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{EntryKind, Hlc};
    use chrono::Utc;

    fn entry(version: u64) -> Entry {
        Entry {
            id: Uuid::now_v7(),
            created_at: Utc::now(),
            modified_at: Utc::now(),
            hlc: Hlc {
                wall_ms: version,
                counter: 0,
                node_id: 1,
            },
            body: "body".to_string(),
            attachments: Vec::new(),
            tags: vec![],
            starred: false,
            kind: EntryKind::Journal,
            deleted: false,
            version,
        }
    }

    #[test]
    fn dirty_checks_version_and_hlc() {
        let entry = entry(2);
        let mut manifest = Manifest::new();
        manifest.insert(
            entry.id,
            ManifestEntry {
                version: 1,
                hlc: entry.hlc,
                checksum: "x".to_string(),
            },
        );
        assert_eq!(dirty_entries([&entry].into_iter(), &manifest), vec![&entry]);
    }
}