guts_realtime/
event.rs

1//! Real-time event types.
2
3use guts_auth::WebhookEvent;
4use serde::{Deserialize, Serialize};
5
6/// A real-time event that can be broadcast to connected clients.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RealtimeEvent {
9    /// Event type identifier.
10    #[serde(rename = "type")]
11    pub event_type: String,
12
13    /// Channel this event belongs to.
14    pub channel: String,
15
16    /// The underlying event kind.
17    pub event: EventKind,
18
19    /// Event payload data.
20    pub data: serde_json::Value,
21
22    /// Unix timestamp when the event occurred.
23    pub timestamp: u64,
24
25    /// Unique event ID.
26    pub event_id: String,
27}
28
29impl RealtimeEvent {
30    /// Create a new real-time event.
31    pub fn new(channel: String, event: EventKind, data: serde_json::Value) -> Self {
32        Self {
33            event_type: "event".to_string(),
34            channel,
35            event,
36            data,
37            timestamp: Self::now(),
38            event_id: uuid::Uuid::new_v4().to_string(),
39        }
40    }
41
42    fn now() -> u64 {
43        std::time::SystemTime::now()
44            .duration_since(std::time::UNIX_EPOCH)
45            .unwrap_or_default()
46            .as_secs()
47    }
48}
49
50/// Specific event types for real-time updates.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum EventKind {
54    // Repository events
55    /// Code pushed to repository.
56    Push,
57    /// Branch created.
58    BranchCreated,
59    /// Branch deleted.
60    BranchDeleted,
61    /// Tag created.
62    TagCreated,
63    /// Tag deleted.
64    TagDeleted,
65
66    // Pull request events
67    /// Pull request opened.
68    PrOpened,
69    /// Pull request closed (without merge).
70    PrClosed,
71    /// Pull request merged.
72    PrMerged,
73    /// Pull request updated (new commits).
74    PrUpdated,
75    /// Pull request reopened.
76    PrReopened,
77    /// Review submitted.
78    PrReview,
79    /// Comment added to PR.
80    PrComment,
81
82    // Issue events
83    /// Issue opened.
84    IssueOpened,
85    /// Issue closed.
86    IssueClosed,
87    /// Issue reopened.
88    IssueReopened,
89    /// Issue updated.
90    IssueUpdated,
91    /// Comment added to issue.
92    IssueComment,
93
94    // Label events
95    /// Label added.
96    LabelAdded,
97    /// Label removed.
98    LabelRemoved,
99
100    // Repository metadata
101    /// Repository created.
102    RepoCreated,
103    /// Repository settings updated.
104    RepoUpdated,
105
106    // Collaboration events
107    /// Collaborator added.
108    CollaboratorAdded,
109    /// Collaborator removed.
110    CollaboratorRemoved,
111}
112
113impl EventKind {
114    /// Convert from WebhookEvent to EventKind.
115    pub fn from_webhook(event: WebhookEvent) -> Self {
116        match event {
117            WebhookEvent::Push => EventKind::Push,
118            WebhookEvent::PullRequest => EventKind::PrOpened,
119            WebhookEvent::PullRequestReview => EventKind::PrReview,
120            WebhookEvent::PullRequestComment => EventKind::PrComment,
121            WebhookEvent::Issue => EventKind::IssueOpened,
122            WebhookEvent::IssueComment => EventKind::IssueComment,
123            WebhookEvent::Create => EventKind::BranchCreated,
124            WebhookEvent::Delete => EventKind::BranchDeleted,
125            WebhookEvent::Fork => EventKind::RepoCreated,
126            WebhookEvent::Star => EventKind::RepoUpdated,
127        }
128    }
129
130    /// Get all event kinds.
131    pub fn all() -> Vec<EventKind> {
132        vec![
133            EventKind::Push,
134            EventKind::BranchCreated,
135            EventKind::BranchDeleted,
136            EventKind::TagCreated,
137            EventKind::TagDeleted,
138            EventKind::PrOpened,
139            EventKind::PrClosed,
140            EventKind::PrMerged,
141            EventKind::PrUpdated,
142            EventKind::PrReopened,
143            EventKind::PrReview,
144            EventKind::PrComment,
145            EventKind::IssueOpened,
146            EventKind::IssueClosed,
147            EventKind::IssueReopened,
148            EventKind::IssueUpdated,
149            EventKind::IssueComment,
150            EventKind::LabelAdded,
151            EventKind::LabelRemoved,
152            EventKind::RepoCreated,
153            EventKind::RepoUpdated,
154            EventKind::CollaboratorAdded,
155            EventKind::CollaboratorRemoved,
156        ]
157    }
158}
159
160impl std::fmt::Display for EventKind {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        match self {
163            EventKind::Push => write!(f, "push"),
164            EventKind::BranchCreated => write!(f, "branch.created"),
165            EventKind::BranchDeleted => write!(f, "branch.deleted"),
166            EventKind::TagCreated => write!(f, "tag.created"),
167            EventKind::TagDeleted => write!(f, "tag.deleted"),
168            EventKind::PrOpened => write!(f, "pr.opened"),
169            EventKind::PrClosed => write!(f, "pr.closed"),
170            EventKind::PrMerged => write!(f, "pr.merged"),
171            EventKind::PrUpdated => write!(f, "pr.updated"),
172            EventKind::PrReopened => write!(f, "pr.reopened"),
173            EventKind::PrReview => write!(f, "pr.review"),
174            EventKind::PrComment => write!(f, "pr.comment"),
175            EventKind::IssueOpened => write!(f, "issue.opened"),
176            EventKind::IssueClosed => write!(f, "issue.closed"),
177            EventKind::IssueReopened => write!(f, "issue.reopened"),
178            EventKind::IssueUpdated => write!(f, "issue.updated"),
179            EventKind::IssueComment => write!(f, "issue.comment"),
180            EventKind::LabelAdded => write!(f, "label.added"),
181            EventKind::LabelRemoved => write!(f, "label.removed"),
182            EventKind::RepoCreated => write!(f, "repo.created"),
183            EventKind::RepoUpdated => write!(f, "repo.updated"),
184            EventKind::CollaboratorAdded => write!(f, "collaborator.added"),
185            EventKind::CollaboratorRemoved => write!(f, "collaborator.removed"),
186        }
187    }
188}
189
190/// Push event data.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct PushEventData {
193    /// Reference that was pushed (e.g., "refs/heads/main").
194    #[serde(rename = "ref")]
195    pub ref_name: String,
196    /// SHA before the push.
197    pub before: String,
198    /// SHA after the push.
199    pub after: String,
200    /// Username of the pusher.
201    pub pusher: String,
202    /// Number of commits.
203    pub commit_count: usize,
204}
205
206/// Pull request event data.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct PullRequestEventData {
209    /// Pull request number.
210    pub number: u64,
211    /// Pull request title.
212    pub title: String,
213    /// Author username.
214    pub author: String,
215    /// Source branch.
216    pub source_branch: String,
217    /// Target branch.
218    pub target_branch: String,
219    /// Current state (open, closed, merged).
220    pub state: String,
221}
222
223/// Issue event data.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct IssueEventData {
226    /// Issue number.
227    pub number: u64,
228    /// Issue title.
229    pub title: String,
230    /// Author username.
231    pub author: String,
232    /// Current state (open, closed).
233    pub state: String,
234}
235
236/// Comment event data.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct CommentEventData {
239    /// Comment ID.
240    pub id: u64,
241    /// Parent number (PR or Issue number).
242    pub parent_number: u64,
243    /// Author username.
244    pub author: String,
245    /// Comment body (truncated for notification).
246    pub body_preview: String,
247}
248
249/// Review event data.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ReviewEventData {
252    /// Review ID.
253    pub id: u64,
254    /// Pull request number.
255    pub pr_number: u64,
256    /// Reviewer username.
257    pub reviewer: String,
258    /// Review state (approved, changes_requested, commented).
259    pub state: String,
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_event_creation() {
268        let data = serde_json::json!({
269            "ref": "refs/heads/main",
270            "before": "abc123",
271            "after": "def456"
272        });
273
274        let event = RealtimeEvent::new("repo:alice/myrepo".to_string(), EventKind::Push, data);
275
276        assert_eq!(event.event_type, "event");
277        assert_eq!(event.channel, "repo:alice/myrepo");
278        assert_eq!(event.event, EventKind::Push);
279        assert!(!event.event_id.is_empty());
280    }
281
282    #[test]
283    fn test_event_kind_display() {
284        assert_eq!(EventKind::Push.to_string(), "push");
285        assert_eq!(EventKind::PrOpened.to_string(), "pr.opened");
286        assert_eq!(EventKind::IssueComment.to_string(), "issue.comment");
287    }
288
289    #[test]
290    fn test_event_serialization() {
291        let data = serde_json::json!({"test": "value"});
292        let event = RealtimeEvent::new("repo:test/repo".to_string(), EventKind::Push, data);
293
294        let json = serde_json::to_string(&event).unwrap();
295        assert!(json.contains("\"type\":\"event\""));
296        assert!(json.contains("\"channel\":\"repo:test/repo\""));
297    }
298}