ricecoder_workflows/
approval.rs

1//! Approval gate implementation for workflow steps
2
3use crate::error::{WorkflowError, WorkflowResult};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Approval decision for a step
9#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
10pub enum ApprovalDecision {
11    /// Step was approved
12    #[serde(rename = "approved")]
13    Approved,
14    /// Step was rejected
15    #[serde(rename = "rejected")]
16    Rejected,
17}
18
19/// Approval request for a workflow step
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ApprovalRequest {
22    /// Unique approval request ID
23    pub id: String,
24    /// Step ID requiring approval
25    pub step_id: String,
26    /// Approval message
27    pub message: String,
28    /// Request creation time
29    pub created_at: DateTime<Utc>,
30    /// Request timeout
31    pub timeout_ms: u64,
32    /// Whether approval has been received
33    pub approved: bool,
34    /// Approval decision (if received)
35    pub decision: Option<ApprovalDecision>,
36    /// Approval timestamp (if received)
37    pub approved_at: Option<DateTime<Utc>>,
38    /// Approval comments
39    pub comments: Option<String>,
40}
41
42impl ApprovalRequest {
43    /// Create a new approval request
44    pub fn new(step_id: String, message: String, timeout_ms: u64) -> Self {
45        ApprovalRequest {
46            id: uuid::Uuid::new_v4().to_string(),
47            step_id,
48            message,
49            created_at: Utc::now(),
50            timeout_ms,
51            approved: false,
52            decision: None,
53            approved_at: None,
54            comments: None,
55        }
56    }
57
58    /// Check if the approval request has timed out
59    pub fn is_timed_out(&self) -> bool {
60        let timeout_duration = Duration::milliseconds(self.timeout_ms as i64);
61        Utc::now() > self.created_at + timeout_duration
62    }
63
64    /// Check if the approval request is still pending
65    pub fn is_pending(&self) -> bool {
66        !self.approved && !self.is_timed_out()
67    }
68
69    /// Approve the request
70    pub fn approve(&mut self, comments: Option<String>) {
71        self.approved = true;
72        self.decision = Some(ApprovalDecision::Approved);
73        self.approved_at = Some(Utc::now());
74        self.comments = comments;
75    }
76
77    /// Reject the request
78    pub fn reject(&mut self, comments: Option<String>) {
79        self.approved = true;
80        self.decision = Some(ApprovalDecision::Rejected);
81        self.approved_at = Some(Utc::now());
82        self.comments = comments;
83    }
84}
85
86/// Manages approval gates for workflow steps
87pub struct ApprovalGate {
88    /// Active approval requests
89    requests: HashMap<String, ApprovalRequest>,
90}
91
92impl Default for ApprovalGate {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl ApprovalGate {
99    /// Create a new approval gate manager
100    pub fn new() -> Self {
101        ApprovalGate {
102            requests: HashMap::new(),
103        }
104    }
105
106    /// Request approval for a step
107    ///
108    /// Creates an approval request and returns the request ID.
109    /// The request will timeout after the specified duration.
110    pub fn request_approval(
111        &mut self,
112        step_id: String,
113        message: String,
114        timeout_ms: u64,
115    ) -> WorkflowResult<String> {
116        let request = ApprovalRequest::new(step_id, message, timeout_ms);
117        let request_id = request.id.clone();
118        self.requests.insert(request_id.clone(), request);
119        Ok(request_id)
120    }
121
122    /// Approve a pending request
123    ///
124    /// Marks the approval request as approved.
125    /// Returns error if the request is not found or already decided.
126    pub fn approve(&mut self, request_id: &str, comments: Option<String>) -> WorkflowResult<()> {
127        let request = self.requests.get_mut(request_id).ok_or_else(|| {
128            WorkflowError::NotFound(format!("Approval request not found: {}", request_id))
129        })?;
130
131        if request.approved {
132            return Err(WorkflowError::Invalid(format!(
133                "Approval request already decided: {}",
134                request_id
135            )));
136        }
137
138        if request.is_timed_out() {
139            return Err(WorkflowError::ApprovalTimeout);
140        }
141
142        request.approve(comments);
143        Ok(())
144    }
145
146    /// Reject a pending request
147    ///
148    /// Marks the approval request as rejected.
149    /// Returns error if the request is not found or already decided.
150    pub fn reject(&mut self, request_id: &str, comments: Option<String>) -> WorkflowResult<()> {
151        let request = self.requests.get_mut(request_id).ok_or_else(|| {
152            WorkflowError::NotFound(format!("Approval request not found: {}", request_id))
153        })?;
154
155        if request.approved {
156            return Err(WorkflowError::Invalid(format!(
157                "Approval request already decided: {}",
158                request_id
159            )));
160        }
161
162        if request.is_timed_out() {
163            return Err(WorkflowError::ApprovalTimeout);
164        }
165
166        request.reject(comments);
167        Ok(())
168    }
169
170    /// Get the status of an approval request
171    pub fn get_request_status(&self, request_id: &str) -> WorkflowResult<ApprovalRequest> {
172        self.requests.get(request_id).cloned().ok_or_else(|| {
173            WorkflowError::NotFound(format!("Approval request not found: {}", request_id))
174        })
175    }
176
177    /// Check if a step is approved
178    ///
179    /// Returns true if the step has been approved, false if rejected or pending.
180    /// Returns error if the request is not found or timed out.
181    pub fn is_approved(&self, request_id: &str) -> WorkflowResult<bool> {
182        let request = self.get_request_status(request_id)?;
183
184        if request.is_timed_out() {
185            return Err(WorkflowError::ApprovalTimeout);
186        }
187
188        if !request.approved {
189            return Ok(false);
190        }
191
192        Ok(request.decision == Some(ApprovalDecision::Approved))
193    }
194
195    /// Check if a step is rejected
196    ///
197    /// Returns true if the step has been rejected, false if approved or pending.
198    /// Returns error if the request is not found or timed out.
199    pub fn is_rejected(&self, request_id: &str) -> WorkflowResult<bool> {
200        let request = self.get_request_status(request_id)?;
201
202        if request.is_timed_out() {
203            return Err(WorkflowError::ApprovalTimeout);
204        }
205
206        if !request.approved {
207            return Ok(false);
208        }
209
210        Ok(request.decision == Some(ApprovalDecision::Rejected))
211    }
212
213    /// Check if a request is still pending
214    pub fn is_pending(&self, request_id: &str) -> WorkflowResult<bool> {
215        let request = self.get_request_status(request_id)?;
216        Ok(request.is_pending())
217    }
218
219    /// Get all pending requests
220    pub fn get_pending_requests(&self) -> Vec<ApprovalRequest> {
221        self.requests
222            .values()
223            .filter(|r| r.is_pending())
224            .cloned()
225            .collect()
226    }
227
228    /// Get all requests for a specific step
229    pub fn get_step_requests(&self, step_id: &str) -> Vec<ApprovalRequest> {
230        self.requests
231            .values()
232            .filter(|r| r.step_id == step_id)
233            .cloned()
234            .collect()
235    }
236
237    /// Clear all requests (for testing)
238    #[cfg(test)]
239    pub fn clear(&mut self) {
240        self.requests.clear();
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_create_approval_request() {
250        let request = ApprovalRequest::new(
251            "step1".to_string(),
252            "Please approve this step".to_string(),
253            5000,
254        );
255
256        assert_eq!(request.step_id, "step1");
257        assert_eq!(request.message, "Please approve this step");
258        assert_eq!(request.timeout_ms, 5000);
259        assert!(!request.approved);
260        assert!(request.is_pending());
261    }
262
263    #[test]
264    fn test_approval_request_timeout() {
265        let request = ApprovalRequest::new(
266            "step1".to_string(),
267            "Please approve this step".to_string(),
268            1, // 1ms timeout
269        );
270
271        // Wait a bit to ensure timeout
272        std::thread::sleep(std::time::Duration::from_millis(10));
273
274        assert!(request.is_timed_out());
275        assert!(!request.is_pending());
276    }
277
278    #[test]
279    fn test_approve_request() {
280        let mut request = ApprovalRequest::new(
281            "step1".to_string(),
282            "Please approve this step".to_string(),
283            5000,
284        );
285
286        request.approve(Some("Looks good".to_string()));
287
288        assert!(request.approved);
289        assert_eq!(request.decision, Some(ApprovalDecision::Approved));
290        assert_eq!(request.comments, Some("Looks good".to_string()));
291        assert!(!request.is_pending());
292    }
293
294    #[test]
295    fn test_reject_request() {
296        let mut request = ApprovalRequest::new(
297            "step1".to_string(),
298            "Please approve this step".to_string(),
299            5000,
300        );
301
302        request.reject(Some("Needs changes".to_string()));
303
304        assert!(request.approved);
305        assert_eq!(request.decision, Some(ApprovalDecision::Rejected));
306        assert_eq!(request.comments, Some("Needs changes".to_string()));
307        assert!(!request.is_pending());
308    }
309
310    #[test]
311    fn test_approval_gate_request_approval() {
312        let mut gate = ApprovalGate::new();
313
314        let request_id = gate
315            .request_approval("step1".to_string(), "Please approve".to_string(), 5000)
316            .unwrap();
317
318        assert!(!request_id.is_empty());
319        assert_eq!(gate.get_pending_requests().len(), 1);
320    }
321
322    #[test]
323    fn test_approval_gate_approve() {
324        let mut gate = ApprovalGate::new();
325
326        let request_id = gate
327            .request_approval("step1".to_string(), "Please approve".to_string(), 5000)
328            .unwrap();
329
330        gate.approve(&request_id, Some("Approved".to_string()))
331            .unwrap();
332
333        assert!(gate.is_approved(&request_id).unwrap());
334        assert!(!gate.is_rejected(&request_id).unwrap());
335        assert!(!gate.is_pending(&request_id).unwrap());
336    }
337
338    #[test]
339    fn test_approval_gate_reject() {
340        let mut gate = ApprovalGate::new();
341
342        let request_id = gate
343            .request_approval("step1".to_string(), "Please approve".to_string(), 5000)
344            .unwrap();
345
346        gate.reject(&request_id, Some("Rejected".to_string()))
347            .unwrap();
348
349        assert!(!gate.is_approved(&request_id).unwrap());
350        assert!(gate.is_rejected(&request_id).unwrap());
351        assert!(!gate.is_pending(&request_id).unwrap());
352    }
353
354    #[test]
355    fn test_approval_gate_get_step_requests() {
356        let mut gate = ApprovalGate::new();
357
358        let _req1 = gate
359            .request_approval("step1".to_string(), "Please approve".to_string(), 5000)
360            .unwrap();
361
362        let _req2 = gate
363            .request_approval(
364                "step1".to_string(),
365                "Please approve again".to_string(),
366                5000,
367            )
368            .unwrap();
369
370        let _req3 = gate
371            .request_approval("step2".to_string(), "Please approve".to_string(), 5000)
372            .unwrap();
373
374        let step1_requests = gate.get_step_requests("step1");
375        assert_eq!(step1_requests.len(), 2);
376
377        let step2_requests = gate.get_step_requests("step2");
378        assert_eq!(step2_requests.len(), 1);
379    }
380
381    #[test]
382    fn test_approval_gate_cannot_approve_twice() {
383        let mut gate = ApprovalGate::new();
384
385        let request_id = gate
386            .request_approval("step1".to_string(), "Please approve".to_string(), 5000)
387            .unwrap();
388
389        gate.approve(&request_id, None).unwrap();
390
391        let result = gate.approve(&request_id, None);
392        assert!(result.is_err());
393    }
394
395    #[test]
396    fn test_approval_gate_cannot_approve_after_timeout() {
397        let mut gate = ApprovalGate::new();
398
399        let request_id = gate
400            .request_approval(
401                "step1".to_string(),
402                "Please approve".to_string(),
403                1, // 1ms timeout
404            )
405            .unwrap();
406
407        // Wait for timeout
408        std::thread::sleep(std::time::Duration::from_millis(10));
409
410        let result = gate.approve(&request_id, None);
411        assert!(result.is_err());
412    }
413}