use std::{any::Any, collections::HashMap};
use rustrails_support::callbacks::{
AroundContinuation, Callback, CallbackConditions, CallbackFilter, CallbackKind, CallbackResult,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModelEvent {
BeforeValidation,
AfterValidation,
BeforeSave,
AfterSave,
BeforeCreate,
AfterCreate,
BeforeUpdate,
AfterUpdate,
BeforeDestroy,
AfterDestroy,
}
impl ModelEvent {
#[must_use]
pub fn all() -> &'static [ModelEvent] {
static EVENTS: [ModelEvent; 10] = [
ModelEvent::BeforeValidation,
ModelEvent::AfterValidation,
ModelEvent::BeforeSave,
ModelEvent::AfterSave,
ModelEvent::BeforeCreate,
ModelEvent::AfterCreate,
ModelEvent::BeforeUpdate,
ModelEvent::AfterUpdate,
ModelEvent::BeforeDestroy,
ModelEvent::AfterDestroy,
];
&EVENTS
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::BeforeValidation => "before_validation",
Self::AfterValidation => "after_validation",
Self::BeforeSave => "before_save",
Self::AfterSave => "after_save",
Self::BeforeCreate => "before_create",
Self::AfterCreate => "after_create",
Self::BeforeUpdate => "before_update",
Self::AfterUpdate => "after_update",
Self::BeforeDestroy => "before_destroy",
Self::AfterDestroy => "after_destroy",
}
}
}
#[derive(Debug)]
struct EventCallbacks<T: Send + Sync + 'static> {
name: &'static str,
callbacks: Vec<Callback<T>>,
}
impl<T: Send + Sync + 'static> EventCallbacks<T> {
fn new(event: ModelEvent) -> Self {
Self {
name: event.as_str(),
callbacks: Vec::new(),
}
}
fn add(&mut self, callback: Callback<T>) {
self.skip(&callback.name);
self.callbacks.push(callback);
}
fn run(&self, target: &mut T) -> CallbackResult {
if self.callbacks.is_empty() {
return CallbackResult::Continue;
}
let target_any = &*target as &dyn Any;
let applicable = self
.callbacks
.iter()
.filter(|callback| conditions_match(&callback.conditions, target_any, self.name))
.collect::<Vec<_>>();
for callback in applicable
.iter()
.copied()
.filter(|callback| callback.kind == CallbackKind::Before)
{
let result = match &callback.filter {
CallbackFilter::Standard(filter) => filter(target),
CallbackFilter::Around(_) => CallbackResult::Continue,
};
if result == CallbackResult::Halt {
return result;
}
}
let around = applicable
.iter()
.copied()
.filter(|callback| callback.kind == CallbackKind::Around)
.collect::<Vec<_>>();
let result = invoke_around(&around, target, Box::new(|_| CallbackResult::Continue));
if result == CallbackResult::Halt {
return result;
}
for callback in applicable
.iter()
.copied()
.filter(|callback| callback.kind == CallbackKind::After)
{
if let CallbackFilter::Standard(filter) = &callback.filter {
let _ = filter(target);
}
}
result
}
fn skip(&mut self, name: &str) {
self.callbacks.retain(|callback| callback.name != name);
}
fn reset(&mut self, kind: Option<CallbackKind>) {
match kind {
Some(kind) => self.callbacks.retain(|callback| callback.kind != kind),
None => self.callbacks.clear(),
}
}
}
#[derive(Debug)]
pub struct ModelCallbacks<T: Send + Sync + 'static> {
chains: HashMap<ModelEvent, EventCallbacks<T>>,
}
impl<T: Send + Sync + 'static> Default for ModelCallbacks<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Send + Sync + 'static> ModelCallbacks<T> {
#[must_use]
pub fn new() -> Self {
let chains = ModelEvent::all()
.iter()
.copied()
.map(|event| (event, EventCallbacks::new(event)))
.collect();
Self { chains }
}
pub fn add(&mut self, event: ModelEvent, callback: Callback<T>) {
self.chains
.entry(event)
.or_insert_with(|| EventCallbacks::new(event))
.add(callback);
}
pub fn run(&self, event: ModelEvent, target: &mut T) -> CallbackResult {
self.chains
.get(&event)
.map_or(CallbackResult::Continue, |callbacks| callbacks.run(target))
}
pub fn skip(&mut self, event: ModelEvent, name: &str) {
if let Some(callbacks) = self.chains.get_mut(&event) {
callbacks.skip(name);
}
}
pub fn reset(&mut self, event: ModelEvent) {
self.reset_callbacks(event, None);
}
pub fn reset_callbacks(&mut self, event: ModelEvent, kind: Option<CallbackKind>) {
if let Some(callbacks) = self.chains.get_mut(&event) {
callbacks.reset(kind);
}
}
}
fn invoke_around<'a, T: Send + Sync + 'static>(
callbacks: &'a [&'a Callback<T>],
target: &mut T,
action: AroundContinuation<'a, T>,
) -> CallbackResult {
match callbacks.split_first() {
Some((callback, rest)) => match &callback.filter {
CallbackFilter::Around(filter) => filter(
target,
Box::new(move |target| invoke_around(rest, target, action)),
),
CallbackFilter::Standard(_) => invoke_around(rest, target, action),
},
None => action(target),
}
}
fn conditions_match(conditions: &CallbackConditions, target: &dyn Any, action_name: &str) -> bool {
if let Some(only) = &conditions.only
&& !only.iter().any(|candidate| candidate == action_name)
{
return false;
}
if let Some(except) = &conditions.except
&& except.iter().any(|candidate| candidate == action_name)
{
return false;
}
if let Some(predicate) = &conditions.if_cond
&& !predicate(target)
{
return false;
}
if let Some(predicate) = &conditions.unless_cond
&& predicate(target)
{
return false;
}
true
}
pub trait HasModelCallbacks: Send + Sync + Sized + 'static {
fn model_callbacks() -> &'static ModelCallbacks<Self>;
fn run_callbacks(&mut self, event: ModelEvent) -> CallbackResult {
Self::model_callbacks().run(event, self)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use once_cell::sync::Lazy;
use rustrails_support::callbacks::{
Callback, CallbackChain, CallbackConditions, CallbackKind, CallbackResult,
};
use super::{HasModelCallbacks, ModelCallbacks, ModelEvent};
#[derive(Debug, Default)]
struct TestModel {
log: Vec<&'static str>,
allow: bool,
blocked: bool,
}
impl TestModel {
fn push(&mut self, entry: &'static str) {
self.log.push(entry);
}
}
static TEST_MODEL_CALLBACKS: Lazy<ModelCallbacks<TestModel>> = Lazy::new(|| {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeValidation,
Callback::before("record_before_validation", |model: &mut TestModel| {
model.push("before_validation");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::AfterValidation,
Callback::after("record_after_validation", |model: &mut TestModel| {
model.push("after_validation");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::AfterCreate,
Callback::after("record_after_create", |model: &mut TestModel| {
model.push("after_create");
CallbackResult::Continue
}),
);
callbacks
});
impl HasModelCallbacks for TestModel {
fn model_callbacks() -> &'static ModelCallbacks<Self> {
&TEST_MODEL_CALLBACKS
}
}
#[test]
fn before_and_after_validation_callbacks_fire() {
let mut model = TestModel::default();
assert_eq!(
model.run_callbacks(ModelEvent::BeforeValidation),
CallbackResult::Continue
);
assert_eq!(
model.run_callbacks(ModelEvent::AfterValidation),
CallbackResult::Continue
);
assert_eq!(model.log, vec!["before_validation", "after_validation"]);
}
#[test]
fn before_save_can_halt() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("halt_save", |model: &mut TestModel| {
model.push("before_save");
CallbackResult::Halt
}),
);
callbacks.add(
ModelEvent::AfterSave,
Callback::after("after_save", |model: &mut TestModel| {
model.push("after_save");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Halt);
assert_eq!(model.log, vec!["before_save"]);
}
#[test]
fn after_create_fires() {
let mut model = TestModel::default();
let result = model.run_callbacks(ModelEvent::AfterCreate);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["after_create"]);
}
#[test]
fn before_callbacks_run_in_declaration_order() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeValidation,
Callback::before("first", |model: &mut TestModel| {
model.push("first");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::BeforeValidation,
Callback::before("second", |model: &mut TestModel| {
model.push("second");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::BeforeValidation,
Callback::before("third", |model: &mut TestModel| {
model.push("third");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["first", "second", "third"]);
}
#[test]
fn skip_callback_by_name() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeValidation,
Callback::before("keep", |model: &mut TestModel| {
model.push("keep");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::BeforeValidation,
Callback::before("skip_me", |model: &mut TestModel| {
model.push("skip_me");
CallbackResult::Continue
}),
);
callbacks.skip(ModelEvent::BeforeValidation, "skip_me");
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["keep"]);
}
#[test]
fn reset_clears_callbacks_for_an_event() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeDestroy,
Callback::before("cleanup", |model: &mut TestModel| {
model.push("cleanup");
CallbackResult::Continue
}),
);
callbacks.reset(ModelEvent::BeforeDestroy);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeDestroy, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert!(model.log.is_empty());
}
#[test]
fn reset_callbacks_can_clear_one_kind_without_disturbing_other_kinds() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("before", |model: &mut TestModel| {
model.push("before");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::BeforeSave,
Callback::after("after", |model: &mut TestModel| {
model.push("after");
CallbackResult::Continue
}),
);
callbacks.reset_callbacks(ModelEvent::BeforeSave, Some(CallbackKind::Before));
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["after"]);
}
#[test]
fn all_model_events_are_covered_once() {
let expected = [
(ModelEvent::BeforeValidation, "before_validation"),
(ModelEvent::AfterValidation, "after_validation"),
(ModelEvent::BeforeSave, "before_save"),
(ModelEvent::AfterSave, "after_save"),
(ModelEvent::BeforeCreate, "before_create"),
(ModelEvent::AfterCreate, "after_create"),
(ModelEvent::BeforeUpdate, "before_update"),
(ModelEvent::AfterUpdate, "after_update"),
(ModelEvent::BeforeDestroy, "before_destroy"),
(ModelEvent::AfterDestroy, "after_destroy"),
];
assert_eq!(ModelEvent::all().len(), expected.len());
assert_eq!(
ModelEvent::all()
.iter()
.map(|event| (*event, event.as_str()))
.collect::<Vec<_>>(),
expected.to_vec()
);
let names = ModelEvent::all()
.iter()
.map(ModelEvent::as_str)
.collect::<HashSet<_>>();
assert_eq!(names.len(), ModelEvent::all().len());
}
#[test]
fn before_save_callbacks_fire() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("audit", |model: &mut TestModel| {
model.push("before_save");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["before_save"]);
}
#[test]
fn after_save_callbacks_fire() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::AfterSave,
Callback::after("publish", |model: &mut TestModel| {
model.push("after_save");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::AfterSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["after_save"]);
}
#[test]
fn after_callbacks_run_in_declaration_order() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::AfterSave,
Callback::after("first", |model: &mut TestModel| {
model.push("first");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::AfterSave,
Callback::after("second", |model: &mut TestModel| {
model.push("second");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::AfterSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["first", "second"]);
}
#[test]
fn before_save_halt_prevents_later_before_callbacks() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("halt", |model: &mut TestModel| {
model.push("halt");
CallbackResult::Halt
}),
);
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("later", |model: &mut TestModel| {
model.push("later");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Halt);
assert_eq!(model.log, vec!["halt"]);
}
#[test]
fn around_callbacks_wrap_default_action() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeValidation,
Callback::around("wrap", |model: &mut TestModel, next| {
model.push("around-before");
let result = next(model);
model.push("around-after");
result
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["around-before", "around-after"]);
}
#[test]
fn around_callback_can_halt_before_inner_action() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeValidation,
Callback::around("halt", |model: &mut TestModel, _next| {
model.push("around-only");
CallbackResult::Halt
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeValidation, &mut model);
assert_eq!(result, CallbackResult::Halt);
assert_eq!(model.log, vec!["around-only"]);
}
#[test]
fn conditional_only_matches_event_name() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("only_save", |model: &mut TestModel| {
model.push("only_save");
CallbackResult::Continue
})
.with_conditions(
rustrails_support::callbacks::CallbackConditions::new().only(["before_save"]),
),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["only_save"]);
}
#[test]
fn conditional_except_skips_matching_event() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("skip_save", |model: &mut TestModel| {
model.push("skip_save");
CallbackResult::Continue
})
.with_conditions(
rustrails_support::callbacks::CallbackConditions::new().except(["before_save"]),
),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert!(model.log.is_empty());
}
#[test]
fn conditional_if_runs_for_allowed_model() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("if_allowed", |model: &mut TestModel| {
model.push("if_allowed");
CallbackResult::Continue
})
.with_conditions(
rustrails_support::callbacks::CallbackConditions::new().if_cond(|target| {
target
.downcast_ref::<TestModel>()
.map(|model| model.allow)
.unwrap_or(false)
}),
),
);
let mut model = TestModel {
allow: true,
..TestModel::default()
};
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["if_allowed"]);
}
#[test]
fn conditional_if_skips_for_disallowed_model() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("if_allowed", |model: &mut TestModel| {
model.push("if_allowed");
CallbackResult::Continue
})
.with_conditions(
rustrails_support::callbacks::CallbackConditions::new().if_cond(|target| {
target
.downcast_ref::<TestModel>()
.map(|model| model.allow)
.unwrap_or(false)
}),
),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert!(model.log.is_empty());
}
#[test]
fn conditional_unless_skips_blocked_model() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("unless_blocked", |model: &mut TestModel| {
model.push("unless_blocked");
CallbackResult::Continue
})
.with_conditions(
rustrails_support::callbacks::CallbackConditions::new().unless_cond(|target| {
target
.downcast_ref::<TestModel>()
.map(|model| model.blocked)
.unwrap_or(false)
}),
),
);
let mut model = TestModel {
blocked: true,
..TestModel::default()
};
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert!(model.log.is_empty());
}
#[test]
fn conditional_unless_runs_when_not_blocked() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("unless_blocked", |model: &mut TestModel| {
model.push("unless_blocked");
CallbackResult::Continue
})
.with_conditions(
rustrails_support::callbacks::CallbackConditions::new().unless_cond(|target| {
target
.downcast_ref::<TestModel>()
.map(|model| model.blocked)
.unwrap_or(false)
}),
),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["unless_blocked"]);
}
#[test]
fn reset_clears_one_event_without_touching_other_events() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("before_save", |model: &mut TestModel| {
model.push("before_save");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::AfterSave,
Callback::after("after_save", |model: &mut TestModel| {
model.push("after_save");
CallbackResult::Continue
}),
);
callbacks.reset(ModelEvent::BeforeSave);
let mut model = TestModel::default();
assert_eq!(
callbacks.run(ModelEvent::BeforeSave, &mut model),
CallbackResult::Continue
);
assert!(model.log.is_empty());
assert_eq!(
callbacks.run(ModelEvent::AfterSave, &mut model),
CallbackResult::Continue
);
assert_eq!(model.log, vec!["after_save"]);
}
#[test]
fn skip_missing_callback_is_noop() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("keep", |model: &mut TestModel| {
model.push("keep");
CallbackResult::Continue
}),
);
callbacks.skip(ModelEvent::BeforeSave, "missing");
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["keep"]);
}
#[test]
fn add_replaces_existing_callback_with_same_name() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("audit", |model: &mut TestModel| {
model.push("first");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::BeforeSave,
Callback::before("audit", |model: &mut TestModel| {
model.push("second");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::BeforeSave, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["second"]);
}
#[test]
fn test_complete_callback_chain() {
let mut chain = CallbackChain::new("create");
chain.set_run_after_on_halt(false);
chain.add(Callback::before(
"before_create",
|model: &mut TestModel| {
model.push("before_create");
CallbackResult::Continue
},
));
chain.add(Callback::around(
"around_create",
|model: &mut TestModel, next| {
model.push("before_around_create");
let result = next(model);
model.push("after_around_create");
result
},
));
chain.add(Callback::after(
"final_callback",
|model: &mut TestModel| {
model.push("final_callback");
CallbackResult::Continue
},
));
chain.add(Callback::after("after_create", |model: &mut TestModel| {
model.push("after_create");
CallbackResult::Continue
}));
let mut model = TestModel::default();
let result = chain.run_with(&mut model, "create", |model| {
model.push("create");
CallbackResult::Continue
});
assert_eq!(result, CallbackResult::Continue);
assert_eq!(
model.log,
vec![
"before_create",
"before_around_create",
"create",
"after_around_create",
"final_callback",
"after_create",
]
);
}
#[test]
fn test_the_callback_chain_is_not_halted_when_around_or_after_callbacks_return_false() {
let mut chain = CallbackChain::new("create");
chain.set_run_after_on_halt(false);
chain.add(Callback::before(
"before_create",
|model: &mut TestModel| {
model.push("before_create");
CallbackResult::Continue
},
));
chain.add(Callback::around(
"around_create",
|model: &mut TestModel, next| {
model.push("before_around_create");
let _ = next(model);
model.push("after_around_create");
CallbackResult::Continue
},
));
chain.add(Callback::after(
"final_callback",
|model: &mut TestModel| {
model.push("final_callback");
CallbackResult::Continue
},
));
chain.add(Callback::after("after_create", |model: &mut TestModel| {
model.push("after_create");
CallbackResult::Halt
}));
let mut model = TestModel::default();
let result = chain.run_with(&mut model, "create", |model| {
model.push("create");
CallbackResult::Continue
});
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log.last(), Some(&"after_create"));
}
#[test]
fn test_the_callback_chain_is_not_halted_when_a_before_callback_returns_false() {
let mut chain = CallbackChain::new("create");
chain.set_run_after_on_halt(false);
chain.add(Callback::before(
"before_create",
|model: &mut TestModel| {
model.push("before_create");
CallbackResult::Continue
},
));
chain.add(Callback::after(
"final_callback",
|model: &mut TestModel| {
model.push("final_callback");
CallbackResult::Continue
},
));
let mut model = TestModel::default();
let result = chain.run_with(&mut model, "create", |model| {
model.push("create");
CallbackResult::Continue
});
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["before_create", "create", "final_callback"]);
}
#[test]
fn test_the_callback_chain_is_halted_when_a_callback_throws_abort() {
let mut chain = CallbackChain::new("create");
chain.set_run_after_on_halt(false);
chain.add(Callback::before(
"before_create",
|model: &mut TestModel| {
model.push("before_create");
CallbackResult::Halt
},
));
chain.add(Callback::around(
"around_create",
|model: &mut TestModel, next| {
model.push("before_around_create");
next(model)
},
));
let mut model = TestModel::default();
let result = chain.run_with(&mut model, "create", |model| {
model.push("create");
CallbackResult::Continue
});
assert_eq!(result, CallbackResult::Halt);
assert_eq!(model.log, vec!["before_create"]);
}
#[test]
fn test_after_callbacks_are_not_executed_if_the_block_returns_false() {
let mut chain = CallbackChain::new("create");
chain.set_run_after_on_halt(false);
chain.add(Callback::before(
"before_create",
|model: &mut TestModel| {
model.push("before_create");
CallbackResult::Continue
},
));
chain.add(Callback::around(
"around_create",
|model: &mut TestModel, next| {
model.push("before_around_create");
let result = next(model);
model.push("after_around_create");
result
},
));
chain.add(Callback::after("after_create", |model: &mut TestModel| {
model.push("after_create");
CallbackResult::Continue
}));
let mut model = TestModel::default();
let result = chain.run_with(&mut model, "create", |model| {
model.push("create");
CallbackResult::Halt
});
assert_eq!(result, CallbackResult::Halt);
assert_eq!(
model.log,
vec![
"before_create",
"before_around_create",
"create",
"after_around_create",
]
);
}
#[test]
#[ignore = "Rails-specific: define_model_callbacks generates class-level before/around/after helpers that ModelCallbacks does not expose"]
fn test_only_selects_which_types_of_callbacks_should_be_created() {}
#[test]
#[ignore = "Rails-specific: define_model_callbacks array filtering is part of the callback DSL, not the runtime registry"]
fn test_only_selects_which_types_of_callbacks_should_be_created_from_an_array_list() {}
#[test]
#[ignore = "Rails-specific: define_model_callbacks with only: [] produces no helper methods, which ModelCallbacks does not generate"]
fn test_no_callbacks_should_be_created() {}
#[test]
fn test_the_if_option_array_should_not_be_mutated_by_an_after_callback() {
let only = vec!["create".to_string()];
let conditions = CallbackConditions::new().only(only.iter().map(String::as_str));
let mut chain = CallbackChain::new("create");
chain.set_run_after_on_halt(false);
chain.add(
Callback::after("after_create", |model: &mut TestModel| {
model.push("after_create");
CallbackResult::Continue
})
.with_conditions(conditions),
);
let mut model = TestModel::default();
let result = chain.run_with(&mut model, "create", |model| {
model.push("create");
CallbackResult::Continue
});
assert_eq!(result, CallbackResult::Continue);
assert_eq!(only, vec!["create".to_string()]);
assert_eq!(model.log, vec!["create", "after_create"]);
}
#[test]
#[ignore = "Rails-specific: the Rust callback registry registers one callback per add call instead of supporting varargs DSL declarations"]
fn test_after_create_callbacks_with_both_callbacks_declared_in_one_line() {}
#[test]
fn test_after_create_callbacks_with_both_callbacks_declared_in_different_lines() {
let mut callbacks = ModelCallbacks::new();
callbacks.add(
ModelEvent::AfterCreate,
Callback::after("callback1", |model: &mut TestModel| {
model.push("callback1");
CallbackResult::Continue
}),
);
callbacks.add(
ModelEvent::AfterCreate,
Callback::after("callback2", |model: &mut TestModel| {
model.push("callback2");
CallbackResult::Continue
}),
);
let mut model = TestModel::default();
let result = callbacks.run(ModelEvent::AfterCreate, &mut model);
assert_eq!(result, CallbackResult::Continue);
assert_eq!(model.log, vec!["callback1", "callback2"]);
}
}