use std::sync::Arc;
use async_trait::async_trait;
#[async_trait]
pub trait TriggerHandler<Ctx, Evt>: Send + Sync {
fn events(&self) -> Vec<Evt>;
async fn handle(&self, ctx: &Ctx) -> anyhow::Result<()>;
fn priority(&self) -> i32 { 0 }
fn continue_on_error(&self) -> bool { false }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TriggerEvent {
BeforeCreate,
AfterCreate,
BeforeUpdate,
AfterUpdate,
BeforeDelete,
AfterDelete,
OnTransition { from: String, to: String, transition: String },
OnEnterState(String),
OnExitState(String),
}
impl TriggerEvent {
pub fn is_before_event(&self) -> bool {
matches!(self, Self::BeforeCreate | Self::BeforeUpdate | Self::BeforeDelete)
}
}
#[derive(Debug, Clone)]
pub struct TriggerContext<T: Clone> {
pub event: TriggerEvent,
pub entity: T,
pub previous: Option<T>,
pub user_id: Option<String>,
pub metadata: std::collections::HashMap<String, String>,
}
impl<T: Clone> TriggerContext<T> {
pub fn new(event: TriggerEvent, entity: T) -> Self {
Self { event, entity, previous: None, user_id: None, metadata: std::collections::HashMap::new() }
}
pub fn with_previous(mut self, previous: T) -> Self {
self.previous = Some(previous);
self
}
pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn is_before_event(&self) -> bool {
self.event.is_before_event()
}
}
#[derive(Debug)]
pub struct TriggerContextMut<T: Clone> {
pub event: TriggerEvent,
pub entity: T,
pub previous: Option<T>,
pub user_id: Option<String>,
pub metadata: std::collections::HashMap<String, String>,
}
impl<T: Clone> TriggerContextMut<T> {
pub fn new(event: TriggerEvent, entity: T) -> Self {
Self { event, entity, previous: None, user_id: None, metadata: std::collections::HashMap::new() }
}
pub fn into_entity(self) -> T {
self.entity
}
pub fn into_context(self) -> TriggerContext<T> {
TriggerContext {
event: self.event,
entity: self.entity,
previous: self.previous,
user_id: self.user_id,
metadata: self.metadata,
}
}
}
pub struct ActionExecutor;
impl ActionExecutor {
pub fn new() -> Self { Self }
pub async fn send_email<C>(&self, _ctx: &C, template: &str) -> anyhow::Result<()> {
tracing::info!("Sending email with template: {}", template);
Ok(())
}
pub async fn notify<C>(&self, _ctx: &C, channel: &str) -> anyhow::Result<()> {
tracing::info!("Sending notification to channel: {}", channel);
Ok(())
}
pub async fn webhook<C>(&self, _ctx: &C, url: &str) -> anyhow::Result<()> {
tracing::info!("Calling webhook: {}", url);
Ok(())
}
pub async fn emit<C>(&self, _ctx: &C, event_name: &str) -> anyhow::Result<()> {
tracing::info!("Emitting event: {}", event_name);
Ok(())
}
}
impl Default for ActionExecutor {
fn default() -> Self { Self::new() }
}
pub struct TriggerRegistry<T: Clone + Send + Sync + 'static> {
handlers: Vec<Arc<dyn TriggerHandler<TriggerContext<T>, TriggerEvent>>>,
}
impl<T: Clone + Send + Sync + 'static> TriggerRegistry<T> {
pub fn new() -> Self {
Self { handlers: Vec::new() }
}
pub fn register(&mut self, handler: Arc<dyn TriggerHandler<TriggerContext<T>, TriggerEvent>>) {
self.handlers.push(handler);
self.handlers.sort_by_key(|h| h.priority());
}
pub async fn execute(&self, ctx: &TriggerContext<T>) -> anyhow::Result<()> {
for handler in &self.handlers {
if !handler.events().iter().any(|e| event_matches(e, &ctx.event)) {
continue;
}
match handler.handle(ctx).await {
Ok(()) => {}
Err(e) => {
tracing::error!("Trigger handler error: {:?}", e);
if !handler.continue_on_error() {
return Err(e);
}
}
}
}
Ok(())
}
pub fn with_defaults() -> Self {
Self::new()
}
pub fn build(f: impl FnOnce(&mut Self)) -> Self {
let mut r = Self::new();
f(&mut r);
r
}
}
impl<T: Clone + Send + Sync + 'static> Default for TriggerRegistry<T> {
fn default() -> Self { Self::with_defaults() }
}
fn event_matches(a: &TriggerEvent, b: &TriggerEvent) -> bool {
match (a, b) {
(TriggerEvent::BeforeCreate, TriggerEvent::BeforeCreate) => true,
(TriggerEvent::AfterCreate, TriggerEvent::AfterCreate) => true,
(TriggerEvent::BeforeUpdate, TriggerEvent::BeforeUpdate) => true,
(TriggerEvent::AfterUpdate, TriggerEvent::AfterUpdate) => true,
(TriggerEvent::BeforeDelete, TriggerEvent::BeforeDelete) => true,
(TriggerEvent::AfterDelete, TriggerEvent::AfterDelete) => true,
(TriggerEvent::OnEnterState(x), TriggerEvent::OnEnterState(y)) => x == y,
(TriggerEvent::OnExitState(x), TriggerEvent::OnExitState(y)) => x == y,
(TriggerEvent::OnTransition { transition: t1, .. },
TriggerEvent::OnTransition { transition: t2, .. }) => t1 == t2,
_ => false,
}
}