use ironflow_store::entities::Assignee;
use serde::{Deserialize, Serialize};
use strum::IntoStaticStr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, IntoStaticStr)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum EscalationPolicy {
AutoApprove,
AutoReject,
Notify(Vec<NotificationTarget>),
Escalate(Assignee),
Chain(Vec<EscalationPolicy>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotificationTarget {
Webhook {
url: String,
},
Slack {
webhook_url: String,
channel: String,
},
}
impl EscalationPolicy {
pub fn stage(&self, index: usize) -> Option<&EscalationPolicy> {
match self {
EscalationPolicy::Chain(policies) => policies.get(index),
other => (index == 0).then_some(other),
}
}
pub fn is_terminal(&self) -> bool {
matches!(
self,
EscalationPolicy::AutoApprove | EscalationPolicy::AutoReject
)
}
pub fn is_repeating(&self) -> bool {
matches!(
self,
EscalationPolicy::Notify(_) | EscalationPolicy::Escalate(_)
)
}
pub fn len(&self) -> usize {
match self {
EscalationPolicy::Chain(policies) => policies.len(),
_ => 1,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slack() -> NotificationTarget {
NotificationTarget::Slack {
webhook_url: "https://hooks.slack.com/services/T/B/X".to_string(),
channel: "#deploys".to_string(),
}
}
#[test]
fn auto_approve_serializes_as_a_bare_string() {
let json = serde_json::to_string(&EscalationPolicy::AutoApprove).expect("serialize");
assert_eq!(json, "\"auto_approve\"");
}
#[test]
fn auto_reject_serializes_as_a_bare_string() {
let json = serde_json::to_string(&EscalationPolicy::AutoReject).expect("serialize");
assert_eq!(json, "\"auto_reject\"");
}
#[test]
fn notify_is_externally_tagged() {
let policy = EscalationPolicy::Notify(vec![NotificationTarget::Webhook {
url: "https://example.com/sla".to_string(),
}]);
let json = serde_json::to_string(&policy).expect("serialize");
assert!(json.starts_with("{\"notify\":["), "got {json}");
let back: EscalationPolicy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, policy);
}
#[test]
fn escalate_is_externally_tagged() {
let policy = EscalationPolicy::Escalate(Assignee::group("sre-oncall"));
let json = serde_json::to_string(&policy).expect("serialize");
assert_eq!(json, "{\"escalate\":\"group:sre-oncall\"}");
let back: EscalationPolicy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, policy);
}
#[test]
fn chain_is_externally_tagged() {
let policy = EscalationPolicy::Chain(vec![
EscalationPolicy::Notify(vec![slack()]),
EscalationPolicy::AutoReject,
]);
let json = serde_json::to_string(&policy).expect("serialize");
assert!(json.starts_with("{\"chain\":["), "got {json}");
let back: EscalationPolicy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, policy);
}
#[test]
fn nested_chain_roundtrips() {
let policy = EscalationPolicy::Chain(vec![
EscalationPolicy::Chain(vec![EscalationPolicy::AutoApprove]),
EscalationPolicy::AutoReject,
]);
let json = serde_json::to_string(&policy).expect("serialize");
let back: EscalationPolicy = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, policy);
}
#[test]
fn notification_targets_roundtrip() {
for target in [
NotificationTarget::Webhook {
url: "https://example.com/sla".to_string(),
},
slack(),
] {
let json = serde_json::to_string(&target).expect("serialize");
let back: NotificationTarget = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, target);
}
}
#[test]
fn stage_of_a_single_policy_is_only_zero() {
let policy = EscalationPolicy::Notify(vec![slack()]);
assert_eq!(policy.stage(0), Some(&policy));
assert!(policy.stage(1).is_none());
assert!(policy.stage(99).is_none());
}
#[test]
fn stage_walks_a_chain_in_order() {
let policy = EscalationPolicy::Chain(vec![
EscalationPolicy::Notify(vec![slack()]),
EscalationPolicy::Escalate(Assignee::group("sre-oncall")),
EscalationPolicy::AutoReject,
]);
assert_eq!(
policy.stage(0),
Some(&EscalationPolicy::Notify(vec![slack()]))
);
assert_eq!(
policy.stage(1),
Some(&EscalationPolicy::Escalate(Assignee::group("sre-oncall")))
);
assert_eq!(policy.stage(2), Some(&EscalationPolicy::AutoReject));
assert!(policy.stage(3).is_none());
}
#[test]
fn terminal_and_repeating_are_mutually_exclusive() {
let cases = [
(EscalationPolicy::AutoApprove, true, false),
(EscalationPolicy::AutoReject, true, false),
(EscalationPolicy::Notify(vec![slack()]), false, true),
(
EscalationPolicy::Escalate(Assignee::group("sre")),
false,
true,
),
(EscalationPolicy::Chain(Vec::new()), false, false),
];
for (policy, terminal, repeating) in cases {
assert_eq!(policy.is_terminal(), terminal, "{policy:?}");
assert_eq!(policy.is_repeating(), repeating, "{policy:?}");
}
}
#[test]
fn len_reports_chain_length_and_one_otherwise() {
assert_eq!(EscalationPolicy::AutoApprove.len(), 1);
assert_eq!(EscalationPolicy::Escalate(Assignee::group("sre")).len(), 1);
assert_eq!(
EscalationPolicy::Chain(vec![
EscalationPolicy::AutoApprove,
EscalationPolicy::AutoReject
])
.len(),
2
);
}
#[test]
fn only_an_empty_chain_is_empty() {
assert!(EscalationPolicy::Chain(Vec::new()).is_empty());
assert!(!EscalationPolicy::Notify(Vec::new()).is_empty());
assert!(!EscalationPolicy::AutoReject.is_empty());
}
}