Skip to main content

meritocrab_api/
credit_commands.rs

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