use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use serde::{Deserialize, Serialize};
use crate::bus::EventBus;
use crate::error::Result;
#[derive(Clone, Debug)]
pub struct ConditionLimits {
pub max_path_depth: usize,
pub max_operators: usize,
pub max_string_length: usize,
}
impl Default for ConditionLimits {
fn default() -> Self {
Self {
max_path_depth: 10,
max_operators: 20,
max_string_length: 10_000,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TriggerContext {
pub event_name: String,
pub event_data: serde_json::Value,
pub rule_id: String,
pub rule_name: String,
pub action_config: serde_json::Value,
}
pub type TriggerActionFn = Arc<
dyn Fn(TriggerContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
+ Send
+ Sync,
>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TriggerRule {
pub id: String,
#[serde(default)]
pub name: String,
pub event_pattern: String,
#[serde(default)]
pub condition: Option<serde_json::Value>,
pub action_type: String,
#[serde(default)]
pub action_config: serde_json::Value,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub priority: i32,
}
fn default_true() -> bool {
true
}
pub trait RuleStorage: Send + Sync + 'static {
fn add(&self, rule: TriggerRule);
fn remove(&self, rule_id: &str) -> Option<TriggerRule>;
fn get(&self, rule_id: &str) -> Option<TriggerRule>;
fn update(&self, rule: TriggerRule) -> bool;
fn list(&self) -> Vec<TriggerRule>;
fn count(&self) -> usize;
}
pub struct InMemoryRuleStorage {
rules: RwLock<Vec<TriggerRule>>,
}
impl InMemoryRuleStorage {
pub fn new() -> Self {
Self {
rules: RwLock::new(Vec::new()),
}
}
}
impl Default for InMemoryRuleStorage {
fn default() -> Self {
Self::new()
}
}
impl RuleStorage for InMemoryRuleStorage {
fn add(&self, rule: TriggerRule) {
let mut rules = self.rules.write().unwrap();
rules.push(rule);
rules.sort_by_key(|r| r.priority);
}
fn remove(&self, rule_id: &str) -> Option<TriggerRule> {
let mut rules = self.rules.write().unwrap();
let pos = rules.iter().position(|r| r.id == rule_id)?;
Some(rules.remove(pos))
}
fn get(&self, rule_id: &str) -> Option<TriggerRule> {
self.rules
.read()
.unwrap()
.iter()
.find(|r| r.id == rule_id)
.cloned()
}
fn update(&self, rule: TriggerRule) -> bool {
let mut rules = self.rules.write().unwrap();
if let Some(pos) = rules.iter().position(|r| r.id == rule.id) {
rules[pos] = rule;
rules.sort_by_key(|r| r.priority);
true
} else {
false
}
}
fn list(&self) -> Vec<TriggerRule> {
self.rules.read().unwrap().clone()
}
fn count(&self) -> usize {
self.rules.read().unwrap().len()
}
}
struct TriggerEngineState {
storage: Arc<dyn RuleStorage>,
actions: RwLock<HashMap<String, TriggerActionFn>>,
running: AtomicBool,
limits: ConditionLimits,
}
pub struct TriggerRuleEngine {
bus: EventBus,
state: Arc<TriggerEngineState>,
limits: ConditionLimits,
}
impl TriggerRuleEngine {
pub fn new(bus: EventBus) -> Self {
Self {
bus,
state: Arc::new(TriggerEngineState {
storage: Arc::new(InMemoryRuleStorage::new()),
actions: RwLock::new(HashMap::new()),
running: AtomicBool::new(false),
limits: ConditionLimits::default(),
}),
limits: ConditionLimits::default(),
}
}
pub fn with_storage(bus: EventBus, storage: Arc<dyn RuleStorage>) -> Self {
Self {
bus,
state: Arc::new(TriggerEngineState {
storage,
actions: RwLock::new(HashMap::new()),
running: AtomicBool::new(false),
limits: ConditionLimits::default(),
}),
limits: ConditionLimits::default(),
}
}
pub fn with_limits(bus: EventBus, limits: ConditionLimits) -> Self {
Self {
bus,
state: Arc::new(TriggerEngineState {
storage: Arc::new(InMemoryRuleStorage::new()),
actions: RwLock::new(HashMap::new()),
running: AtomicBool::new(false),
limits: limits.clone(),
}),
limits,
}
}
pub fn with_storage_and_limits(
bus: EventBus,
storage: Arc<dyn RuleStorage>,
limits: ConditionLimits,
) -> Self {
Self {
bus,
state: Arc::new(TriggerEngineState {
storage,
actions: RwLock::new(HashMap::new()),
running: AtomicBool::new(false),
limits: limits.clone(),
}),
limits,
}
}
pub fn storage(&self) -> &Arc<dyn RuleStorage> {
&self.state.storage
}
pub fn register_action<F, Fut>(&self, action_type: &str, handler: F)
where
F: Fn(TriggerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
let wrapped: TriggerActionFn = Arc::new(move |ctx| {
let fut = handler(ctx);
Box::pin(fut)
as std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
});
self.state
.actions
.write()
.unwrap()
.insert(action_type.to_string(), wrapped);
}
pub fn add_rule(&self, rule: TriggerRule) {
self.state.storage.add(rule);
}
pub fn add_rules(&self, new_rules: Vec<TriggerRule>) {
for rule in new_rules {
self.state.storage.add(rule);
}
}
pub fn remove_rule(&self, rule_id: &str) -> Option<TriggerRule> {
self.state.storage.remove(rule_id)
}
pub fn update_rule(&self, rule: TriggerRule) -> bool {
self.state.storage.update(rule)
}
pub fn enable_rule(&self, rule_id: &str) -> bool {
if let Some(mut rule) = self.state.storage.get(rule_id) {
rule.enabled = true;
self.state.storage.update(rule)
} else {
false
}
}
pub fn disable_rule(&self, rule_id: &str) -> bool {
if let Some(mut rule) = self.state.storage.get(rule_id) {
rule.enabled = false;
self.state.storage.update(rule)
} else {
false
}
}
pub fn list_rules(&self) -> Vec<TriggerRule> {
self.state.storage.list()
}
pub fn get_rule(&self, rule_id: &str) -> Option<TriggerRule> {
self.state.storage.get(rule_id)
}
pub fn rule_count(&self) -> usize {
self.state.storage.count()
}
pub fn limits(&self) -> &ConditionLimits {
&self.limits
}
pub fn list_action_types(&self) -> Vec<String> {
self.state.actions.read().unwrap().keys().cloned().collect()
}
pub fn is_running(&self) -> bool {
self.state.running.load(Ordering::Relaxed)
}
pub async fn start(&self) -> Result<()> {
if self.state.running.load(Ordering::Relaxed) {
return Ok(());
}
let state = self.state.clone();
self.bus.register_publish_callback(Arc::new(
move |event_name: &str, data: serde_json::Value| {
let state = state.clone();
let event_name = event_name.to_string();
tokio::spawn(async move {
if !state.running.load(Ordering::Relaxed) {
return;
}
let _ =
TriggerRuleEngine::evaluate_rules(&state, &event_name, &data).await;
});
},
));
self.state.running.store(true, Ordering::Relaxed);
Ok(())
}
pub fn stop(&self) {
self.state.running.store(false, Ordering::Relaxed);
}
pub async fn process_event(
&self,
event_name: &str,
event_data: &serde_json::Value,
) -> Vec<Result<()>> {
Self::evaluate_rules(&self.state, event_name, event_data).await
}
async fn evaluate_rules(
state: &Arc<TriggerEngineState>,
event_name: &str,
event_data: &serde_json::Value,
) -> Vec<Result<()>> {
let rules = state.storage.list();
let actions = state.actions.read().unwrap().clone();
let mut results = Vec::new();
for rule in &rules {
if !rule.enabled {
continue;
}
if !crate::topic::matches(&rule.event_pattern, event_name) {
continue;
}
if let Some(ref condition) = rule.condition {
if !matches_condition(event_data, condition, &state.limits) {
continue;
}
}
let Some(action_fn) = actions.get(&rule.action_type) else {
tracing::warn!(
rule_id = %rule.id,
action_type = %rule.action_type,
"Trigger rule references unknown action type"
);
continue;
};
let ctx = TriggerContext {
event_name: event_name.to_string(),
event_data: event_data.clone(),
rule_id: rule.id.clone(),
rule_name: rule.name.clone(),
action_config: rule.action_config.clone(),
};
let result = action_fn(ctx).await;
results.push(result);
}
results
}
}
impl Drop for TriggerRuleEngine {
fn drop(&mut self) {
self.stop();
}
}
#[derive(Clone, Debug)]
pub struct TriggerEvent {
pub name: String,
pub data: serde_json::Value,
}
impl crate::event::Event for TriggerEvent {
fn event_name() -> &'static str {
"trigger.event"
}
fn topic() -> &'static str {
"trigger"
}
}
fn matches_condition(
data: &serde_json::Value,
condition: &serde_json::Value,
limits: &ConditionLimits,
) -> bool {
let Some(condition_obj) = condition.as_object() else {
return true;
};
let mut operator_count = 0;
for (field, ops) in condition_obj {
let value = json_path_get(data, field, limits.max_path_depth);
let Some(value) = value else {
return false;
};
if !match_operators(
value,
ops,
&mut operator_count,
limits.max_operators,
limits.max_string_length,
) {
return false;
}
}
true
}
fn json_path_get<'a>(
data: &'a serde_json::Value,
path: &str,
max_depth: usize,
) -> Option<&'a serde_json::Value> {
let mut current = data;
let mut depth = 0;
for segment in path.split('.') {
if depth >= max_depth {
tracing::warn!(
path = %path,
depth = depth,
max = max_depth,
"json_path_get exceeded maximum depth, rejecting"
);
return None;
}
current = current.get(segment)?;
depth += 1;
}
Some(current)
}
fn match_operators(
value: &serde_json::Value,
ops: &serde_json::Value,
operator_count: &mut usize,
max_operators: usize,
max_string_length: usize,
) -> bool {
let Some(ops_obj) = ops.as_object() else {
return value == ops;
};
for (op, expected) in ops_obj {
*operator_count += 1;
if *operator_count > max_operators {
tracing::warn!(
count = *operator_count,
max = max_operators,
"Condition exceeded maximum operator count, rejecting"
);
return false;
}
match op.as_str() {
"$eq" => {
if value != expected {
return false;
}
}
"$ne" => {
if value == expected {
return false;
}
}
"$gt" => {
if !json_value_gt(value, expected) {
return false;
}
}
"$gte" => {
if !json_value_gte(value, expected) {
return false;
}
}
"$lt" => {
if !json_value_lt(value, expected) {
return false;
}
}
"$lte" => {
if !json_value_lte(value, expected) {
return false;
}
}
"$in" => {
let Some(arr) = expected.as_array() else {
return false;
};
if !arr.contains(value) {
return false;
}
}
"$contains" => {
let (Some(s), Some(pattern)) = (value.as_str(), expected.as_str()) else {
return false;
};
if s.len() > max_string_length || pattern.len() > max_string_length {
tracing::warn!(
s_len = s.len(),
p_len = pattern.len(),
max = max_string_length,
"$contains string exceeded length limit, rejecting"
);
return false;
}
if !s.contains(pattern) {
return false;
}
}
_ => {
tracing::warn!(operator = %op, "Unknown condition operator, ignoring");
}
}
}
true
}
fn json_value_gt(a: &serde_json::Value, b: &serde_json::Value) -> bool {
match (a, b) {
(serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
return a > b;
}
false
}
(serde_json::Value::String(a), serde_json::Value::String(b)) => a > b,
_ => false,
}
}
fn json_value_gte(a: &serde_json::Value, b: &serde_json::Value) -> bool {
a == b || json_value_gt(a, b)
}
fn json_value_lt(a: &serde_json::Value, b: &serde_json::Value) -> bool {
match (a, b) {
(serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
return a < b;
}
false
}
(serde_json::Value::String(a), serde_json::Value::String(b)) => a < b,
_ => false,
}
}
fn json_value_lte(a: &serde_json::Value, b: &serde_json::Value) -> bool {
a == b || json_value_lt(a, b)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rule(
id: &str,
pattern: &str,
action_type: &str,
action_config: serde_json::Value,
) -> TriggerRule {
TriggerRule {
id: id.to_string(),
name: format!("Rule {}", id),
event_pattern: pattern.to_string(),
condition: None,
action_type: action_type.to_string(),
action_config,
enabled: true,
priority: 0,
}
}
#[test]
fn test_add_and_list_rules() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
engine.add_rule(make_rule(
"r1",
"user.*",
"log",
serde_json::json!({}),
));
engine.add_rule(make_rule(
"r2",
"order.**",
"notify",
serde_json::json!({}),
));
assert_eq!(engine.rule_count(), 2);
let rules = engine.list_rules();
assert_eq!(rules.len(), 2);
}
#[test]
fn test_remove_rule() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
engine.add_rule(make_rule("r2", "order.*", "log", serde_json::json!({})));
let removed = engine.remove_rule("r1").unwrap();
assert_eq!(removed.id, "r1");
assert_eq!(engine.rule_count(), 1);
assert!(engine.remove_rule("nonexistent").is_none());
}
#[test]
fn test_update_rule() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
let mut updated = make_rule("r1", "user.**", "notify", serde_json::json!({}));
updated.name = "Updated Rule".to_string();
assert!(engine.update_rule(updated));
let rule = engine.get_rule("r1").unwrap();
assert_eq!(rule.event_pattern, "user.**");
assert_eq!(rule.action_type, "notify");
assert_eq!(rule.name, "Updated Rule");
}
#[test]
fn test_enable_disable_rule() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
assert!(engine.disable_rule("r1"));
assert!(!engine.get_rule("r1").unwrap().enabled);
assert!(engine.enable_rule("r1"));
assert!(engine.get_rule("r1").unwrap().enabled);
assert!(!engine.disable_rule("nonexistent"));
}
#[test]
fn test_priority_sorting() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
let mut r1 = make_rule("r1", "a", "log", serde_json::json!({}));
r1.priority = 10;
let mut r2 = make_rule("r2", "b", "log", serde_json::json!({}));
r2.priority = 1;
let mut r3 = make_rule("r3", "c", "log", serde_json::json!({}));
r3.priority = 5;
engine.add_rules(vec![r1, r2, r3]);
let rules = engine.list_rules();
assert_eq!(rules[0].id, "r2"); assert_eq!(rules[1].id, "r3"); assert_eq!(rules[2].id, "r1"); }
#[test]
fn test_register_action() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
engine.register_action("log", |_ctx: TriggerContext| async { Ok(()) });
engine.register_action("notify", |_ctx: TriggerContext| async { Ok(()) });
let types = engine.list_action_types();
assert_eq!(types.len(), 2);
assert!(types.contains(&"log".to_string()));
}
#[test]
fn test_condition_eq() {
let data = serde_json::json!({"status": "published", "level": 3});
let condition = serde_json::json!({"status": {"$eq": "published"}});
assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
let condition = serde_json::json!({"status": {"$eq": "draft"}});
assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_ne() {
let data = serde_json::json!({"status": "published"});
let condition = serde_json::json!({"status": {"$ne": "draft"}});
assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_gt_lt() {
let data = serde_json::json!({"amount": 500});
let condition = serde_json::json!({"amount": {"$gt": 100, "$lt": 1000}});
assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
let data = serde_json::json!({"amount": 50});
assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_in() {
let data = serde_json::json!({"category": "books"});
let condition = serde_json::json!({"category": {"$in": ["books", "electronics"]}});
assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
let data = serde_json::json!({"category": "clothing"});
assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_contains() {
let data = serde_json::json!({"title": "Hello World Article"});
let condition = serde_json::json!({"title": {"$contains": "World"}});
assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
let condition = serde_json::json!({"title": {"$contains": "Missing"}});
assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_nested_path() {
let data = serde_json::json!({"user": {"level": 5}});
let condition = serde_json::json!({"user.level": {"$gte": 3}});
assert!(matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_missing_field() {
let data = serde_json::json!({"status": "ok"});
let condition = serde_json::json!({"missing_field": {"$eq": "value"}});
assert!(!matches_condition(&data, &condition, &ConditionLimits::default()));
}
#[test]
fn test_condition_no_condition() {
let data = serde_json::json!({"status": "ok"});
assert!(matches_condition(&data, &serde_json::Value::Null, &ConditionLimits::default()));
}
#[test]
fn test_condition_limits_path_depth() {
let data = serde_json::json!({"a": {"b": {"c": {"d": {"e": "deep"}}}}});
let limits = ConditionLimits::default();
assert!(matches_condition(
&data,
&serde_json::json!({"a.b.c.d.e": {"$eq": "deep"}}),
&limits
));
let strict_limits = ConditionLimits {
max_path_depth: 2,
..Default::default()
};
assert!(!matches_condition(
&data,
&serde_json::json!({"a.b.c.d.e": {"$eq": "deep"}}),
&strict_limits
));
}
#[test]
fn test_condition_limits_operator_count() {
let data = serde_json::json!({"value": 42});
let limits = ConditionLimits::default();
assert!(matches_condition(
&data,
&serde_json::json!({"value": {"$gt": 0, "$lt": 100, "$ne": 50}}),
&limits
));
let strict_limits = ConditionLimits {
max_operators: 2,
..Default::default()
};
assert!(!matches_condition(
&data,
&serde_json::json!({"value": {"$gt": 0, "$lt": 100, "$ne": 50}}),
&strict_limits
));
}
#[test]
fn test_condition_limits_string_length() {
let long_string = "a".repeat(20_000);
let data = serde_json::json!({"text": long_string});
let limits = ConditionLimits::default();
assert!(!matches_condition(
&data,
&serde_json::json!({"text": {"$contains": "a"}}),
&limits
));
let relaxed_limits = ConditionLimits {
max_string_length: 100_000,
..Default::default()
};
assert!(matches_condition(
&data,
&serde_json::json!({"text": {"$contains": "a"}}),
&relaxed_limits
));
}
#[tokio::test]
async fn test_process_event_basic_matching() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
let executed_clone = executed.clone();
engine.register_action("collect", move |ctx: TriggerContext| {
let executed_clone = executed_clone.clone();
async move {
executed_clone
.write()
.unwrap()
.push(ctx.rule_id.clone());
Ok(())
}
});
engine.add_rule(make_rule(
"r1",
"user.*",
"collect",
serde_json::json!({}),
));
engine.add_rule(make_rule(
"r2",
"order.*",
"collect",
serde_json::json!({}),
));
let results = engine
.process_event("user.created", &serde_json::json!({}))
.await;
assert_eq!(results.len(), 1);
assert!(results[0].is_ok());
let executed = executed.read().unwrap();
assert_eq!(executed.len(), 1);
assert_eq!(executed[0], "r1");
}
#[tokio::test]
async fn test_process_event_with_condition() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
let executed_clone = executed.clone();
engine.register_action("collect", move |ctx: TriggerContext| {
let executed_clone = executed_clone.clone();
async move {
executed_clone
.write()
.unwrap()
.push(ctx.rule_id.clone());
Ok(())
}
});
let mut rule = make_rule("r1", "order.*", "collect", serde_json::json!({}));
rule.condition = Some(serde_json::json!({"amount": {"$gt": 100}}));
engine.add_rule(rule);
let results = engine
.process_event("order.created", &serde_json::json!({"amount": 50}))
.await;
assert_eq!(results.len(), 0);
let results = engine
.process_event("order.created", &serde_json::json!({"amount": 200}))
.await;
assert_eq!(results.len(), 1);
}
#[tokio::test]
async fn test_process_event_disabled_rule() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
let executed_clone = executed.clone();
engine.register_action("collect", move |ctx: TriggerContext| {
let executed_clone = executed_clone.clone();
async move {
executed_clone
.write()
.unwrap()
.push(ctx.rule_id.clone());
Ok(())
}
});
let mut rule = make_rule("r1", "user.*", "collect", serde_json::json!({}));
rule.enabled = false;
engine.add_rule(rule);
let results = engine
.process_event("user.created", &serde_json::json!({}))
.await;
assert_eq!(results.len(), 0);
}
#[tokio::test]
async fn test_process_event_unknown_action() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
engine.add_rule(make_rule("r1", "user.*", "unknown_action", serde_json::json!({})));
let results = engine
.process_event("user.created", &serde_json::json!({}))
.await;
assert_eq!(results.len(), 0); }
#[tokio::test]
async fn test_process_event_wildcard_pattern() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
let executed_clone = executed.clone();
engine.register_action("collect", move |ctx: TriggerContext| {
let executed_clone = executed_clone.clone();
async move {
executed_clone
.write()
.unwrap()
.push(ctx.rule_id.clone());
Ok(())
}
});
engine.add_rule(make_rule("r1", "user.**", "collect", serde_json::json!({})));
let results = engine
.process_event("user.profile.updated", &serde_json::json!({}))
.await;
assert_eq!(results.len(), 1);
}
#[test]
fn test_trigger_rule_serialization() {
let rule = TriggerRule {
id: "rule-1".to_string(),
name: "Test Rule".to_string(),
event_pattern: "user.*".to_string(),
condition: Some(serde_json::json!({"status": {"$eq": "active"}})),
action_type: "notify".to_string(),
action_config: serde_json::json!({"channel": "email"}),
enabled: true,
priority: 0,
};
let json = serde_json::to_string(&rule).unwrap();
let deserialized: TriggerRule = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, "rule-1");
assert_eq!(deserialized.event_pattern, "user.*");
assert!(deserialized.condition.is_some());
}
#[tokio::test]
async fn test_start_stop() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
assert!(!engine.is_running());
engine.start().await.unwrap();
assert!(engine.is_running());
engine.start().await.unwrap();
assert!(engine.is_running());
engine.stop();
assert!(!engine.is_running());
}
#[tokio::test]
async fn test_process_event_works_regardless_of_running_state() {
let bus = EventBus::new();
let engine = TriggerRuleEngine::new(bus);
let executed: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
let executed_clone = executed.clone();
engine.register_action("collect", move |ctx: TriggerContext| {
let executed_clone = executed_clone.clone();
async move {
executed_clone
.write()
.unwrap()
.push(ctx.rule_id.clone());
Ok(())
}
});
engine.add_rule(make_rule("r1", "user.*", "collect", serde_json::json!({})));
let results = engine
.process_event("user.created", &serde_json::json!({}))
.await;
assert_eq!(results.len(), 1);
assert!(results[0].is_ok());
let log = executed.read().unwrap();
assert_eq!(log.len(), 1);
assert_eq!(log[0], "r1");
}
#[test]
fn test_in_memory_rule_storage_basic() {
let storage = InMemoryRuleStorage::new();
storage.add(make_rule("r1", "user.*", "log", serde_json::json!({})));
storage.add(make_rule("r2", "order.*", "notify", serde_json::json!({})));
assert_eq!(storage.count(), 2);
let rules = storage.list();
assert_eq!(rules.len(), 2);
assert!(storage.get("r1").is_some());
assert!(storage.get("nonexistent").is_none());
let removed = storage.remove("r1").unwrap();
assert_eq!(removed.id, "r1");
assert_eq!(storage.count(), 1);
assert!(storage.remove("nonexistent").is_none());
let mut updated = make_rule("r2", "order.**", "email", serde_json::json!({}));
updated.name = "Updated".to_string();
assert!(storage.update(updated));
assert_eq!(storage.get("r2").unwrap().name, "Updated");
assert!(!storage.update(make_rule("r99", "x", "y", serde_json::json!({}))));
}
#[test]
fn test_with_custom_storage() {
let storage: Arc<dyn RuleStorage> = Arc::new(InMemoryRuleStorage::new());
let bus = EventBus::new();
let engine = TriggerRuleEngine::with_storage(bus, storage.clone());
engine.add_rule(make_rule("r1", "user.*", "log", serde_json::json!({})));
assert_eq!(storage.count(), 1);
assert_eq!(storage.get("r1").unwrap().event_pattern, "user.*");
assert_eq!(engine.rule_count(), 1);
assert_eq!(engine.list_rules()[0].id, "r1");
}
}