auto_gitmoji/matcher/
mod.rs

1#[cfg(feature = "llm")]
2pub mod llm;
3pub mod simple;
4
5use anyhow::Result;
6
7/// Return type for emoji matches
8pub type MatcherResult = Option<(String, String)>; // (emoji_code, format_message)
9
10/// Core trait for gitmoji matching strategies
11pub trait GitmojiMatcher {
12    /// Match a commit message to an appropriate gitmoji
13    /// Returns (emoji_code, formatted_message) or None
14    fn match_emoji(&self, message: &str) -> Result<MatcherResult>;
15
16    /// Get the name of this matcher
17    fn name(&self) -> &'static str;
18}
19
20/// Factory for creating matcher instances
21pub struct MatcherFactory;
22
23impl MatcherFactory {
24    /// Create a simple keyword-based matcher
25    pub fn simple() -> Box<dyn GitmojiMatcher> {
26        Box::new(simple::SimpleMatcher::new())
27    }
28
29    /// Create an LLM matcher with the given configuration
30    #[cfg(feature = "llm")]
31    pub fn llm(config: llm::LLMConfig) -> Box<dyn GitmojiMatcher> {
32        Box::new(llm::LLMMatcher::new(config))
33    }
34
35    /// Create an LLM matcher with fallback to simple matcher
36    #[cfg(feature = "llm")]
37    pub fn llm_with_fallback(config: llm::LLMConfig) -> Box<dyn GitmojiMatcher> {
38        Box::new(llm::LLMWithFallbackMatcher::new(config))
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn test_matcher_factory_simple() {
48        let matcher = MatcherFactory::simple();
49        assert_eq!(matcher.name(), "simple");
50    }
51
52    #[test]
53    fn test_matcher_result_none() {
54        let result: MatcherResult = None;
55        assert!(result.is_none());
56    }
57
58    #[test]
59    fn test_gitmoji_matcher_trait() {
60        let matcher = MatcherFactory::simple();
61
62        // Test trait methods
63        assert_eq!(matcher.name(), "simple");
64
65        // Test match_emoji returns proper result
66        let result = matcher.match_emoji("fix bug").unwrap();
67        assert!(result.is_some());
68
69        let (code, format_message) = result.unwrap();
70        assert!(!code.is_empty());
71        assert!(!format_message.is_empty());
72    }
73
74    #[test]
75    fn test_multiple_matcher_instances() {
76        let matcher1 = MatcherFactory::simple();
77        let matcher2 = MatcherFactory::simple();
78
79        // Both should have same name
80        assert_eq!(matcher1.name(), matcher2.name());
81
82        // Both should produce same results for same input
83        let message = "fix authentication bug";
84        let result1 = matcher1.match_emoji(message).unwrap();
85        let result2 = matcher2.match_emoji(message).unwrap();
86
87        assert_eq!(result1, result2);
88    }
89
90    #[test]
91    fn test_matcher_error_handling() {
92        let matcher = MatcherFactory::simple();
93
94        // Test that matcher handles various edge cases without panicking
95        let test_cases = vec![
96            "",
97            "   ",
98            "\n\t",
99            "🎉🐛✨",
100            "very_long_message_with_no_punctuation_or_spaces_that_might_cause_issues",
101            "Mix3d c@se w1th numb3rs & $ymb0ls!",
102        ];
103
104        for message in test_cases {
105            let result = matcher.match_emoji(message);
106            assert!(result.is_ok(), "Matcher should handle: '{message}'");
107            assert!(
108                result.unwrap().is_some(),
109                "Should always return some result for: '{message}'"
110            );
111        }
112    }
113
114    #[test]
115    fn test_format_message_structure() {
116        let matcher = MatcherFactory::simple();
117
118        let test_messages = vec![
119            "fix critical bug", // Should have keyword match
120            "add new feature",  // Should have keyword match
121            "random text here", // Should have fallback
122        ];
123
124        for message in test_messages {
125            let result = matcher.match_emoji(message).unwrap();
126            assert!(result.is_some());
127            let (code, format_message) = result.unwrap();
128
129            // Code should follow :name: format
130            assert!(code.starts_with(':'), "Code should start with ':': {code}");
131            assert!(code.ends_with(':'), "Code should end with ':': {code}");
132
133            // Format message should contain the original message
134            assert!(
135                format_message.contains(message),
136                "Format message should contain original message: '{format_message}' should contain '{message}'"
137            );
138
139            // Format message should start with the emoji code
140            assert!(
141                format_message.starts_with(&code),
142                "Format message should start with emoji code: '{format_message}' should start with '{code}'"
143            );
144        }
145    }
146
147    #[test]
148    fn test_emoji_code_format() {
149        let matcher = MatcherFactory::simple();
150
151        let result = matcher.match_emoji("fix bug").unwrap().unwrap();
152        let (code, _format_message) = result;
153
154        // Emoji code should follow :name: format
155        assert!(code.starts_with(':'), "Code should start with ':': {code}");
156        assert!(code.ends_with(':'), "Code should end with ':': {code}");
157        assert!(code.len() > 2, "Code should be more than just '::': {code}");
158    }
159
160    #[test]
161    fn test_formatted_message_content() {
162        let matcher = MatcherFactory::simple();
163
164        let result = matcher.match_emoji("fix bug").unwrap().unwrap();
165        let (_code, format_message) = result;
166
167        // Format message should contain meaningful content
168        assert!(
169            !format_message.is_empty(),
170            "Format message should not be empty"
171        );
172        assert!(
173            format_message.contains("fix bug"),
174            "Format message should contain original text"
175        );
176    }
177
178    #[test]
179    fn test_consistent_results() {
180        let matcher = MatcherFactory::simple();
181
182        // Same input should always produce same output
183        let message = "fix authentication issue";
184        let results: Vec<_> = (0..5)
185            .map(|_| matcher.match_emoji(message).unwrap())
186            .collect();
187
188        // All results should be identical
189        let first_result = &results[0];
190        for result in &results[1..] {
191            assert_eq!(result, first_result, "Results should be consistent");
192        }
193    }
194
195    #[test]
196    fn test_matcher_trait_object() {
197        // Test that we can use the matcher as a trait object
198        let matcher: Box<dyn GitmojiMatcher> = MatcherFactory::simple();
199
200        let result = matcher.match_emoji("add feature").unwrap();
201        assert!(result.is_some());
202
203        let (code, format_message) = result.unwrap();
204        assert!(!code.is_empty());
205        assert!(!format_message.is_empty());
206    }
207
208    #[test]
209    fn test_factory_pattern() {
210        // Test that factory properly creates instances
211        let matcher = MatcherFactory::simple();
212
213        // Should implement the trait correctly
214        assert_eq!(matcher.name(), "simple");
215
216        // Should be functional
217        let result = matcher.match_emoji("test message");
218        assert!(result.is_ok());
219    }
220}