cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Generic `link` command — argument schema and structured / human
//! rendering of the `Linked` outcome shared by issues and decision
//! records.
//!
//! `link` is a parent subcommand that dispatches to `add`, `remove`,
//! and `list`. Cascades on the target side fire on the source's
//! `proposed → accepted` transition, not at link-write time.

use clap::{Arg, Command};

use crate::infra::driving::cli::output::OutputFormat;
use crate::infra::driving::cli::render_structured;
use crate::infra::driving::cli::theme;

pub(crate) fn subcommand(
    noun: &'static str,
    source_help: &'static str,
    target_help: &'static str,
    relationship_help: &'static str,
    list_id_help: &'static str,
) -> Command {
    let article = super::indefinite(noun);
    Command::new("link")
        .about(format!("Manage links on {article}"))
        .subcommand_required(true)
        .arg_required_else_help(true)
        .subcommand(
            Command::new("add")
                .about(format!("Add a link from an existing {noun} to another"))
                .arg(id_arg(source_help))
                .arg(target_arg(target_help))
                .arg(relationship_arg(relationship_help)),
        )
        .subcommand(
            Command::new("remove")
                .about(format!("Remove a link from an existing {noun}"))
                .arg(id_arg(source_help))
                .arg(target_arg(target_help))
                .arg(relationship_arg(relationship_help)),
        )
        .subcommand(
            Command::new("list")
                .about(format!("List all links of {article}"))
                .arg(id_arg(list_id_help)),
        )
}

fn id_arg(help: &'static str) -> Arg {
    Arg::new("id").required(true).value_name("ID").help(help)
}

fn target_arg(help: &'static str) -> Arg {
    Arg::new("target")
        .required(true)
        .value_name("TARGET_ID")
        .help(help)
}

fn relationship_arg(help: &'static str) -> Arg {
    Arg::new("relationship")
        .long("relationship")
        .required(true)
        .value_name("TYPE")
        .help(help)
}

pub(crate) enum View {
    Linked,
    Unlinked,
}

pub(crate) fn render(
    view: View,
    source_display_id: &str,
    source_canonical_id: &str,
    target_id: &str,
    relationship: &str,
    output_fmt: OutputFormat,
) {
    if output_fmt.is_structured() {
        #[derive(serde::Serialize)]
        struct LinkResult<'a> {
            id: &'a str,
            target: &'a str,
            relationship: &'a str,
            outcome: &'a str,
        }
        let r = match &view {
            View::Linked => LinkResult {
                id: source_canonical_id,
                target: target_id,
                relationship,
                outcome: "linked",
            },
            View::Unlinked => LinkResult {
                id: source_canonical_id,
                target: target_id,
                relationship,
                outcome: "unlinked",
            },
        };
        render_structured(&r, output_fmt);
    } else {
        match view {
            View::Linked => println!(
                "{}",
                theme::success(&format!("Linked {source_display_id}{target_id}"))
            ),
            View::Unlinked => println!(
                "{}",
                theme::success(&format!("Unlinked {source_display_id}{target_id}"))
            ),
        }
    }
}