Skip to main content

backbone_core/
trigger.rs

1//! Generic trigger handler trait and types for entity lifecycle events.
2//!
3//! Phase 0 generic base for the `trigger.rs` generator (Category C).
4//!
5//! Every generated `{Model}TriggerHandler` used to re-define the same
6//! boilerplate. This module provides the single generic version so that
7//! generated files collapse to type aliases:
8//!
9//! ```rust,ignore
10//! use backbone_core::trigger::{TriggerEvent, TriggerContext, TriggerContextMut,
11//!                               TriggerRegistry, ActionExecutor, TriggerHandler};
12//! use crate::domain::entity::Order;
13//!
14//! pub type OrderTriggerEvent      = TriggerEvent;
15//! pub type OrderTriggerContext    = TriggerContext<Order>;
16//! pub type OrderTriggerContextMut = TriggerContextMut<Order>;
17//! pub type OrderActionExecutor    = ActionExecutor;
18//! pub type OrderTriggerRegistry   = TriggerRegistry<Order>;
19//! pub type OrderTriggerHandlerObj = dyn TriggerHandler<TriggerContext<Order>, TriggerEvent>;
20//! ```
21
22use std::sync::Arc;
23use async_trait::async_trait;
24
25// ─── Handler trait ───────────────────────────────────────────────────────────
26
27/// Generic lifecycle trigger handler.
28///
29/// - `Ctx` — entity-specific trigger context (e.g. `TriggerContext<Order>`)
30/// - `Evt` — trigger event type (e.g. `TriggerEvent`)
31#[async_trait]
32pub trait TriggerHandler<Ctx, Evt>: Send + Sync {
33    /// Events this handler responds to.
34    fn events(&self) -> Vec<Evt>;
35
36    /// Handle the trigger event.
37    async fn handle(&self, ctx: &Ctx) -> anyhow::Result<()>;
38
39    /// Execution priority — lower values run first. Default: 0.
40    fn priority(&self) -> i32 { 0 }
41
42    /// Whether to keep dispatching after this handler errors. Default: false.
43    fn continue_on_error(&self) -> bool { false }
44}
45
46// ─── Shared event enum ───────────────────────────────────────────────────────
47
48/// Lifecycle trigger events — identical for every entity.
49///
50/// Generated files use a type alias: `pub type OrderTriggerEvent = TriggerEvent;`
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum TriggerEvent {
53    BeforeCreate,
54    AfterCreate,
55    BeforeUpdate,
56    AfterUpdate,
57    BeforeDelete,
58    AfterDelete,
59    OnTransition { from: String, to: String, transition: String },
60    OnEnterState(String),
61    OnExitState(String),
62}
63
64impl TriggerEvent {
65    pub fn is_before_event(&self) -> bool {
66        matches!(self, Self::BeforeCreate | Self::BeforeUpdate | Self::BeforeDelete)
67    }
68}
69
70// ─── Generic context ─────────────────────────────────────────────────────────
71
72/// Immutable trigger context passed to handlers.
73#[derive(Debug, Clone)]
74pub struct TriggerContext<T: Clone> {
75    pub event: TriggerEvent,
76    pub entity: T,
77    pub previous: Option<T>,
78    pub user_id: Option<String>,
79    pub metadata: std::collections::HashMap<String, String>,
80}
81
82impl<T: Clone> TriggerContext<T> {
83    pub fn new(event: TriggerEvent, entity: T) -> Self {
84        Self { event, entity, previous: None, user_id: None, metadata: std::collections::HashMap::new() }
85    }
86
87    pub fn with_previous(mut self, previous: T) -> Self {
88        self.previous = Some(previous);
89        self
90    }
91
92    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
93        self.user_id = Some(user_id.into());
94        self
95    }
96
97    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
98        self.metadata.insert(key.into(), value.into());
99        self
100    }
101
102    pub fn is_before_event(&self) -> bool {
103        self.event.is_before_event()
104    }
105}
106
107// ─── Generic mutable context ─────────────────────────────────────────────────
108
109/// Mutable trigger context for before-hooks that may modify the entity.
110#[derive(Debug)]
111pub struct TriggerContextMut<T: Clone> {
112    pub event: TriggerEvent,
113    pub entity: T,
114    pub previous: Option<T>,
115    pub user_id: Option<String>,
116    pub metadata: std::collections::HashMap<String, String>,
117}
118
119impl<T: Clone> TriggerContextMut<T> {
120    pub fn new(event: TriggerEvent, entity: T) -> Self {
121        Self { event, entity, previous: None, user_id: None, metadata: std::collections::HashMap::new() }
122    }
123
124    pub fn into_entity(self) -> T {
125        self.entity
126    }
127
128    pub fn into_context(self) -> TriggerContext<T> {
129        TriggerContext {
130            event: self.event,
131            entity: self.entity,
132            previous: self.previous,
133            user_id: self.user_id,
134            metadata: self.metadata,
135        }
136    }
137}
138
139// ─── Shared action executor ───────────────────────────────────────────────────
140
141/// Shared executor for side-effect actions (email, notify, webhook, emit).
142///
143/// All methods are stub implementations pending real integrations.
144pub struct ActionExecutor;
145
146impl ActionExecutor {
147    pub fn new() -> Self { Self }
148
149    pub async fn send_email<C>(&self, _ctx: &C, template: &str) -> anyhow::Result<()> {
150        tracing::info!("Sending email with template: {}", template);
151        Ok(())
152    }
153
154    pub async fn notify<C>(&self, _ctx: &C, channel: &str) -> anyhow::Result<()> {
155        tracing::info!("Sending notification to channel: {}", channel);
156        Ok(())
157    }
158
159    pub async fn webhook<C>(&self, _ctx: &C, url: &str) -> anyhow::Result<()> {
160        tracing::info!("Calling webhook: {}", url);
161        Ok(())
162    }
163
164    pub async fn emit<C>(&self, _ctx: &C, event_name: &str) -> anyhow::Result<()> {
165        tracing::info!("Emitting event: {}", event_name);
166        Ok(())
167    }
168}
169
170impl Default for ActionExecutor {
171    fn default() -> Self { Self::new() }
172}
173
174// ─── Generic registry ────────────────────────────────────────────────────────
175
176/// Registry that dispatches trigger events to registered handlers.
177pub struct TriggerRegistry<T: Clone + Send + Sync + 'static> {
178    handlers: Vec<Arc<dyn TriggerHandler<TriggerContext<T>, TriggerEvent>>>,
179}
180
181impl<T: Clone + Send + Sync + 'static> TriggerRegistry<T> {
182    pub fn new() -> Self {
183        Self { handlers: Vec::new() }
184    }
185
186    pub fn register(&mut self, handler: Arc<dyn TriggerHandler<TriggerContext<T>, TriggerEvent>>) {
187        self.handlers.push(handler);
188        self.handlers.sort_by_key(|h| h.priority());
189    }
190
191    pub async fn execute(&self, ctx: &TriggerContext<T>) -> anyhow::Result<()> {
192        for handler in &self.handlers {
193            if !handler.events().iter().any(|e| event_matches(e, &ctx.event)) {
194                continue;
195            }
196            match handler.handle(ctx).await {
197                Ok(()) => {}
198                Err(e) => {
199                    tracing::error!("Trigger handler error: {:?}", e);
200                    if !handler.continue_on_error() {
201                        return Err(e);
202                    }
203                }
204            }
205        }
206        Ok(())
207    }
208
209    pub fn with_defaults() -> Self {
210        Self::new()
211    }
212
213    /// Build a registry by registering handlers via a closure.
214    ///
215    /// ```rust,ignore
216    /// let registry = TriggerRegistry::<Order>::build(|r| {
217    ///     r.register(Arc::new(OrderAfterCreateHandler::new()));
218    /// });
219    /// ```
220    pub fn build(f: impl FnOnce(&mut Self)) -> Self {
221        let mut r = Self::new();
222        f(&mut r);
223        r
224    }
225}
226
227impl<T: Clone + Send + Sync + 'static> Default for TriggerRegistry<T> {
228    fn default() -> Self { Self::with_defaults() }
229}
230
231fn event_matches(a: &TriggerEvent, b: &TriggerEvent) -> bool {
232    match (a, b) {
233        (TriggerEvent::BeforeCreate,  TriggerEvent::BeforeCreate)  => true,
234        (TriggerEvent::AfterCreate,   TriggerEvent::AfterCreate)   => true,
235        (TriggerEvent::BeforeUpdate,  TriggerEvent::BeforeUpdate)  => true,
236        (TriggerEvent::AfterUpdate,   TriggerEvent::AfterUpdate)   => true,
237        (TriggerEvent::BeforeDelete,  TriggerEvent::BeforeDelete)  => true,
238        (TriggerEvent::AfterDelete,   TriggerEvent::AfterDelete)   => true,
239        (TriggerEvent::OnEnterState(x), TriggerEvent::OnEnterState(y)) => x == y,
240        (TriggerEvent::OnExitState(x),  TriggerEvent::OnExitState(y))  => x == y,
241        (TriggerEvent::OnTransition { transition: t1, .. },
242         TriggerEvent::OnTransition { transition: t2, .. }) => t1 == t2,
243        _ => false,
244    }
245}