Skip to main content

ralph/queue/operations/batch/
validation.rs

1//! Batch operation validation and bookkeeping helpers.
2//!
3//! Responsibilities:
4//! - Collect and summarize per-task batch operation results.
5//! - Normalize task ID lists before batch execution.
6//! - Validate that referenced task IDs exist in the active queue.
7//!
8//! Does not handle:
9//! - Applying batch mutations (handled by sibling batch modules).
10//! - CLI argument parsing or result rendering.
11//!
12//! Assumptions/invariants:
13//! - Task IDs are trimmed before deduplication and existence checks.
14//! - Collector counts must stay synchronized with recorded results.
15//! - Validation is performed against an already loaded `QueueFile`.
16
17use crate::contracts::{QueueFile, Task};
18
19use super::{BatchOperationResult, BatchTaskResult};
20
21/// Collect unique task IDs from a list of tasks.
22pub fn collect_task_ids(tasks: &[&Task]) -> Vec<String> {
23    tasks.iter().map(|task| task.id.clone()).collect()
24}
25
26/// Deduplicate task IDs while preserving order.
27pub(crate) fn deduplicate_task_ids(task_ids: &[String]) -> Vec<String> {
28    let mut seen = std::collections::HashSet::new();
29    let mut result = Vec::new();
30    for id in task_ids {
31        let trimmed = id.trim().to_string();
32        if !trimmed.is_empty() && seen.insert(trimmed.clone()) {
33            result.push(trimmed);
34        }
35    }
36    result
37}
38
39/// Validate that all task IDs exist in the queue.
40///
41/// Returns an error if any task ID is not found.
42pub(crate) fn validate_task_ids_exist(
43    queue: &QueueFile,
44    task_ids: &[String],
45) -> anyhow::Result<()> {
46    use anyhow::bail;
47
48    for task_id in task_ids {
49        let needle = task_id.trim();
50        if needle.is_empty() {
51            bail!("Empty task ID provided");
52        }
53        if !queue.tasks.iter().any(|task| task.id.trim() == needle) {
54            bail!(
55                "{}",
56                crate::error_messages::task_not_found_batch_failure(needle)
57            );
58        }
59    }
60    Ok(())
61}
62
63/// Collector for batch operation results with standardized error handling.
64///
65/// Responsibilities:
66/// - Track success/failure counts.
67/// - Collect individual task results.
68/// - Handle continue-on-error semantics.
69///
70/// Does not handle:
71/// - Task ID deduplication (use `preprocess_batch_ids`).
72/// - Actual operation execution (caller responsibility).
73pub(crate) struct BatchResultCollector {
74    total: usize,
75    results: Vec<BatchTaskResult>,
76    succeeded: usize,
77    failed: usize,
78    continue_on_error: bool,
79    op_name: &'static str,
80}
81
82impl BatchResultCollector {
83    /// Create a new collector for a batch operation.
84    pub fn new(total: usize, continue_on_error: bool, op_name: &'static str) -> Self {
85        Self {
86            total,
87            results: Vec::with_capacity(total),
88            succeeded: 0,
89            failed: 0,
90            continue_on_error,
91            op_name,
92        }
93    }
94
95    /// Record a successful operation on a task.
96    pub fn record_success(&mut self, task_id: String, created_task_ids: Vec<String>) {
97        self.results.push(BatchTaskResult {
98            task_id,
99            success: true,
100            error: None,
101            created_task_ids,
102        });
103        self.succeeded += 1;
104    }
105
106    /// Record a failed operation on a task.
107    ///
108    /// Returns an error if not in continue-on-error mode, allowing caller to propagate.
109    pub fn record_failure(&mut self, task_id: String, error: String) -> anyhow::Result<()> {
110        self.results.push(BatchTaskResult {
111            task_id: task_id.clone(),
112            success: false,
113            error: Some(error.clone()),
114            created_task_ids: Vec::new(),
115        });
116        self.failed += 1;
117
118        if !self.continue_on_error {
119            anyhow::bail!(
120                "Batch {} failed at task {}: {}. Use --continue-on-error to process remaining tasks.",
121                self.op_name,
122                task_id,
123                error
124            );
125        }
126        Ok(())
127    }
128
129    /// Consume the collector and return the final result.
130    pub fn finish(self) -> BatchOperationResult {
131        BatchOperationResult {
132            total: self.total,
133            succeeded: self.succeeded,
134            failed: self.failed,
135            results: self.results,
136        }
137    }
138}
139
140/// Preprocess task IDs for batch operations.
141///
142/// Deduplicates task IDs and validates the list is not empty.
143pub(crate) fn preprocess_batch_ids(
144    task_ids: &[String],
145    op_name: &str,
146) -> anyhow::Result<Vec<String>> {
147    let unique_ids = deduplicate_task_ids(task_ids);
148    if unique_ids.is_empty() {
149        anyhow::bail!("No task IDs provided for batch {}", op_name);
150    }
151    Ok(unique_ids)
152}