1use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum WebhookEvent {
10 Push,
12 PullRequest,
14 PullRequestReview,
16 PullRequestComment,
18 Issue,
20 IssueComment,
22 Create,
24 Delete,
26 Fork,
28 Star,
30}
31
32impl WebhookEvent {
33 pub fn parse(s: &str) -> Option<Self> {
35 match s.to_lowercase().as_str() {
36 "push" => Some(WebhookEvent::Push),
37 "pull_request" | "pr" => Some(WebhookEvent::PullRequest),
38 "pull_request_review" | "pr_review" => Some(WebhookEvent::PullRequestReview),
39 "pull_request_comment" | "pr_comment" => Some(WebhookEvent::PullRequestComment),
40 "issue" | "issues" => Some(WebhookEvent::Issue),
41 "issue_comment" => Some(WebhookEvent::IssueComment),
42 "create" => Some(WebhookEvent::Create),
43 "delete" => Some(WebhookEvent::Delete),
44 "fork" => Some(WebhookEvent::Fork),
45 "star" => Some(WebhookEvent::Star),
46 _ => None,
47 }
48 }
49
50 pub fn all() -> Vec<WebhookEvent> {
52 vec![
53 WebhookEvent::Push,
54 WebhookEvent::PullRequest,
55 WebhookEvent::PullRequestReview,
56 WebhookEvent::PullRequestComment,
57 WebhookEvent::Issue,
58 WebhookEvent::IssueComment,
59 WebhookEvent::Create,
60 WebhookEvent::Delete,
61 WebhookEvent::Fork,
62 WebhookEvent::Star,
63 ]
64 }
65}
66
67impl std::fmt::Display for WebhookEvent {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 WebhookEvent::Push => write!(f, "push"),
71 WebhookEvent::PullRequest => write!(f, "pull_request"),
72 WebhookEvent::PullRequestReview => write!(f, "pull_request_review"),
73 WebhookEvent::PullRequestComment => write!(f, "pull_request_comment"),
74 WebhookEvent::Issue => write!(f, "issue"),
75 WebhookEvent::IssueComment => write!(f, "issue_comment"),
76 WebhookEvent::Create => write!(f, "create"),
77 WebhookEvent::Delete => write!(f, "delete"),
78 WebhookEvent::Fork => write!(f, "fork"),
79 WebhookEvent::Star => write!(f, "star"),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Webhook {
87 pub id: u64,
89 pub repo_key: String,
91 pub url: String,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub secret: Option<String>,
96 pub events: HashSet<WebhookEvent>,
98 pub active: bool,
100 pub content_type: String,
102 pub insecure_ssl: bool,
104 pub created_at: u64,
106 pub updated_at: u64,
108 pub delivery_count: u64,
110 pub failure_count: u64,
112}
113
114impl Webhook {
115 pub fn new(id: u64, repo_key: String, url: String, events: HashSet<WebhookEvent>) -> Self {
117 let now = Self::now();
118 Self {
119 id,
120 repo_key,
121 url,
122 secret: None,
123 events,
124 active: true,
125 content_type: "application/json".into(),
126 insecure_ssl: false,
127 created_at: now,
128 updated_at: now,
129 delivery_count: 0,
130 failure_count: 0,
131 }
132 }
133
134 pub fn with_secret(mut self, secret: String) -> Self {
136 self.secret = Some(secret);
137 self
138 }
139
140 pub fn should_fire(&self, event: WebhookEvent) -> bool {
142 self.active && self.events.contains(&event)
143 }
144
145 pub fn add_event(&mut self, event: WebhookEvent) {
147 self.events.insert(event);
148 self.updated_at = Self::now();
149 }
150
151 pub fn remove_event(&mut self, event: WebhookEvent) -> bool {
153 let removed = self.events.remove(&event);
154 if removed {
155 self.updated_at = Self::now();
156 }
157 removed
158 }
159
160 pub fn enable(&mut self) {
162 self.active = true;
163 self.updated_at = Self::now();
164 }
165
166 pub fn disable(&mut self) {
168 self.active = false;
169 self.updated_at = Self::now();
170 }
171
172 pub fn record_success(&mut self) {
174 self.delivery_count += 1;
175 }
176
177 pub fn record_failure(&mut self) {
179 self.delivery_count += 1;
180 self.failure_count += 1;
181 }
182
183 fn now() -> u64 {
184 std::time::SystemTime::now()
185 .duration_since(std::time::UNIX_EPOCH)
186 .unwrap_or_default()
187 .as_secs()
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct CreateWebhookRequest {
194 pub url: String,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub secret: Option<String>,
199 pub events: Vec<String>,
201 #[serde(default = "default_content_type")]
203 pub content_type: String,
204 #[serde(default)]
206 pub insecure_ssl: bool,
207}
208
209fn default_content_type() -> String {
210 "application/json".into()
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct UpdateWebhookRequest {
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub url: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub secret: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub events: Option<Vec<String>>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub active: Option<bool>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct WebhookPayload {
233 pub event: WebhookEvent,
235 pub delivery_id: String,
237 pub repository: WebhookRepository,
239 pub payload: serde_json::Value,
241 pub timestamp: u64,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct WebhookRepository {
248 pub key: String,
250 pub name: String,
252 pub owner: String,
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_webhook_event_parsing() {
262 assert_eq!(WebhookEvent::parse("push"), Some(WebhookEvent::Push));
263 assert_eq!(WebhookEvent::parse("PUSH"), Some(WebhookEvent::Push));
264 assert_eq!(
265 WebhookEvent::parse("pull_request"),
266 Some(WebhookEvent::PullRequest)
267 );
268 assert_eq!(WebhookEvent::parse("pr"), Some(WebhookEvent::PullRequest));
269 assert_eq!(WebhookEvent::parse("invalid"), None);
270 }
271
272 #[test]
273 fn test_webhook_creation() {
274 let mut events = HashSet::new();
275 events.insert(WebhookEvent::Push);
276 events.insert(WebhookEvent::PullRequest);
277
278 let webhook = Webhook::new(
279 1,
280 "acme/api".into(),
281 "https://example.com/hook".into(),
282 events,
283 );
284
285 assert_eq!(webhook.id, 1);
286 assert!(webhook.active);
287 assert!(webhook.should_fire(WebhookEvent::Push));
288 assert!(webhook.should_fire(WebhookEvent::PullRequest));
289 assert!(!webhook.should_fire(WebhookEvent::Issue));
290 }
291
292 #[test]
293 fn test_webhook_disable() {
294 let mut events = HashSet::new();
295 events.insert(WebhookEvent::Push);
296
297 let mut webhook = Webhook::new(
298 1,
299 "acme/api".into(),
300 "https://example.com/hook".into(),
301 events,
302 );
303
304 assert!(webhook.should_fire(WebhookEvent::Push));
305
306 webhook.disable();
307 assert!(!webhook.should_fire(WebhookEvent::Push));
308
309 webhook.enable();
310 assert!(webhook.should_fire(WebhookEvent::Push));
311 }
312
313 #[test]
314 fn test_webhook_events() {
315 let events = HashSet::new();
316 let mut webhook = Webhook::new(
317 1,
318 "acme/api".into(),
319 "https://example.com/hook".into(),
320 events,
321 );
322
323 assert!(!webhook.should_fire(WebhookEvent::Push));
324
325 webhook.add_event(WebhookEvent::Push);
326 assert!(webhook.should_fire(WebhookEvent::Push));
327
328 webhook.remove_event(WebhookEvent::Push);
329 assert!(!webhook.should_fire(WebhookEvent::Push));
330 }
331
332 #[test]
333 fn test_webhook_delivery_tracking() {
334 let events = HashSet::new();
335 let mut webhook = Webhook::new(
336 1,
337 "acme/api".into(),
338 "https://example.com/hook".into(),
339 events,
340 );
341
342 assert_eq!(webhook.delivery_count, 0);
343 assert_eq!(webhook.failure_count, 0);
344
345 webhook.record_success();
346 assert_eq!(webhook.delivery_count, 1);
347 assert_eq!(webhook.failure_count, 0);
348
349 webhook.record_failure();
350 assert_eq!(webhook.delivery_count, 2);
351 assert_eq!(webhook.failure_count, 1);
352 }
353}