use crate::{FqlPattern, HookAction, HookContext, HookPoint};
pub trait Hook: Send + Sync {
fn id(&self) -> &str;
fn fql_pattern(&self) -> &FqlPattern;
fn hook_point(&self) -> HookPoint;
fn priority(&self) -> i32 {
100
}
fn execute(&self, ctx: HookContext) -> HookAction;
}
#[cfg(any(test, feature = "test-utils"))]
pub mod testing {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
pub struct MockHook {
pub id: String,
pub fql: FqlPattern,
pub point: HookPoint,
pub priority: i32,
pub action_fn: Box<dyn Fn(HookContext) -> HookAction + Send + Sync>,
pub call_count: Arc<AtomicUsize>,
}
impl MockHook {
pub fn pass_through(id: &str, fql: &str, point: HookPoint) -> Self {
Self {
id: id.to_string(),
fql: FqlPattern::parse(fql).expect("valid FQL for MockHook"),
point,
priority: 100,
action_fn: Box::new(|ctx| HookAction::Continue(Box::new(ctx))),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn modifier(
id: &str,
fql: &str,
point: HookPoint,
modifier: impl Fn(&mut HookContext) + Send + Sync + 'static,
) -> Self {
Self {
id: id.to_string(),
fql: FqlPattern::parse(fql).expect("valid FQL for MockHook"),
point,
priority: 100,
action_fn: Box::new(move |mut ctx| {
modifier(&mut ctx);
HookAction::Continue(Box::new(ctx))
}),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn aborter(id: &str, fql: &str, point: HookPoint, reason: &str) -> Self {
let reason = reason.to_string();
Self {
id: id.to_string(),
fql: FqlPattern::parse(fql).expect("valid FQL for MockHook"),
point,
priority: 100,
action_fn: Box::new(move |_ctx| HookAction::Abort {
reason: reason.clone(),
}),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn skipper(id: &str, fql: &str, point: HookPoint, value: serde_json::Value) -> Self {
Self {
id: id.to_string(),
fql: FqlPattern::parse(fql).expect("valid FQL for MockHook"),
point,
priority: 100,
action_fn: Box::new(move |_ctx| HookAction::Skip(value.clone())),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn replacer(id: &str, fql: &str, point: HookPoint, value: serde_json::Value) -> Self {
Self {
id: id.to_string(),
fql: FqlPattern::parse(fql).expect("valid FQL for MockHook"),
point,
priority: 100,
action_fn: Box::new(move |_ctx| HookAction::Replace(value.clone())),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
#[must_use]
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn calls(&self) -> usize {
self.call_count.load(Ordering::SeqCst)
}
}
impl Hook for MockHook {
fn id(&self) -> &str {
&self.id
}
fn fql_pattern(&self) -> &FqlPattern {
&self.fql
}
fn hook_point(&self) -> HookPoint {
self.point
}
fn priority(&self) -> i32 {
self.priority
}
fn execute(&self, ctx: HookContext) -> HookAction {
self.call_count.fetch_add(1, Ordering::SeqCst);
(self.action_fn)(ctx)
}
}
}
#[cfg(test)]
mod tests {
use super::testing::MockHook;
use super::*;
use orcs_types::{ChannelId, ComponentId, Principal};
use serde_json::json;
fn test_ctx() -> HookContext {
HookContext::new(
HookPoint::RequestPreDispatch,
ComponentId::builtin("llm"),
ChannelId::new(),
Principal::System,
0,
json!({"op": "test"}),
)
}
#[test]
fn mock_pass_through() {
let hook = MockHook::pass_through("test", "*::*", HookPoint::RequestPreDispatch);
let ctx = test_ctx();
let action = hook.execute(ctx.clone());
assert!(action.is_continue());
assert_eq!(hook.calls(), 1);
}
#[test]
fn mock_aborter() {
let hook = MockHook::aborter("test", "*::*", HookPoint::RequestPreDispatch, "blocked");
let action = hook.execute(test_ctx());
assert!(action.is_abort());
if let HookAction::Abort { reason } = action {
assert_eq!(reason, "blocked");
}
}
#[test]
fn mock_modifier() {
let hook = MockHook::modifier("test", "*::*", HookPoint::RequestPreDispatch, |ctx| {
ctx.payload = json!({"modified": true});
});
let action = hook.execute(test_ctx());
if let HookAction::Continue(ctx) = action {
assert_eq!(ctx.payload, json!({"modified": true}));
} else {
panic!("expected Continue");
}
}
#[test]
fn mock_priority() {
let hook =
MockHook::pass_through("test", "*::*", HookPoint::RequestPreDispatch).with_priority(50);
assert_eq!(hook.priority(), 50);
}
#[test]
fn mock_call_count_increments() {
let hook = MockHook::pass_through("test", "*::*", HookPoint::RequestPreDispatch);
hook.execute(test_ctx());
hook.execute(test_ctx());
hook.execute(test_ctx());
assert_eq!(hook.calls(), 3);
}
#[test]
fn hook_default_priority() {
let hook = MockHook::pass_through("test", "*::*", HookPoint::RequestPreDispatch);
assert_eq!(hook.priority(), 100);
}
}