oh-my-todo 0.2.0

Local-first terminal task manager with a mouse-first TUI and CLI.
Documentation
use crate::domain::{IdError, Space, SpaceId, TaskId};
use thiserror::Error;

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ReferenceError {
    #[error(transparent)]
    InvalidId(#[from] IdError),
    #[error("task reference `{input}` is ambiguous")]
    AmbiguousTaskReference { input: String, matches: Vec<TaskId> },
    #[error("space reference `{input}` is ambiguous")]
    AmbiguousSpaceReference {
        input: String,
        matches: Vec<SpaceId>,
    },
    #[error("task reference `{0}` was not found")]
    TaskNotFound(String),
    #[error("space reference `{0}` was not found")]
    SpaceNotFound(String),
}

pub fn resolve_task_ref<'a, I>(reference: &str, ids: I) -> Result<TaskId, ReferenceError>
where
    I: IntoIterator<Item = &'a TaskId>,
{
    let ids = ids.into_iter().cloned().collect::<Vec<_>>();

    if let Some(exact) = ids.iter().find(|id| id.as_str() == reference) {
        return Ok(exact.clone());
    }

    let matches = ids
        .iter()
        .filter_map(|id| match id.matches_ref(reference) {
            Ok(true) => Some(Ok(id.clone())),
            Ok(false) => None,
            Err(error) => Some(Err(error)),
        })
        .collect::<Result<Vec<_>, _>>()?;

    match matches.len() {
        0 => Err(ReferenceError::TaskNotFound(reference.to_owned())),
        1 => Ok(matches[0].clone()),
        _ => Err(ReferenceError::AmbiguousTaskReference {
            input: reference.to_owned(),
            matches,
        }),
    }
}

pub fn resolve_space_ref<'a, I>(reference: &str, spaces: I) -> Result<SpaceId, ReferenceError>
where
    I: IntoIterator<Item = &'a Space>,
{
    let spaces = spaces.into_iter().collect::<Vec<_>>();

    if let Some(exact) = spaces.iter().find(|space| space.id.as_str() == reference) {
        return Ok(exact.id.clone());
    }

    let id_matches = spaces
        .iter()
        .filter_map(|space| match space.id.matches_ref(reference) {
            Ok(true) => Some(Ok(space.id.clone())),
            Ok(false) => None,
            Err(error) => Some(Err(error)),
        })
        .collect::<Result<Vec<_>, _>>();

    match id_matches {
        Ok(matches) if matches.len() == 1 => return Ok(matches[0].clone()),
        Ok(matches) if matches.len() > 1 => {
            return Err(ReferenceError::AmbiguousSpaceReference {
                input: reference.to_owned(),
                matches,
            });
        }
        Ok(_) => {}
        Err(IdError::InvalidPrefix { .. }) | Err(IdError::InvalidReferenceLength { .. }) => {}
        Err(error) => return Err(error.into()),
    }

    let slug_matches = spaces
        .iter()
        .filter(|space| space.slug == reference)
        .map(|space| space.id.clone())
        .collect::<Vec<_>>();

    match slug_matches.len() {
        0 => Err(ReferenceError::SpaceNotFound(reference.to_owned())),
        1 => Ok(slug_matches[0].clone()),
        _ => Err(ReferenceError::AmbiguousSpaceReference {
            input: reference.to_owned(),
            matches: slug_matches,
        }),
    }
}