1use serde::{Deserialize, Serialize};
6
7use crate::auto_reply::types::{TriggerConfig, TriggerType};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct AutoReplyTrigger {
12 pub id: String,
14 pub name: String,
16 #[serde(default = "default_true")]
18 pub enabled: bool,
19 pub trigger_type: TriggerType,
21 pub config: TriggerConfig,
23 #[serde(default = "default_priority")]
25 pub priority: u32,
26 #[serde(default)]
28 pub response_template: Option<String>,
29}
30
31fn default_true() -> bool {
32 true
33}
34
35fn default_priority() -> u32 {
36 100
37}
38
39pub struct TriggerRegistry {
41 triggers: Vec<AutoReplyTrigger>,
43}
44
45impl Default for TriggerRegistry {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl TriggerRegistry {
52 pub fn new() -> Self {
54 Self {
55 triggers: Vec::new(),
56 }
57 }
58
59 pub fn register(&mut self, trigger: AutoReplyTrigger) {
61 self.triggers.push(trigger);
62 }
63
64 pub fn unregister(&mut self, trigger_id: &str) -> Option<AutoReplyTrigger> {
66 if let Some(pos) = self.triggers.iter().position(|t| t.id == trigger_id) {
67 Some(self.triggers.remove(pos))
68 } else {
69 None
70 }
71 }
72
73 pub fn get_enabled_triggers(&self) -> Vec<&AutoReplyTrigger> {
75 let mut triggers: Vec<_> = self.triggers.iter().filter(|t| t.enabled).collect();
76 triggers.sort_by_key(|t| t.priority);
77 triggers
78 }
79
80 pub fn get_trigger(&self, trigger_id: &str) -> Option<&AutoReplyTrigger> {
82 self.triggers.iter().find(|t| t.id == trigger_id)
83 }
84
85 pub fn update_trigger(&mut self, trigger: AutoReplyTrigger) -> bool {
87 if let Some(existing) = self.triggers.iter_mut().find(|t| t.id == trigger.id) {
88 *existing = trigger;
89 true
90 } else {
91 false
92 }
93 }
94
95 pub fn get_all_triggers(&self) -> &[AutoReplyTrigger] {
99 &self.triggers
100 }
101
102 pub fn len(&self) -> usize {
104 self.triggers.len()
105 }
106
107 pub fn is_empty(&self) -> bool {
109 self.triggers.is_empty()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::auto_reply::types::KeywordTriggerConfig;
117 use proptest::prelude::*;
118
119 fn create_test_trigger(id: &str, priority: u32, enabled: bool) -> AutoReplyTrigger {
121 AutoReplyTrigger {
122 id: id.to_string(),
123 name: format!("Test Trigger {}", id),
124 enabled,
125 trigger_type: TriggerType::Keyword,
126 config: TriggerConfig::Keyword(KeywordTriggerConfig {
127 patterns: vec!["test".to_string()],
128 case_insensitive: false,
129 use_regex: false,
130 }),
131 priority,
132 response_template: None,
133 }
134 }
135
136 fn create_mention_trigger(id: &str, priority: u32) -> AutoReplyTrigger {
138 AutoReplyTrigger {
139 id: id.to_string(),
140 name: format!("Mention Trigger {}", id),
141 enabled: true,
142 trigger_type: TriggerType::Mention,
143 config: TriggerConfig::Mention,
144 priority,
145 response_template: Some("Hello!".to_string()),
146 }
147 }
148
149 #[test]
152 fn test_registry_new() {
153 let registry = TriggerRegistry::new();
154 assert!(registry.get_enabled_triggers().is_empty());
155 }
156
157 #[test]
158 fn test_registry_default() {
159 let registry = TriggerRegistry::default();
160 assert!(registry.get_enabled_triggers().is_empty());
161 }
162
163 #[test]
164 fn test_register_trigger() {
165 let mut registry = TriggerRegistry::new();
166 let trigger = create_test_trigger("t1", 10, true);
167
168 registry.register(trigger);
169
170 assert_eq!(registry.get_enabled_triggers().len(), 1);
171 assert!(registry.get_trigger("t1").is_some());
172 }
173
174 #[test]
175 fn test_register_multiple_triggers() {
176 let mut registry = TriggerRegistry::new();
177
178 registry.register(create_test_trigger("t1", 10, true));
179 registry.register(create_test_trigger("t2", 20, true));
180 registry.register(create_test_trigger("t3", 5, true));
181
182 assert_eq!(registry.get_enabled_triggers().len(), 3);
183 }
184
185 #[test]
186 fn test_unregister_existing_trigger() {
187 let mut registry = TriggerRegistry::new();
188 registry.register(create_test_trigger("t1", 10, true));
189 registry.register(create_test_trigger("t2", 20, true));
190
191 let removed = registry.unregister("t1");
192
193 assert!(removed.is_some());
194 assert_eq!(removed.unwrap().id, "t1");
195 assert!(registry.get_trigger("t1").is_none());
196 assert!(registry.get_trigger("t2").is_some());
197 }
198
199 #[test]
200 fn test_unregister_nonexistent_trigger() {
201 let mut registry = TriggerRegistry::new();
202 registry.register(create_test_trigger("t1", 10, true));
203
204 let removed = registry.unregister("nonexistent");
205
206 assert!(removed.is_none());
207 assert!(registry.get_trigger("t1").is_some());
208 }
209
210 #[test]
211 fn test_get_enabled_triggers_filters_disabled() {
212 let mut registry = TriggerRegistry::new();
213 registry.register(create_test_trigger("t1", 10, true));
214 registry.register(create_test_trigger("t2", 20, false)); registry.register(create_test_trigger("t3", 5, true));
216
217 let enabled = registry.get_enabled_triggers();
218
219 assert_eq!(enabled.len(), 2);
220 assert!(enabled.iter().all(|t| t.enabled));
221 }
222
223 #[test]
224 fn test_get_enabled_triggers_sorted_by_priority() {
225 let mut registry = TriggerRegistry::new();
226 registry.register(create_test_trigger("t1", 100, true));
227 registry.register(create_test_trigger("t2", 10, true));
228 registry.register(create_test_trigger("t3", 50, true));
229
230 let enabled = registry.get_enabled_triggers();
231
232 assert_eq!(enabled.len(), 3);
233 assert_eq!(enabled[0].id, "t2"); assert_eq!(enabled[1].id, "t3"); assert_eq!(enabled[2].id, "t1"); }
238
239 #[test]
240 fn test_get_trigger_existing() {
241 let mut registry = TriggerRegistry::new();
242 registry.register(create_test_trigger("t1", 10, true));
243
244 let trigger = registry.get_trigger("t1");
245
246 assert!(trigger.is_some());
247 assert_eq!(trigger.unwrap().id, "t1");
248 }
249
250 #[test]
251 fn test_get_trigger_nonexistent() {
252 let registry = TriggerRegistry::new();
253
254 let trigger = registry.get_trigger("nonexistent");
255
256 assert!(trigger.is_none());
257 }
258
259 #[test]
260 fn test_update_existing_trigger() {
261 let mut registry = TriggerRegistry::new();
262 registry.register(create_test_trigger("t1", 10, true));
263
264 let mut updated = create_test_trigger("t1", 50, false);
265 updated.name = "Updated Name".to_string();
266
267 let result = registry.update_trigger(updated);
268
269 assert!(result);
270 let trigger = registry.get_trigger("t1").unwrap();
271 assert_eq!(trigger.name, "Updated Name");
272 assert_eq!(trigger.priority, 50);
273 assert!(!trigger.enabled);
274 }
275
276 #[test]
277 fn test_update_nonexistent_trigger() {
278 let mut registry = TriggerRegistry::new();
279 registry.register(create_test_trigger("t1", 10, true));
280
281 let new_trigger = create_test_trigger("t2", 20, true);
282 let result = registry.update_trigger(new_trigger);
283
284 assert!(!result);
285 assert!(registry.get_trigger("t2").is_none());
286 }
287
288 #[test]
291 fn test_trigger_serialization_roundtrip() {
292 let trigger = create_mention_trigger("mention-1", 5);
293
294 let json = serde_json::to_string(&trigger).expect("Should serialize");
295 let parsed: AutoReplyTrigger = serde_json::from_str(&json).expect("Should deserialize");
296
297 assert_eq!(parsed.id, trigger.id);
298 assert_eq!(parsed.name, trigger.name);
299 assert_eq!(parsed.enabled, trigger.enabled);
300 assert_eq!(parsed.priority, trigger.priority);
301 assert_eq!(parsed.response_template, trigger.response_template);
302 }
303
304 #[test]
305 fn test_trigger_default_values() {
306 let json = r#"{
308 "id": "test",
309 "name": "Test",
310 "trigger_type": "mention",
311 "config": { "type": "mention" }
312 }"#;
313
314 let trigger: AutoReplyTrigger =
315 serde_json::from_str(json).expect("Should deserialize with defaults");
316
317 assert!(trigger.enabled); assert_eq!(trigger.priority, 100); assert!(trigger.response_template.is_none()); }
321
322 #[test]
323 fn test_trigger_with_keyword_config() {
324 let trigger = create_test_trigger("kw-1", 10, true);
325
326 match &trigger.config {
327 TriggerConfig::Keyword(config) => {
328 assert_eq!(config.patterns, vec!["test".to_string()]);
329 assert!(!config.case_insensitive);
330 assert!(!config.use_regex);
331 }
332 _ => panic!("Expected Keyword config"),
333 }
334 }
335
336 #[test]
337 fn test_trigger_clone() {
338 let trigger = create_mention_trigger("m1", 10);
339 let cloned = trigger.clone();
340
341 assert_eq!(cloned.id, trigger.id);
342 assert_eq!(cloned.name, trigger.name);
343 assert_eq!(cloned.priority, trigger.priority);
344 }
345
346 fn arb_trigger_id() -> impl Strategy<Value = String> {
354 "[a-zA-Z][a-zA-Z0-9_-]{0,19}".prop_map(|s| s)
355 }
356
357 fn arb_priority() -> impl Strategy<Value = u32> {
359 0u32..=1000u32
360 }
361
362 fn arb_trigger_config() -> impl Strategy<Value = (TriggerType, TriggerConfig)> {
364 prop_oneof![
365 Just((TriggerType::Mention, TriggerConfig::Mention)),
366 Just((TriggerType::DirectMessage, TriggerConfig::DirectMessage)),
367 prop::collection::vec("[a-zA-Z0-9]{1,10}", 1..5).prop_map(|patterns| {
368 (
369 TriggerType::Keyword,
370 TriggerConfig::Keyword(KeywordTriggerConfig {
371 patterns,
372 case_insensitive: false,
373 use_regex: false,
374 }),
375 )
376 }),
377 ]
378 }
379
380 fn arb_trigger() -> impl Strategy<Value = AutoReplyTrigger> {
382 (
383 arb_trigger_id(),
384 arb_priority(),
385 any::<bool>(),
386 arb_trigger_config(),
387 )
388 .prop_map(
389 |(id, priority, enabled, (trigger_type, config))| AutoReplyTrigger {
390 id,
391 name: "Test Trigger".to_string(),
392 enabled,
393 trigger_type,
394 config,
395 priority,
396 response_template: None,
397 },
398 )
399 }
400
401 fn arb_triggers_with_unique_ids() -> impl Strategy<Value = Vec<AutoReplyTrigger>> {
403 prop::collection::vec(arb_trigger(), 0..20).prop_map(|triggers| {
404 let mut seen_ids = std::collections::HashSet::new();
406 triggers
407 .into_iter()
408 .enumerate()
409 .map(|(i, mut t)| {
410 while seen_ids.contains(&t.id) {
412 t.id = format!("{}_{}", t.id, i);
413 }
414 seen_ids.insert(t.id.clone());
415 t
416 })
417 .collect()
418 })
419 }
420
421 proptest! {
422 #![proptest_config(ProptestConfig::with_cases(100))]
423
424 #[test]
429 fn prop_get_enabled_triggers_returns_all_enabled(
430 triggers in arb_triggers_with_unique_ids()
431 ) {
432 let mut registry = TriggerRegistry::new();
436 for trigger in &triggers {
437 registry.register(trigger.clone());
438 }
439
440 let enabled = registry.get_enabled_triggers();
441 let expected_enabled_count = triggers.iter().filter(|t| t.enabled).count();
442
443 prop_assert_eq!(
445 enabled.len(),
446 expected_enabled_count,
447 "Expected {} enabled triggers, got {}",
448 expected_enabled_count,
449 enabled.len()
450 );
451
452 for trigger in &enabled {
454 prop_assert!(
455 trigger.enabled,
456 "Trigger {} should be enabled",
457 trigger.id
458 );
459 }
460 }
461
462 #[test]
467 fn prop_get_enabled_triggers_sorted_by_priority(
468 triggers in arb_triggers_with_unique_ids()
469 ) {
470 let mut registry = TriggerRegistry::new();
474 for trigger in &triggers {
475 registry.register(trigger.clone());
476 }
477
478 let enabled = registry.get_enabled_triggers();
479
480 for i in 1..enabled.len() {
482 prop_assert!(
483 enabled[i - 1].priority <= enabled[i].priority,
484 "Triggers should be sorted by priority: {} (priority {}) should come before {} (priority {})",
485 enabled[i - 1].id,
486 enabled[i - 1].priority,
487 enabled[i].id,
488 enabled[i].priority
489 );
490 }
491 }
492
493 #[test]
498 fn prop_empty_registry_returns_empty_list(_seed in any::<u64>()) {
499 let registry = TriggerRegistry::new();
503 let enabled = registry.get_enabled_triggers();
504
505 prop_assert!(
506 enabled.is_empty(),
507 "Empty registry should return empty list, got {} triggers",
508 enabled.len()
509 );
510 }
511
512 #[test]
515 fn prop_all_disabled_returns_empty_list(
516 triggers in prop::collection::vec(arb_trigger(), 1..10)
517 ) {
518 let mut registry = TriggerRegistry::new();
522
523 for (i, mut trigger) in triggers.into_iter().enumerate() {
525 trigger.id = format!("trigger_{}", i); trigger.enabled = false;
527 registry.register(trigger);
528 }
529
530 let enabled = registry.get_enabled_triggers();
531
532 prop_assert!(
533 enabled.is_empty(),
534 "All disabled triggers should return empty list, got {} triggers",
535 enabled.len()
536 );
537 }
538
539 #[test]
542 fn prop_first_trigger_has_highest_priority(
543 triggers in arb_triggers_with_unique_ids()
544 .prop_filter("Need at least one enabled trigger", |ts| ts.iter().any(|t| t.enabled))
545 ) {
546 let mut registry = TriggerRegistry::new();
550 for trigger in &triggers {
551 registry.register(trigger.clone());
552 }
553
554 let enabled = registry.get_enabled_triggers();
555
556 let min_priority = triggers
558 .iter()
559 .filter(|t| t.enabled)
560 .map(|t| t.priority)
561 .min()
562 .unwrap();
563
564 prop_assert_eq!(
566 enabled[0].priority,
567 min_priority,
568 "First trigger should have minimum priority {}, got {}",
569 min_priority,
570 enabled[0].priority
571 );
572 }
573
574 #[test]
577 fn prop_registration_order_does_not_affect_priority_sort(
578 triggers in arb_triggers_with_unique_ids()
579 .prop_filter("Need at least 2 enabled triggers", |ts| ts.iter().filter(|t| t.enabled).count() >= 2)
580 ) {
581 let mut registry1 = TriggerRegistry::new();
586 for trigger in &triggers {
587 registry1.register(trigger.clone());
588 }
589
590 let mut registry2 = TriggerRegistry::new();
592 for trigger in triggers.iter().rev() {
593 registry2.register(trigger.clone());
594 }
595
596 let enabled1 = registry1.get_enabled_triggers();
597 let enabled2 = registry2.get_enabled_triggers();
598
599 prop_assert_eq!(
601 enabled1.len(),
602 enabled2.len(),
603 "Both registries should have same number of enabled triggers"
604 );
605
606 for (t1, t2) in enabled1.iter().zip(enabled2.iter()) {
608 prop_assert_eq!(
609 t1.priority,
610 t2.priority,
611 "Priority order should be consistent regardless of registration order"
612 );
613 }
614 }
615
616 #[test]
619 fn prop_same_priority_triggers_all_returned(
620 base_priority in arb_priority(),
621 count in 2usize..5
622 ) {
623 let mut registry = TriggerRegistry::new();
627
628 for i in 0..count {
630 let trigger = AutoReplyTrigger {
631 id: format!("trigger_{}", i),
632 name: format!("Trigger {}", i),
633 enabled: true,
634 trigger_type: TriggerType::Mention,
635 config: TriggerConfig::Mention,
636 priority: base_priority,
637 response_template: None,
638 };
639 registry.register(trigger);
640 }
641
642 let enabled = registry.get_enabled_triggers();
643
644 prop_assert_eq!(
646 enabled.len(),
647 count,
648 "All {} triggers with same priority should be returned, got {}",
649 count,
650 enabled.len()
651 );
652
653 for trigger in &enabled {
655 prop_assert_eq!(
656 trigger.priority,
657 base_priority,
658 "All triggers should have priority {}, got {}",
659 base_priority,
660 trigger.priority
661 );
662 }
663 }
664
665 #[test]
668 fn prop_disabled_triggers_do_not_affect_enabled_order(
669 enabled_priorities in prop::collection::vec(arb_priority(), 1..5),
670 disabled_priorities in prop::collection::vec(arb_priority(), 1..5)
671 ) {
672 let mut registry = TriggerRegistry::new();
676
677 for (i, priority) in enabled_priorities.iter().enumerate() {
679 let trigger = AutoReplyTrigger {
680 id: format!("enabled_{}", i),
681 name: format!("Enabled Trigger {}", i),
682 enabled: true,
683 trigger_type: TriggerType::Mention,
684 config: TriggerConfig::Mention,
685 priority: *priority,
686 response_template: None,
687 };
688 registry.register(trigger);
689 }
690
691 for (i, priority) in disabled_priorities.iter().enumerate() {
693 let trigger = AutoReplyTrigger {
694 id: format!("disabled_{}", i),
695 name: format!("Disabled Trigger {}", i),
696 enabled: false,
697 trigger_type: TriggerType::Mention,
698 config: TriggerConfig::Mention,
699 priority: *priority,
700 response_template: None,
701 };
702 registry.register(trigger);
703 }
704
705 let enabled = registry.get_enabled_triggers();
706
707 prop_assert_eq!(
709 enabled.len(),
710 enabled_priorities.len(),
711 "Should only return enabled triggers"
712 );
713
714 for trigger in &enabled {
716 prop_assert!(
717 !trigger.id.starts_with("disabled_"),
718 "Disabled trigger {} should not be in result",
719 trigger.id
720 );
721 }
722
723 for i in 1..enabled.len() {
725 prop_assert!(
726 enabled[i - 1].priority <= enabled[i].priority,
727 "Enabled triggers should be sorted by priority"
728 );
729 }
730 }
731 }
732}