use clap::{ArgMatches, Command};
use serde::Serialize;
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::issue::backlog::{build_backlog, BacklogReport};
use crate::domain::usecases::issue::IssueRepository;
use crate::infra::driving::cli::theme;
use crate::infra::driving::cli::{render_structured, Context};
pub(in super::super) fn subcommand() -> Command {
Command::new("backlog")
.about("List decision records pending a decision and open issues grouped by priority")
}
fn priority_value(issue: &crate::domain::model::issue::Issue) -> Option<String> {
for tag in issue.tags.iter() {
if let Some(rest) = tag.as_str().strip_prefix("priority:") {
if !rest.is_empty() {
return Some(rest.to_string());
}
}
}
None
}
fn flow_value(issue: &crate::domain::model::issue::Issue) -> Option<String> {
for tag in issue.tags.iter() {
if let Some(rest) = tag.as_str().strip_prefix("flow:") {
if !rest.is_empty() {
return Some(rest.to_string());
}
}
}
None
}
fn size_value(issue: &crate::domain::model::issue::Issue) -> Option<String> {
for tag in issue.tags.iter() {
if let Some(rest) = tag.as_str().strip_prefix("size:") {
if !rest.is_empty() {
return Some(rest.to_string());
}
}
}
None
}
pub(in super::super) fn execute(_sub: &ArgMatches, ctx: &Context<'_>) {
let issues = ctx.issue_repository().list().unwrap_or_default().into_vec();
let mut drs_by_kind = Vec::new();
for kind_cfg in &ctx.config().decision_kinds {
let repo = ctx.decision_record_repository(kind_cfg);
let records = repo.list().unwrap_or_default().into_vec();
drs_by_kind.push((kind_cfg.kind.clone(), records));
}
let levels: Vec<String> = ctx
.config()
.tag_descriptors_for("issues")
.get("priority")
.map(|d| d.levels.clone())
.unwrap_or_default();
let report = build_backlog(issues, drs_by_kind, &levels);
if ctx.output_fmt.is_structured() {
let view = ReportView::from(&report);
render_structured(&view, ctx.output_fmt);
return;
}
print_human(&report);
}
fn print_human(report: &BacklogReport) {
if report.total_drs == 0 && report.total_issues == 0 {
println!("Nothing to do — backlog is empty.");
return;
}
println!("Backlog");
println!();
for section in &report.dr_sections {
if section.records.is_empty() {
continue;
}
println!(
"{} ({})",
theme::section(&format!("{}s to resolve", section.kind.to_uppercase())),
section.records.len(),
);
for r in §ion.records {
println!(
" {} {} {}",
theme::id(&r.id.to_string()),
r.title,
theme::dr_status(r.status),
);
}
println!();
}
if !report.priority_buckets.is_empty() {
println!(
"{} ({} open)",
theme::section("Issues to do"),
report.total_issues,
);
println!();
for bucket in &report.priority_buckets {
let label = bucket.priority_label.as_deref().unwrap_or("No priority");
println!(" {} ({})", theme::label(label), bucket.issues.len());
for issue in &bucket.issues {
let size = size_value(issue).unwrap_or_else(|| "-".to_string());
let flow = flow_value(issue).unwrap_or_else(|| "-".to_string());
println!(
" {} {} {} {}",
theme::id(&issue.id.to_string()),
issue.title,
flow,
size,
);
}
println!();
}
}
}
#[derive(Serialize)]
struct ReportView<'a> {
total_drs: usize,
total_issues: usize,
dr_sections: Vec<DrSectionView<'a>>,
priority_buckets: Vec<PriorityBucketView<'a>>,
}
#[derive(Serialize)]
struct DrSectionView<'a> {
kind: &'a str,
records: Vec<DrRecordView<'a>>,
}
#[derive(Serialize)]
struct DrRecordView<'a> {
id: String,
title: &'a str,
status: &'a str,
}
#[derive(Serialize)]
struct PriorityBucketView<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
priority: Option<&'a str>,
issues: Vec<IssueLineView>,
}
#[derive(Serialize)]
struct IssueLineView {
id: String,
title: String,
#[serde(rename = "flow", skip_serializing_if = "Option::is_none")]
flow: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
priority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<String>,
}
impl<'a> From<&'a BacklogReport> for ReportView<'a> {
fn from(r: &'a BacklogReport) -> Self {
ReportView {
total_drs: r.total_drs,
total_issues: r.total_issues,
dr_sections: r
.dr_sections
.iter()
.map(|s| DrSectionView {
kind: &s.kind,
records: s
.records
.iter()
.map(|rec| DrRecordView {
id: rec.id.to_string(),
title: rec.title.as_str(),
status: rec.status.as_str(),
})
.collect(),
})
.collect(),
priority_buckets: r
.priority_buckets
.iter()
.map(|b| PriorityBucketView {
priority: b.priority_label.as_deref(),
issues: b
.issues
.iter()
.map(|i| IssueLineView {
id: i.id.to_string(),
title: i.title.as_str().to_string(),
flow: flow_value(i),
priority: priority_value(i),
size: size_value(i),
})
.collect(),
})
.collect(),
}
}
}