use crate::result::TaskResultValue;
use crate::TaskId;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct GroupChild {
pub task_id: TaskId,
pub result: Option<TaskResultValue>,
}
impl GroupChild {
#[must_use]
pub const fn new(task_id: TaskId) -> Self {
Self {
task_id,
result: None,
}
}
#[must_use]
pub const fn with_result(task_id: TaskId, result: TaskResultValue) -> Self {
Self {
task_id,
result: Some(result),
}
}
#[inline]
#[must_use]
pub fn is_ready(&self) -> bool {
self.result.as_ref().is_some_and(TaskResultValue::is_ready)
}
#[inline]
#[must_use]
pub fn is_successful(&self) -> bool {
self.result
.as_ref()
.is_some_and(TaskResultValue::is_successful)
}
#[inline]
#[must_use]
pub fn is_failed(&self) -> bool {
self.result.as_ref().is_some_and(TaskResultValue::is_failed)
}
#[inline]
#[must_use]
pub fn is_revoked(&self) -> bool {
matches!(self.result, Some(TaskResultValue::Revoked))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupStatus {
Pending,
Succeeded,
Failed,
}
impl GroupStatus {
#[inline]
#[must_use]
pub const fn is_ready(&self) -> bool {
matches!(self, GroupStatus::Succeeded | GroupStatus::Failed)
}
#[inline]
#[must_use]
pub const fn is_successful(&self) -> bool {
matches!(self, GroupStatus::Succeeded)
}
#[inline]
#[must_use]
pub const fn is_failed(&self) -> bool {
matches!(self, GroupStatus::Failed)
}
}
#[derive(Debug, Clone)]
pub struct ResultGroup {
group_id: TaskId,
order: Vec<TaskId>,
results: HashMap<TaskId, TaskResultValue>,
}
impl ResultGroup {
#[must_use]
pub fn new(group_id: TaskId, children: Vec<TaskId>) -> Self {
let mut order = Vec::with_capacity(children.len());
for child in children {
if !order.contains(&child) {
order.push(child);
}
}
Self {
group_id,
order,
results: HashMap::new(),
}
}
#[must_use]
pub fn empty(group_id: TaskId) -> Self {
Self {
group_id,
order: Vec::new(),
results: HashMap::new(),
}
}
#[inline]
#[must_use]
pub fn group_id(&self) -> TaskId {
self.group_id
}
#[inline]
#[must_use]
pub fn child_ids(&self) -> &[TaskId] {
&self.order
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.order.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
pub fn add_child(&mut self, task_id: TaskId) {
if !self.order.contains(&task_id) {
self.order.push(task_id);
}
}
pub fn add_child_with_result(&mut self, task_id: TaskId, result: TaskResultValue) {
self.add_child(task_id);
self.results.insert(task_id, result);
}
pub fn set_result(&mut self, task_id: TaskId, result: TaskResultValue) {
self.add_child(task_id);
self.results.insert(task_id, result);
}
#[must_use]
pub fn result_for(&self, task_id: TaskId) -> Option<&TaskResultValue> {
self.results.get(&task_id)
}
#[must_use]
pub fn children(&self) -> Vec<GroupChild> {
self.order
.iter()
.map(|id| GroupChild {
task_id: *id,
result: self.results.get(id).cloned(),
})
.collect()
}
#[must_use]
pub fn reported_count(&self) -> usize {
self.results.len()
}
#[must_use]
pub fn completed_count(&self) -> usize {
self.order
.iter()
.filter(|id| self.results.get(id).is_some_and(TaskResultValue::is_ready))
.count()
}
#[must_use]
pub fn successful_count(&self) -> usize {
self.order
.iter()
.filter(|id| {
self.results
.get(id)
.is_some_and(TaskResultValue::is_successful)
})
.count()
}
#[must_use]
pub fn failed_count(&self) -> usize {
self.order
.iter()
.filter(|id| self.results.get(id).is_some_and(TaskResultValue::is_failed))
.count()
}
#[must_use]
pub fn revoked_count(&self) -> usize {
self.order
.iter()
.filter(|id| matches!(self.results.get(id), Some(TaskResultValue::Revoked)))
.count()
}
#[must_use]
pub fn completion_ratio(&self) -> f64 {
if self.order.is_empty() {
return 1.0;
}
self.completed_count() as f64 / self.order.len() as f64
}
#[must_use]
pub fn ready(&self) -> bool {
self.order
.iter()
.all(|id| self.results.get(id).is_some_and(TaskResultValue::is_ready))
}
#[must_use]
pub fn successful(&self) -> bool {
self.ready()
&& self.order.iter().all(|id| {
self.results
.get(id)
.is_some_and(TaskResultValue::is_successful)
})
}
#[must_use]
pub fn failed(&self) -> bool {
self.ready() && !self.successful()
}
#[must_use]
pub fn status(&self) -> GroupStatus {
if !self.ready() {
GroupStatus::Pending
} else if self.successful() {
GroupStatus::Succeeded
} else {
GroupStatus::Failed
}
}
#[must_use]
pub fn results(&self) -> Vec<Option<TaskResultValue>> {
self.order
.iter()
.map(|id| self.results.get(id).cloned())
.collect()
}
#[must_use]
pub fn collected_values(&self) -> Option<Vec<Value>> {
if !self.ready() {
return None;
}
let mut values = Vec::with_capacity(self.order.len());
for id in &self.order {
match self
.results
.get(id)
.and_then(TaskResultValue::success_value)
{
Some(value) => values.push(value.clone()),
None => return None,
}
}
Some(values)
}
#[must_use]
pub fn successful_values(&self) -> Vec<Value> {
self.order
.iter()
.filter_map(|id| {
self.results
.get(id)
.and_then(TaskResultValue::success_value)
.cloned()
})
.collect()
}
#[must_use]
pub fn error_messages(&self) -> Vec<String> {
self.order
.iter()
.filter_map(|id| {
self.results
.get(id)
.and_then(TaskResultValue::error_message)
.map(String::from)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use uuid::Uuid;
fn ids(n: usize) -> Vec<TaskId> {
(0..n).map(|_| Uuid::new_v4()).collect()
}
#[test]
fn empty_group_is_ready_and_successful() {
let group = ResultGroup::empty(Uuid::new_v4());
assert!(group.is_empty());
assert!(group.ready());
assert!(group.successful());
assert!(!group.failed());
assert_eq!(group.status(), GroupStatus::Succeeded);
assert_eq!(group.collected_values(), Some(vec![]));
assert_eq!(group.completion_ratio(), 1.0);
}
#[test]
fn readiness_requires_all_children() {
let v = ids(3);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
assert!(!group.ready());
assert_eq!(group.status(), GroupStatus::Pending);
group.set_result(v[0], TaskResultValue::Success(json!(10)));
group.set_result(v[1], TaskResultValue::Success(json!(20)));
assert!(!group.ready());
assert_eq!(group.completed_count(), 2);
assert_eq!(group.completion_ratio(), 2.0 / 3.0);
group.set_result(v[2], TaskResultValue::Success(json!(30)));
assert!(group.ready());
assert_eq!(group.completed_count(), 3);
}
#[test]
fn non_terminal_result_does_not_count_as_ready() {
let v = ids(2);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
group.set_result(v[0], TaskResultValue::Success(json!(1)));
group.set_result(v[1], TaskResultValue::Started);
assert!(!group.ready());
assert_eq!(group.completed_count(), 1);
assert_eq!(group.reported_count(), 2);
}
#[test]
fn success_rollup_and_ordered_values() {
let v = ids(3);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
group.set_result(v[2], TaskResultValue::Success(json!("c")));
group.set_result(v[0], TaskResultValue::Success(json!("a")));
group.set_result(v[1], TaskResultValue::Success(json!("b")));
assert!(group.successful());
assert_eq!(group.status(), GroupStatus::Succeeded);
assert_eq!(group.successful_count(), 3);
let values = group.collected_values().unwrap();
assert_eq!(values, vec![json!("a"), json!("b"), json!("c")]);
}
#[test]
fn failure_rollup() {
let v = ids(3);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
group.set_result(v[0], TaskResultValue::Success(json!(1)));
group.set_result(
v[1],
TaskResultValue::Failure {
error: "boom".to_string(),
traceback: None,
},
);
group.set_result(v[2], TaskResultValue::Success(json!(3)));
assert!(group.ready());
assert!(!group.successful());
assert!(group.failed());
assert_eq!(group.status(), GroupStatus::Failed);
assert_eq!(group.failed_count(), 1);
assert_eq!(group.successful_count(), 2);
assert!(group.collected_values().is_none());
assert_eq!(group.successful_values(), vec![json!(1), json!(3)]);
assert_eq!(group.error_messages(), vec!["boom".to_string()]);
}
#[test]
fn revoked_child_marks_failure() {
let v = ids(2);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
group.set_result(v[0], TaskResultValue::Success(json!(1)));
group.set_result(v[1], TaskResultValue::Revoked);
assert!(group.ready());
assert!(group.failed());
assert_eq!(group.status(), GroupStatus::Failed);
assert_eq!(group.revoked_count(), 1);
assert!(group.collected_values().is_none());
}
#[test]
fn rejected_child_marks_failure() {
let v = ids(2);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
group.set_result(v[0], TaskResultValue::Success(json!(1)));
group.set_result(
v[1],
TaskResultValue::Rejected {
reason: "bad input".to_string(),
},
);
assert!(group.failed());
assert_eq!(group.failed_count(), 1);
assert_eq!(group.error_messages(), vec!["bad input".to_string()]);
}
#[test]
fn duplicate_children_are_deduped() {
let id = Uuid::new_v4();
let group = ResultGroup::new(Uuid::new_v4(), vec![id, id, id]);
assert_eq!(group.len(), 1);
assert_eq!(group.child_ids(), &[id]);
}
#[test]
fn set_result_for_unknown_child_registers_it() {
let mut group = ResultGroup::empty(Uuid::new_v4());
let id = Uuid::new_v4();
group.set_result(id, TaskResultValue::Success(json!(42)));
assert_eq!(group.len(), 1);
assert!(group.ready());
assert_eq!(group.collected_values(), Some(vec![json!(42)]));
}
#[test]
fn children_snapshot_matches_order() {
let v = ids(2);
let mut group = ResultGroup::new(Uuid::new_v4(), v.clone());
group.set_result(v[0], TaskResultValue::Success(json!(1)));
let children = group.children();
assert_eq!(children.len(), 2);
assert_eq!(children[0].task_id, v[0]);
assert!(children[0].is_ready());
assert!(children[0].is_successful());
assert_eq!(children[1].task_id, v[1]);
assert!(!children[1].is_ready());
assert!(children[1].result.is_none());
}
#[test]
fn group_status_predicates() {
assert!(GroupStatus::Succeeded.is_ready());
assert!(GroupStatus::Succeeded.is_successful());
assert!(!GroupStatus::Succeeded.is_failed());
assert!(GroupStatus::Failed.is_ready());
assert!(GroupStatus::Failed.is_failed());
assert!(!GroupStatus::Pending.is_ready());
}
#[test]
fn add_child_with_result_and_result_for() {
let mut group = ResultGroup::empty(Uuid::new_v4());
let id = Uuid::new_v4();
group.add_child_with_result(id, TaskResultValue::Success(json!(7)));
assert_eq!(
group
.result_for(id)
.and_then(TaskResultValue::success_value),
Some(&json!(7))
);
assert!(group.result_for(Uuid::new_v4()).is_none());
}
}