use crate::ExecutionContext;
use oxify_model::{ApprovalConfig, NodeId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use uuid::Uuid;
pub type ApprovalId = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ApprovalStatus {
Pending,
Approved,
Rejected,
TimedOut,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalRequest {
pub id: ApprovalId,
pub execution_id: String,
pub node_id: NodeId,
pub config: ApprovalConfig,
pub status: ApprovalStatus,
pub context: serde_json::Value,
pub requested_at: std::time::SystemTime,
pub resolved_at: Option<std::time::SystemTime>,
pub resolved_by: Option<String>,
pub comments: Option<String>,
}
impl ApprovalRequest {
pub fn new(
execution_id: String,
node_id: NodeId,
config: ApprovalConfig,
context: &ExecutionContext,
) -> Self {
Self {
id: Uuid::new_v4(),
execution_id,
node_id,
config,
status: ApprovalStatus::Pending,
context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
requested_at: std::time::SystemTime::now(),
resolved_at: None,
resolved_by: None,
comments: None,
}
}
pub fn is_timed_out(&self) -> bool {
if let Some(timeout_seconds) = self.config.timeout_seconds {
if let Ok(elapsed) = self.requested_at.elapsed() {
return elapsed.as_secs() > timeout_seconds;
}
}
false
}
pub fn approve(&mut self, user_id: String, comments: Option<String>) {
self.status = ApprovalStatus::Approved;
self.resolved_at = Some(std::time::SystemTime::now());
self.resolved_by = Some(user_id);
self.comments = comments;
}
pub fn reject(&mut self, user_id: String, comments: Option<String>) {
self.status = ApprovalStatus::Rejected;
self.resolved_at = Some(std::time::SystemTime::now());
self.resolved_by = Some(user_id);
self.comments = comments;
}
pub fn mark_timed_out(&mut self) {
self.status = ApprovalStatus::TimedOut;
self.resolved_at = Some(std::time::SystemTime::now());
}
}
#[derive(Debug, Clone)]
pub struct ApprovalStore {
approvals: Arc<RwLock<HashMap<ApprovalId, ApprovalRequest>>>,
}
impl ApprovalStore {
pub fn new() -> Self {
Self {
approvals: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn add(&self, request: ApprovalRequest) -> ApprovalId {
let id = request.id;
self.approvals.write().unwrap().insert(id, request);
id
}
pub fn get(&self, id: ApprovalId) -> Option<ApprovalRequest> {
self.approvals.read().unwrap().get(&id).cloned()
}
pub fn list_pending(&self) -> Vec<ApprovalRequest> {
self.approvals
.read()
.unwrap()
.values()
.filter(|a| a.status == ApprovalStatus::Pending)
.cloned()
.collect()
}
pub fn list_pending_for_execution(&self, execution_id: &str) -> Vec<ApprovalRequest> {
self.approvals
.read()
.unwrap()
.values()
.filter(|a| a.execution_id == execution_id && a.status == ApprovalStatus::Pending)
.cloned()
.collect()
}
pub fn update(&self, request: ApprovalRequest) {
self.approvals.write().unwrap().insert(request.id, request);
}
pub fn approve(&self, id: ApprovalId, user_id: String, comments: Option<String>) -> bool {
if let Some(mut request) = self.get(id) {
request.approve(user_id, comments);
self.update(request);
true
} else {
false
}
}
pub fn reject(&self, id: ApprovalId, user_id: String, comments: Option<String>) -> bool {
if let Some(mut request) = self.get(id) {
request.reject(user_id, comments);
self.update(request);
true
} else {
false
}
}
pub fn cleanup_old(&self, max_age_seconds: u64) {
let cutoff = std::time::SystemTime::now() - std::time::Duration::from_secs(max_age_seconds);
self.approvals.write().unwrap().retain(|_, request| {
if let Some(resolved_at) = request.resolved_at {
resolved_at > cutoff
} else {
true }
});
}
}
impl Default for ApprovalStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxify_model::NodeId;
#[test]
fn test_approval_request_creation() {
use uuid::Uuid;
let config = ApprovalConfig {
message: "Approve deployment".to_string(),
description: Some("Deploy to production".to_string()),
approvers: vec!["admin".to_string()],
timeout_seconds: Some(3600),
context_data: serde_json::json!({"env": "production"}),
};
let ctx = ExecutionContext::new(Uuid::new_v4());
let request = ApprovalRequest::new("exec123".to_string(), NodeId::new_v4(), config, &ctx);
assert_eq!(request.status, ApprovalStatus::Pending);
assert!(request.resolved_at.is_none());
assert!(request.resolved_by.is_none());
}
#[test]
fn test_approval_workflow() {
use uuid::Uuid;
let config = ApprovalConfig {
message: "Approve".to_string(),
description: None,
approvers: vec![],
timeout_seconds: None,
context_data: serde_json::Value::Null,
};
let ctx = ExecutionContext::new(Uuid::new_v4());
let mut request =
ApprovalRequest::new("exec123".to_string(), NodeId::new_v4(), config, &ctx);
request.approve("user1".to_string(), Some("LGTM".to_string()));
assert_eq!(request.status, ApprovalStatus::Approved);
assert!(request.resolved_at.is_some());
assert_eq!(request.resolved_by, Some("user1".to_string()));
assert_eq!(request.comments, Some("LGTM".to_string()));
}
#[test]
fn test_approval_store() {
use uuid::Uuid;
let store = ApprovalStore::new();
let config = ApprovalConfig {
message: "Approve".to_string(),
description: None,
approvers: vec![],
timeout_seconds: None,
context_data: serde_json::Value::Null,
};
let ctx = ExecutionContext::new(Uuid::new_v4());
let request = ApprovalRequest::new("exec123".to_string(), NodeId::new_v4(), config, &ctx);
let id = store.add(request.clone());
let pending = store.list_pending();
assert_eq!(pending.len(), 1);
store.approve(id, "user1".to_string(), None);
let pending = store.list_pending();
assert_eq!(pending.len(), 0);
let retrieved = store.get(id).unwrap();
assert_eq!(retrieved.status, ApprovalStatus::Approved);
}
}