auto_gitmoji/matcher/
simple.rs

1use super::{GitmojiMatcher, MatcherResult};
2use crate::emoji::EmojiLookup;
3use anyhow::Result;
4use serde_json;
5use std::collections::HashMap;
6
7/// Simple keyword-based matcher that loads keywords from JSON
8pub struct SimpleMatcher {
9    keyword_map: HashMap<String, String>,
10}
11
12impl Default for SimpleMatcher {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl SimpleMatcher {
19    pub fn new() -> Self {
20        let keyword_map = Self::load_keyword_map().unwrap_or_else(|_| Self::default_keyword_map());
21
22        Self { keyword_map }
23    }
24
25    /// Load keyword mapping from JSON file
26    fn load_keyword_map() -> Result<HashMap<String, String>> {
27        let json_content = include_str!("../../fixtures/keyword_map.json");
28        let map: HashMap<String, String> = serde_json::from_str(json_content)?;
29        Ok(map)
30    }
31
32    /// Fallback keyword map if JSON loading fails
33    fn default_keyword_map() -> HashMap<String, String> {
34        let mut map = HashMap::new();
35
36        // Essential fallback keywords
37        map.insert("add".to_string(), ":sparkles:".to_string());
38        map.insert("fix".to_string(), ":bug:".to_string());
39        map.insert("docs".to_string(), ":memo:".to_string());
40        map.insert("test".to_string(), ":white_check_mark:".to_string());
41        map.insert("refactor".to_string(), ":recycle:".to_string());
42        map.insert("remove".to_string(), ":fire:".to_string());
43        map.insert("perf".to_string(), ":zap:".to_string());
44        map.insert("style".to_string(), ":lipstick:".to_string());
45        map.insert("init".to_string(), ":tada:".to_string());
46        map.insert("security".to_string(), ":lock:".to_string());
47
48        map
49    }
50
51    /// Split message into words and find first matching keyword
52    /// Strategy:
53    /// 1. Split the message into words (lowercase, alphanumeric only)
54    /// 2. Check each word in order against the keyword map
55    /// 3. Return the first match found
56    fn find_first_keyword_match(&self, message: &str) -> Option<&str> {
57        // Split message into words, convert to lowercase, remove punctuation
58        let words: Vec<String> = message
59            .split_whitespace()
60            .map(|word| {
61                word.chars()
62                    .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
63                    .collect::<String>()
64                    .to_lowercase()
65            })
66            .filter(|word| !word.is_empty())
67            .collect();
68
69        // Find first word that exists in keyword map
70        for word in words {
71            if let Some(emoji_code) = self.keyword_map.get(&word) {
72                return Some(emoji_code);
73            }
74        }
75
76        None
77    }
78}
79
80impl GitmojiMatcher for SimpleMatcher {
81    fn match_emoji(&self, message: &str) -> Result<MatcherResult> {
82        if let Some(emoji_code) = self.find_first_keyword_match(message) {
83            if let Some(emoji_unicode) = EmojiLookup::code_to_unicode(emoji_code) {
84                // High confidence for exact keyword matches
85                return Ok(Some((
86                    emoji_code.to_string(),
87                    emoji_unicode.to_string(),
88                    1.0,
89                )));
90            }
91        }
92
93        // Fallback to sparkles for general changes if no keyword match
94        Ok(Some((":sparkles:".to_string(), "✨".to_string(), 0.3)))
95    }
96
97    fn name(&self) -> &'static str {
98        "simple"
99    }
100}