use super::event::{EventHookEvent, EventHookEventType};
use crate::automation::callable::{Callable, ExecutionContext, Signature, Value};
use crate::error::DbxResult;
pub enum EventHookCondition {
Always,
UdfCondition(String),
Custom(Box<dyn Fn(&EventHookEvent) -> bool + Send + Sync>),
}
type EventHookActionFn =
Box<dyn Fn(&ExecutionContext, &EventHookEvent) -> DbxResult<()> + Send + Sync>;
pub enum EventHookAction {
CallUdf(String),
Custom(EventHookActionFn),
}
pub struct EventHook {
name: String,
event_type: EventHookEventType,
table: String,
condition: EventHookCondition,
action: EventHookAction,
signature: Signature,
}
impl EventHook {
pub fn new(
name: impl Into<String>,
event_type: EventHookEventType,
table: impl Into<String>,
condition: EventHookCondition,
action: EventHookAction,
) -> Self {
Self {
name: name.into(),
event_type,
table: table.into(),
condition,
action,
signature: Signature {
params: vec![],
return_type: crate::automation::callable::DataType::Null,
is_variadic: false,
},
}
}
pub fn matches(&self, event: &EventHookEvent) -> bool {
self.event_type == event.event_type && self.table == event.table
}
pub fn evaluate_condition(&self, ctx: &ExecutionContext, event: &EventHookEvent) -> bool {
match &self.condition {
EventHookCondition::Always => true,
EventHookCondition::UdfCondition(udf_name) => {
match ctx.dbx.call_udf(udf_name, &[]) {
Ok(value) => value.is_truthy(),
Err(_) => false, }
}
EventHookCondition::Custom(func) => func(event),
}
}
pub fn execute_action(&self, ctx: &ExecutionContext, event: &EventHookEvent) -> DbxResult<()> {
match &self.action {
EventHookAction::CallUdf(udf_name) => {
let args: Vec<Value> = event.data.values().cloned().collect();
ctx.dbx.call_udf(udf_name, &args)?;
Ok(())
}
EventHookAction::Custom(func) => func(ctx, event),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn event_type(&self) -> &EventHookEventType {
&self.event_type
}
pub fn table(&self) -> &str {
&self.table
}
pub fn fire(&self, ctx: &ExecutionContext, event: &EventHookEvent) -> DbxResult<bool> {
if !self.matches(event) {
return Ok(false);
}
if !self.evaluate_condition(ctx, event) {
return Ok(false);
}
self.execute_action(ctx, event)?;
Ok(true)
}
}
impl Callable for EventHook {
fn call(&self, _ctx: &ExecutionContext, _args: &[Value]) -> DbxResult<Value> {
Ok(Value::Null)
}
fn name(&self) -> &str {
&self.name
}
fn signature(&self) -> &Signature {
&self.signature
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::Database;
use std::sync::Arc;
#[test]
fn test_event_hook_creation() {
let hook = EventHook::new(
"test_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(|_ctx, _event| Ok(()))),
);
assert_eq!(hook.name(), "test_trigger");
assert_eq!(hook.event_type(), &EventHookEventType::AfterInsert);
assert_eq!(hook.table(), "users");
}
#[test]
fn test_event_hook_matching() {
let hook = EventHook::new(
"test_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(|_ctx, _event| Ok(()))),
);
let event = EventHookEvent::new(EventHookEventType::AfterInsert, "users");
assert!(hook.matches(&event));
let event2 = EventHookEvent::new(EventHookEventType::AfterInsert, "posts");
assert!(!hook.matches(&event2));
}
#[test]
fn test_event_hook_condition() {
let db = Database::open_in_memory().unwrap();
let ctx = ExecutionContext::new(Arc::new(db));
let hook = EventHook::new(
"test_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Custom(Box::new(|event| event.data.contains_key("id"))),
EventHookAction::Custom(Box::new(|_ctx, _event| Ok(()))),
);
let event = EventHookEvent::new(EventHookEventType::AfterInsert, "users")
.with_data("id", Value::Int(1));
assert!(hook.evaluate_condition(&ctx, &event));
}
#[test]
fn test_event_hook_action() {
let executed = Arc::new(std::sync::Mutex::new(false));
let executed_clone = Arc::clone(&executed);
let hook = EventHook::new(
"test_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(move |_ctx, _event| {
*executed_clone.lock().unwrap() = true;
Ok(())
})),
);
let db = Database::open_in_memory().unwrap();
let ctx = ExecutionContext::new(Arc::new(db));
let event = EventHookEvent::new(EventHookEventType::AfterInsert, "users");
hook.execute_action(&ctx, &event).unwrap();
assert!(*executed.lock().unwrap());
}
#[test]
fn test_event_hook_fire_integration() {
let executed = Arc::new(std::sync::Mutex::new(false));
let executed_clone = Arc::clone(&executed);
let hook = EventHook::new(
"fire_test",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(move |_ctx, _event| {
*executed_clone.lock().unwrap() = true;
Ok(())
})),
);
let db = Database::open_in_memory().unwrap();
let ctx = ExecutionContext::new(Arc::new(db));
let event = EventHookEvent::new(EventHookEventType::AfterInsert, "users");
assert!(hook.fire(&ctx, &event).unwrap());
assert!(*executed.lock().unwrap());
let event2 = EventHookEvent::new(EventHookEventType::AfterDelete, "posts");
assert!(!hook.fire(&ctx, &event2).unwrap());
}
#[test]
fn test_event_hook_call_udf_action() {
use crate::automation::callable::{DataType, Signature};
let db = Database::open_in_memory().unwrap();
db.register_scalar_udf(
"log_insert",
Signature {
params: vec![DataType::Int],
return_type: DataType::Int,
is_variadic: true,
},
|args| {
Ok(args.first().cloned().unwrap_or(Value::Null))
},
)
.unwrap();
let hook = EventHook::new(
"udf_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::CallUdf("log_insert".to_string()),
);
let ctx = ExecutionContext::new(Arc::new(db));
let event = EventHookEvent::new(EventHookEventType::AfterInsert, "users")
.with_data("id", Value::Int(42));
assert!(hook.fire(&ctx, &event).is_ok());
}
}