use crate::HookPoint;
use orcs_types::{ChannelId, ComponentId, Principal};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub const DEFAULT_MAX_DEPTH: u8 = 4;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookContext {
pub hook_point: HookPoint,
pub component_id: ComponentId,
pub channel_id: ChannelId,
pub principal: Principal,
pub timestamp_ms: u64,
pub payload: Value,
pub metadata: HashMap<String, Value>,
pub depth: u8,
pub max_depth: u8,
}
impl HookContext {
#[must_use]
pub fn new(
hook_point: HookPoint,
component_id: ComponentId,
channel_id: ChannelId,
principal: Principal,
timestamp_ms: u64,
payload: Value,
) -> Self {
Self {
hook_point,
component_id,
channel_id,
principal,
timestamp_ms,
payload,
metadata: HashMap::new(),
depth: 0,
max_depth: DEFAULT_MAX_DEPTH,
}
}
#[must_use]
pub fn with_incremented_depth(&self) -> Self {
let mut ctx = self.clone();
ctx.depth = ctx.depth.saturating_add(1);
ctx
}
#[must_use]
pub fn is_depth_exceeded(&self) -> bool {
self.depth >= self.max_depth
}
#[must_use]
pub fn with_max_depth(mut self, max_depth: u8) -> Self {
self.max_depth = max_depth;
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use orcs_types::PrincipalId;
use serde_json::json;
fn test_ctx() -> HookContext {
HookContext::new(
HookPoint::RequestPreDispatch,
ComponentId::builtin("llm"),
ChannelId::new(),
Principal::User(PrincipalId::new()),
12345,
json!({"operation": "chat"}),
)
}
#[test]
fn new_has_correct_defaults() {
let ctx = test_ctx();
assert_eq!(ctx.depth, 0);
assert_eq!(ctx.max_depth, DEFAULT_MAX_DEPTH);
assert!(ctx.metadata.is_empty());
}
#[test]
fn depth_increment() {
let ctx = test_ctx();
let incremented = ctx.with_incremented_depth();
assert_eq!(incremented.depth, 1);
assert_eq!(ctx.depth, 0); }
#[test]
fn depth_saturation() {
let mut ctx = test_ctx();
ctx.depth = u8::MAX;
let incremented = ctx.with_incremented_depth();
assert_eq!(incremented.depth, u8::MAX);
}
#[test]
fn depth_exceeded() {
let mut ctx = test_ctx();
ctx.max_depth = 3;
ctx.depth = 2;
assert!(!ctx.is_depth_exceeded());
ctx.depth = 3;
assert!(ctx.is_depth_exceeded());
ctx.depth = 4;
assert!(ctx.is_depth_exceeded());
}
#[test]
fn with_max_depth() {
let ctx = test_ctx().with_max_depth(8);
assert_eq!(ctx.max_depth, 8);
}
#[test]
fn with_metadata() {
let ctx = test_ctx().with_metadata("audit_id", json!("abc-123"));
assert_eq!(ctx.metadata.get("audit_id"), Some(&json!("abc-123")));
}
#[test]
fn serde_roundtrip() {
let ctx = test_ctx().with_metadata("key", json!(42)).with_max_depth(8);
let json = serde_json::to_string(&ctx).expect("HookContext should serialize to JSON");
let restored: HookContext =
serde_json::from_str(&json).expect("HookContext should deserialize from JSON");
assert_eq!(restored.hook_point, ctx.hook_point);
assert_eq!(restored.depth, ctx.depth);
assert_eq!(restored.max_depth, ctx.max_depth);
assert_eq!(restored.payload, ctx.payload);
assert_eq!(restored.metadata, ctx.metadata);
}
#[test]
fn clone_is_independent() {
let mut ctx = test_ctx();
let cloned = ctx.clone();
ctx.payload = json!({"modified": true});
assert_ne!(ctx.payload, cloned.payload);
}
}