cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Generic `title` command — argument schema and rendering shared by
//! issues and decision records.
//!
//! `title` is a parent subcommand that dispatches to `show` (read the
//! current title) and `update` (rewrite it in place). Title changes are
//! not recorded in the event log (per DDR-018QWJVHRH35B); git history
//! is the audit trail.

use clap::{Arg, ArgMatches, Command};

use crate::infra::driving::cli::errors::{die1, CliError};
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, id_help: &'static str) -> Command {
    let article = super::indefinite(noun);
    Command::new("title")
        .about(format!("Read or change the title of {article}"))
        .subcommand_required(true)
        .arg_required_else_help(true)
        .subcommand(
            Command::new("show")
                .about(format!("Print the current title of {article}"))
                .arg(id_arg(id_help)),
        )
        .subcommand(
            Command::new("update")
                .about(format!("Rewrite the title of {article}"))
                .arg(id_arg(id_help))
                .arg(
                    Arg::new("title")
                        .required(true)
                        .value_name("TITLE")
                        .help("New title (non-empty, leading/trailing whitespace stripped)"),
                ),
        )
}

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

pub(crate) fn required_title_arg(sub: &ArgMatches) -> &str {
    crate::infra::driving::cli::helpers::required_str(sub, "title")
}

pub(crate) enum View {
    NotFound,
    Shown { title: String },
    Updated,
    NoOp,
}

pub(crate) fn render(
    view: View,
    display_id: &str,
    canonical_id: &str,
    noun: &str,
    output_fmt: OutputFormat,
) {
    match view {
        View::NotFound => die1(
            CliError::not_found(format!("{noun} '{display_id}'"), &format!("{noun} list")),
            output_fmt,
        ),
        view if output_fmt.is_structured() => {
            render_structured_view(view, canonical_id, output_fmt)
        }
        View::Shown { title } => println!("{title}"),
        View::Updated => println!("{}", theme::success(&format!("Updated {display_id}"))),
        View::NoOp => println!(
            "{}",
            theme::noop(&format!(
                "No change: {noun} {display_id} already has that title",
            )),
        ),
    }
}

fn render_structured_view(view: View, canonical_id: &str, output_fmt: OutputFormat) {
    #[derive(serde::Serialize)]
    #[serde(untagged)]
    enum TitleResult<'a> {
        Show { id: &'a str, title: &'a str },
        Outcome { id: &'a str, outcome: &'a str },
    }
    let r = match &view {
        View::Shown { title } => TitleResult::Show {
            id: canonical_id,
            title,
        },
        View::Updated => TitleResult::Outcome {
            id: canonical_id,
            outcome: "updated",
        },
        View::NoOp => TitleResult::Outcome {
            id: canonical_id,
            outcome: "noop",
        },
        View::NotFound => unreachable!(),
    };
    render_structured(&r, output_fmt);
}