cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use clap::{ArgMatches, Command};

use crate::domain::model::issue::{IssueLink, IssueRelationship};
use crate::domain::usecases::issue::{link_issue, list_links, unlink_issue};
use crate::infra::driving::cli::commands::generic::link as generic;
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::helpers::required_str;
use crate::infra::driving::cli::id_parsing::parse_issue_id;
use crate::infra::driving::cli::{render_structured, Context};

pub(super) fn subcommand() -> Command {
    generic::subcommand(
        "issue",
        "Source issue ID",
        "Target issue ID (e.g. ISSUE-01H8MSQGXYZ12)",
        "Relationship type (blocks, blocked-by, parent-of, child-of)",
        "Issue ID (e.g. ISSUE-01H8MSQGXYZ12)",
    )
}

pub(super) fn execute(matches: &ArgMatches, ctx: &Context<'_>) {
    match matches.subcommand() {
        Some(("add", sub)) => execute_add(sub, ctx),
        Some(("remove", sub)) => execute_remove(sub, ctx),
        Some(("list", sub)) => execute_list(sub, ctx),
        _ => unreachable!(),
    }
}

fn execute_add(sub: &ArgMatches, ctx: &Context<'_>) {
    let (id, target, target_id, rel_str, relationship) = parse_link_args(sub, ctx);
    let repo = ctx.issue_repository();
    let link = IssueLink {
        target,
        relationship,
    };
    link_issue(&repo, &id, link)
        .unwrap_or_else(|e| die1(CliError::new(e.to_string()), ctx.output_fmt));
    generic::render(
        generic::View::Linked,
        &id.to_string(),
        &id.to_string(),
        &target_id,
        rel_str,
        ctx.output_fmt,
    );
}

fn execute_remove(sub: &ArgMatches, ctx: &Context<'_>) {
    let (id, target, target_id, rel_str, relationship) = parse_link_args(sub, ctx);
    let repo = ctx.issue_repository();
    let link = IssueLink {
        target,
        relationship,
    };
    unlink_issue(&repo, &id, link)
        .unwrap_or_else(|e| die1(CliError::new(e.to_string()), ctx.output_fmt));
    generic::render(
        generic::View::Unlinked,
        &id.to_string(),
        &id.to_string(),
        &target_id,
        rel_str,
        ctx.output_fmt,
    );
}

fn execute_list(sub: &ArgMatches, ctx: &Context<'_>) {
    let output_fmt = ctx.output_fmt;
    let id_str = required_str(sub, "id");
    let id = parse_issue_id(id_str, ctx.issues_id_prefix()).unwrap_or_else(|e| {
        die1(
            CliError::new(format!("invalid issue ID '{id_str}': {e}")).kind("validation"),
            output_fmt,
        );
    });
    let repo = ctx.issue_repository();
    match list_links(&repo, &id) {
        Ok(links) => {
            if output_fmt.is_structured() {
                render_structured(&links, output_fmt);
            } else if links.is_empty() {
                println!("No links for this issue");
            } else {
                for link in &links {
                    println!("{}: {}", link.relationship, link.target);
                }
            }
        }
        Err(e) => die1(CliError::new(e.to_string()), output_fmt),
    }
}

type LinkArgs<'a> = (
    crate::domain::model::record_ref::IssueRef,
    crate::domain::model::record_ref::IssueRef,
    String,
    &'a str,
    IssueRelationship,
);

fn parse_link_args<'a>(sub: &'a ArgMatches, ctx: &Context<'_>) -> LinkArgs<'a> {
    let output_fmt = ctx.output_fmt;
    let id_str = required_str(sub, "id");
    let id = parse_issue_id(id_str, ctx.issues_id_prefix()).unwrap_or_else(|e| {
        die1(
            CliError::new(format!("invalid issue ID '{id_str}': {e}")).kind("validation"),
            output_fmt,
        );
    });
    let target_id = required_str(sub, "target").to_owned();
    let target = crate::domain::model::record_ref::IssueRef::new(&target_id).unwrap_or_else(|e| {
        die1(
            CliError::new(format!("invalid target ID '{target_id}': {e}")).kind("validation"),
            output_fmt,
        );
    });
    let rel_str = required_str(sub, "relationship");
    let relationship = rel_str.parse::<IssueRelationship>().unwrap_or_else(|e| {
        die1(CliError::new(e.to_string()).kind("validation"), output_fmt);
    });
    (id, target, target_id, rel_str, relationship)
}