Skip to main content

meritocrab_api/
credit_commands.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3
4/// Parsed /credit command
5#[derive(Debug, Clone, PartialEq)]
6pub enum CreditCommand {
7    /// `/credit check @username`
8    Check { username: String },
9    /// `/credit override @username +10 "reason"`
10    Override {
11        username: String,
12        delta: i32,
13        reason: String,
14    },
15    /// `/credit blacklist @username`
16    Blacklist { username: String },
17}
18
19lazy_static! {
20    // Match: /credit check @username
21    static ref CHECK_REGEX: Regex = Regex::new(r#"(?m)^/credit\s+check\s+@(\w+)\s*$"#).unwrap();
22
23    // Match: /credit override @username +10 "reason" or /credit override @username -20 "reason"
24    static ref OVERRIDE_REGEX: Regex = Regex::new(r#"(?m)^/credit\s+override\s+@(\w+)\s+([+-]\d+)\s+"([^"]+)"\s*$"#).unwrap();
25
26    // Match: /credit blacklist @username
27    static ref BLACKLIST_REGEX: Regex = Regex::new(r#"(?m)^/credit\s+blacklist\s+@(\w+)\s*$"#).unwrap();
28}
29
30/// Parse /credit command from comment body
31///
32/// Returns Some(CreditCommand) if a valid /credit command is found, None otherwise.
33/// If multiple commands are present, only the first one is parsed.
34pub fn parse_credit_command(comment_body: &str) -> Option<CreditCommand> {
35    // Try to match /credit check @username
36    if let Some(captures) = CHECK_REGEX.captures(comment_body) {
37        let username = captures.get(1).unwrap().as_str().to_string();
38        return Some(CreditCommand::Check { username });
39    }
40
41    // Try to match /credit override @username +10 "reason"
42    if let Some(captures) = OVERRIDE_REGEX.captures(comment_body) {
43        let username = captures.get(1).unwrap().as_str().to_string();
44        let delta_str = captures.get(2).unwrap().as_str();
45        let reason = captures.get(3).unwrap().as_str().to_string();
46
47        // Parse delta (includes sign)
48        if let Ok(delta) = delta_str.parse::<i32>() {
49            return Some(CreditCommand::Override {
50                username,
51                delta,
52                reason,
53            });
54        }
55    }
56
57    // Try to match /credit blacklist @username
58    if let Some(captures) = BLACKLIST_REGEX.captures(comment_body) {
59        let username = captures.get(1).unwrap().as_str().to_string();
60        return Some(CreditCommand::Blacklist { username });
61    }
62
63    None
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_parse_check_command() {
72        let comment = "/credit check @user123";
73        let cmd = parse_credit_command(comment);
74        assert_eq!(
75            cmd,
76            Some(CreditCommand::Check {
77                username: "user123".to_string()
78            })
79        );
80    }
81
82    #[test]
83    fn test_parse_check_command_with_whitespace() {
84        let comment = "/credit check @user123  ";
85        let cmd = parse_credit_command(comment);
86        assert_eq!(
87            cmd,
88            Some(CreditCommand::Check {
89                username: "user123".to_string()
90            })
91        );
92    }
93
94    #[test]
95    fn test_parse_override_positive() {
96        let comment = r#"/credit override @user123 +10 "good first contribution""#;
97        let cmd = parse_credit_command(comment);
98        assert_eq!(
99            cmd,
100            Some(CreditCommand::Override {
101                username: "user123".to_string(),
102                delta: 10,
103                reason: "good first contribution".to_string()
104            })
105        );
106    }
107
108    #[test]
109    fn test_parse_override_negative() {
110        let comment = r#"/credit override @spammer -20 "spam PR""#;
111        let cmd = parse_credit_command(comment);
112        assert_eq!(
113            cmd,
114            Some(CreditCommand::Override {
115                username: "spammer".to_string(),
116                delta: -20,
117                reason: "spam PR".to_string()
118            })
119        );
120    }
121
122    #[test]
123    fn test_parse_blacklist_command() {
124        let comment = "/credit blacklist @badactor";
125        let cmd = parse_credit_command(comment);
126        assert_eq!(
127            cmd,
128            Some(CreditCommand::Blacklist {
129                username: "badactor".to_string()
130            })
131        );
132    }
133
134    #[test]
135    fn test_parse_no_command() {
136        let comment = "This is just a regular comment";
137        let cmd = parse_credit_command(comment);
138        assert_eq!(cmd, None);
139    }
140
141    #[test]
142    fn test_parse_invalid_command() {
143        let comment = "/credit unknown @user";
144        let cmd = parse_credit_command(comment);
145        assert_eq!(cmd, None);
146    }
147
148    #[test]
149    fn test_parse_command_in_multi_line_comment() {
150        let comment = r#"Some context before
151
152/credit check @user123
153
154Some context after"#;
155        let cmd = parse_credit_command(comment);
156        assert_eq!(
157            cmd,
158            Some(CreditCommand::Check {
159                username: "user123".to_string()
160            })
161        );
162    }
163
164    #[test]
165    fn test_parse_override_with_multiword_reason() {
166        let comment = r#"/credit override @user +5 "excellent bug fix with detailed explanation""#;
167        let cmd = parse_credit_command(comment);
168        assert_eq!(
169            cmd,
170            Some(CreditCommand::Override {
171                username: "user".to_string(),
172                delta: 5,
173                reason: "excellent bug fix with detailed explanation".to_string()
174            })
175        );
176    }
177}