use super::CalendarStateError;
use crate::event::EventInstanceId;
use std::{collections::HashSet, path::Path};
pub(crate) const SYNCED_IDS_FILE_NAME: &str = "known_event_ids";
#[derive(Debug)]
pub(crate) struct SyncedEventIds(HashSet<EventInstanceId>);
impl SyncedEventIds {
pub fn new() -> Self {
Self(HashSet::new())
}
pub fn insert(&mut self, id: EventInstanceId) {
self.0.insert(id);
}
pub fn contains(&self, id: &EventInstanceId) -> bool {
self.0.contains(id)
}
pub fn load(path: &Path) -> Result<Self, CalendarStateError> {
if path.is_file() {
let contents = std::fs::read_to_string(path)?;
let ids = contents
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.map(EventInstanceId::from)
.collect::<HashSet<_>>();
Ok(Self(ids))
} else {
Ok(Self::new())
}
}
pub fn write(&self, path: &Path) -> Result<(), CalendarStateError> {
let mut lines = self.0.iter().map(ToString::to_string).collect::<Vec<_>>();
lines.sort_unstable();
let contents = lines.join("\n");
let parent = path.parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"synced ids path has no parent directory",
)
})?;
let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
std::io::Write::write_all(&mut tmp, contents.as_bytes())?;
tmp.persist(path).map_err(|e| e.error)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EventTime;
use crate::event::{EventUid, RecurrenceId};
use chrono::{NaiveDate, TimeZone, Utc};
use pretty_assertions::assert_eq;
fn sample_ids() -> HashSet<EventInstanceId> {
HashSet::from([
EventInstanceId::new(
EventUid::new("t5slp0vorqgoasogqkvadjt9jj@hooli.com".to_string()),
Some(RecurrenceId::from_event_time(EventTime::DateTimeUtc(
Utc.with_ymd_and_hms(2024, 6, 25, 17, 0, 0).unwrap(),
))),
),
EventInstanceId::new(
EventUid::new("t5slp0vorqgoasogqkvadjt9jj@hooli.com".to_string()),
Some(RecurrenceId::from_event_time(EventTime::DateTimeFloating(
NaiveDate::from_ymd_opt(2024, 6, 25)
.unwrap()
.and_hms_opt(18, 0, 0)
.unwrap(),
))),
),
EventInstanceId::new(
EventUid::new("t81pd0rkq8ujaughbrjhh87svo@hooli.com".to_string()),
None,
),
])
}
fn sample_lines() -> Vec<&'static str> {
vec![
"t5slp0vorqgoasogqkvadjt9jj@hooli.com__20240625T170000Z",
"t5slp0vorqgoasogqkvadjt9jj@hooli.com__20240625T180000",
"t81pd0rkq8ujaughbrjhh87svo@hooli.com",
]
}
#[test]
fn loads_ids_from_plaintext_file() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("known_event_ids");
std::fs::write(&path, sample_lines().join("\n")).unwrap();
let loaded = SyncedEventIds::load(&path).unwrap();
assert_eq!(loaded.0, sample_ids());
}
#[test]
fn write_replaces_existing_file_contents() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("known_event_ids");
std::fs::write(&path, "stale-id@example.com").unwrap();
SyncedEventIds(sample_ids()).write(&path).unwrap();
let got = std::fs::read_to_string(&path).unwrap();
assert!(!got.contains("stale-id@example.com"));
assert!(got.contains("t81pd0rkq8ujaughbrjhh87svo@hooli.com"));
}
#[test]
fn writes_ids_to_plaintext_file() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("known_event_ids");
let ids = SyncedEventIds(sample_ids());
ids.write(&path).unwrap();
let got = std::fs::read_to_string(&path).unwrap();
assert_eq!(got, sample_lines().join("\n"));
}
}