use std::collections::BTreeMap;
use crate::domain::model::issue::{Issue, IssueRelationship};
use crate::domain::model::record_ref::IssueRef;
use crate::domain::model::status::RollupHistogram;
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::issue::rollup_status::compute_status_rollup;
use crate::domain::usecases::issue::rollup_tags::{compute_tag_rollups, TagRollupValue};
use crate::domain::usecases::issue::IssueRepository;
pub fn show_issue(repo: &dyn IssueRepository, id: &IssueRef) -> anyhow::Result<Option<Issue>> {
repo.find_by_id(id)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IssueFamily {
pub parent: Option<IssueRef>,
pub children: Vec<IssueRef>,
pub rollup: Option<RollupHistogram>,
pub tag_rollups: BTreeMap<String, TagRollupValue>,
}
pub fn show_issue_with_family(
repo: &dyn IssueRepository,
id: &IssueRef,
) -> anyhow::Result<Option<(Issue, IssueFamily)>> {
show_issue_with_family_and_tags(repo, id, &TagDescriptors::default())
}
pub fn show_issue_with_family_and_tags(
repo: &dyn IssueRepository,
id: &IssueRef,
descriptors: &TagDescriptors,
) -> anyhow::Result<Option<(Issue, IssueFamily)>> {
let Some(issue) = repo.find_by_id(id)? else {
return Ok(None);
};
let parent = issue
.links
.iter()
.filter(|l| l.relationship == IssueRelationship::ChildOf)
.map(|l| l.target.clone())
.min();
let mut children: Vec<IssueRef> = issue
.links
.iter()
.filter(|l| l.relationship == IssueRelationship::ParentOf)
.map(|l| l.target.clone())
.collect();
children.sort();
let resolved_children: Vec<Issue> = children
.iter()
.filter_map(|r| repo.find_by_id(r).ok().flatten())
.collect();
let resolved_refs: Vec<&Issue> = resolved_children.iter().collect();
let rollup = compute_status_rollup(&resolved_refs);
let tag_rollups = compute_tag_rollups(&resolved_refs, descriptors);
Ok(Some((
issue,
IssueFamily {
parent,
children,
rollup,
tag_rollups,
},
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::record_ref::IssueRef;
use crate::domain::usecases::issue::tests::{feature, ir, FakeIssueRepository, IssueFixture};
#[test]
fn show_returns_the_issue_when_it_exists() {
scenario()
.given(feature("Add login").with_id("ISSUE-0001"))
.when_show("ISSUE-0001")
.then_title("Add login");
}
#[test]
fn show_returns_none_for_an_unknown_id() {
scenario().when_show("ISSUE-0099").then_not_found();
}
fn scenario() -> Scenario {
Scenario {
repo: FakeIssueRepository::new(),
}
}
struct Scenario {
repo: FakeIssueRepository,
}
impl Scenario {
fn given(mut self, fixture: IssueFixture) -> Self {
let raw = fixture
.id
.as_deref()
.expect("given() requires an explicit id — use .with_id()")
.to_string();
let numeric =
IssueRef::new(&raw).unwrap_or_else(|_| panic!("given(): invalid id {raw:?}"));
self.repo.push_issue(fixture.build(numeric));
self
}
fn when_show(self, id: &str) -> ShowOutcome {
let issue_ref =
IssueRef::new(id).unwrap_or_else(|_| panic!("when_show: invalid id {id:?}"));
ShowOutcome {
result: show_issue(&self.repo, &issue_ref).expect("show_issue failed unexpectedly"),
}
}
}
struct ShowOutcome {
result: Option<Issue>,
}
impl ShowOutcome {
fn then_title(self, expected: &str) -> Self {
let issue = self.result.as_ref().expect("expected Some, got None");
assert_eq!(issue.title.as_str(), expected);
self
}
fn then_not_found(self) -> Self {
assert!(self.result.is_none(), "expected None, got Some");
self
}
}
#[test]
fn family_returns_parent_and_children() {
let repo = FakeIssueRepository::with_issues(vec![
feature("Epic")
.with_id("ISSUE-0001")
.with_link("ISSUE-0002", "parent-of")
.build(ir(1)),
feature("Story")
.with_id("ISSUE-0002")
.with_link("ISSUE-0001", "child-of")
.build(ir(2)),
feature("Standalone").with_id("ISSUE-0003").build(ir(3)),
]);
let id = IssueRef::new("ISSUE-0002").unwrap();
let (_, family) = show_issue_with_family(&repo, &id).unwrap().unwrap();
assert_eq!(family.parent.unwrap().to_string(), "ISSUE-0001");
assert!(family.children.is_empty());
}
#[test]
fn family_returns_children_sorted_by_id() {
let repo = FakeIssueRepository::with_issues(vec![
feature("Epic")
.with_id("ISSUE-0001")
.with_link("ISSUE-0003", "parent-of")
.with_link("ISSUE-0002", "parent-of")
.build(ir(1)),
feature("Story A").with_id("ISSUE-0002").build(ir(2)),
feature("Story B").with_id("ISSUE-0003").build(ir(3)),
]);
let id = IssueRef::new("ISSUE-0001").unwrap();
let (_, family) = show_issue_with_family(&repo, &id).unwrap().unwrap();
let ids: Vec<String> = family.children.iter().map(|r| r.to_string()).collect();
assert_eq!(ids, vec!["ISSUE-0002", "ISSUE-0003"]);
assert!(family.parent.is_none());
}
#[test]
fn family_is_empty_for_orphan_with_no_children() {
let repo = FakeIssueRepository::with_issues(vec![feature("Standalone")
.with_id("ISSUE-0001")
.build(ir(1))]);
let id = IssueRef::new("ISSUE-0001").unwrap();
let (_, family) = show_issue_with_family(&repo, &id).unwrap().unwrap();
assert!(family.parent.is_none());
assert!(family.children.is_empty());
}
}