toggl-jira-sync 0.1.19

Local Toggl to Jira worklog sync CLI with SQLite state and a Ratatui status UI
Documentation
use std::fmt;

use crate::{
    db::{Database, DbError, NewJiraIssueSiteCache},
    jira::{JiraClient, JiraError},
    sync::planner::IssueSiteMapping,
};

#[derive(Debug, Clone)]
pub struct ResolverSite {
    pub key: String,
    pub client: JiraClient,
}

#[derive(Debug)]
pub struct IssueSiteResolver<'a> {
    database: &'a Database,
    sites: Vec<ResolverSite>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedIssueSite {
    pub issue_key: String,
    pub jira_site_key: String,
}

#[derive(Debug)]
pub enum IssueSiteResolutionError {
    Db(DbError),
    Jira {
        site_key: String,
        issue_key: String,
        source: JiraError,
    },
    NoMatchingSite {
        issue_key: String,
    },
    MultipleMatchingSites {
        issue_key: String,
        site_keys: Vec<String>,
    },
}

impl fmt::Display for IssueSiteResolutionError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Db(error) => write!(formatter, "{error}"),
            Self::Jira {
                site_key,
                issue_key,
                source,
            } => write!(
                formatter,
                "failed to discover Jira issue {issue_key} on site {site_key}: {source}"
            ),
            Self::NoMatchingSite { issue_key } => {
                write!(
                    formatter,
                    "Jira issue {issue_key} was not found on any enabled site"
                )
            }
            Self::MultipleMatchingSites {
                issue_key,
                site_keys,
            } => write!(
                formatter,
                "Jira issue {issue_key} exists on multiple enabled sites: {}",
                site_keys.join(", ")
            ),
        }
    }
}

impl std::error::Error for IssueSiteResolutionError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Db(error) => Some(error),
            Self::Jira { source, .. } => Some(source),
            Self::NoMatchingSite { .. } | Self::MultipleMatchingSites { .. } => None,
        }
    }
}

impl From<DbError> for IssueSiteResolutionError {
    fn from(error: DbError) -> Self {
        Self::Db(error)
    }
}

impl<'a> IssueSiteResolver<'a> {
    pub fn new(database: &'a Database, sites: Vec<ResolverSite>) -> Self {
        Self { database, sites }
    }

    pub async fn resolve_issue_key(
        &self,
        issue_key: &str,
    ) -> Result<ResolvedIssueSite, IssueSiteResolutionError> {
        if let Some(cached) = self.database.get_jira_issue_site_cache(issue_key)? {
            return Ok(ResolvedIssueSite {
                issue_key: cached.issue_key,
                jira_site_key: cached.jira_site_key,
            });
        }

        let mut matches = Vec::new();
        for site in &self.sites {
            let exists = site
                .client
                .issue_exists(issue_key)
                .await
                .map_err(|source| IssueSiteResolutionError::Jira {
                    site_key: site.key.clone(),
                    issue_key: issue_key.to_owned(),
                    source,
                })?;
            if exists {
                matches.push(site.key.clone());
            }
        }

        match matches.as_slice() {
            [site_key] => {
                self.database
                    .upsert_jira_issue_site_cache(&NewJiraIssueSiteCache {
                        issue_key,
                        jira_site_key: site_key,
                        source: "discovery",
                    })?;
                Ok(ResolvedIssueSite {
                    issue_key: issue_key.to_owned(),
                    jira_site_key: site_key.clone(),
                })
            }
            [] => Err(IssueSiteResolutionError::NoMatchingSite {
                issue_key: issue_key.to_owned(),
            }),
            _ => Err(IssueSiteResolutionError::MultipleMatchingSites {
                issue_key: issue_key.to_owned(),
                site_keys: matches,
            }),
        }
    }
}

impl From<ResolvedIssueSite> for IssueSiteMapping {
    fn from(resolved: ResolvedIssueSite) -> Self {
        Self {
            issue_key: resolved.issue_key,
            jira_site_key: resolved.jira_site_key,
        }
    }
}