tatuin-providers 0.6.1

Providers lib of the Tatuin project
Documentation
// SPDX-License-Identifier: MIT

pub mod client;
mod project;
mod task;

use std::{cmp::Ordering, error::Error, fmt::Debug};
use tatuin_core::{
    StringError, filter,
    project::Project as ProjectTrait,
    provider::{Capabilities, ProjectProviderTrait, ProviderTrait, TaskProviderTrait},
    task::{Priority, State, Task as TaskTrait},
    task_patch::{DatePatchItem, PatchError, TaskPatch},
};

use async_trait::async_trait;

use crate::config::Config;

pub const PROVIDER_NAME: &str = "Todoist";

pub struct Provider {
    cfg: Config,
    c: client::Client,
    projects: Vec<project::Project>,
    tasks: Vec<task::Task>,
    last_filter: Option<filter::Filter>,
    last_project: Option<Box<dyn ProjectTrait>>,
}

impl Provider {
    pub fn new(cfg: Config, api_key: &str) -> Self {
        Self {
            cfg,
            c: client::Client::new(api_key),
            projects: Vec::new(),
            tasks: Vec::new(),
            last_filter: None,
            last_project: None,
        }
    }

    async fn load_projects(&mut self) -> Result<(), Box<dyn Error>> {
        if self.projects.is_empty() {
            self.projects = self.c.projects().await?;
            for p in &mut self.projects {
                p.provider = Some(self.cfg.name());
            }
        }
        Ok(())
    }

    pub async fn project_by_id(&mut self, id: &str) -> Result<project::Project, Box<dyn Error>> {
        self.load_projects().await?;
        let project = self.projects.iter().find(|p| p.id() == id);
        if let Some(p) = project {
            return Ok(p.clone());
        }
        Ok(project::Project {
            id: id.to_string(),
            ..project::Project::default()
        })
    }
}

impl Debug for Provider {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Provider name={}", ProviderTrait::name(self))
    }
}

#[async_trait]
impl ProjectProviderTrait for Provider {
    async fn list(&mut self) -> Result<Vec<Box<dyn ProjectTrait>>, StringError> {
        self.load_projects().await?;
        let mut result: Vec<Box<dyn ProjectTrait>> = Vec::new();
        for p in &self.projects {
            result.push(Box::new(p.clone()));
        }

        Ok(result)
    }
}

#[async_trait]
impl TaskProviderTrait for Provider {
    #[tracing::instrument(level = "info", target = "todoist_tasks")]
    async fn list(
        &mut self,
        project: Option<Box<dyn ProjectTrait>>,
        f: &filter::Filter,
    ) -> Result<Vec<Box<dyn TaskTrait>>, StringError> {
        let mut should_clear = false;
        if let Some(last_filter) = self.last_filter.as_mut() {
            should_clear = last_filter != f;
        }

        match &project {
            Some(p) => {
                if let Some(pp) = self.last_project.as_mut() {
                    should_clear |= p.id().cmp(&pp.id()) != Ordering::Equal;
                } else {
                    should_clear = true
                }
            }
            None => {
                if self.last_project.is_some() {
                    should_clear = true
                }
            }
        }

        if should_clear {
            self.tasks.clear();
        }

        if self.tasks.is_empty() {
            if f.states.contains(&filter::FilterState::Todo) {
                match self.c.tasks_by_filter(&project, f).await {
                    Ok(mut t) => self.tasks.append(&mut t),
                    Err(e) => {
                        tracing::error!(error=?e,  "Get tasks by filter");
                        return Err(e.into());
                    }
                }
            }

            if f.states.contains(&filter::FilterState::Completed) {
                match self.c.completed_tasks(&project.as_ref().map(|p| p.id()), f).await {
                    Ok(mut tasks) => self.tasks.append(&mut tasks),
                    Err(e) => {
                        tracing::error!(error=?e,  "Get completed tasks");
                        return Err(e.into());
                    }
                }
            }
            self.last_project = project;
        }

        let mut result: Vec<Box<dyn TaskTrait>> = Vec::new();

        for t in &mut self.tasks.to_vec() {
            t.project = Some(self.project_by_id(t.project_id.as_str()).await?);
            t.provider = Some(self.name());
            result.push(Box::new(t.clone()));
        }

        self.last_filter = Some(f.clone());

        Ok(result)
    }

    async fn create(&mut self, project_id: &str, tp: &TaskPatch) -> Result<(), StringError> {
        let mut due_custom_dt = String::new();

        let name = tp.name.value().unwrap();
        let description = tp.description.value();
        let r = client::CreateTaskRequest {
            content: name.as_str(),
            description: description.as_deref(),
            project_id: Some(project_id),
            due_string: tp.due.value().map(|due| match due {
                DatePatchItem::NoDate => "no date",
                DatePatchItem::Today => "today",
                DatePatchItem::Tomorrow => "tomorrow",
                DatePatchItem::ThisWeekend => "weekend",
                DatePatchItem::NextWeek => "next week",
                DatePatchItem::Custom(dt) => {
                    due_custom_dt = dt.format("%Y-%m-%d").to_string();
                    &due_custom_dt
                }
            }),
            priority: tp.priority.value().map(|p| task::priority_to_int(&p)),
        };
        self.c.create_task(&r).await.map_err(|e| e.into())
    }

    async fn update(&mut self, patches: &[TaskPatch]) -> Vec<PatchError> {
        let mut errors = Vec::new();

        for p in patches {
            let task = p.task.as_ref().unwrap();

            if let Some(state) = &p.state.value() {
                match state {
                    State::Completed => match self.c.close_task(task.id().as_str()).await {
                        Ok(_) => self.tasks.clear(),
                        Err(e) => errors.push(PatchError {
                            task: task.clone_boxed(),
                            error: e.to_string(),
                        }),
                    },
                    State::InProgress | State::Unknown(_) => errors.push(PatchError {
                        task: task.clone_boxed(),
                        error: format!("The state {state} is unsupported"),
                    }),
                    State::Uncompleted => match self.c.reopen_task(task.id().as_str()).await {
                        Ok(_) => self.tasks.clear(),
                        Err(e) => errors.push(PatchError {
                            task: task.clone_boxed(),
                            error: e.to_string(),
                        }),
                    },
                }
            }

            if p.due.is_set() || p.priority.is_set() || p.description.is_set() || p.name.is_set() {
                let mut due_custom_dt = String::new();

                let name = p.name.value();
                let description = p.description.value();
                let r = client::UpdateTaskRequest {
                    content: name.as_deref(),
                    description: description.as_deref(),
                    due_string: p.due.value().map(|due| match due {
                        DatePatchItem::NoDate => "no date",
                        DatePatchItem::Today => "today",
                        DatePatchItem::Tomorrow => "tomorrow",
                        DatePatchItem::ThisWeekend => "weekend",
                        DatePatchItem::NextWeek => "next week",
                        DatePatchItem::Custom(dt) => {
                            due_custom_dt = dt.format("%Y-%m-%d").to_string();
                            &due_custom_dt
                        }
                    }),
                    priority: p.priority.value().map(|p| task::priority_to_int(&p)),
                };
                match self.c.update_task(task.id().as_str(), &r).await {
                    Ok(_) => self.tasks.clear(),
                    Err(e) => errors.push(PatchError {
                        task: task.clone_boxed(),
                        error: e.to_string(),
                    }),
                }
            }
        }

        errors
    }

    async fn delete(&mut self, t: &dyn TaskTrait) -> Result<(), StringError> {
        self.c.delete_task(t.id().as_str()).await.map_err(|e| e.into())
    }
}

#[async_trait]
impl ProviderTrait for Provider {
    fn name(&self) -> String {
        self.cfg.name()
    }

    fn type_name(&self) -> String {
        PROVIDER_NAME.to_string()
    }

    async fn reload(&mut self) {
        self.projects.clear();
        self.tasks.clear();
    }

    fn capabilities(&self) -> Capabilities {
        Capabilities { create_task: true }
    }

    fn supported_priorities(&self) -> Vec<Priority> {
        task::SUPPORTED_PRIORITIES.into()
    }
}