use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WebhookEvent {
Push,
PullRequest,
PullRequestReview,
PullRequestComment,
Issue,
IssueComment,
Create,
Delete,
Fork,
Star,
}
impl WebhookEvent {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"push" => Some(WebhookEvent::Push),
"pull_request" | "pr" => Some(WebhookEvent::PullRequest),
"pull_request_review" | "pr_review" => Some(WebhookEvent::PullRequestReview),
"pull_request_comment" | "pr_comment" => Some(WebhookEvent::PullRequestComment),
"issue" | "issues" => Some(WebhookEvent::Issue),
"issue_comment" => Some(WebhookEvent::IssueComment),
"create" => Some(WebhookEvent::Create),
"delete" => Some(WebhookEvent::Delete),
"fork" => Some(WebhookEvent::Fork),
"star" => Some(WebhookEvent::Star),
_ => None,
}
}
pub fn all() -> Vec<WebhookEvent> {
vec![
WebhookEvent::Push,
WebhookEvent::PullRequest,
WebhookEvent::PullRequestReview,
WebhookEvent::PullRequestComment,
WebhookEvent::Issue,
WebhookEvent::IssueComment,
WebhookEvent::Create,
WebhookEvent::Delete,
WebhookEvent::Fork,
WebhookEvent::Star,
]
}
}
impl std::fmt::Display for WebhookEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WebhookEvent::Push => write!(f, "push"),
WebhookEvent::PullRequest => write!(f, "pull_request"),
WebhookEvent::PullRequestReview => write!(f, "pull_request_review"),
WebhookEvent::PullRequestComment => write!(f, "pull_request_comment"),
WebhookEvent::Issue => write!(f, "issue"),
WebhookEvent::IssueComment => write!(f, "issue_comment"),
WebhookEvent::Create => write!(f, "create"),
WebhookEvent::Delete => write!(f, "delete"),
WebhookEvent::Fork => write!(f, "fork"),
WebhookEvent::Star => write!(f, "star"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Webhook {
pub id: u64,
pub repo_key: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
pub events: HashSet<WebhookEvent>,
pub active: bool,
pub content_type: String,
pub insecure_ssl: bool,
pub created_at: u64,
pub updated_at: u64,
pub delivery_count: u64,
pub failure_count: u64,
}
impl Webhook {
pub fn new(id: u64, repo_key: String, url: String, events: HashSet<WebhookEvent>) -> Self {
let now = Self::now();
Self {
id,
repo_key,
url,
secret: None,
events,
active: true,
content_type: "application/json".into(),
insecure_ssl: false,
created_at: now,
updated_at: now,
delivery_count: 0,
failure_count: 0,
}
}
pub fn with_secret(mut self, secret: String) -> Self {
self.secret = Some(secret);
self
}
pub fn should_fire(&self, event: WebhookEvent) -> bool {
self.active && self.events.contains(&event)
}
pub fn add_event(&mut self, event: WebhookEvent) {
self.events.insert(event);
self.updated_at = Self::now();
}
pub fn remove_event(&mut self, event: WebhookEvent) -> bool {
let removed = self.events.remove(&event);
if removed {
self.updated_at = Self::now();
}
removed
}
pub fn enable(&mut self) {
self.active = true;
self.updated_at = Self::now();
}
pub fn disable(&mut self) {
self.active = false;
self.updated_at = Self::now();
}
pub fn record_success(&mut self) {
self.delivery_count += 1;
}
pub fn record_failure(&mut self) {
self.delivery_count += 1;
self.failure_count += 1;
}
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateWebhookRequest {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
pub events: Vec<String>,
#[serde(default = "default_content_type")]
pub content_type: String,
#[serde(default)]
pub insecure_ssl: bool,
}
fn default_content_type() -> String {
"application/json".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateWebhookRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub events: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookPayload {
pub event: WebhookEvent,
pub delivery_id: String,
pub repository: WebhookRepository,
pub payload: serde_json::Value,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookRepository {
pub key: String,
pub name: String,
pub owner: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webhook_event_parsing() {
assert_eq!(WebhookEvent::parse("push"), Some(WebhookEvent::Push));
assert_eq!(WebhookEvent::parse("PUSH"), Some(WebhookEvent::Push));
assert_eq!(
WebhookEvent::parse("pull_request"),
Some(WebhookEvent::PullRequest)
);
assert_eq!(WebhookEvent::parse("pr"), Some(WebhookEvent::PullRequest));
assert_eq!(WebhookEvent::parse("invalid"), None);
}
#[test]
fn test_webhook_creation() {
let mut events = HashSet::new();
events.insert(WebhookEvent::Push);
events.insert(WebhookEvent::PullRequest);
let webhook = Webhook::new(
1,
"acme/api".into(),
"https://example.com/hook".into(),
events,
);
assert_eq!(webhook.id, 1);
assert!(webhook.active);
assert!(webhook.should_fire(WebhookEvent::Push));
assert!(webhook.should_fire(WebhookEvent::PullRequest));
assert!(!webhook.should_fire(WebhookEvent::Issue));
}
#[test]
fn test_webhook_disable() {
let mut events = HashSet::new();
events.insert(WebhookEvent::Push);
let mut webhook = Webhook::new(
1,
"acme/api".into(),
"https://example.com/hook".into(),
events,
);
assert!(webhook.should_fire(WebhookEvent::Push));
webhook.disable();
assert!(!webhook.should_fire(WebhookEvent::Push));
webhook.enable();
assert!(webhook.should_fire(WebhookEvent::Push));
}
#[test]
fn test_webhook_events() {
let events = HashSet::new();
let mut webhook = Webhook::new(
1,
"acme/api".into(),
"https://example.com/hook".into(),
events,
);
assert!(!webhook.should_fire(WebhookEvent::Push));
webhook.add_event(WebhookEvent::Push);
assert!(webhook.should_fire(WebhookEvent::Push));
webhook.remove_event(WebhookEvent::Push);
assert!(!webhook.should_fire(WebhookEvent::Push));
}
#[test]
fn test_webhook_delivery_tracking() {
let events = HashSet::new();
let mut webhook = Webhook::new(
1,
"acme/api".into(),
"https://example.com/hook".into(),
events,
);
assert_eq!(webhook.delivery_count, 0);
assert_eq!(webhook.failure_count, 0);
webhook.record_success();
assert_eq!(webhook.delivery_count, 1);
assert_eq!(webhook.failure_count, 0);
webhook.record_failure();
assert_eq!(webhook.delivery_count, 2);
assert_eq!(webhook.failure_count, 1);
}
}