Skip to main content

rustrails_model/
callbacks.rs

1use std::{any::Any, collections::HashMap};
2
3use rustrails_support::callbacks::{
4    AroundContinuation, Callback, CallbackConditions, CallbackFilter, CallbackKind, CallbackResult,
5};
6
7/// Model lifecycle events supported by [`ModelCallbacks`].
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum ModelEvent {
10    /// Runs before model validation.
11    BeforeValidation,
12    /// Runs after model validation.
13    AfterValidation,
14    /// Runs before persistence.
15    BeforeSave,
16    /// Runs after persistence.
17    AfterSave,
18    /// Runs before initial persistence.
19    BeforeCreate,
20    /// Runs after initial persistence.
21    AfterCreate,
22    /// Runs before updating an existing record.
23    BeforeUpdate,
24    /// Runs after updating an existing record.
25    AfterUpdate,
26    /// Runs before destruction.
27    BeforeDestroy,
28    /// Runs after destruction.
29    AfterDestroy,
30}
31
32impl ModelEvent {
33    /// Returns every supported lifecycle event in a stable order.
34    #[must_use]
35    pub fn all() -> &'static [ModelEvent] {
36        static EVENTS: [ModelEvent; 10] = [
37            ModelEvent::BeforeValidation,
38            ModelEvent::AfterValidation,
39            ModelEvent::BeforeSave,
40            ModelEvent::AfterSave,
41            ModelEvent::BeforeCreate,
42            ModelEvent::AfterCreate,
43            ModelEvent::BeforeUpdate,
44            ModelEvent::AfterUpdate,
45            ModelEvent::BeforeDestroy,
46            ModelEvent::AfterDestroy,
47        ];
48
49        &EVENTS
50    }
51
52    /// Returns the canonical snake_case name for the lifecycle event.
53    #[must_use]
54    pub const fn as_str(&self) -> &'static str {
55        match self {
56            Self::BeforeValidation => "before_validation",
57            Self::AfterValidation => "after_validation",
58            Self::BeforeSave => "before_save",
59            Self::AfterSave => "after_save",
60            Self::BeforeCreate => "before_create",
61            Self::AfterCreate => "after_create",
62            Self::BeforeUpdate => "before_update",
63            Self::AfterUpdate => "after_update",
64            Self::BeforeDestroy => "before_destroy",
65            Self::AfterDestroy => "after_destroy",
66        }
67    }
68}
69
70/// Callback registry for model lifecycle events.
71#[derive(Debug)]
72struct EventCallbacks<T: Send + Sync + 'static> {
73    name: &'static str,
74    callbacks: Vec<Callback<T>>,
75}
76
77impl<T: Send + Sync + 'static> EventCallbacks<T> {
78    fn new(event: ModelEvent) -> Self {
79        Self {
80            name: event.as_str(),
81            callbacks: Vec::new(),
82        }
83    }
84
85    fn add(&mut self, callback: Callback<T>) {
86        self.skip(&callback.name);
87        self.callbacks.push(callback);
88    }
89
90    fn run(&self, target: &mut T) -> CallbackResult {
91        if self.callbacks.is_empty() {
92            return CallbackResult::Continue;
93        }
94
95        let target_any = &*target as &dyn Any;
96        let applicable = self
97            .callbacks
98            .iter()
99            .filter(|callback| conditions_match(&callback.conditions, target_any, self.name))
100            .collect::<Vec<_>>();
101
102        for callback in applicable
103            .iter()
104            .copied()
105            .filter(|callback| callback.kind == CallbackKind::Before)
106        {
107            let result = match &callback.filter {
108                CallbackFilter::Standard(filter) => filter(target),
109                CallbackFilter::Around(_) => CallbackResult::Continue,
110            };
111
112            if result == CallbackResult::Halt {
113                return result;
114            }
115        }
116
117        let around = applicable
118            .iter()
119            .copied()
120            .filter(|callback| callback.kind == CallbackKind::Around)
121            .collect::<Vec<_>>();
122        let result = invoke_around(&around, target, Box::new(|_| CallbackResult::Continue));
123        if result == CallbackResult::Halt {
124            return result;
125        }
126
127        for callback in applicable
128            .iter()
129            .copied()
130            .filter(|callback| callback.kind == CallbackKind::After)
131        {
132            if let CallbackFilter::Standard(filter) = &callback.filter {
133                let _ = filter(target);
134            }
135        }
136
137        result
138    }
139
140    fn skip(&mut self, name: &str) {
141        self.callbacks.retain(|callback| callback.name != name);
142    }
143
144    fn reset(&mut self, kind: Option<CallbackKind>) {
145        match kind {
146            Some(kind) => self.callbacks.retain(|callback| callback.kind != kind),
147            None => self.callbacks.clear(),
148        }
149    }
150}
151
152#[derive(Debug)]
153pub struct ModelCallbacks<T: Send + Sync + 'static> {
154    chains: HashMap<ModelEvent, EventCallbacks<T>>,
155}
156
157impl<T: Send + Sync + 'static> Default for ModelCallbacks<T> {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163impl<T: Send + Sync + 'static> ModelCallbacks<T> {
164    /// Creates callback chains for every supported lifecycle event.
165    #[must_use]
166    pub fn new() -> Self {
167        let chains = ModelEvent::all()
168            .iter()
169            .copied()
170            .map(|event| (event, EventCallbacks::new(event)))
171            .collect();
172
173        Self { chains }
174    }
175
176    /// Registers a callback for a lifecycle event.
177    pub fn add(&mut self, event: ModelEvent, callback: Callback<T>) {
178        self.chains
179            .entry(event)
180            .or_insert_with(|| EventCallbacks::new(event))
181            .add(callback);
182    }
183
184    /// Runs the callback chain for a lifecycle event.
185    pub fn run(&self, event: ModelEvent, target: &mut T) -> CallbackResult {
186        self.chains
187            .get(&event)
188            .map_or(CallbackResult::Continue, |callbacks| callbacks.run(target))
189    }
190
191    /// Removes a named callback from a lifecycle event.
192    pub fn skip(&mut self, event: ModelEvent, name: &str) {
193        if let Some(callbacks) = self.chains.get_mut(&event) {
194            callbacks.skip(name);
195        }
196    }
197
198    /// Clears every callback registered for a lifecycle event.
199    pub fn reset(&mut self, event: ModelEvent) {
200        self.reset_callbacks(event, None);
201    }
202
203    /// Clears callbacks for a lifecycle event, optionally limiting the reset to one callback kind.
204    pub fn reset_callbacks(&mut self, event: ModelEvent, kind: Option<CallbackKind>) {
205        if let Some(callbacks) = self.chains.get_mut(&event) {
206            callbacks.reset(kind);
207        }
208    }
209}
210
211fn invoke_around<'a, T: Send + Sync + 'static>(
212    callbacks: &'a [&'a Callback<T>],
213    target: &mut T,
214    action: AroundContinuation<'a, T>,
215) -> CallbackResult {
216    match callbacks.split_first() {
217        Some((callback, rest)) => match &callback.filter {
218            CallbackFilter::Around(filter) => filter(
219                target,
220                Box::new(move |target| invoke_around(rest, target, action)),
221            ),
222            CallbackFilter::Standard(_) => invoke_around(rest, target, action),
223        },
224        None => action(target),
225    }
226}
227
228fn conditions_match(conditions: &CallbackConditions, target: &dyn Any, action_name: &str) -> bool {
229    if let Some(only) = &conditions.only
230        && !only.iter().any(|candidate| candidate == action_name)
231    {
232        return false;
233    }
234
235    if let Some(except) = &conditions.except
236        && except.iter().any(|candidate| candidate == action_name)
237    {
238        return false;
239    }
240
241    if let Some(predicate) = &conditions.if_cond
242        && !predicate(target)
243    {
244        return false;
245    }
246
247    if let Some(predicate) = &conditions.unless_cond
248        && predicate(target)
249    {
250        return false;
251    }
252
253    true
254}
255
256/// Trait for models that expose lifecycle callbacks.
257pub trait HasModelCallbacks: Send + Sync + Sized + 'static {
258    /// Returns the model-level callback registry.
259    fn model_callbacks() -> &'static ModelCallbacks<Self>;
260
261    /// Runs the callback chain associated with the lifecycle event.
262    fn run_callbacks(&mut self, event: ModelEvent) -> CallbackResult {
263        Self::model_callbacks().run(event, self)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use std::collections::HashSet;
270
271    use once_cell::sync::Lazy;
272    use rustrails_support::callbacks::{
273        Callback, CallbackChain, CallbackConditions, CallbackKind, CallbackResult,
274    };
275
276    use super::{HasModelCallbacks, ModelCallbacks, ModelEvent};
277
278    #[derive(Debug, Default)]
279    struct TestModel {
280        log: Vec<&'static str>,
281        allow: bool,
282        blocked: bool,
283    }
284
285    impl TestModel {
286        fn push(&mut self, entry: &'static str) {
287            self.log.push(entry);
288        }
289    }
290
291    static TEST_MODEL_CALLBACKS: Lazy<ModelCallbacks<TestModel>> = Lazy::new(|| {
292        let mut callbacks = ModelCallbacks::new();
293        callbacks.add(
294            ModelEvent::BeforeValidation,
295            Callback::before("record_before_validation", |model: &mut TestModel| {
296                model.push("before_validation");
297                CallbackResult::Continue
298            }),
299        );
300        callbacks.add(
301            ModelEvent::AfterValidation,
302            Callback::after("record_after_validation", |model: &mut TestModel| {
303                model.push("after_validation");
304                CallbackResult::Continue
305            }),
306        );
307        callbacks.add(
308            ModelEvent::AfterCreate,
309            Callback::after("record_after_create", |model: &mut TestModel| {
310                model.push("after_create");
311                CallbackResult::Continue
312            }),
313        );
314        callbacks
315    });
316
317    impl HasModelCallbacks for TestModel {
318        fn model_callbacks() -> &'static ModelCallbacks<Self> {
319            &TEST_MODEL_CALLBACKS
320        }
321    }
322
323    #[test]
324    fn before_and_after_validation_callbacks_fire() {
325        let mut model = TestModel::default();
326
327        assert_eq!(
328            model.run_callbacks(ModelEvent::BeforeValidation),
329            CallbackResult::Continue
330        );
331        assert_eq!(
332            model.run_callbacks(ModelEvent::AfterValidation),
333            CallbackResult::Continue
334        );
335
336        assert_eq!(model.log, vec!["before_validation", "after_validation"]);
337    }
338
339    #[test]
340    fn before_save_can_halt() {
341        let mut callbacks = ModelCallbacks::new();
342        callbacks.add(
343            ModelEvent::BeforeSave,
344            Callback::before("halt_save", |model: &mut TestModel| {
345                model.push("before_save");
346                CallbackResult::Halt
347            }),
348        );
349        callbacks.add(
350            ModelEvent::AfterSave,
351            Callback::after("after_save", |model: &mut TestModel| {
352                model.push("after_save");
353                CallbackResult::Continue
354            }),
355        );
356
357        let mut model = TestModel::default();
358        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
359
360        assert_eq!(result, CallbackResult::Halt);
361        assert_eq!(model.log, vec!["before_save"]);
362    }
363
364    #[test]
365    fn after_create_fires() {
366        let mut model = TestModel::default();
367
368        let result = model.run_callbacks(ModelEvent::AfterCreate);
369
370        assert_eq!(result, CallbackResult::Continue);
371        assert_eq!(model.log, vec!["after_create"]);
372    }
373
374    #[test]
375    fn before_callbacks_run_in_declaration_order() {
376        let mut callbacks = ModelCallbacks::new();
377        callbacks.add(
378            ModelEvent::BeforeValidation,
379            Callback::before("first", |model: &mut TestModel| {
380                model.push("first");
381                CallbackResult::Continue
382            }),
383        );
384        callbacks.add(
385            ModelEvent::BeforeValidation,
386            Callback::before("second", |model: &mut TestModel| {
387                model.push("second");
388                CallbackResult::Continue
389            }),
390        );
391        callbacks.add(
392            ModelEvent::BeforeValidation,
393            Callback::before("third", |model: &mut TestModel| {
394                model.push("third");
395                CallbackResult::Continue
396            }),
397        );
398
399        let mut model = TestModel::default();
400        let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
401
402        assert_eq!(result, CallbackResult::Continue);
403        assert_eq!(model.log, vec!["first", "second", "third"]);
404    }
405
406    #[test]
407    fn skip_callback_by_name() {
408        let mut callbacks = ModelCallbacks::new();
409        callbacks.add(
410            ModelEvent::BeforeValidation,
411            Callback::before("keep", |model: &mut TestModel| {
412                model.push("keep");
413                CallbackResult::Continue
414            }),
415        );
416        callbacks.add(
417            ModelEvent::BeforeValidation,
418            Callback::before("skip_me", |model: &mut TestModel| {
419                model.push("skip_me");
420                CallbackResult::Continue
421            }),
422        );
423        callbacks.skip(ModelEvent::BeforeValidation, "skip_me");
424
425        let mut model = TestModel::default();
426        let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
427
428        assert_eq!(result, CallbackResult::Continue);
429        assert_eq!(model.log, vec!["keep"]);
430    }
431
432    #[test]
433    fn reset_clears_callbacks_for_an_event() {
434        let mut callbacks = ModelCallbacks::new();
435        callbacks.add(
436            ModelEvent::BeforeDestroy,
437            Callback::before("cleanup", |model: &mut TestModel| {
438                model.push("cleanup");
439                CallbackResult::Continue
440            }),
441        );
442        callbacks.reset(ModelEvent::BeforeDestroy);
443
444        let mut model = TestModel::default();
445        let result = callbacks.run(ModelEvent::BeforeDestroy, &mut model);
446
447        assert_eq!(result, CallbackResult::Continue);
448        assert!(model.log.is_empty());
449    }
450
451    #[test]
452    fn reset_callbacks_can_clear_one_kind_without_disturbing_other_kinds() {
453        let mut callbacks = ModelCallbacks::new();
454        callbacks.add(
455            ModelEvent::BeforeSave,
456            Callback::before("before", |model: &mut TestModel| {
457                model.push("before");
458                CallbackResult::Continue
459            }),
460        );
461        callbacks.add(
462            ModelEvent::BeforeSave,
463            Callback::after("after", |model: &mut TestModel| {
464                model.push("after");
465                CallbackResult::Continue
466            }),
467        );
468
469        callbacks.reset_callbacks(ModelEvent::BeforeSave, Some(CallbackKind::Before));
470
471        let mut model = TestModel::default();
472        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
473
474        assert_eq!(result, CallbackResult::Continue);
475        assert_eq!(model.log, vec!["after"]);
476    }
477
478    #[test]
479    fn all_model_events_are_covered_once() {
480        let expected = [
481            (ModelEvent::BeforeValidation, "before_validation"),
482            (ModelEvent::AfterValidation, "after_validation"),
483            (ModelEvent::BeforeSave, "before_save"),
484            (ModelEvent::AfterSave, "after_save"),
485            (ModelEvent::BeforeCreate, "before_create"),
486            (ModelEvent::AfterCreate, "after_create"),
487            (ModelEvent::BeforeUpdate, "before_update"),
488            (ModelEvent::AfterUpdate, "after_update"),
489            (ModelEvent::BeforeDestroy, "before_destroy"),
490            (ModelEvent::AfterDestroy, "after_destroy"),
491        ];
492
493        assert_eq!(ModelEvent::all().len(), expected.len());
494        assert_eq!(
495            ModelEvent::all()
496                .iter()
497                .map(|event| (*event, event.as_str()))
498                .collect::<Vec<_>>(),
499            expected.to_vec()
500        );
501
502        let names = ModelEvent::all()
503            .iter()
504            .map(ModelEvent::as_str)
505            .collect::<HashSet<_>>();
506        assert_eq!(names.len(), ModelEvent::all().len());
507    }
508
509    #[test]
510    fn before_save_callbacks_fire() {
511        let mut callbacks = ModelCallbacks::new();
512        callbacks.add(
513            ModelEvent::BeforeSave,
514            Callback::before("audit", |model: &mut TestModel| {
515                model.push("before_save");
516                CallbackResult::Continue
517            }),
518        );
519
520        let mut model = TestModel::default();
521        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
522
523        assert_eq!(result, CallbackResult::Continue);
524        assert_eq!(model.log, vec!["before_save"]);
525    }
526
527    #[test]
528    fn after_save_callbacks_fire() {
529        let mut callbacks = ModelCallbacks::new();
530        callbacks.add(
531            ModelEvent::AfterSave,
532            Callback::after("publish", |model: &mut TestModel| {
533                model.push("after_save");
534                CallbackResult::Continue
535            }),
536        );
537
538        let mut model = TestModel::default();
539        let result = callbacks.run(ModelEvent::AfterSave, &mut model);
540
541        assert_eq!(result, CallbackResult::Continue);
542        assert_eq!(model.log, vec!["after_save"]);
543    }
544
545    #[test]
546    fn after_callbacks_run_in_declaration_order() {
547        let mut callbacks = ModelCallbacks::new();
548        callbacks.add(
549            ModelEvent::AfterSave,
550            Callback::after("first", |model: &mut TestModel| {
551                model.push("first");
552                CallbackResult::Continue
553            }),
554        );
555        callbacks.add(
556            ModelEvent::AfterSave,
557            Callback::after("second", |model: &mut TestModel| {
558                model.push("second");
559                CallbackResult::Continue
560            }),
561        );
562
563        let mut model = TestModel::default();
564        let result = callbacks.run(ModelEvent::AfterSave, &mut model);
565
566        assert_eq!(result, CallbackResult::Continue);
567        assert_eq!(model.log, vec!["first", "second"]);
568    }
569
570    #[test]
571    fn before_save_halt_prevents_later_before_callbacks() {
572        let mut callbacks = ModelCallbacks::new();
573        callbacks.add(
574            ModelEvent::BeforeSave,
575            Callback::before("halt", |model: &mut TestModel| {
576                model.push("halt");
577                CallbackResult::Halt
578            }),
579        );
580        callbacks.add(
581            ModelEvent::BeforeSave,
582            Callback::before("later", |model: &mut TestModel| {
583                model.push("later");
584                CallbackResult::Continue
585            }),
586        );
587
588        let mut model = TestModel::default();
589        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
590
591        assert_eq!(result, CallbackResult::Halt);
592        assert_eq!(model.log, vec!["halt"]);
593    }
594
595    #[test]
596    fn around_callbacks_wrap_default_action() {
597        let mut callbacks = ModelCallbacks::new();
598        callbacks.add(
599            ModelEvent::BeforeValidation,
600            Callback::around("wrap", |model: &mut TestModel, next| {
601                model.push("around-before");
602                let result = next(model);
603                model.push("around-after");
604                result
605            }),
606        );
607
608        let mut model = TestModel::default();
609        let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
610
611        assert_eq!(result, CallbackResult::Continue);
612        assert_eq!(model.log, vec!["around-before", "around-after"]);
613    }
614
615    #[test]
616    fn around_callback_can_halt_before_inner_action() {
617        let mut callbacks = ModelCallbacks::new();
618        callbacks.add(
619            ModelEvent::BeforeValidation,
620            Callback::around("halt", |model: &mut TestModel, _next| {
621                model.push("around-only");
622                CallbackResult::Halt
623            }),
624        );
625
626        let mut model = TestModel::default();
627        let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
628
629        assert_eq!(result, CallbackResult::Halt);
630        assert_eq!(model.log, vec!["around-only"]);
631    }
632
633    #[test]
634    fn conditional_only_matches_event_name() {
635        let mut callbacks = ModelCallbacks::new();
636        callbacks.add(
637            ModelEvent::BeforeSave,
638            Callback::before("only_save", |model: &mut TestModel| {
639                model.push("only_save");
640                CallbackResult::Continue
641            })
642            .with_conditions(
643                rustrails_support::callbacks::CallbackConditions::new().only(["before_save"]),
644            ),
645        );
646
647        let mut model = TestModel::default();
648        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
649
650        assert_eq!(result, CallbackResult::Continue);
651        assert_eq!(model.log, vec!["only_save"]);
652    }
653
654    #[test]
655    fn conditional_except_skips_matching_event() {
656        let mut callbacks = ModelCallbacks::new();
657        callbacks.add(
658            ModelEvent::BeforeSave,
659            Callback::before("skip_save", |model: &mut TestModel| {
660                model.push("skip_save");
661                CallbackResult::Continue
662            })
663            .with_conditions(
664                rustrails_support::callbacks::CallbackConditions::new().except(["before_save"]),
665            ),
666        );
667
668        let mut model = TestModel::default();
669        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
670
671        assert_eq!(result, CallbackResult::Continue);
672        assert!(model.log.is_empty());
673    }
674
675    #[test]
676    fn conditional_if_runs_for_allowed_model() {
677        let mut callbacks = ModelCallbacks::new();
678        callbacks.add(
679            ModelEvent::BeforeSave,
680            Callback::before("if_allowed", |model: &mut TestModel| {
681                model.push("if_allowed");
682                CallbackResult::Continue
683            })
684            .with_conditions(
685                rustrails_support::callbacks::CallbackConditions::new().if_cond(|target| {
686                    target
687                        .downcast_ref::<TestModel>()
688                        .map(|model| model.allow)
689                        .unwrap_or(false)
690                }),
691            ),
692        );
693
694        let mut model = TestModel {
695            allow: true,
696            ..TestModel::default()
697        };
698        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
699
700        assert_eq!(result, CallbackResult::Continue);
701        assert_eq!(model.log, vec!["if_allowed"]);
702    }
703
704    #[test]
705    fn conditional_if_skips_for_disallowed_model() {
706        let mut callbacks = ModelCallbacks::new();
707        callbacks.add(
708            ModelEvent::BeforeSave,
709            Callback::before("if_allowed", |model: &mut TestModel| {
710                model.push("if_allowed");
711                CallbackResult::Continue
712            })
713            .with_conditions(
714                rustrails_support::callbacks::CallbackConditions::new().if_cond(|target| {
715                    target
716                        .downcast_ref::<TestModel>()
717                        .map(|model| model.allow)
718                        .unwrap_or(false)
719                }),
720            ),
721        );
722
723        let mut model = TestModel::default();
724        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
725
726        assert_eq!(result, CallbackResult::Continue);
727        assert!(model.log.is_empty());
728    }
729
730    #[test]
731    fn conditional_unless_skips_blocked_model() {
732        let mut callbacks = ModelCallbacks::new();
733        callbacks.add(
734            ModelEvent::BeforeSave,
735            Callback::before("unless_blocked", |model: &mut TestModel| {
736                model.push("unless_blocked");
737                CallbackResult::Continue
738            })
739            .with_conditions(
740                rustrails_support::callbacks::CallbackConditions::new().unless_cond(|target| {
741                    target
742                        .downcast_ref::<TestModel>()
743                        .map(|model| model.blocked)
744                        .unwrap_or(false)
745                }),
746            ),
747        );
748
749        let mut model = TestModel {
750            blocked: true,
751            ..TestModel::default()
752        };
753        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
754
755        assert_eq!(result, CallbackResult::Continue);
756        assert!(model.log.is_empty());
757    }
758
759    #[test]
760    fn conditional_unless_runs_when_not_blocked() {
761        let mut callbacks = ModelCallbacks::new();
762        callbacks.add(
763            ModelEvent::BeforeSave,
764            Callback::before("unless_blocked", |model: &mut TestModel| {
765                model.push("unless_blocked");
766                CallbackResult::Continue
767            })
768            .with_conditions(
769                rustrails_support::callbacks::CallbackConditions::new().unless_cond(|target| {
770                    target
771                        .downcast_ref::<TestModel>()
772                        .map(|model| model.blocked)
773                        .unwrap_or(false)
774                }),
775            ),
776        );
777
778        let mut model = TestModel::default();
779        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
780
781        assert_eq!(result, CallbackResult::Continue);
782        assert_eq!(model.log, vec!["unless_blocked"]);
783    }
784
785    #[test]
786    fn reset_clears_one_event_without_touching_other_events() {
787        let mut callbacks = ModelCallbacks::new();
788        callbacks.add(
789            ModelEvent::BeforeSave,
790            Callback::before("before_save", |model: &mut TestModel| {
791                model.push("before_save");
792                CallbackResult::Continue
793            }),
794        );
795        callbacks.add(
796            ModelEvent::AfterSave,
797            Callback::after("after_save", |model: &mut TestModel| {
798                model.push("after_save");
799                CallbackResult::Continue
800            }),
801        );
802        callbacks.reset(ModelEvent::BeforeSave);
803
804        let mut model = TestModel::default();
805        assert_eq!(
806            callbacks.run(ModelEvent::BeforeSave, &mut model),
807            CallbackResult::Continue
808        );
809        assert!(model.log.is_empty());
810
811        assert_eq!(
812            callbacks.run(ModelEvent::AfterSave, &mut model),
813            CallbackResult::Continue
814        );
815        assert_eq!(model.log, vec!["after_save"]);
816    }
817
818    #[test]
819    fn skip_missing_callback_is_noop() {
820        let mut callbacks = ModelCallbacks::new();
821        callbacks.add(
822            ModelEvent::BeforeSave,
823            Callback::before("keep", |model: &mut TestModel| {
824                model.push("keep");
825                CallbackResult::Continue
826            }),
827        );
828        callbacks.skip(ModelEvent::BeforeSave, "missing");
829
830        let mut model = TestModel::default();
831        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
832
833        assert_eq!(result, CallbackResult::Continue);
834        assert_eq!(model.log, vec!["keep"]);
835    }
836
837    #[test]
838    fn add_replaces_existing_callback_with_same_name() {
839        let mut callbacks = ModelCallbacks::new();
840        callbacks.add(
841            ModelEvent::BeforeSave,
842            Callback::before("audit", |model: &mut TestModel| {
843                model.push("first");
844                CallbackResult::Continue
845            }),
846        );
847        callbacks.add(
848            ModelEvent::BeforeSave,
849            Callback::before("audit", |model: &mut TestModel| {
850                model.push("second");
851                CallbackResult::Continue
852            }),
853        );
854
855        let mut model = TestModel::default();
856        let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
857
858        assert_eq!(result, CallbackResult::Continue);
859        assert_eq!(model.log, vec!["second"]);
860    }
861
862    #[test]
863    fn test_complete_callback_chain() {
864        let mut chain = CallbackChain::new("create");
865        chain.set_run_after_on_halt(false);
866        chain.add(Callback::before(
867            "before_create",
868            |model: &mut TestModel| {
869                model.push("before_create");
870                CallbackResult::Continue
871            },
872        ));
873        chain.add(Callback::around(
874            "around_create",
875            |model: &mut TestModel, next| {
876                model.push("before_around_create");
877                let result = next(model);
878                model.push("after_around_create");
879                result
880            },
881        ));
882        chain.add(Callback::after(
883            "final_callback",
884            |model: &mut TestModel| {
885                model.push("final_callback");
886                CallbackResult::Continue
887            },
888        ));
889        chain.add(Callback::after("after_create", |model: &mut TestModel| {
890            model.push("after_create");
891            CallbackResult::Continue
892        }));
893
894        let mut model = TestModel::default();
895        let result = chain.run_with(&mut model, "create", |model| {
896            model.push("create");
897            CallbackResult::Continue
898        });
899
900        assert_eq!(result, CallbackResult::Continue);
901        assert_eq!(
902            model.log,
903            vec![
904                "before_create",
905                "before_around_create",
906                "create",
907                "after_around_create",
908                "final_callback",
909                "after_create",
910            ]
911        );
912    }
913
914    #[test]
915    fn test_the_callback_chain_is_not_halted_when_around_or_after_callbacks_return_false() {
916        let mut chain = CallbackChain::new("create");
917        chain.set_run_after_on_halt(false);
918        chain.add(Callback::before(
919            "before_create",
920            |model: &mut TestModel| {
921                model.push("before_create");
922                CallbackResult::Continue
923            },
924        ));
925        chain.add(Callback::around(
926            "around_create",
927            |model: &mut TestModel, next| {
928                model.push("before_around_create");
929                let _ = next(model);
930                model.push("after_around_create");
931                CallbackResult::Continue
932            },
933        ));
934        chain.add(Callback::after(
935            "final_callback",
936            |model: &mut TestModel| {
937                model.push("final_callback");
938                CallbackResult::Continue
939            },
940        ));
941        chain.add(Callback::after("after_create", |model: &mut TestModel| {
942            model.push("after_create");
943            CallbackResult::Halt
944        }));
945
946        let mut model = TestModel::default();
947        let result = chain.run_with(&mut model, "create", |model| {
948            model.push("create");
949            CallbackResult::Continue
950        });
951
952        assert_eq!(result, CallbackResult::Continue);
953        assert_eq!(model.log.last(), Some(&"after_create"));
954    }
955
956    #[test]
957    fn test_the_callback_chain_is_not_halted_when_a_before_callback_returns_false() {
958        let mut chain = CallbackChain::new("create");
959        chain.set_run_after_on_halt(false);
960        chain.add(Callback::before(
961            "before_create",
962            |model: &mut TestModel| {
963                model.push("before_create");
964                CallbackResult::Continue
965            },
966        ));
967        chain.add(Callback::after(
968            "final_callback",
969            |model: &mut TestModel| {
970                model.push("final_callback");
971                CallbackResult::Continue
972            },
973        ));
974
975        let mut model = TestModel::default();
976        let result = chain.run_with(&mut model, "create", |model| {
977            model.push("create");
978            CallbackResult::Continue
979        });
980
981        assert_eq!(result, CallbackResult::Continue);
982        assert_eq!(model.log, vec!["before_create", "create", "final_callback"]);
983    }
984
985    #[test]
986    fn test_the_callback_chain_is_halted_when_a_callback_throws_abort() {
987        let mut chain = CallbackChain::new("create");
988        chain.set_run_after_on_halt(false);
989        chain.add(Callback::before(
990            "before_create",
991            |model: &mut TestModel| {
992                model.push("before_create");
993                CallbackResult::Halt
994            },
995        ));
996        chain.add(Callback::around(
997            "around_create",
998            |model: &mut TestModel, next| {
999                model.push("before_around_create");
1000                next(model)
1001            },
1002        ));
1003
1004        let mut model = TestModel::default();
1005        let result = chain.run_with(&mut model, "create", |model| {
1006            model.push("create");
1007            CallbackResult::Continue
1008        });
1009
1010        assert_eq!(result, CallbackResult::Halt);
1011        assert_eq!(model.log, vec!["before_create"]);
1012    }
1013
1014    #[test]
1015    fn test_after_callbacks_are_not_executed_if_the_block_returns_false() {
1016        let mut chain = CallbackChain::new("create");
1017        chain.set_run_after_on_halt(false);
1018        chain.add(Callback::before(
1019            "before_create",
1020            |model: &mut TestModel| {
1021                model.push("before_create");
1022                CallbackResult::Continue
1023            },
1024        ));
1025        chain.add(Callback::around(
1026            "around_create",
1027            |model: &mut TestModel, next| {
1028                model.push("before_around_create");
1029                let result = next(model);
1030                model.push("after_around_create");
1031                result
1032            },
1033        ));
1034        chain.add(Callback::after("after_create", |model: &mut TestModel| {
1035            model.push("after_create");
1036            CallbackResult::Continue
1037        }));
1038
1039        let mut model = TestModel::default();
1040        let result = chain.run_with(&mut model, "create", |model| {
1041            model.push("create");
1042            CallbackResult::Halt
1043        });
1044
1045        assert_eq!(result, CallbackResult::Halt);
1046        assert_eq!(
1047            model.log,
1048            vec![
1049                "before_create",
1050                "before_around_create",
1051                "create",
1052                "after_around_create",
1053            ]
1054        );
1055    }
1056
1057    #[test]
1058    #[ignore = "Rails-specific: define_model_callbacks generates class-level before/around/after helpers that ModelCallbacks does not expose"]
1059    fn test_only_selects_which_types_of_callbacks_should_be_created() {}
1060
1061    #[test]
1062    #[ignore = "Rails-specific: define_model_callbacks array filtering is part of the callback DSL, not the runtime registry"]
1063    fn test_only_selects_which_types_of_callbacks_should_be_created_from_an_array_list() {}
1064
1065    #[test]
1066    #[ignore = "Rails-specific: define_model_callbacks with only: [] produces no helper methods, which ModelCallbacks does not generate"]
1067    fn test_no_callbacks_should_be_created() {}
1068
1069    #[test]
1070    fn test_the_if_option_array_should_not_be_mutated_by_an_after_callback() {
1071        let only = vec!["create".to_string()];
1072        let conditions = CallbackConditions::new().only(only.iter().map(String::as_str));
1073        let mut chain = CallbackChain::new("create");
1074        chain.set_run_after_on_halt(false);
1075        chain.add(
1076            Callback::after("after_create", |model: &mut TestModel| {
1077                model.push("after_create");
1078                CallbackResult::Continue
1079            })
1080            .with_conditions(conditions),
1081        );
1082
1083        let mut model = TestModel::default();
1084        let result = chain.run_with(&mut model, "create", |model| {
1085            model.push("create");
1086            CallbackResult::Continue
1087        });
1088
1089        assert_eq!(result, CallbackResult::Continue);
1090        assert_eq!(only, vec!["create".to_string()]);
1091        assert_eq!(model.log, vec!["create", "after_create"]);
1092    }
1093
1094    #[test]
1095    #[ignore = "Rails-specific: the Rust callback registry registers one callback per add call instead of supporting varargs DSL declarations"]
1096    fn test_after_create_callbacks_with_both_callbacks_declared_in_one_line() {}
1097
1098    #[test]
1099    fn test_after_create_callbacks_with_both_callbacks_declared_in_different_lines() {
1100        let mut callbacks = ModelCallbacks::new();
1101        callbacks.add(
1102            ModelEvent::AfterCreate,
1103            Callback::after("callback1", |model: &mut TestModel| {
1104                model.push("callback1");
1105                CallbackResult::Continue
1106            }),
1107        );
1108        callbacks.add(
1109            ModelEvent::AfterCreate,
1110            Callback::after("callback2", |model: &mut TestModel| {
1111                model.push("callback2");
1112                CallbackResult::Continue
1113            }),
1114        );
1115
1116        let mut model = TestModel::default();
1117        let result = callbacks.run(ModelEvent::AfterCreate, &mut model);
1118
1119        assert_eq!(result, CallbackResult::Continue);
1120        assert_eq!(model.log, vec!["callback1", "callback2"]);
1121    }
1122}