use std::collections::BTreeMap;
use crate::domain::model::issue::{Issue, IssueFilter, IssueRelationship};
use crate::domain::model::status::RollupHistogram;
use crate::domain::model::tag_descriptor::TagDescriptors;
use super::rollup_status::{compute_status_rollup_via_map, index_issues_by_id};
use super::rollup_tags::{compute_tag_rollups, TagRollupValue};
use super::tree::{build_issue_tree, TreeNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeRollup {
pub status: Option<RollupHistogram>,
pub tags: BTreeMap<String, TagRollupValue>,
}
impl TreeRollup {
pub fn is_empty(&self) -> bool {
self.status.is_none() && self.tags.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeViewNode {
pub issue: Issue,
pub matched: bool,
pub rollup: Option<TreeRollup>,
pub children: Vec<TreeViewNode>,
}
pub fn build_issue_tree_view(
issues: &[Issue],
filter: &IssueFilter<'_>,
descriptors: &TagDescriptors,
) -> Vec<TreeViewNode> {
let forest = build_issue_tree(issues, filter);
let by_id = index_issues_by_id(issues);
forest
.into_iter()
.map(|n| decorate(n, &by_id, descriptors))
.collect()
}
fn decorate(
node: TreeNode,
by_id: &std::collections::HashMap<crate::domain::model::record_ref::IssueRef, &Issue>,
descriptors: &TagDescriptors,
) -> TreeViewNode {
let status = compute_status_rollup_via_map(&node.issue, by_id);
let direct_children: Vec<&Issue> = node
.issue
.links
.iter()
.filter(|l| l.relationship == IssueRelationship::ParentOf)
.filter_map(|l| by_id.get(&l.target).copied())
.collect();
let tags = compute_tag_rollups(&direct_children, descriptors);
let rollup = TreeRollup { status, tags };
let rollup = if rollup.is_empty() {
None
} else {
Some(rollup)
};
TreeViewNode {
issue: node.issue,
matched: node.matched,
rollup,
children: node
.children
.into_iter()
.map(|c| decorate(c, by_id, descriptors))
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::test_fixtures::{feature, ir};
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::issue::tests::enrich_issue;
fn enriched(id: u64, title: &str) -> Issue {
let mut i = feature(title).with_id(&ir(id).to_string()).build(ir(id));
enrich_issue(
&mut i,
&crate::domain::model::status::StatusesConfig::default_issue(),
);
i
}
fn no_filter() -> IssueFilter<'static> {
IssueFilter {
status: None,
active: false,
tags: &[],
}
}
#[test]
fn empty_corpus_yields_empty_forest() {
let view = build_issue_tree_view(&[], &no_filter(), &TagDescriptors::default());
assert!(view.is_empty());
}
#[test]
fn standalone_issue_has_no_rollup() {
let view = build_issue_tree_view(
&[enriched(1, "Solo")],
&no_filter(),
&TagDescriptors::default(),
);
assert_eq!(view.len(), 1);
assert!(view[0].rollup.is_none());
assert!(view[0].children.is_empty());
}
#[test]
fn parent_with_children_carries_a_status_rollup() {
let parent = feature("Parent")
.with_id("ISSUE-0001")
.with_link("ISSUE-0002", "parent-of")
.build(ir(1));
let mut parent = parent;
enrich_issue(
&mut parent,
&crate::domain::model::status::StatusesConfig::default_issue(),
);
let child = feature("Child")
.with_id("ISSUE-0002")
.with_link("ISSUE-0001", "child-of")
.build(ir(2));
let mut child = child;
enrich_issue(
&mut child,
&crate::domain::model::status::StatusesConfig::default_issue(),
);
let view =
build_issue_tree_view(&[parent, child], &no_filter(), &TagDescriptors::default());
let root = view.iter().find(|n| n.issue.id == ir(1)).unwrap();
let rollup = root.rollup.as_ref().expect("parent should carry rollup");
assert!(rollup.status.is_some());
assert_eq!(root.children.len(), 1);
}
}