ito-core 0.1.27

Core functionality and business logic for Ito
Documentation
use super::*;

pub(super) struct SqliteTaskMutationService {
    pub(super) conn: Arc<Mutex<Connection>>,
    pub(super) org: String,
    pub(super) repo: String,
}

impl SqliteTaskMutationService {
    fn lock_conn(&self) -> TaskMutationServiceResult<std::sync::MutexGuard<'_, Connection>> {
        self.conn.lock().map_err(|err| {
            task_mutation_error_from_core(CoreError::sqlite(format!(
                "locking sqlite connection: {err}"
            )))
        })
    }

    fn with_transaction<T, F>(&self, op: F) -> TaskMutationServiceResult<T>
    where
        F: FnOnce(&rusqlite::Transaction<'_>) -> TaskMutationServiceResult<T>,
    {
        let mut conn = self.lock_conn()?;
        let tx = conn.transaction().map_err(|err| {
            task_mutation_error_from_core(CoreError::sqlite(format!(
                "starting sqlite task mutation transaction: {err}"
            )))
        })?;
        let result = op(&tx)?;
        tx.commit().map_err(|err| {
            task_mutation_error_from_core(CoreError::sqlite(format!(
                "committing sqlite task mutation transaction: {err}"
            )))
        })?;
        Ok(result)
    }

    fn load_current_markdown_from_conn(
        conn: &Connection,
        org: &str,
        repo: &str,
        change_id: &str,
    ) -> TaskMutationServiceResult<Option<String>> {
        let mut stmt = conn
            .prepare("SELECT tasks_md FROM changes WHERE org = ?1 AND repo = ?2 AND change_id = ?3")
            .map_err(|err| {
                task_mutation_error_from_core(CoreError::sqlite(format!(
                    "preparing task mutation query: {err}"
                )))
            })?;
        let result: rusqlite::Result<Option<String>> =
            stmt.query_row(rusqlite::params![org, repo, change_id], |row| row.get(0));
        match result {
            Ok(markdown) => Ok(markdown),
            Err(rusqlite::Error::QueryReturnedNoRows) => {
                Err(ito_domain::tasks::TaskMutationError::not_found(format!(
                    "Change '{change_id}' not found"
                )))
            }
            Err(err) => Err(task_mutation_error_from_core(CoreError::sqlite(format!(
                "loading sqlite tasks markdown: {err}"
            )))),
        }
    }

    fn store_markdown_in_conn(
        conn: &Connection,
        org: &str,
        repo: &str,
        change_id: &str,
        markdown: &str,
    ) -> TaskMutationServiceResult<()> {
        let updated = conn
            .execute(
                "UPDATE changes SET tasks_md = ?1, updated_at = ?2 WHERE org = ?3 AND repo = ?4 AND change_id = ?5",
                rusqlite::params![markdown, Utc::now().to_rfc3339(), org, repo, change_id,],
            )
            .map_err(|err| {
                task_mutation_error_from_core(CoreError::sqlite(format!(
                    "updating sqlite tasks markdown: {err}"
                )))
            })?;
        if updated == 0 {
            return Err(ito_domain::tasks::TaskMutationError::not_found(format!(
                "Change '{change_id}' not found"
            )));
        }
        Ok(())
    }

    fn load_current_markdown(&self, change_id: &str) -> TaskMutationServiceResult<Option<String>> {
        let conn = self.lock_conn()?;
        Self::load_current_markdown_from_conn(&conn, &self.org, &self.repo, change_id)
    }

    fn mutate<F>(&self, change_id: &str, op: F) -> TaskMutationServiceResult<TaskMutationResult>
    where
        F: FnOnce(&str) -> Result<crate::tasks::TaskMutationOutcome, CoreError>,
    {
        self.with_transaction(|tx| {
            let markdown = Self::load_current_markdown_from_conn(tx, &self.org, &self.repo, change_id)?;
            let tasks = markdown.ok_or_else(|| {
                ito_domain::tasks::TaskMutationError::not_found(format!(
                    "No backend tasks found for \"{change_id}\". Run \"ito tasks init {change_id}\" first."
                ))
            })?;
            let outcome = op(&tasks).map_err(task_mutation_error_from_core)?;
            Self::store_markdown_in_conn(
                tx,
                &self.org,
                &self.repo,
                change_id,
                &outcome.updated_content,
            )?;
            Ok(TaskMutationResult {
                change_id: change_id.to_string(),
                task: outcome.task,
                revision: None,
            })
        })
    }
}

impl TaskMutationService for SqliteTaskMutationService {
    fn load_tasks_markdown(&self, change_id: &str) -> TaskMutationServiceResult<Option<String>> {
        self.load_current_markdown(change_id)
    }

    fn init_tasks(&self, change_id: &str) -> TaskMutationServiceResult<TaskInitResult> {
        self.with_transaction(|tx| {
            let current =
                Self::load_current_markdown_from_conn(tx, &self.org, &self.repo, change_id)?;
            if current.is_some() {
                return Ok(TaskInitResult {
                    change_id: change_id.to_string(),
                    path: None,
                    existed: true,
                    revision: None,
                });
            }
            let contents = enhanced_tasks_template(change_id, chrono::Local::now());
            Self::store_markdown_in_conn(tx, &self.org, &self.repo, change_id, &contents)?;
            Ok(TaskInitResult {
                change_id: change_id.to_string(),
                path: None,
                existed: false,
                revision: None,
            })
        })
    }

    fn start_task(
        &self,
        change_id: &str,
        task_id: &str,
    ) -> TaskMutationServiceResult<TaskMutationResult> {
        self.mutate(change_id, |tasks| {
            apply_start_task(tasks, change_id, task_id, "backend tasks")
        })
    }

    fn complete_task(
        &self,
        change_id: &str,
        task_id: &str,
        _note: Option<String>,
    ) -> TaskMutationServiceResult<TaskMutationResult> {
        self.mutate(change_id, |tasks| {
            apply_complete_task(tasks, task_id, "backend tasks")
        })
    }

    fn shelve_task(
        &self,
        change_id: &str,
        task_id: &str,
        _reason: Option<String>,
    ) -> TaskMutationServiceResult<TaskMutationResult> {
        self.mutate(change_id, |tasks| {
            apply_shelve_task(tasks, task_id, "backend tasks")
        })
    }

    fn unshelve_task(
        &self,
        change_id: &str,
        task_id: &str,
    ) -> TaskMutationServiceResult<TaskMutationResult> {
        self.mutate(change_id, |tasks| {
            apply_unshelve_task(tasks, task_id, "backend tasks")
        })
    }

    fn add_task(
        &self,
        change_id: &str,
        title: &str,
        wave: Option<u32>,
    ) -> TaskMutationServiceResult<TaskMutationResult> {
        self.mutate(change_id, |tasks| {
            apply_add_task(tasks, title, wave, "backend tasks")
        })
    }
}