use crate::error::DbError;
use crate::hooks::HookContext;
use crate::Value;
use std::collections::HashMap;
use std::sync::RwLock;
pub type BehaviorResult<T> = Result<T, DbError>;
pub trait Behavior: Send + Sync {
fn name(&self) -> &'static str;
fn before_insert(
&self,
_ctx: &HookContext,
_attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
Ok(())
}
fn before_update(
&self,
_ctx: &HookContext,
_attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
Ok(())
}
fn before_delete(
&self,
_ctx: &HookContext,
_attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
Ok(())
}
fn after_find(
&self,
_ctx: &HookContext,
_attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
Ok(())
}
}
pub struct TimestampBehavior {
pub created_field: &'static str,
pub updated_field: &'static str,
}
impl TimestampBehavior {
pub fn new(created_field: &'static str, updated_field: &'static str) -> Self {
Self {
created_field,
updated_field,
}
}
pub fn default_fields() -> Self {
Self::new("created_at", "updated_at")
}
}
impl Behavior for TimestampBehavior {
fn name(&self) -> &'static str {
"TimestampBehavior"
}
fn before_insert(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
let ts = Value::I64(ctx.timestamp as i64);
attrs.insert(self.created_field.to_string(), ts.clone());
attrs.insert(self.updated_field.to_string(), ts);
Ok(())
}
fn before_update(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
attrs.insert(
self.updated_field.to_string(),
Value::I64(ctx.timestamp as i64),
);
Ok(())
}
}
pub struct BlameableBehavior {
pub created_field: &'static str,
pub updated_field: &'static str,
}
impl BlameableBehavior {
pub fn new(created_field: &'static str, updated_field: &'static str) -> Self {
Self {
created_field,
updated_field,
}
}
pub fn default_fields() -> Self {
Self::new("created_by", "updated_by")
}
}
impl Behavior for BlameableBehavior {
fn name(&self) -> &'static str {
"BlameableBehavior"
}
fn before_insert(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
if let Some(op) = ctx.operator_id {
let v = Value::I64(op);
attrs.insert(self.created_field.to_string(), v.clone());
attrs.insert(self.updated_field.to_string(), v);
}
Ok(())
}
fn before_update(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
if let Some(op) = ctx.operator_id {
attrs.insert(self.updated_field.to_string(), Value::I64(op));
}
Ok(())
}
}
pub struct AttributeBehavior {
pub name_str: &'static str,
pub event: crate::hooks::HookEvent,
pub target_field: &'static str,
pub generator: Box<dyn Fn(&HookContext) -> Value + Send + Sync>,
}
impl AttributeBehavior {
pub fn new(
name: &'static str,
event: crate::hooks::HookEvent,
target_field: &'static str,
generator: impl Fn(&HookContext) -> Value + Send + Sync + 'static,
) -> Self {
Self {
name_str: name,
event,
target_field,
generator: Box::new(generator),
}
}
}
impl Behavior for AttributeBehavior {
fn name(&self) -> &'static str {
self.name_str
}
fn before_insert(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
if self.event == crate::hooks::HookEvent::BeforeInsert
|| self.event == crate::hooks::HookEvent::BeforeWrite
|| self.event == crate::hooks::HookEvent::BeforeSave
{
let v = (self.generator)(ctx);
attrs.insert(self.target_field.to_string(), v);
}
Ok(())
}
fn before_update(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
if self.event == crate::hooks::HookEvent::BeforeUpdate
|| self.event == crate::hooks::HookEvent::BeforeWrite
|| self.event == crate::hooks::HookEvent::BeforeSave
{
let v = (self.generator)(ctx);
attrs.insert(self.target_field.to_string(), v);
}
Ok(())
}
fn after_find(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
if self.event == crate::hooks::HookEvent::AfterFind {
let v = (self.generator)(ctx);
attrs.insert(self.target_field.to_string(), v);
}
Ok(())
}
}
pub struct BehaviorRegistry {
behaviors: RwLock<Vec<Box<dyn Behavior>>>,
}
impl BehaviorRegistry {
pub fn new() -> Self {
Self {
behaviors: RwLock::new(Vec::new()),
}
}
pub fn register(&self, behavior: Box<dyn Behavior>) {
let mut guards = self.behaviors.write().unwrap();
guards.push(behavior);
}
pub fn unregister(&self, name: &str) -> bool {
let mut guards = self.behaviors.write().unwrap();
let before = guards.len();
guards.retain(|b| b.name() != name);
guards.len() < before
}
pub fn count(&self) -> usize {
self.behaviors.read().unwrap().len()
}
pub fn names(&self) -> Vec<&'static str> {
self.behaviors
.read()
.unwrap()
.iter()
.map(|b| b.name())
.collect()
}
pub fn before_insert(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
let guards = self.behaviors.read().unwrap();
for b in guards.iter() {
b.before_insert(ctx, attrs)?;
}
Ok(())
}
pub fn before_update(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
let guards = self.behaviors.read().unwrap();
for b in guards.iter() {
b.before_update(ctx, attrs)?;
}
Ok(())
}
pub fn before_delete(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
let guards = self.behaviors.read().unwrap();
for b in guards.iter() {
b.before_delete(ctx, attrs)?;
}
Ok(())
}
pub fn after_find(
&self,
ctx: &HookContext,
attrs: &mut HashMap<String, Value>,
) -> BehaviorResult<()> {
let guards = self.behaviors.read().unwrap();
for b in guards.iter() {
b.after_find(ctx, attrs)?;
}
Ok(())
}
pub fn clear(&self) {
self.behaviors.write().unwrap().clear();
}
}
impl Default for BehaviorRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::HookEvent;
#[test]
fn test_timestamp_behavior_before_insert() {
let b = TimestampBehavior::default_fields();
let ctx = HookContext::default().with_timestamp(1700000000);
let mut attrs = HashMap::new();
b.before_insert(&ctx, &mut attrs).unwrap();
assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1700000000)));
}
#[test]
fn test_timestamp_behavior_before_update() {
let b = TimestampBehavior::default_fields();
let ctx = HookContext::default().with_timestamp(1800000000);
let mut attrs = HashMap::new();
b.before_update(&ctx, &mut attrs).unwrap();
assert!(!attrs.contains_key("created_at"));
assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1800000000)));
}
#[test]
fn test_timestamp_behavior_custom_fields() {
let b = TimestampBehavior::new("create_time", "update_time");
let ctx = HookContext::default().with_timestamp(100);
let mut attrs = HashMap::new();
b.before_insert(&ctx, &mut attrs).unwrap();
assert_eq!(attrs.get("create_time"), Some(&Value::I64(100)));
assert_eq!(attrs.get("update_time"), Some(&Value::I64(100)));
}
#[test]
fn test_timestamp_behavior_name() {
let b = TimestampBehavior::default_fields();
assert_eq!(b.name(), "TimestampBehavior");
}
#[test]
fn test_blameable_behavior_before_insert() {
let b = BlameableBehavior::default_fields();
let ctx = HookContext::default().with_operator(42);
let mut attrs = HashMap::new();
b.before_insert(&ctx, &mut attrs).unwrap();
assert_eq!(attrs.get("created_by"), Some(&Value::I64(42)));
assert_eq!(attrs.get("updated_by"), Some(&Value::I64(42)));
}
#[test]
fn test_blameable_behavior_before_update() {
let b = BlameableBehavior::default_fields();
let ctx = HookContext::default().with_operator(99);
let mut attrs = HashMap::new();
b.before_update(&ctx, &mut attrs).unwrap();
assert!(!attrs.contains_key("created_by"));
assert_eq!(attrs.get("updated_by"), Some(&Value::I64(99)));
}
#[test]
fn test_blameable_behavior_no_operator_skips() {
let b = BlameableBehavior::default_fields();
let ctx = HookContext::default(); let mut attrs = HashMap::new();
b.before_insert(&ctx, &mut attrs).unwrap();
assert!(!attrs.contains_key("created_by"));
assert!(!attrs.contains_key("updated_by"));
}
#[test]
fn test_blameable_behavior_name() {
let b = BlameableBehavior::default_fields();
assert_eq!(b.name(), "BlameableBehavior");
}
#[test]
fn test_attribute_behavior_before_insert() {
let b = AttributeBehavior::new("uuid_gen", HookEvent::BeforeInsert, "uuid", |_ctx| {
Value::String("auto-uuid".to_string())
});
let ctx = HookContext::default();
let mut attrs = HashMap::new();
b.before_insert(&ctx, &mut attrs).unwrap();
assert_eq!(
attrs.get("uuid"),
Some(&Value::String("auto-uuid".to_string()))
);
}
#[test]
fn test_attribute_behavior_event_filter() {
let b = AttributeBehavior::new("test", HookEvent::BeforeInsert, "field", |_ctx| {
Value::I64(1)
});
let ctx = HookContext::default();
let mut attrs = HashMap::new();
b.before_update(&ctx, &mut attrs).unwrap();
assert!(!attrs.contains_key("field"));
}
#[test]
fn test_registry_register_and_count() {
let r = BehaviorRegistry::new();
assert_eq!(r.count(), 0);
r.register(Box::new(TimestampBehavior::default_fields()));
assert_eq!(r.count(), 1);
r.register(Box::new(BlameableBehavior::default_fields()));
assert_eq!(r.count(), 2);
}
#[test]
fn test_registry_unregister_by_name() {
let r = BehaviorRegistry::new();
r.register(Box::new(TimestampBehavior::default_fields()));
r.register(Box::new(BlameableBehavior::default_fields()));
assert_eq!(r.count(), 2);
let removed = r.unregister("TimestampBehavior");
assert!(removed);
assert_eq!(r.count(), 1);
let removed2 = r.unregister("NonExistent");
assert!(!removed2);
}
#[test]
fn test_registry_names() {
let r = BehaviorRegistry::new();
r.register(Box::new(TimestampBehavior::default_fields()));
r.register(Box::new(BlameableBehavior::default_fields()));
let names = r.names();
assert!(names.contains(&"TimestampBehavior"));
assert!(names.contains(&"BlameableBehavior"));
}
#[test]
fn test_registry_before_insert_dispatches_all() {
let r = BehaviorRegistry::new();
r.register(Box::new(TimestampBehavior::default_fields()));
r.register(Box::new(BlameableBehavior::default_fields()));
let ctx = HookContext::default()
.with_operator(100)
.with_timestamp(1700000000);
let mut attrs = HashMap::new();
r.before_insert(&ctx, &mut attrs).unwrap();
assert_eq!(attrs.get("created_at"), Some(&Value::I64(1700000000)));
assert_eq!(attrs.get("created_by"), Some(&Value::I64(100)));
}
#[test]
fn test_registry_before_update_dispatches_all() {
let r = BehaviorRegistry::new();
r.register(Box::new(TimestampBehavior::default_fields()));
r.register(Box::new(BlameableBehavior::default_fields()));
let ctx = HookContext::default()
.with_operator(200)
.with_timestamp(1800000000);
let mut attrs = HashMap::new();
r.before_update(&ctx, &mut attrs).unwrap();
assert!(!attrs.contains_key("created_at"));
assert_eq!(attrs.get("updated_at"), Some(&Value::I64(1800000000)));
assert!(!attrs.contains_key("created_by"));
assert_eq!(attrs.get("updated_by"), Some(&Value::I64(200)));
}
#[test]
fn test_registry_clear() {
let r = BehaviorRegistry::new();
r.register(Box::new(TimestampBehavior::default_fields()));
r.register(Box::new(BlameableBehavior::default_fields()));
assert_eq!(r.count(), 2);
r.clear();
assert_eq!(r.count(), 0);
}
#[test]
fn test_registry_default() {
let r = BehaviorRegistry::default();
assert_eq!(r.count(), 0);
}
#[test]
fn test_registry_empty_dispatches_no_op() {
let r = BehaviorRegistry::new();
let ctx = HookContext::default();
let mut attrs = HashMap::new();
assert!(r.before_insert(&ctx, &mut attrs).is_ok());
assert!(r.before_update(&ctx, &mut attrs).is_ok());
assert!(r.before_delete(&ctx, &mut attrs).is_ok());
assert!(r.after_find(&ctx, &mut attrs).is_ok());
assert!(attrs.is_empty());
}
#[test]
fn test_combined_timestamp_and_blameable() {
let r = BehaviorRegistry::new();
r.register(Box::new(TimestampBehavior::default_fields()));
r.register(Box::new(BlameableBehavior::default_fields()));
let ctx1 = HookContext::default().with_operator(1).with_timestamp(1000);
let mut attrs1 = HashMap::new();
r.before_insert(&ctx1, &mut attrs1).unwrap();
assert_eq!(attrs1.get("created_at"), Some(&Value::I64(1000)));
assert_eq!(attrs1.get("updated_at"), Some(&Value::I64(1000)));
assert_eq!(attrs1.get("created_by"), Some(&Value::I64(1)));
assert_eq!(attrs1.get("updated_by"), Some(&Value::I64(1)));
let ctx2 = HookContext::default().with_operator(2).with_timestamp(2000);
let mut attrs2 = HashMap::new();
r.before_update(&ctx2, &mut attrs2).unwrap();
assert!(!attrs2.contains_key("created_at"));
assert_eq!(attrs2.get("updated_at"), Some(&Value::I64(2000)));
assert!(!attrs2.contains_key("created_by"));
assert_eq!(attrs2.get("updated_by"), Some(&Value::I64(2)));
}
}