1#![allow(dead_code)]
2use std::collections::HashMap;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct GateId(u64);
14
15impl GateId {
16 #[must_use]
18 pub fn new(id: u64) -> Self {
19 Self(id)
20 }
21
22 #[must_use]
24 pub fn value(self) -> u64 {
25 self.0
26 }
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum ApprovalPolicy {
32 Any,
34 All,
36 Quorum(usize),
38 Role(String),
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub enum GateState {
45 Pending,
47 Approved,
49 Rejected,
51 AutoApproved,
53 TimedOut,
55 Escalated,
57}
58
59#[derive(Debug, Clone)]
61pub struct ApprovalDecision {
62 pub approver: String,
64 pub approved: bool,
66 pub comment: Option<String>,
68 pub decided_at: Instant,
70}
71
72#[derive(Debug, Clone)]
74pub struct EscalationRule {
75 pub after: Duration,
77 pub escalate_to: String,
79 pub message: Option<String>,
81}
82
83impl EscalationRule {
84 pub fn new(after: Duration, escalate_to: impl Into<String>) -> Self {
86 Self {
87 after,
88 escalate_to: escalate_to.into(),
89 message: None,
90 }
91 }
92
93 pub fn with_message(mut self, msg: impl Into<String>) -> Self {
95 self.message = Some(msg.into());
96 self
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct ApprovalGateConfig {
103 pub name: String,
105 pub description: Option<String>,
107 pub approvers: Vec<String>,
109 pub policy: ApprovalPolicy,
111 pub auto_approve_after: Option<Duration>,
113 pub timeout: Option<Duration>,
115 pub escalation_rules: Vec<EscalationRule>,
117 pub metadata: HashMap<String, String>,
119}
120
121impl ApprovalGateConfig {
122 pub fn new(name: impl Into<String>, approvers: Vec<String>, policy: ApprovalPolicy) -> Self {
124 Self {
125 name: name.into(),
126 description: None,
127 approvers,
128 policy,
129 auto_approve_after: None,
130 timeout: None,
131 escalation_rules: Vec::new(),
132 metadata: HashMap::new(),
133 }
134 }
135
136 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
138 self.description = Some(desc.into());
139 self
140 }
141
142 #[must_use]
144 pub fn with_auto_approve(mut self, after: Duration) -> Self {
145 self.auto_approve_after = Some(after);
146 self
147 }
148
149 #[must_use]
151 pub fn with_timeout(mut self, timeout: Duration) -> Self {
152 self.timeout = Some(timeout);
153 self
154 }
155
156 #[must_use]
158 pub fn add_escalation(mut self, rule: EscalationRule) -> Self {
159 self.escalation_rules.push(rule);
160 self
161 }
162
163 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
165 self.metadata.insert(key.into(), value.into());
166 self
167 }
168}
169
170#[derive(Debug)]
172pub struct ApprovalGate {
173 pub id: GateId,
175 pub config: ApprovalGateConfig,
177 pub state: GateState,
179 pub decisions: Vec<ApprovalDecision>,
181 pub opened_at: Instant,
183 pub closed_at: Option<Instant>,
185}
186
187impl ApprovalGate {
188 #[must_use]
190 pub fn new(id: GateId, config: ApprovalGateConfig) -> Self {
191 Self {
192 id,
193 config,
194 state: GateState::Pending,
195 decisions: Vec::new(),
196 opened_at: Instant::now(),
197 closed_at: None,
198 }
199 }
200
201 pub fn submit_decision(&mut self, decision: ApprovalDecision) {
203 if self.state != GateState::Pending {
204 return;
205 }
206 self.decisions.push(decision);
207 self.evaluate();
208 }
209
210 pub fn check_timeouts(&mut self) {
212 if self.state != GateState::Pending {
213 return;
214 }
215 let elapsed = self.opened_at.elapsed();
216
217 if let Some(auto_dur) = self.config.auto_approve_after {
218 if elapsed >= auto_dur {
219 self.state = GateState::AutoApproved;
220 self.closed_at = Some(Instant::now());
221 return;
222 }
223 }
224
225 if let Some(timeout) = self.config.timeout {
226 if elapsed >= timeout {
227 self.state = GateState::TimedOut;
228 self.closed_at = Some(Instant::now());
229 return;
230 }
231 }
232
233 for rule in &self.config.escalation_rules {
235 if elapsed >= rule.after && self.state == GateState::Pending {
236 self.state = GateState::Escalated;
237 return;
238 }
239 }
240 }
241
242 #[must_use]
244 pub fn is_pending(&self) -> bool {
245 self.state == GateState::Pending
246 }
247
248 #[must_use]
250 pub fn is_approved(&self) -> bool {
251 matches!(self.state, GateState::Approved | GateState::AutoApproved)
252 }
253
254 #[must_use]
256 pub fn is_rejected(&self) -> bool {
257 self.state == GateState::Rejected
258 }
259
260 #[must_use]
262 pub fn approval_count(&self) -> usize {
263 self.decisions.iter().filter(|d| d.approved).count()
264 }
265
266 #[must_use]
268 pub fn rejection_count(&self) -> usize {
269 self.decisions.iter().filter(|d| !d.approved).count()
270 }
271
272 #[must_use]
274 pub fn elapsed(&self) -> Duration {
275 self.opened_at.elapsed()
276 }
277
278 fn evaluate(&mut self) {
280 let approvals = self.approval_count();
281 let rejections = self.rejection_count();
282 let total = self.config.approvers.len();
283
284 match &self.config.policy {
285 ApprovalPolicy::Any => {
286 if approvals >= 1 {
287 self.state = GateState::Approved;
288 self.closed_at = Some(Instant::now());
289 } else if rejections == total {
290 self.state = GateState::Rejected;
291 self.closed_at = Some(Instant::now());
292 }
293 }
294 ApprovalPolicy::All => {
295 if approvals == total {
296 self.state = GateState::Approved;
297 self.closed_at = Some(Instant::now());
298 } else if rejections >= 1 {
299 self.state = GateState::Rejected;
300 self.closed_at = Some(Instant::now());
301 }
302 }
303 ApprovalPolicy::Quorum(n) => {
304 if approvals >= *n {
305 self.state = GateState::Approved;
306 self.closed_at = Some(Instant::now());
307 } else if rejections > total.saturating_sub(*n) {
308 self.state = GateState::Rejected;
309 self.closed_at = Some(Instant::now());
310 }
311 }
312 ApprovalPolicy::Role(role) => {
313 for decision in &self.decisions {
315 if decision.approver == *role {
316 if decision.approved {
317 self.state = GateState::Approved;
318 } else {
319 self.state = GateState::Rejected;
320 }
321 self.closed_at = Some(Instant::now());
322 return;
323 }
324 }
325 }
326 }
327 }
328}
329
330#[derive(Debug)]
332pub struct ApprovalGateRegistry {
333 gates: HashMap<GateId, ApprovalGate>,
335 next_id: u64,
337}
338
339impl Default for ApprovalGateRegistry {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl ApprovalGateRegistry {
346 #[must_use]
348 pub fn new() -> Self {
349 Self {
350 gates: HashMap::new(),
351 next_id: 1,
352 }
353 }
354
355 pub fn open_gate(&mut self, config: ApprovalGateConfig) -> GateId {
357 let id = GateId::new(self.next_id);
358 self.next_id += 1;
359 let gate = ApprovalGate::new(id, config);
360 self.gates.insert(id, gate);
361 id
362 }
363
364 #[must_use]
366 pub fn get_gate(&self, id: GateId) -> Option<&ApprovalGate> {
367 self.gates.get(&id)
368 }
369
370 pub fn get_gate_mut(&mut self, id: GateId) -> Option<&mut ApprovalGate> {
372 self.gates.get_mut(&id)
373 }
374
375 pub fn submit_decision(&mut self, gate_id: GateId, decision: ApprovalDecision) -> bool {
377 if let Some(gate) = self.gates.get_mut(&gate_id) {
378 gate.submit_decision(decision);
379 true
380 } else {
381 false
382 }
383 }
384
385 pub fn check_all_timeouts(&mut self) {
387 for gate in self.gates.values_mut() {
388 gate.check_timeouts();
389 }
390 }
391
392 #[must_use]
394 pub fn pending_gates(&self) -> Vec<GateId> {
395 self.gates
396 .iter()
397 .filter(|(_, g)| g.is_pending())
398 .map(|(id, _)| *id)
399 .collect()
400 }
401
402 #[must_use]
404 pub fn gate_count(&self) -> usize {
405 self.gates.len()
406 }
407
408 pub fn remove_gate(&mut self, id: GateId) -> Option<ApprovalGate> {
410 self.gates.remove(&id)
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 fn make_config(approvers: Vec<&str>, policy: ApprovalPolicy) -> ApprovalGateConfig {
419 ApprovalGateConfig::new(
420 "test-gate",
421 approvers.into_iter().map(String::from).collect(),
422 policy,
423 )
424 }
425
426 fn approve(approver: &str) -> ApprovalDecision {
427 ApprovalDecision {
428 approver: approver.to_string(),
429 approved: true,
430 comment: None,
431 decided_at: Instant::now(),
432 }
433 }
434
435 fn reject(approver: &str) -> ApprovalDecision {
436 ApprovalDecision {
437 approver: approver.to_string(),
438 approved: false,
439 comment: Some("Not ready".to_string()),
440 decided_at: Instant::now(),
441 }
442 }
443
444 #[test]
445 fn test_gate_id() {
446 let id = GateId::new(42);
447 assert_eq!(id.value(), 42);
448 }
449
450 #[test]
451 fn test_new_gate_is_pending() {
452 let config = make_config(vec!["alice"], ApprovalPolicy::Any);
453 let gate = ApprovalGate::new(GateId::new(1), config);
454 assert!(gate.is_pending());
455 assert!(!gate.is_approved());
456 assert!(!gate.is_rejected());
457 }
458
459 #[test]
460 fn test_any_policy_single_approval() {
461 let config = make_config(vec!["alice", "bob"], ApprovalPolicy::Any);
462 let mut gate = ApprovalGate::new(GateId::new(1), config);
463 gate.submit_decision(approve("alice"));
464 assert!(gate.is_approved());
465 assert_eq!(gate.approval_count(), 1);
466 }
467
468 #[test]
469 fn test_all_policy_requires_all() {
470 let config = make_config(vec!["alice", "bob"], ApprovalPolicy::All);
471 let mut gate = ApprovalGate::new(GateId::new(1), config);
472 gate.submit_decision(approve("alice"));
473 assert!(gate.is_pending());
474 gate.submit_decision(approve("bob"));
475 assert!(gate.is_approved());
476 }
477
478 #[test]
479 fn test_all_policy_rejects_on_single_rejection() {
480 let config = make_config(vec!["alice", "bob"], ApprovalPolicy::All);
481 let mut gate = ApprovalGate::new(GateId::new(1), config);
482 gate.submit_decision(reject("alice"));
483 assert!(gate.is_rejected());
484 }
485
486 #[test]
487 fn test_quorum_policy() {
488 let config = make_config(vec!["a", "b", "c"], ApprovalPolicy::Quorum(2));
489 let mut gate = ApprovalGate::new(GateId::new(1), config);
490 gate.submit_decision(approve("a"));
491 assert!(gate.is_pending());
492 gate.submit_decision(approve("b"));
493 assert!(gate.is_approved());
494 }
495
496 #[test]
497 fn test_quorum_policy_rejection() {
498 let config = make_config(vec!["a", "b", "c"], ApprovalPolicy::Quorum(2));
499 let mut gate = ApprovalGate::new(GateId::new(1), config);
500 gate.submit_decision(reject("a"));
501 gate.submit_decision(reject("b"));
502 assert!(gate.is_rejected());
503 }
504
505 #[test]
506 fn test_role_policy_approved() {
507 let config = make_config(vec!["admin"], ApprovalPolicy::Role("admin".to_string()));
508 let mut gate = ApprovalGate::new(GateId::new(1), config);
509 gate.submit_decision(approve("admin"));
510 assert!(gate.is_approved());
511 }
512
513 #[test]
514 fn test_role_policy_rejected() {
515 let config = make_config(vec!["admin"], ApprovalPolicy::Role("admin".to_string()));
516 let mut gate = ApprovalGate::new(GateId::new(1), config);
517 gate.submit_decision(reject("admin"));
518 assert!(gate.is_rejected());
519 }
520
521 #[test]
522 fn test_auto_approve_timeout() {
523 let config = make_config(vec!["alice"], ApprovalPolicy::Any)
524 .with_auto_approve(Duration::from_millis(0));
525 let mut gate = ApprovalGate::new(GateId::new(1), config);
526 gate.check_timeouts();
527 assert_eq!(gate.state, GateState::AutoApproved);
528 assert!(gate.is_approved());
529 }
530
531 #[test]
532 fn test_hard_timeout() {
533 let config =
534 make_config(vec!["alice"], ApprovalPolicy::Any).with_timeout(Duration::from_millis(0));
535 let mut gate = ApprovalGate::new(GateId::new(1), config);
536 gate.check_timeouts();
537 assert_eq!(gate.state, GateState::TimedOut);
538 }
539
540 #[test]
541 fn test_registry_open_and_get() {
542 let mut registry = ApprovalGateRegistry::new();
543 let config = make_config(vec!["alice"], ApprovalPolicy::Any);
544 let id = registry.open_gate(config);
545 assert!(registry.get_gate(id).is_some());
546 assert_eq!(registry.gate_count(), 1);
547 }
548
549 #[test]
550 fn test_registry_submit_and_pending() {
551 let mut registry = ApprovalGateRegistry::new();
552 let config1 = make_config(vec!["alice"], ApprovalPolicy::Any);
553 let config2 = make_config(vec!["bob"], ApprovalPolicy::Any);
554 let id1 = registry.open_gate(config1);
555 let id2 = registry.open_gate(config2);
556 assert_eq!(registry.pending_gates().len(), 2);
557
558 registry.submit_decision(id1, approve("alice"));
559 assert_eq!(registry.pending_gates().len(), 1);
560 assert_eq!(registry.pending_gates()[0], id2);
561 }
562
563 #[test]
564 fn test_registry_remove_gate() {
565 let mut registry = ApprovalGateRegistry::new();
566 let config = make_config(vec!["alice"], ApprovalPolicy::Any);
567 let id = registry.open_gate(config);
568 assert_eq!(registry.gate_count(), 1);
569 let removed = registry.remove_gate(id);
570 assert!(removed.is_some());
571 assert_eq!(registry.gate_count(), 0);
572 }
573
574 #[test]
575 fn test_config_builder_methods() {
576 let config = make_config(vec!["alice"], ApprovalPolicy::Any)
577 .with_description("Review final output")
578 .with_timeout(Duration::from_secs(3600))
579 .with_metadata("project", "alpha");
580 assert_eq!(config.description.as_deref(), Some("Review final output"));
581 assert!(config.timeout.is_some());
582 assert_eq!(
583 config.metadata.get("project").map(|s| s.as_str()),
584 Some("alpha")
585 );
586 }
587
588 #[test]
589 fn test_escalation_rule() {
590 let rule = EscalationRule::new(Duration::from_secs(60), "manager")
591 .with_message("Urgent: please review");
592 assert_eq!(rule.escalate_to, "manager");
593 assert_eq!(rule.message.as_deref(), Some("Urgent: please review"));
594 }
595
596 #[test]
597 fn test_decision_after_close_is_ignored() {
598 let config = make_config(vec!["alice", "bob"], ApprovalPolicy::Any);
599 let mut gate = ApprovalGate::new(GateId::new(1), config);
600 gate.submit_decision(approve("alice"));
601 assert!(gate.is_approved());
602 gate.submit_decision(reject("bob"));
604 assert!(gate.is_approved()); assert_eq!(gate.decisions.len(), 1);
606 }
607
608 #[test]
609 fn test_default_registry() {
610 let registry = ApprovalGateRegistry::default();
611 assert_eq!(registry.gate_count(), 0);
612 }
613}