Skip to main content

backbone_core/
trigger.rs

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