radicle-tui 0.3.0

Radicle terminal user interface
Documentation
use anyhow::Result;

use radicle::cob::issue::{Issue, IssueId};
use radicle::cob::Label;
use radicle::issue::cache::Issues;
use radicle::issue::State;
use radicle::prelude::{Did, Signer};
use radicle::storage::git::Repository;
use radicle::Profile;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Filter {
    state: Option<State>,
    assigned: bool,
    assignees: Vec<Did>,
}

impl Default for Filter {
    fn default() -> Self {
        Self {
            state: Some(State::default()),
            assigned: false,
            assignees: vec![],
        }
    }
}

impl Filter {
    pub fn with_state(mut self, state: Option<State>) -> Self {
        self.state = state;
        self
    }

    pub fn with_assgined(mut self, assigned: bool) -> Self {
        self.assigned = assigned;
        self
    }

    pub fn with_assginee(mut self, assignee: Did) -> Self {
        self.assignees.push(assignee);
        self
    }
}

impl ToString for Filter {
    fn to_string(&self) -> String {
        let mut filter = String::new();

        if let Some(state) = &self.state {
            filter.push_str(&format!("is:{}", state));
            filter.push(' ');
        }
        if self.assigned {
            filter.push_str("is:assigned");
            filter.push(' ');
        }
        if !self.assignees.is_empty() {
            filter.push_str("assignees:");
            filter.push('[');

            let mut assignees = self.assignees.iter().peekable();
            while let Some(assignee) = assignees.next() {
                filter.push_str(&assignee.encode());

                if assignees.peek().is_some() {
                    filter.push(',');
                }
            }
            filter.push(']');
        }

        filter
    }
}

pub fn all(profile: &Profile, repository: &Repository) -> Result<Vec<(IssueId, Issue)>> {
    let cache = profile.issues(repository)?;
    let issues = cache.list()?;

    Ok(issues.flatten().collect())
}

pub fn find(profile: &Profile, repository: &Repository, id: &IssueId) -> Result<Option<Issue>> {
    let cache = profile.issues(repository)?;
    Ok(cache.get(id)?)
}

pub fn create<G: Signer>(
    profile: &Profile,
    repository: &Repository,
    signer: &G,
    title: String,
    description: String,
    labels: &[Label],
    assignees: &[Did],
) -> Result<IssueId> {
    let mut issues = profile.issues_mut(repository)?;
    let issue = issues.create(title, description.trim(), labels, assignees, [], signer)?;

    Ok(*issue.id())
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use anyhow::Result;
    use radicle::issue;

    use super::*;

    #[test]
    fn issue_filter_display_with_state_should_succeed() -> Result<()> {
        let actual = Filter::default().with_state(Some(issue::State::Open));

        assert_eq!(String::from("is:open "), actual.to_string());

        Ok(())
    }

    #[test]
    fn issue_filter_display_with_state_and_assigned_should_succeed() -> Result<()> {
        let actual = Filter::default()
            .with_state(Some(issue::State::Open))
            .with_assgined(true);

        assert_eq!(String::from("is:open is:assigned "), actual.to_string());

        Ok(())
    }

    #[test]
    fn issue_filter_display_with_status_and_author_should_succeed() -> Result<()> {
        let actual = Filter::default()
            .with_state(Some(issue::State::Open))
            .with_assginee(Did::from_str(
                "did:key:z6MkswQE8gwZw924amKatxnNCXA55BMupMmRg7LvJuim2C1V",
            )?);

        assert_eq!(
            String::from(
                "is:open assignees:[did:key:z6MkswQE8gwZw924amKatxnNCXA55BMupMmRg7LvJuim2C1V]"
            ),
            actual.to_string()
        );

        Ok(())
    }
}