Skip to main content

guts_collaboration/
review.rs

1//! Code review types for pull requests.
2
3use serde::{Deserialize, Serialize};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6/// State of a code review.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ReviewState {
10    /// Review approves the changes.
11    Approved,
12    /// Review requests changes before merging.
13    ChangesRequested,
14    /// Review is just a comment without approval/rejection.
15    Commented,
16    /// Review was dismissed.
17    Dismissed,
18}
19
20impl std::fmt::Display for ReviewState {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            ReviewState::Approved => write!(f, "approved"),
24            ReviewState::ChangesRequested => write!(f, "changes_requested"),
25            ReviewState::Commented => write!(f, "commented"),
26            ReviewState::Dismissed => write!(f, "dismissed"),
27        }
28    }
29}
30
31/// A code review on a pull request.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Review {
34    /// Unique identifier.
35    pub id: u64,
36    /// Repository key (owner/repo).
37    pub repo_key: String,
38    /// Pull request number.
39    pub pr_number: u32,
40    /// Reviewer's public key (hex encoded).
41    pub author: String,
42    /// Review state.
43    pub state: ReviewState,
44    /// Review body/summary (optional).
45    pub body: Option<String>,
46    /// Commit SHA that was reviewed.
47    pub commit_id: String,
48    /// Unix timestamp when the review was created.
49    pub created_at: u64,
50    /// Unix timestamp when the review was submitted.
51    pub submitted_at: Option<u64>,
52}
53
54impl Review {
55    /// Creates a new review.
56    pub fn new(
57        id: u64,
58        repo_key: impl Into<String>,
59        pr_number: u32,
60        author: impl Into<String>,
61        state: ReviewState,
62        commit_id: impl Into<String>,
63    ) -> Self {
64        let now = SystemTime::now()
65            .duration_since(UNIX_EPOCH)
66            .unwrap()
67            .as_secs();
68
69        Self {
70            id,
71            repo_key: repo_key.into(),
72            pr_number,
73            author: author.into(),
74            state,
75            body: None,
76            commit_id: commit_id.into(),
77            created_at: now,
78            submitted_at: Some(now),
79        }
80    }
81
82    /// Creates a new review with a body.
83    pub fn with_body(mut self, body: impl Into<String>) -> Self {
84        self.body = Some(body.into());
85        self
86    }
87
88    /// Returns true if this review approves the PR.
89    pub fn is_approved(&self) -> bool {
90        self.state == ReviewState::Approved
91    }
92
93    /// Returns true if this review requests changes.
94    pub fn requests_changes(&self) -> bool {
95        self.state == ReviewState::ChangesRequested
96    }
97
98    /// Returns true if this review was dismissed.
99    pub fn is_dismissed(&self) -> bool {
100        self.state == ReviewState::Dismissed
101    }
102
103    /// Dismisses this review.
104    pub fn dismiss(&mut self) {
105        self.state = ReviewState::Dismissed;
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_review_creation() {
115        let review = Review::new(
116            1,
117            "alice/repo",
118            1,
119            "bob_pubkey",
120            ReviewState::Approved,
121            "abc123",
122        )
123        .with_body("LGTM!");
124
125        assert_eq!(review.id, 1);
126        assert_eq!(review.pr_number, 1);
127        assert_eq!(review.author, "bob_pubkey");
128        assert!(review.is_approved());
129        assert!(!review.requests_changes());
130        assert_eq!(review.body, Some("LGTM!".to_string()));
131    }
132
133    #[test]
134    fn test_changes_requested() {
135        let review = Review::new(
136            2,
137            "alice/repo",
138            1,
139            "carol_pubkey",
140            ReviewState::ChangesRequested,
141            "def456",
142        )
143        .with_body("Please add tests");
144
145        assert!(!review.is_approved());
146        assert!(review.requests_changes());
147        assert!(!review.is_dismissed());
148    }
149
150    #[test]
151    fn test_dismiss_review() {
152        let mut review = Review::new(
153            3,
154            "alice/repo",
155            1,
156            "dave_pubkey",
157            ReviewState::ChangesRequested,
158            "ghi789",
159        );
160
161        review.dismiss();
162        assert!(review.is_dismissed());
163        assert!(!review.requests_changes());
164    }
165
166    #[test]
167    fn test_commented_review() {
168        let review = Review::new(
169            4,
170            "alice/repo",
171            2,
172            "eve_pubkey",
173            ReviewState::Commented,
174            "jkl012",
175        )
176        .with_body("Have you considered using a different approach?");
177
178        assert!(!review.is_approved());
179        assert!(!review.requests_changes());
180        assert!(!review.is_dismissed());
181        assert_eq!(review.state, ReviewState::Commented);
182    }
183}