use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WebhookEventSubscription {
TaskCreated,
TaskStarted,
TaskCompleted,
TaskFailed,
TaskStatusChanged,
LoopStarted,
LoopStopped,
PhaseStarted,
PhaseCompleted,
QueueUnblocked,
#[serde(rename = "*")]
Wildcard,
}
impl WebhookEventSubscription {
pub fn as_str(&self) -> &'static str {
match self {
Self::TaskCreated => "task_created",
Self::TaskStarted => "task_started",
Self::TaskCompleted => "task_completed",
Self::TaskFailed => "task_failed",
Self::TaskStatusChanged => "task_status_changed",
Self::LoopStarted => "loop_started",
Self::LoopStopped => "loop_stopped",
Self::PhaseStarted => "phase_started",
Self::PhaseCompleted => "phase_completed",
Self::QueueUnblocked => "queue_unblocked",
Self::Wildcard => "*",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WebhookQueuePolicy {
#[default]
DropOldest,
DropNew,
BlockWithTimeout,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct WebhookConfig {
pub enabled: Option<bool>,
pub url: Option<String>,
pub secret: Option<String>,
pub events: Option<Vec<WebhookEventSubscription>>,
#[schemars(range(min = 1, max = 300))]
pub timeout_secs: Option<u32>,
#[schemars(range(min = 0, max = 10))]
pub retry_count: Option<u32>,
#[schemars(range(min = 100, max = 30000))]
pub retry_backoff_ms: Option<u32>,
#[schemars(range(min = 10, max = 10000))]
pub queue_capacity: Option<u32>,
#[schemars(range(min = 1.0, max = 10.0))]
pub parallel_queue_multiplier: Option<f32>,
pub queue_policy: Option<WebhookQueuePolicy>,
}
impl WebhookConfig {
pub fn merge_from(&mut self, other: Self) {
if other.enabled.is_some() {
self.enabled = other.enabled;
}
if other.url.is_some() {
self.url = other.url;
}
if other.secret.is_some() {
self.secret = other.secret;
}
if other.events.is_some() {
self.events = other.events;
}
if other.timeout_secs.is_some() {
self.timeout_secs = other.timeout_secs;
}
if other.retry_count.is_some() {
self.retry_count = other.retry_count;
}
if other.retry_backoff_ms.is_some() {
self.retry_backoff_ms = other.retry_backoff_ms;
}
if other.queue_capacity.is_some() {
self.queue_capacity = other.queue_capacity;
}
if other.parallel_queue_multiplier.is_some() {
self.parallel_queue_multiplier = other.parallel_queue_multiplier;
}
if other.queue_policy.is_some() {
self.queue_policy = other.queue_policy;
}
}
const DEFAULT_EVENTS_V1: [&'static str; 5] = [
"task_created",
"task_started",
"task_completed",
"task_failed",
"task_status_changed",
];
pub fn is_event_enabled(&self, event: &str) -> bool {
if !self.enabled.unwrap_or(false) {
return false;
}
match &self.events {
None => Self::DEFAULT_EVENTS_V1.contains(&event),
Some(events) => events
.iter()
.any(|e| e.as_str() == event || e.as_str() == "*"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_subscription_serialization() {
let sub = WebhookEventSubscription::TaskCreated;
assert_eq!(serde_json::to_string(&sub).unwrap(), "\"task_created\"");
let wild = WebhookEventSubscription::Wildcard;
assert_eq!(serde_json::to_string(&wild).unwrap(), "\"*\"");
}
#[test]
fn test_event_subscription_deserialization() {
let sub: WebhookEventSubscription = serde_json::from_str("\"task_created\"").unwrap();
assert_eq!(sub, WebhookEventSubscription::TaskCreated);
let wild: WebhookEventSubscription = serde_json::from_str("\"*\"").unwrap();
assert_eq!(wild, WebhookEventSubscription::Wildcard);
}
#[test]
fn test_invalid_event_rejected() {
let result: Result<WebhookEventSubscription, _> = serde_json::from_str("\"task_creatd\"");
assert!(result.is_err());
}
#[test]
fn test_is_event_enabled_with_subscription_type() {
let config = WebhookConfig {
enabled: Some(true),
events: Some(vec![
WebhookEventSubscription::TaskCreated,
WebhookEventSubscription::Wildcard,
]),
..Default::default()
};
assert!(config.is_event_enabled("task_created"));
assert!(config.is_event_enabled("loop_started")); }
#[test]
fn test_is_event_enabled_default_events_when_none() {
let config = WebhookConfig {
enabled: Some(true),
events: None,
..Default::default()
};
assert!(config.is_event_enabled("task_created"));
assert!(config.is_event_enabled("task_started"));
assert!(!config.is_event_enabled("loop_started")); }
#[test]
fn test_is_event_enabled_disabled_when_not_enabled() {
let config = WebhookConfig {
enabled: Some(false),
events: Some(vec![WebhookEventSubscription::TaskCreated]),
..Default::default()
};
assert!(!config.is_event_enabled("task_created"));
}
}