use std::collections::{HashMap, HashSet};
use crate::model::{Issue, Relation, RelationKind, Status};
#[derive(Debug)]
pub struct DagNode {
pub issue: Issue,
pub forward: HashSet<i64>,
pub reverse: HashSet<i64>,
}
#[derive(Debug)]
pub struct Dag {
pub nodes: HashMap<i64, DagNode>,
}
impl Dag {
pub fn build(issues: &[Issue], relations: &[Relation]) -> Self {
let mut nodes: HashMap<i64, DagNode> = issues
.iter()
.map(|i| {
(
i.id,
DagNode {
issue: i.clone(),
forward: HashSet::new(),
reverse: HashSet::new(),
},
)
})
.collect();
for rel in relations {
match rel.kind {
RelationKind::Blocks => {
if let Some(node) = nodes.get_mut(&rel.from_id) {
node.forward.insert(rel.to_id);
}
if let Some(node) = nodes.get_mut(&rel.to_id) {
node.reverse.insert(rel.from_id);
}
}
RelationKind::DependsOn => {
if let Some(node) = nodes.get_mut(&rel.to_id) {
node.forward.insert(rel.from_id);
}
if let Some(node) = nodes.get_mut(&rel.from_id) {
node.reverse.insert(rel.to_id);
}
}
_ => {} }
}
Dag { nodes }
}
pub fn is_leaf(&self, id: i64) -> bool {
!self.nodes.values().any(|n| n.issue.parent_id == Some(id))
}
}
pub fn find_ready(dag: &Dag) -> Vec<&Issue> {
let allowed_statuses = [Status::Backlog, Status::Todo];
let done = Status::Done;
let mut ready: Vec<&Issue> = dag
.nodes
.values()
.filter(|node| {
if !allowed_statuses.contains(&node.issue.status) {
return false;
}
if !dag.is_leaf(node.issue.id) {
return false;
}
node.reverse.iter().all(|blocker_id| {
dag.nodes
.get(blocker_id)
.map(|n| n.issue.status == done)
.unwrap_or(true) })
})
.map(|n| &n.issue)
.collect();
ready.sort_by(|a, b| b.priority.cmp(&a.priority).then_with(|| a.id.cmp(&b.id)));
ready
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Kind, Priority, Status};
use chrono::Utc;
fn make_issue(id: i64, status: Status, priority: Priority) -> Issue {
Issue {
id,
parent_id: None,
title: format!("Issue {id}"),
description: String::new(),
status,
priority,
kind: Kind::Task,
assignee: None,
labels: vec![],
files: vec![],
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
fn make_relation(from_id: i64, to_id: i64, kind: RelationKind) -> Relation {
Relation {
id: 0,
from_id,
to_id,
kind,
}
}
#[test]
fn dag_construction_blocks() {
let issues = vec![
make_issue(1, Status::Todo, Priority::High),
make_issue(2, Status::Backlog, Priority::Medium),
];
let relations = vec![make_relation(1, 2, RelationKind::Blocks)];
let dag = Dag::build(&issues, &relations);
assert!(dag.nodes[&1].forward.contains(&2));
assert!(dag.nodes[&2].reverse.contains(&1));
}
#[test]
fn find_ready_unblocked() {
let issues = vec![
make_issue(1, Status::Done, Priority::High),
make_issue(2, Status::Todo, Priority::Medium),
];
let relations = vec![make_relation(1, 2, RelationKind::Blocks)];
let dag = Dag::build(&issues, &relations);
let ready = find_ready(&dag);
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].id, 2);
}
#[test]
fn find_ready_blocked() {
let issues = vec![
make_issue(1, Status::InProgress, Priority::High),
make_issue(2, Status::Todo, Priority::Medium),
];
let relations = vec![make_relation(1, 2, RelationKind::Blocks)];
let dag = Dag::build(&issues, &relations);
let ready = find_ready(&dag);
assert!(ready.iter().all(|i| i.id != 2));
}
}