fn validate_assignee_nonempty(rhei: &Rhei, report: &mut ValidationReport) {
for_each_node(rhei, |task| {
if let Some(assignee) = &task.assignee {
if assignee.trim().is_empty() {
report.warnings.push(format!("Task {} has an empty **Assignee:** value", task.id));
}
}
});
}
fn validate_sibling_uniqueness(rhei: &Rhei, report: &mut ValidationReport) {
fn recurse(parent: Option<&Task>, siblings: &[Task], report: &mut ValidationReport) {
let mut seen: HashSet<TaskId> = HashSet::new();
for task in siblings {
if let Some(p) = parent {
if !task.id.extends(&p.id) {
report.errors.push(format!(
"Task {} must extend parent Task {} by exactly one segment",
task.id, p.id
));
}
}
if !seen.insert(task.id.clone()) {
report.errors.push(format!(
"Duplicate sibling task id: Task {}{}",
task.id,
parent.map(|p| format!(" under Task {}", p.id)).unwrap_or_default()
));
}
recurse(Some(task), &task.children, report);
}
}
recurse(None, &rhei.tasks, report);
}
fn validate_terminal_tree_coherence(
rhei: &Rhei,
machines: &MachineSet,
report: &mut ValidationReport,
) {
fn is_terminal(state_raw: &str, machine: &StateMachine) -> bool {
let parsed = parse_task_state(state_raw, machine);
machine.states.get(&parsed.state).map(|d| d.terminal).unwrap_or(false)
}
fn check_descendants(
ancestor: &Task,
node: &Task,
machine: &StateMachine,
report: &mut ValidationReport,
) {
for child in &node.children {
if !is_terminal(&child.state, machine) {
report.errors.push(format!(
"Task {} is in terminal state '{}' but descendant Task {} ('{}') is in non-terminal state '{}'",
ancestor.id, ancestor.state, child.id, child.title, child.state
));
}
check_descendants(ancestor, child, machine, report);
}
}
for_each_node(rhei, |task| {
let machine = machines.for_task(&task.id);
if is_terminal(&task.state, machine) {
check_descendants(task, task, machine, report);
}
});
}