pub mod filter;
mod merge;
mod store;
mod sync_manager;
pub use store::{CacheStore, CacheStoreError, Result as CacheStoreResult};
pub use sync_manager::{Result as SyncResult, SyncError, SyncManager};
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use todoist_api_rs::sync::{
Collaborator, CollaboratorState, Filter, Item, Label, Note, Project, ProjectNote, Reminder,
Section, User,
};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct CacheIndexes {
pub projects_by_id: HashMap<String, usize>,
pub projects_by_name: HashMap<String, usize>,
pub sections_by_id: HashMap<String, usize>,
pub sections_by_name: HashMap<String, Vec<(String, usize)>>,
pub labels_by_id: HashMap<String, usize>,
pub labels_by_name: HashMap<String, usize>,
pub items_by_id: HashMap<String, usize>,
pub collaborators_by_id: HashMap<String, usize>,
pub collaborators_by_project: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Cache {
pub sync_token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub full_sync_date_utc: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_sync: Option<DateTime<Utc>>,
#[serde(default)]
pub items: Vec<Item>,
#[serde(default)]
pub projects: Vec<Project>,
#[serde(default)]
pub labels: Vec<Label>,
#[serde(default)]
pub sections: Vec<Section>,
#[serde(default)]
pub notes: Vec<Note>,
#[serde(default)]
pub project_notes: Vec<ProjectNote>,
#[serde(default)]
pub reminders: Vec<Reminder>,
#[serde(default)]
pub filters: Vec<Filter>,
#[serde(default)]
pub collaborators: Vec<Collaborator>,
#[serde(default)]
pub collaborator_states: Vec<CollaboratorState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
#[serde(skip)]
indexes: CacheIndexes,
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
impl Cache {
pub fn new() -> Self {
Self {
sync_token: "*".to_string(),
full_sync_date_utc: None,
last_sync: None,
items: Vec::new(),
projects: Vec::new(),
labels: Vec::new(),
sections: Vec::new(),
notes: Vec::new(),
project_notes: Vec::new(),
reminders: Vec::new(),
filters: Vec::new(),
collaborators: Vec::new(),
collaborator_states: Vec::new(),
user: None,
indexes: CacheIndexes::default(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn with_data(
sync_token: String,
full_sync_date_utc: Option<DateTime<Utc>>,
last_sync: Option<DateTime<Utc>>,
items: Vec<Item>,
projects: Vec<Project>,
labels: Vec<Label>,
sections: Vec<Section>,
notes: Vec<Note>,
project_notes: Vec<ProjectNote>,
reminders: Vec<Reminder>,
filters: Vec<Filter>,
user: Option<User>,
) -> Self {
let mut cache = Self {
sync_token,
full_sync_date_utc,
last_sync,
items,
projects,
labels,
sections,
notes,
project_notes,
reminders,
filters,
collaborators: Vec::new(),
collaborator_states: Vec::new(),
user,
indexes: CacheIndexes::default(),
};
cache.rebuild_indexes();
cache
}
pub fn rebuild_indexes(&mut self) {
let mut indexes = CacheIndexes::default();
indexes.projects_by_id.reserve(self.projects.len());
indexes.projects_by_name.reserve(self.projects.len());
indexes.sections_by_id.reserve(self.sections.len());
indexes.sections_by_name.reserve(self.sections.len());
indexes.labels_by_id.reserve(self.labels.len());
indexes.labels_by_name.reserve(self.labels.len());
indexes.items_by_id.reserve(self.items.len());
indexes
.collaborators_by_id
.reserve(self.collaborators.len());
indexes
.collaborators_by_project
.reserve(self.collaborator_states.len());
for (i, project) in self.projects.iter().enumerate() {
if !project.is_deleted {
indexes.projects_by_id.insert(project.id.clone(), i);
indexes
.projects_by_name
.insert(project.name.to_lowercase(), i);
}
}
for (i, section) in self.sections.iter().enumerate() {
if !section.is_deleted {
indexes.sections_by_id.insert(section.id.clone(), i);
indexes
.sections_by_name
.entry(section.name.to_lowercase())
.or_default()
.push((section.project_id.clone(), i));
}
}
for (i, label) in self.labels.iter().enumerate() {
if !label.is_deleted {
indexes.labels_by_id.insert(label.id.clone(), i);
indexes.labels_by_name.insert(label.name.to_lowercase(), i);
}
}
for (i, item) in self.items.iter().enumerate() {
if !item.is_deleted {
indexes.items_by_id.insert(item.id.clone(), i);
}
}
for (i, collaborator) in self.collaborators.iter().enumerate() {
indexes
.collaborators_by_id
.insert(collaborator.id.clone(), i);
}
for collaborator_state in &self.collaborator_states {
if collaborator_state.state != "deleted" {
indexes
.collaborators_by_project
.entry(collaborator_state.project_id.clone())
.or_default()
.push(collaborator_state.user_id.clone());
}
}
self.indexes = indexes;
}
pub fn find_project(&self, name_or_id: &str) -> Option<&Project> {
if let Some(&idx) = self.indexes.projects_by_id.get(name_or_id) {
return self.projects.get(idx);
}
let name_lower = name_or_id.to_lowercase();
if let Some(&idx) = self.indexes.projects_by_name.get(&name_lower) {
return self.projects.get(idx);
}
None
}
pub fn find_section(&self, name_or_id: &str, project_id: Option<&str>) -> Option<&Section> {
if let Some(&idx) = self.indexes.sections_by_id.get(name_or_id) {
let section = self.sections.get(idx)?;
if project_id.is_none() || project_id == Some(section.project_id.as_str()) {
return Some(section);
}
}
let name_lower = name_or_id.to_lowercase();
if let Some(matches) = self.indexes.sections_by_name.get(&name_lower) {
if let Some(proj_id) = project_id {
for (section_proj_id, idx) in matches {
if section_proj_id == proj_id {
return self.sections.get(*idx);
}
}
} else if matches.len() == 1 {
return self.sections.get(matches[0].1);
}
}
None
}
pub fn find_label(&self, name_or_id: &str) -> Option<&Label> {
if let Some(&idx) = self.indexes.labels_by_id.get(name_or_id) {
return self.labels.get(idx);
}
let name_lower = name_or_id.to_lowercase();
if let Some(&idx) = self.indexes.labels_by_name.get(&name_lower) {
return self.labels.get(idx);
}
None
}
pub fn find_item(&self, id: &str) -> Option<&Item> {
if let Some(&idx) = self.indexes.items_by_id.get(id) {
return self.items.get(idx);
}
None
}
pub fn is_empty(&self) -> bool {
self.sync_token == "*"
}
pub fn needs_full_sync(&self) -> bool {
self.sync_token == "*"
}
pub fn apply_sync_response(&mut self, response: &todoist_api_rs::sync::SyncResponse) {
merge::apply_sync_response(self, response);
}
pub fn apply_mutation_response(&mut self, response: &todoist_api_rs::sync::SyncResponse) {
merge::apply_mutation_response(self, response);
}
}
#[cfg(test)]
#[path = "cache_tests.rs"]
mod tests;