govctl 0.9.1

Project governance CLI for RFC, ADR, and Work Item management
use super::output::output_list;
use super::summaries::{AdrSummary, ClauseSummary, GuardSummary, RfcSummary, WorkItemSummary};
use crate::OutputFormat;
use crate::model::{GuardEntry, ProjectIndex, WorkItemStatus};
use serde::Serialize;

pub(super) fn list_rfcs(
    index: &ProjectIndex,
    filter: Option<&str>,
    limit: Option<usize>,
    output: OutputFormat,
    tags: &[String],
) {
    let mut rfcs: Vec<_> = index.rfcs.iter().collect();

    if let Some(f) = filter {
        rfcs.retain(|r| {
            r.rfc.status.as_ref() == f || r.rfc.phase.as_ref() == f || r.rfc.rfc_id.contains(f)
        });
    }

    retain_by_tags(&mut rfcs, tags, |r| r.rfc.tags.as_slice());

    rfcs.sort_by(|a, b| a.rfc.rfc_id.cmp(&b.rfc.rfc_id));

    output_resource_list(
        &mut rfcs,
        limit,
        &["RFC", "Version", "Status", "Phase", "Title"],
        output,
        |rfc| RfcSummary::from_entry(rfc),
        RfcSummary::row,
    );
}

pub(super) fn list_clauses(
    index: &ProjectIndex,
    filter: Option<&str>,
    limit: Option<usize>,
    output: OutputFormat,
    tags: &[String],
) {
    let mut clauses: Vec<_> = index
        .iter_clauses()
        .map(|(rfc, clause)| (rfc.rfc.rfc_id.clone(), clause))
        .collect();

    if let Some(f) = filter {
        clauses.retain(|(rfc_id, c)| {
            rfc_id == f || c.spec.clause_id.contains(f) || c.spec.status.as_ref() == f
        });
    }

    retain_by_tags(&mut clauses, tags, |(_, c)| c.spec.tags.as_slice());

    clauses.sort_by(|a, b| {
        a.0.cmp(&b.0)
            .then_with(|| a.1.spec.clause_id.cmp(&b.1.spec.clause_id))
    });

    output_resource_list(
        &mut clauses,
        limit,
        &["Clause", "RFC", "Kind", "Status", "Title"],
        output,
        |(rfc_id, clause)| ClauseSummary::from_entry(rfc_id, clause),
        ClauseSummary::row,
    );
}

pub(super) fn list_adrs(
    index: &ProjectIndex,
    filter: Option<&str>,
    limit: Option<usize>,
    output: OutputFormat,
    tags: &[String],
) {
    let mut adrs: Vec<_> = index.adrs.iter().collect();

    if let Some(f) = filter {
        adrs.retain(|a| a.meta().status.as_ref() == f || a.meta().id.contains(f));
    }

    retain_by_tags(&mut adrs, tags, |a| a.meta().tags.as_slice());

    adrs.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));

    output_resource_list(
        &mut adrs,
        limit,
        &["ADR", "Status", "Date", "Title"],
        output,
        |adr| AdrSummary::from_entry(adr),
        AdrSummary::row,
    );
}

pub(super) fn list_guards(
    guards: &[GuardEntry],
    filter: Option<&str>,
    limit: Option<usize>,
    output: OutputFormat,
    tags: &[String],
) {
    let mut items: Vec<_> = guards.iter().collect();

    if let Some(f) = filter {
        items.retain(|g| g.meta().id.contains(f) || g.meta().title.contains(f));
    }

    retain_by_tags(&mut items, tags, |g| g.meta().tags.as_slice());

    items.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));

    output_resource_list(
        &mut items,
        limit,
        &["Guard", "Title", "Command"],
        output,
        |guard| GuardSummary::from_entry(guard),
        GuardSummary::row,
    );
}

pub(super) fn list_work_items(
    index: &ProjectIndex,
    filter: Option<&str>,
    limit: Option<usize>,
    output: OutputFormat,
    tags: &[String],
) {
    let mut items: Vec<_> = index.work_items.iter().collect();

    if let Some(f) = filter {
        match f {
            "all" => {}
            "pending" => {
                items.retain(|i| {
                    i.meta().status == WorkItemStatus::Queue
                        || i.meta().status == WorkItemStatus::Active
                });
            }
            "queue" => items.retain(|i| i.meta().status == WorkItemStatus::Queue),
            "active" => items.retain(|i| i.meta().status == WorkItemStatus::Active),
            "done" => items.retain(|i| i.meta().status == WorkItemStatus::Done),
            "cancelled" => items.retain(|i| i.meta().status == WorkItemStatus::Cancelled),
            other => {
                items.retain(|i| i.meta().status.as_ref() == other || i.meta().id.contains(other));
            }
        }
    }

    retain_by_tags(&mut items, tags, |i| i.meta().tags.as_slice());

    items.sort_by(|a, b| a.meta().id.cmp(&b.meta().id));

    output_resource_list(
        &mut items,
        limit,
        &["ID", "Status", "Title"],
        output,
        |item| WorkItemSummary::from_entry(item),
        WorkItemSummary::row,
    );
}

fn output_resource_list<T, S>(
    items: &mut Vec<T>,
    limit: Option<usize>,
    headers: &[&str],
    output: OutputFormat,
    to_summary: impl Fn(&T) -> S,
    to_row: impl Fn(&S) -> Vec<String>,
) where
    S: Serialize,
{
    apply_limit(items, limit);
    let summaries = items.iter().map(to_summary).collect::<Vec<_>>();
    output_list(&summaries, headers, output, to_row);
}

fn apply_limit<T>(items: &mut Vec<T>, limit: Option<usize>) {
    if let Some(n) = limit {
        items.truncate(n);
    }
}

fn retain_by_tags<T, F>(items: &mut Vec<T>, required: &[String], mut tags_for: F)
where
    F: for<'a> FnMut(&'a T) -> &'a [String],
{
    if required.is_empty() {
        return;
    }
    items.retain(|item| {
        let item_tags = tags_for(item);
        required.iter().all(|tag| item_tags.contains(tag))
    });
}