use crate::domain::model::check::{CheckViolationKind, Severity};
use crate::domain::model::issue::Issue;
use crate::domain::model::record_ref::IssueRef;
use crate::domain::usecases::check::{CheckViolation, IssueCheckCtx, IssueFinding, IssueRule};
pub struct MultiParentRule;
pub const RULE_ID: &str = "issue/multi-parent";
impl IssueRule for MultiParentRule {
fn id(&self) -> &'static str {
RULE_ID
}
fn find(&self, ctx: &IssueCheckCtx<'_>) -> anyhow::Result<Vec<IssueFinding>> {
let issues: Vec<Issue> = ctx.issues.iter().map(|(_, i)| i.clone()).collect();
let mut out = Vec::new();
for (child, parents) in find_violations(&issues) {
let kind = CheckViolationKind::MultipleParents {
child: child.as_entity_ref().clone(),
parents: parents.iter().map(|p| p.as_entity_ref().clone()).collect(),
};
for parent in &parents {
if let Some(path) = ctx.path_of(parent) {
out.push(CheckViolation {
rule_id: RULE_ID,
path: path.to_path_buf(),
severity: Severity::Error,
kind: kind.clone(),
});
}
}
}
Ok(out.into_iter().map(IssueFinding::report).collect())
}
}
fn find_violations(issues: &[Issue]) -> Vec<(IssueRef, Vec<IssueRef>)> {
let mut out: Vec<(IssueRef, Vec<IssueRef>)> = issues
.iter()
.filter_map(|child| {
let mut parents: Vec<IssueRef> = child.parents().cloned().collect();
if parents.len() > 1 {
parents.sort();
Some((child.id.clone(), parents))
} else {
None
}
})
.collect();
out.sort_by(|(a, _), (b, _)| a.cmp(b));
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::{IssueLink, IssueRelationship};
use crate::domain::usecases::issue::tests::{feature, IssueFixture};
fn build(fix: IssueFixture) -> Issue {
let raw = fix.id.as_deref().expect("id required").to_string();
let numeric = IssueRef::new(&raw).unwrap();
fix.build(numeric)
}
fn child_of_link(target: &str) -> IssueLink {
IssueLink {
target: IssueRef::new(target).unwrap(),
relationship: IssueRelationship::ChildOf,
}
}
fn issue_with_child_of_links(id: &str, parents: &[&str]) -> Issue {
let mut issue = build(feature("Issue").with_id(id));
for p in parents {
issue.links.push(child_of_link(p));
}
issue
}
#[test]
fn find_multi_parent_returns_targets_with_multiple_parents() {
let issues = vec![
issue_with_child_of_links("ISSUE-0001", &[]),
issue_with_child_of_links("ISSUE-0002", &[]),
issue_with_child_of_links("ISSUE-0003", &["ISSUE-0001", "ISSUE-0002"]),
];
let v = find_violations(&issues);
let summary: Vec<(&str, Vec<&str>)> = v
.iter()
.map(|(c, ps)| (c.as_str(), ps.iter().map(|p| p.as_str()).collect()))
.collect();
assert_eq!(
summary,
vec![("ISSUE-0003", vec!["ISSUE-0001", "ISSUE-0002"])]
);
}
#[test]
fn find_multi_parent_returns_empty_when_invariant_holds() {
let issues = vec![
issue_with_child_of_links("ISSUE-0003", &["ISSUE-0001"]),
issue_with_child_of_links("ISSUE-0004", &["ISSUE-0002"]),
];
assert!(find_violations(&issues).is_empty());
}
}