1use std::collections::HashMap;
45use std::sync::Arc;
46use std::sync::RwLock;
47use std::sync::atomic::{AtomicBool, Ordering};
48
49use serde::{Deserialize, Serialize};
50
51use crate::bus::EventBus;
52use crate::error::Result;
53
54#[derive(Clone, Debug)]
61pub struct ConditionLimits {
62 pub max_path_depth: usize,
67 pub max_operators: usize,
72 pub max_string_length: usize,
77}
78
79impl Default for ConditionLimits {
80 fn default() -> Self {
81 Self {
82 max_path_depth: 10,
83 max_operators: 20,
84 max_string_length: 10_000,
85 }
86 }
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize)]
95pub struct TriggerContext {
96 pub event_name: String,
98 pub event_data: serde_json::Value,
100 pub rule_id: String,
102 pub rule_name: String,
104 pub action_config: serde_json::Value,
106}
107
108pub type TriggerActionFn = Arc<
112 dyn Fn(TriggerContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
113 + Send
114 + Sync,
115>;
116
117#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct TriggerRule {
124 pub id: String,
126 #[serde(default)]
128 pub name: String,
129 pub event_pattern: String,
136 #[serde(default)]
140 pub condition: Option<serde_json::Value>,
141 pub action_type: String,
143 #[serde(default)]
145 pub action_config: serde_json::Value,
146 #[serde(default = "default_true")]
148 pub enabled: bool,
149 #[serde(default)]
151 pub priority: i32,
152}
153
154fn default_true() -> bool {
155 true
156}
157
158pub trait RuleStorage: Send + Sync + 'static {
174 fn add(&self, rule: TriggerRule);
176
177 fn remove(&self, rule_id: &str) -> Option<TriggerRule>;
179
180 fn get(&self, rule_id: &str) -> Option<TriggerRule>;
182
183 fn update(&self, rule: TriggerRule) -> bool;
185
186 fn list(&self) -> Vec<TriggerRule>;
188
189 fn count(&self) -> usize;
191}
192
193pub struct InMemoryRuleStorage {
199 rules: RwLock<Vec<TriggerRule>>,
200}
201
202impl InMemoryRuleStorage {
203 pub fn new() -> Self {
205 Self {
206 rules: RwLock::new(Vec::new()),
207 }
208 }
209}
210
211impl Default for InMemoryRuleStorage {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217impl RuleStorage for InMemoryRuleStorage {
218 fn add(&self, rule: TriggerRule) {
219 let mut rules = self.rules.write().unwrap();
220 rules.push(rule);
221 rules.sort_by_key(|r| r.priority);
222 }
223
224 fn remove(&self, rule_id: &str) -> Option<TriggerRule> {
225 let mut rules = self.rules.write().unwrap();
226 let pos = rules.iter().position(|r| r.id == rule_id)?;
227 Some(rules.remove(pos))
228 }
229
230 fn get(&self, rule_id: &str) -> Option<TriggerRule> {
231 self.rules
232 .read()
233 .unwrap()
234 .iter()
235 .find(|r| r.id == rule_id)
236 .cloned()
237 }
238
239 fn update(&self, rule: TriggerRule) -> bool {
240 let mut rules = self.rules.write().unwrap();
241 if let Some(pos) = rules.iter().position(|r| r.id == rule.id) {
242 rules[pos] = rule;
243 rules.sort_by_key(|r| r.priority);
244 true
245 } else {
246 false
247 }
248 }
249
250 fn list(&self) -> Vec<TriggerRule> {
251 self.rules.read().unwrap().clone()
252 }
253
254 fn count(&self) -> usize {
255 self.rules.read().unwrap().len()
256 }
257}
258
259struct TriggerEngineState {
264 storage: Arc<dyn RuleStorage>,
265 actions: RwLock<HashMap<String, TriggerActionFn>>,
266 running: AtomicBool,
267 limits: ConditionLimits,
269}
270
271pub struct TriggerRuleEngine {
285 bus: EventBus,
286 state: Arc<TriggerEngineState>,
287 limits: ConditionLimits,
289}
290
291impl TriggerRuleEngine {
292 pub fn new(bus: EventBus) -> Self {
297 Self {
298 bus,
299 state: Arc::new(TriggerEngineState {
300 storage: Arc::new(InMemoryRuleStorage::new()),
301 actions: RwLock::new(HashMap::new()),
302 running: AtomicBool::new(false),
303 limits: ConditionLimits::default(),
304 }),
305 limits: ConditionLimits::default(),
306 }
307 }
308
309 pub fn with_storage(bus: EventBus, storage: Arc<dyn RuleStorage>) -> Self {
313 Self {
314 bus,
315 state: Arc::new(TriggerEngineState {
316 storage,
317 actions: RwLock::new(HashMap::new()),
318 running: AtomicBool::new(false),
319 limits: ConditionLimits::default(),
320 }),
321 limits: ConditionLimits::default(),
322 }
323 }
324
325 pub fn with_limits(bus: EventBus, limits: ConditionLimits) -> Self {
327 Self {
328 bus,
329 state: Arc::new(TriggerEngineState {
330 storage: Arc::new(InMemoryRuleStorage::new()),
331 actions: RwLock::new(HashMap::new()),
332 running: AtomicBool::new(false),
333 limits: limits.clone(),
334 }),
335 limits,
336 }
337 }
338
339 pub fn with_storage_and_limits(
341 bus: EventBus,
342 storage: Arc<dyn RuleStorage>,
343 limits: ConditionLimits,
344 ) -> Self {
345 Self {
346 bus,
347 state: Arc::new(TriggerEngineState {
348 storage,
349 actions: RwLock::new(HashMap::new()),
350 running: AtomicBool::new(false),
351 limits: limits.clone(),
352 }),
353 limits,
354 }
355 }
356
357 pub fn storage(&self) -> &Arc<dyn RuleStorage> {
361 &self.state.storage
362 }
363
364 pub fn register_action<F, Fut>(&self, action_type: &str, handler: F)
368 where
369 F: Fn(TriggerContext) -> Fut + Send + Sync + 'static,
370 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
371 {
372 let wrapped: TriggerActionFn = Arc::new(move |ctx| {
373 let fut = handler(ctx);
374 Box::pin(fut)
375 as std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
376 });
377 self.state
378 .actions
379 .write()
380 .unwrap()
381 .insert(action_type.to_string(), wrapped);
382 }
383
384 pub fn add_rule(&self, rule: TriggerRule) {
386 self.state.storage.add(rule);
387 }
388
389 pub fn add_rules(&self, new_rules: Vec<TriggerRule>) {
391 for rule in new_rules {
392 self.state.storage.add(rule);
393 }
394 }
395
396 pub fn remove_rule(&self, rule_id: &str) -> Option<TriggerRule> {
400 self.state.storage.remove(rule_id)
401 }
402
403 pub fn update_rule(&self, rule: TriggerRule) -> bool {
407 self.state.storage.update(rule)
408 }
409
410 pub fn enable_rule(&self, rule_id: &str) -> bool {
412 if let Some(mut rule) = self.state.storage.get(rule_id) {
413 rule.enabled = true;
414 self.state.storage.update(rule)
415 } else {
416 false
417 }
418 }
419
420 pub fn disable_rule(&self, rule_id: &str) -> bool {
422 if let Some(mut rule) = self.state.storage.get(rule_id) {
423 rule.enabled = false;
424 self.state.storage.update(rule)
425 } else {
426 false
427 }
428 }
429
430 pub fn list_rules(&self) -> Vec<TriggerRule> {
432 self.state.storage.list()
433 }
434
435 pub fn get_rule(&self, rule_id: &str) -> Option<TriggerRule> {
437 self.state.storage.get(rule_id)
438 }
439
440 pub fn rule_count(&self) -> usize {
442 self.state.storage.count()
443 }
444
445 pub fn limits(&self) -> &ConditionLimits {
447 &self.limits
448 }
449
450 pub fn list_action_types(&self) -> Vec<String> {
452 self.state.actions.read().unwrap().keys().cloned().collect()
453 }
454
455 pub fn is_running(&self) -> bool {
457 self.state.running.load(Ordering::Relaxed)
458 }
459
460 pub async fn start(&self) -> Result<()> {
466 if self.state.running.load(Ordering::Relaxed) {
467 return Ok(());
468 }
469
470 let state = self.state.clone();
471 self.bus.register_publish_callback(Arc::new(
472 move |event_name: &str, data: serde_json::Value| {
473 let state = state.clone();
474 let event_name = event_name.to_string();
475 tokio::spawn(async move {
476 if !state.running.load(Ordering::Relaxed) {
478 return;
479 }
480 let _ =
481 TriggerRuleEngine::evaluate_rules(&state, &event_name, &data).await;
482 });
483 },
484 ));
485
486 self.state.running.store(true, Ordering::Relaxed);
487 Ok(())
488 }
489
490 pub fn stop(&self) {
495 self.state.running.store(false, Ordering::Relaxed);
496 }
497
498 pub async fn process_event(
503 &self,
504 event_name: &str,
505 event_data: &serde_json::Value,
506 ) -> Vec<Result<()>> {
507 Self::evaluate_rules(&self.state, event_name, event_data).await
508 }
509
510 async fn evaluate_rules(
514 state: &Arc<TriggerEngineState>,
515 event_name: &str,
516 event_data: &serde_json::Value,
517 ) -> Vec<Result<()>> {
518 let rules = state.storage.list();
521 let actions = state.actions.read().unwrap().clone();
522
523 let mut results = Vec::new();
524
525 for rule in &rules {
526 if !rule.enabled {
527 continue;
528 }
529
530 if !crate::topic::matches(&rule.event_pattern, event_name) {
532 continue;
533 }
534
535 if let Some(ref condition) = rule.condition {
537 if !matches_condition(event_data, condition, &state.limits) {
538 continue;
539 }
540 }
541
542 let Some(action_fn) = actions.get(&rule.action_type) else {
544 tracing::warn!(
545 rule_id = %rule.id,
546 action_type = %rule.action_type,
547 "Trigger rule references unknown action type"
548 );
549 continue;
550 };
551
552 let ctx = TriggerContext {
554 event_name: event_name.to_string(),
555 event_data: event_data.clone(),
556 rule_id: rule.id.clone(),
557 rule_name: rule.name.clone(),
558 action_config: rule.action_config.clone(),
559 };
560
561 let result = action_fn(ctx).await;
562 results.push(result);
563 }
564
565 results
566 }
567}
568
569impl Drop for TriggerRuleEngine {
570 fn drop(&mut self) {
571 self.stop();
572 }
573}
574
575#[derive(Clone, Debug)]
581pub struct TriggerEvent {
582 pub name: String,
584 pub data: serde_json::Value,
586}
587
588impl crate::event::Event for TriggerEvent {
589 fn event_name() -> &'static str {
590 "trigger.event"
591 }
592
593 fn topic() -> &'static str {
594 "trigger"
595 }
596}
597
598fn matches_condition(
612 data: &serde_json::Value,
613 condition: &serde_json::Value,
614 limits: &ConditionLimits,
615) -> bool {
616 let Some(condition_obj) = condition.as_object() else {
617 return true;
618 };
619
620 let mut operator_count = 0;
621
622 for (field, ops) in condition_obj {
623 let value = json_path_get(data, field, limits.max_path_depth);
624 let Some(value) = value else {
625 return false;
626 };
627
628 if !match_operators(
629 value,
630 ops,
631 &mut operator_count,
632 limits.max_operators,
633 limits.max_string_length,
634 ) {
635 return false;
636 }
637 }
638
639 true
640}
641
642fn json_path_get<'a>(
644 data: &'a serde_json::Value,
645 path: &str,
646 max_depth: usize,
647) -> Option<&'a serde_json::Value> {
648 let mut current = data;
649 let mut depth = 0;
650
651 for segment in path.split('.') {
652 if depth >= max_depth {
653 tracing::warn!(
654 path = %path,
655 depth = depth,
656 max = max_depth,
657 "json_path_get exceeded maximum depth, rejecting"
658 );
659 return None;
660 }
661 current = current.get(segment)?;
662 depth += 1;
663 }
664
665 Some(current)
666}
667
668fn match_operators(
670 value: &serde_json::Value,
671 ops: &serde_json::Value,
672 operator_count: &mut usize,
673 max_operators: usize,
674 max_string_length: usize,
675) -> bool {
676 let Some(ops_obj) = ops.as_object() else {
677 return value == ops;
679 };
680
681 for (op, expected) in ops_obj {
682 *operator_count += 1;
683
684 if *operator_count > max_operators {
685 tracing::warn!(
686 count = *operator_count,
687 max = max_operators,
688 "Condition exceeded maximum operator count, rejecting"
689 );
690 return false;
691 }
692
693 match op.as_str() {
694 "$eq" => {
695 if value != expected {
696 return false;
697 }
698 }
699 "$ne" => {
700 if value == expected {
701 return false;
702 }
703 }
704 "$gt" => {
705 if !json_value_gt(value, expected) {
706 return false;
707 }
708 }
709 "$gte" => {
710 if !json_value_gte(value, expected) {
711 return false;
712 }
713 }
714 "$lt" => {
715 if !json_value_lt(value, expected) {
716 return false;
717 }
718 }
719 "$lte" => {
720 if !json_value_lte(value, expected) {
721 return false;
722 }
723 }
724 "$in" => {
725 let Some(arr) = expected.as_array() else {
726 return false;
727 };
728 if !arr.contains(value) {
729 return false;
730 }
731 }
732 "$contains" => {
733 let (Some(s), Some(pattern)) = (value.as_str(), expected.as_str()) else {
734 return false;
735 };
736 if s.len() > max_string_length || pattern.len() > max_string_length {
737 tracing::warn!(
738 s_len = s.len(),
739 p_len = pattern.len(),
740 max = max_string_length,
741 "$contains string exceeded length limit, rejecting"
742 );
743 return false;
744 }
745 if !s.contains(pattern) {
746 return false;
747 }
748 }
749 _ => {
750 tracing::warn!(operator = %op, "Unknown condition operator, ignoring");
751 }
752 }
753 }
754
755 true
756}
757
758fn json_value_gt(a: &serde_json::Value, b: &serde_json::Value) -> bool {
759 match (a, b) {
760 (serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
761 if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
762 return a > b;
763 }
764 false
765 }
766 (serde_json::Value::String(a), serde_json::Value::String(b)) => a > b,
767 _ => false,
768 }
769}
770
771fn json_value_gte(a: &serde_json::Value, b: &serde_json::Value) -> bool {
772 a == b || json_value_gt(a, b)
773}
774
775fn json_value_lt(a: &serde_json::Value, b: &serde_json::Value) -> bool {
776 match (a, b) {
777 (serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
778 if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
779 return a < b;
780 }
781 false
782 }
783 (serde_json::Value::String(a), serde_json::Value::String(b)) => a < b,
784 _ => false,
785 }
786}
787
788fn json_value_lte(a: &serde_json::Value, b: &serde_json::Value) -> bool {
789 a == b || json_value_lt(a, b)
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795
796 fn make_rule(
797 id: &str,
798 pattern: &str,
799 action_type: &str,
800 action_config: serde_json::Value,
801 ) -> TriggerRule {
802 TriggerRule {
803 id: id.to_string(),
804 name: format!("Rule {}", id),
805 event_pattern: pattern.to_string(),
806 condition: None,
807 action_type: action_type.to_string(),
808 action_config,
809 enabled: true,
810 priority: 0,
811 }
812 }
813
814 #[test]
817 fn test_add_and_list_rules() {
818 let bus = EventBus::new();
819 let engine = TriggerRuleEngine::new(bus);
820
821 engine.add_rule(make_rule(
822 "r1",
823 "user.*",
824 "log",
825 serde_json::json!({}),
826 ));
827 engine.add_rule(make_rule(
828 "r2",
829 "order.**",
830 "notify",
831 serde_json::json!({}),
832 ));
833
834 assert_eq!(engine.rule_count(), 2);
835 let rules = engine.list_rules();
836 assert_eq!(rules.len(), 2);
837 }
838
839 #[test]
840 fn test_remove_rule() {
841 let bus = EventBus::new();
842 let engine = TriggerRuleEngine::new(bus);
843
844 engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
845 engine.add_rule(make_rule("r2", "order.*", "log", serde_json::json!({})));
846
847 let removed = engine.remove_rule("r1").unwrap();
848 assert_eq!(removed.id, "r1");
849 assert_eq!(engine.rule_count(), 1);
850 assert!(engine.remove_rule("nonexistent").is_none());
851 }
852
853 #[test]
854 fn test_update_rule() {
855 let bus = EventBus::new();
856 let engine = TriggerRuleEngine::new(bus);
857
858 engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
859
860 let mut updated = make_rule("r1", "user.**", "notify", serde_json::json!({}));
861 updated.name = "Updated Rule".to_string();
862 assert!(engine.update_rule(updated));
863
864 let rule = engine.get_rule("r1").unwrap();
865 assert_eq!(rule.event_pattern, "user.**");
866 assert_eq!(rule.action_type, "notify");
867 assert_eq!(rule.name, "Updated Rule");
868 }
869
870 #[test]
871 fn test_enable_disable_rule() {
872 let bus = EventBus::new();
873 let engine = TriggerRuleEngine::new(bus);
874
875 engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
876
877 assert!(engine.disable_rule("r1"));
878 assert!(!engine.get_rule("r1").unwrap().enabled);
879
880 assert!(engine.enable_rule("r1"));
881 assert!(engine.get_rule("r1").unwrap().enabled);
882
883 assert!(!engine.disable_rule("nonexistent"));
884 }
885
886 #[test]
887 fn test_priority_sorting() {
888 let bus = EventBus::new();
889 let engine = TriggerRuleEngine::new(bus);
890
891 let mut r1 = make_rule("r1", "a", "log", serde_json::json!({}));
892 r1.priority = 10;
893 let mut r2 = make_rule("r2", "b", "log", serde_json::json!({}));
894 r2.priority = 1;
895 let mut r3 = make_rule("r3", "c", "log", serde_json::json!({}));
896 r3.priority = 5;
897
898 engine.add_rules(vec![r1, r2, r3]);
899
900 let rules = engine.list_rules();
901 assert_eq!(rules[0].id, "r2"); assert_eq!(rules[1].id, "r3"); assert_eq!(rules[2].id, "r1"); }
905
906 #[test]
907 fn test_register_action() {
908 let bus = EventBus::new();
909 let engine = TriggerRuleEngine::new(bus);
910
911 engine.register_action("log", |_ctx: TriggerContext| async { Ok(()) });
912 engine.register_action("notify", |_ctx: TriggerContext| async { Ok(()) });
913
914 let types = engine.list_action_types();
915 assert_eq!(types.len(), 2);
916 assert!(types.contains(&"log".to_string()));
917 }
918
919 #[test]
922 fn test_condition_eq() {
923 let data = serde_json::json!({"status": "published", "level": 3});
924 let condition = serde_json::json!({"status": {"$eq": "published"}});
925 assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
926
927 let condition = serde_json::json!({"status": {"$eq": "draft"}});
928 assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
929 }
930
931 #[test]
932 fn test_condition_ne() {
933 let data = serde_json::json!({"status": "published"});
934 let condition = serde_json::json!({"status": {"$ne": "draft"}});
935 assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
936 }
937
938 #[test]
939 fn test_condition_gt_lt() {
940 let data = serde_json::json!({"amount": 500});
941 let condition = serde_json::json!({"amount": {"$gt": 100, "$lt": 1000}});
942 assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
943
944 let data = serde_json::json!({"amount": 50});
945 assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
946 }
947
948 #[test]
949 fn test_condition_in() {
950 let data = serde_json::json!({"category": "books"});
951 let condition = serde_json::json!({"category": {"$in": ["books", "electronics"]}});
952 assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
953
954 let data = serde_json::json!({"category": "clothing"});
955 assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
956 }
957
958 #[test]
959 fn test_condition_contains() {
960 let data = serde_json::json!({"title": "Hello World Article"});
961 let condition = serde_json::json!({"title": {"$contains": "World"}});
962 assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
963
964 let condition = serde_json::json!({"title": {"$contains": "Missing"}});
965 assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
966 }
967
968 #[test]
969 fn test_condition_nested_path() {
970 let data = serde_json::json!({"user": {"level": 5}});
971 let condition = serde_json::json!({"user.level": {"$gte": 3}});
972 assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
973 }
974
975 #[test]
976 fn test_condition_missing_field() {
977 let data = serde_json::json!({"status": "ok"});
978 let condition = serde_json::json!({"missing_field": {"$eq": "value"}});
979 assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
980 }
981
982 #[test]
983 fn test_condition_no_condition() {
984 let data = serde_json::json!({"status": "ok"});
985 assert!(matches_condition(&data, &serde_json::Value::Null, &ConditionLimits::default()));
986 }
987
988 #[test]
989 fn test_condition_limits_path_depth() {
990 let data = serde_json::json!({"a": {"b": {"c": {"d": {"e": "deep"}}}}});
991
992 let limits = ConditionLimits::default();
994 assert!(matches_condition(
995 &data,
996 &serde_json::json!({"a.b.c.d.e": {"$eq": "deep"}}),
997 &limits
998 ));
999
1000 let strict_limits = ConditionLimits {
1002 max_path_depth: 2,
1003 ..Default::default()
1004 };
1005 assert!(!matches_condition(
1006 &data,
1007 &serde_json::json!({"a.b.c.d.e": {"$eq": "deep"}}),
1008 &strict_limits
1009 ));
1010 }
1011
1012 #[test]
1013 fn test_condition_limits_operator_count() {
1014 let data = serde_json::json!({"value": 42});
1015
1016 let limits = ConditionLimits::default();
1018 assert!(matches_condition(
1019 &data,
1020 &serde_json::json!({"value": {"$gt": 0, "$lt": 100, "$ne": 50}}),
1021 &limits
1022 ));
1023
1024 let strict_limits = ConditionLimits {
1026 max_operators: 2,
1027 ..Default::default()
1028 };
1029 assert!(!matches_condition(
1030 &data,
1031 &serde_json::json!({"value": {"$gt": 0, "$lt": 100, "$ne": 50}}),
1032 &strict_limits
1033 ));
1034 }
1035
1036 #[test]
1037 fn test_condition_limits_string_length() {
1038 let long_string = "a".repeat(20_000);
1039 let data = serde_json::json!({"text": long_string});
1040
1041 let limits = ConditionLimits::default();
1043 assert!(!matches_condition(
1044 &data,
1045 &serde_json::json!({"text": {"$contains": "a"}}),
1046 &limits
1047 ));
1048
1049 let relaxed_limits = ConditionLimits {
1051 max_string_length: 100_000,
1052 ..Default::default()
1053 };
1054 assert!(matches_condition(
1055 &data,
1056 &serde_json::json!({"text": {"$contains": "a"}}),
1057 &relaxed_limits
1058 ));
1059 }
1060
1061 #[tokio::test]
1064 async fn test_process_event_basic_matching() {
1065 let bus = EventBus::new();
1066 let engine = TriggerRuleEngine::new(bus);
1067
1068 let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
1069 let executed_clone = executed.clone();
1070
1071 engine.register_action("collect", move |ctx: TriggerContext| {
1072 let executed_clone = executed_clone.clone();
1073 async move {
1074 executed_clone
1075 .write()
1076 .unwrap()
1077 .push(ctx.rule_id.clone());
1078 Ok(())
1079 }
1080 });
1081
1082 engine.add_rule(make_rule(
1083 "r1",
1084 "user.*",
1085 "collect",
1086 serde_json::json!({}),
1087 ));
1088 engine.add_rule(make_rule(
1089 "r2",
1090 "order.*",
1091 "collect",
1092 serde_json::json!({}),
1093 ));
1094
1095 let results = engine
1096 .process_event("user.created", &serde_json::json!({}))
1097 .await;
1098
1099 assert_eq!(results.len(), 1);
1100 assert!(results[0].is_ok());
1101 let executed = executed.read().unwrap();
1102 assert_eq!(executed.len(), 1);
1103 assert_eq!(executed[0], "r1");
1104 }
1105
1106 #[tokio::test]
1107 async fn test_process_event_with_condition() {
1108 let bus = EventBus::new();
1109 let engine = TriggerRuleEngine::new(bus);
1110
1111 let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
1112 let executed_clone = executed.clone();
1113
1114 engine.register_action("collect", move |ctx: TriggerContext| {
1115 let executed_clone = executed_clone.clone();
1116 async move {
1117 executed_clone
1118 .write()
1119 .unwrap()
1120 .push(ctx.rule_id.clone());
1121 Ok(())
1122 }
1123 });
1124
1125 let mut rule = make_rule("r1", "order.*", "collect", serde_json::json!({}));
1126 rule.condition = Some(serde_json::json!({"amount": {"$gt": 100}}));
1127 engine.add_rule(rule);
1128
1129 let results = engine
1131 .process_event("order.created", &serde_json::json!({"amount": 50}))
1132 .await;
1133 assert_eq!(results.len(), 0);
1134
1135 let results = engine
1137 .process_event("order.created", &serde_json::json!({"amount": 200}))
1138 .await;
1139 assert_eq!(results.len(), 1);
1140 }
1141
1142 #[tokio::test]
1143 async fn test_process_event_disabled_rule() {
1144 let bus = EventBus::new();
1145 let engine = TriggerRuleEngine::new(bus);
1146
1147 let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
1148 let executed_clone = executed.clone();
1149
1150 engine.register_action("collect", move |ctx: TriggerContext| {
1151 let executed_clone = executed_clone.clone();
1152 async move {
1153 executed_clone
1154 .write()
1155 .unwrap()
1156 .push(ctx.rule_id.clone());
1157 Ok(())
1158 }
1159 });
1160
1161 let mut rule = make_rule("r1", "user.*", "collect", serde_json::json!({}));
1162 rule.enabled = false;
1163 engine.add_rule(rule);
1164
1165 let results = engine
1166 .process_event("user.created", &serde_json::json!({}))
1167 .await;
1168 assert_eq!(results.len(), 0);
1169 }
1170
1171 #[tokio::test]
1172 async fn test_process_event_unknown_action() {
1173 let bus = EventBus::new();
1174 let engine = TriggerRuleEngine::new(bus);
1175
1176 engine.add_rule(make_rule("r1", "user.*", "unknown_action", serde_json::json!({})));
1178
1179 let results = engine
1180 .process_event("user.created", &serde_json::json!({}))
1181 .await;
1182 assert_eq!(results.len(), 0); }
1184
1185 #[tokio::test]
1186 async fn test_process_event_wildcard_pattern() {
1187 let bus = EventBus::new();
1188 let engine = TriggerRuleEngine::new(bus);
1189
1190 let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
1191 let executed_clone = executed.clone();
1192
1193 engine.register_action("collect", move |ctx: TriggerContext| {
1194 let executed_clone = executed_clone.clone();
1195 async move {
1196 executed_clone
1197 .write()
1198 .unwrap()
1199 .push(ctx.rule_id.clone());
1200 Ok(())
1201 }
1202 });
1203
1204 engine.add_rule(make_rule("r1", "user.**", "collect", serde_json::json!({})));
1205
1206 let results = engine
1208 .process_event("user.profile.updated", &serde_json::json!({}))
1209 .await;
1210 assert_eq!(results.len(), 1);
1211 }
1212
1213 #[test]
1214 fn test_trigger_rule_serialization() {
1215 let rule = TriggerRule {
1216 id: "rule-1".to_string(),
1217 name: "Test Rule".to_string(),
1218 event_pattern: "user.*".to_string(),
1219 condition: Some(serde_json::json!({"status": {"$eq": "active"}})),
1220 action_type: "notify".to_string(),
1221 action_config: serde_json::json!({"channel": "email"}),
1222 enabled: true,
1223 priority: 0,
1224 };
1225
1226 let json = serde_json::to_string(&rule).unwrap();
1227 let deserialized: TriggerRule = serde_json::from_str(&json).unwrap();
1228 assert_eq!(deserialized.id, "rule-1");
1229 assert_eq!(deserialized.event_pattern, "user.*");
1230 assert!(deserialized.condition.is_some());
1231 }
1232
1233 #[tokio::test]
1236 async fn test_start_stop() {
1237 let bus = EventBus::new();
1238 let engine = TriggerRuleEngine::new(bus);
1239
1240 assert!(!engine.is_running());
1241
1242 engine.start().await.unwrap();
1243 assert!(engine.is_running());
1244
1245 engine.start().await.unwrap();
1247 assert!(engine.is_running());
1248
1249 engine.stop();
1250 assert!(!engine.is_running());
1251 }
1252
1253 #[tokio::test]
1254 async fn test_process_event_works_regardless_of_running_state() {
1255 let bus = EventBus::new();
1259 let engine = TriggerRuleEngine::new(bus);
1260
1261 let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
1262 let executed_clone = executed.clone();
1263
1264 engine.register_action("collect", move |ctx: TriggerContext| {
1265 let executed_clone = executed_clone.clone();
1266 async move {
1267 executed_clone
1268 .write()
1269 .unwrap()
1270 .push(ctx.rule_id.clone());
1271 Ok(())
1272 }
1273 });
1274
1275 engine.add_rule(make_rule("r1", "user.*", "collect", serde_json::json!({})));
1276
1277 let results = engine
1279 .process_event("user.created", &serde_json::json!({}))
1280 .await;
1281 assert_eq!(results.len(), 1);
1282 assert!(results[0].is_ok());
1283 let log = executed.read().unwrap();
1284 assert_eq!(log.len(), 1);
1285 assert_eq!(log[0], "r1");
1286 }
1287
1288 #[test]
1291 fn test_in_memory_rule_storage_basic() {
1292 let storage = InMemoryRuleStorage::new();
1293
1294 storage.add(make_rule("r1", "user.*", "log", serde_json::json!({})));
1295 storage.add(make_rule("r2", "order.*", "notify", serde_json::json!({})));
1296
1297 assert_eq!(storage.count(), 2);
1298
1299 let rules = storage.list();
1300 assert_eq!(rules.len(), 2);
1301
1302 assert!(storage.get("r1").is_some());
1303 assert!(storage.get("nonexistent").is_none());
1304
1305 let removed = storage.remove("r1").unwrap();
1306 assert_eq!(removed.id, "r1");
1307 assert_eq!(storage.count(), 1);
1308 assert!(storage.remove("nonexistent").is_none());
1309
1310 let mut updated = make_rule("r2", "order.**", "email", serde_json::json!({}));
1311 updated.name = "Updated".to_string();
1312 assert!(storage.update(updated));
1313 assert_eq!(storage.get("r2").unwrap().name, "Updated");
1314 assert!(!storage.update(make_rule("r99", "x", "y", serde_json::json!({}))));
1315 }
1316
1317 #[test]
1318 fn test_with_custom_storage() {
1319 let storage: Arc<dyn RuleStorage> = Arc::new(InMemoryRuleStorage::new());
1320 let bus = EventBus::new();
1321 let engine = TriggerRuleEngine::with_storage(bus, storage.clone());
1322
1323 engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
1324
1325 assert_eq!(storage.count(), 1);
1327 assert_eq!(storage.get("r1").unwrap().event_pattern, "user.*");
1328
1329 assert_eq!(engine.rule_count(), 1);
1331 assert_eq!(engine.list_rules()[0].id, "r1");
1332 }
1333}