use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc};
thread_local! {
static HOOK_CONTEXT: RefCell<Option<Rc<HookContext>>> = const { RefCell::new(None) };
}
pub struct HookContext {
pub(crate) states: RefCell<HashMap<usize, Box<dyn Any>>>,
current_hook: RefCell<usize>,
}
impl HookContext {
pub fn new() -> Self {
Self {
states: RefCell::new(HashMap::new()),
current_hook: RefCell::new(0),
}
}
pub fn current_hook_index(&self) -> usize {
*self.current_hook.borrow()
}
pub fn next_hook_index(&self) -> usize {
let mut current = self.current_hook.borrow_mut();
let index = *current;
*current += 1;
index
}
pub fn reset_hook_index(&self) {
*self.current_hook.borrow_mut() = 0;
}
pub fn get_state<T: 'static + Clone>(&self, index: usize) -> Option<T> {
self.states
.borrow()
.get(&index)
.and_then(|boxed| boxed.downcast_ref::<T>())
.cloned()
}
pub fn set_state<T: 'static>(&self, index: usize, value: T) {
self.states.borrow_mut().insert(index, Box::new(value));
}
pub fn get_or_init_state<T: 'static, F>(
&self,
index: usize,
init: F,
) -> std::rc::Rc<std::cell::RefCell<T>>
where
F: FnOnce() -> T,
{
use std::cell::RefCell;
use std::rc::Rc;
let mut states = self.states.borrow_mut();
if let Some(existing) = states.get(&index) {
if let Some(typed_state) = existing.downcast_ref::<Rc<RefCell<T>>>() {
return typed_state.clone();
}
}
let new_state = Rc::new(RefCell::new(init()));
states.insert(index, Box::new(new_state.clone()));
new_state
}
pub fn has_state(&self, index: usize) -> bool {
self.states.borrow().contains_key(&index)
}
pub fn clear(&self) {
self.states.borrow_mut().clear();
self.reset_hook_index();
}
}
impl Default for HookContext {
fn default() -> Self {
Self::new()
}
}
pub fn set_hook_context(context: Rc<HookContext>) {
HOOK_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = Some(context);
});
}
pub fn get_hook_context() -> Option<Rc<HookContext>> {
HOOK_CONTEXT.with(|ctx| ctx.borrow().clone())
}
pub fn clear_hook_context() {
HOOK_CONTEXT.with(|ctx| {
*ctx.borrow_mut() = None;
});
}
pub fn with_hook_context<R>(f: impl FnOnce(&HookContext) -> R) -> R {
let context =
get_hook_context().expect("with_hook_context must be called within a hook context");
f(&context)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_context_creation() {
let context = HookContext::new();
assert_eq!(*context.current_hook.borrow(), 0);
assert!(context.states.borrow().is_empty());
}
#[test]
fn test_hook_index_management() {
let context = HookContext::new();
assert_eq!(context.next_hook_index(), 0);
assert_eq!(context.next_hook_index(), 1);
assert_eq!(context.next_hook_index(), 2);
context.reset_hook_index();
assert_eq!(context.next_hook_index(), 0);
}
#[test]
fn test_state_management() {
let context = HookContext::new();
context.set_state(0, 42i32);
assert_eq!(context.get_state::<i32>(0), Some(42));
context.set_state(1, "hello".to_string());
assert_eq!(context.get_state::<String>(1), Some("hello".to_string()));
assert_eq!(context.get_state::<i32>(99), None);
}
#[test]
fn test_has_state() {
let context = HookContext::new();
assert!(!context.has_state(0));
context.set_state(0, 42i32);
assert!(context.has_state(0));
assert!(!context.has_state(1));
}
#[test]
fn test_clear_state() {
let context = HookContext::new();
context.set_state(0, 42i32);
context.set_state(1, "test".to_string());
context.next_hook_index();
assert!(context.has_state(0));
assert!(context.has_state(1));
context.clear();
assert!(!context.has_state(0));
assert!(!context.has_state(1));
assert_eq!(*context.current_hook.borrow(), 0);
}
#[test]
fn test_thread_local_context_management() {
let context = Rc::new(HookContext::new());
assert!(get_hook_context().is_none());
set_hook_context(context.clone());
assert!(get_hook_context().is_some());
clear_hook_context();
assert!(get_hook_context().is_none());
}
#[test]
fn test_with_hook_context() {
let context = Rc::new(HookContext::new());
context.set_state(0, 100i32);
set_hook_context(context.clone());
let result = with_hook_context(|ctx| {
assert_eq!(ctx.get_state::<i32>(0), Some(100));
"test_result"
});
assert_eq!(result, "test_result");
clear_hook_context();
}
#[test]
fn test_type_safety() {
let context = HookContext::new();
context.set_state(0, 42i32);
assert_eq!(context.get_state::<String>(0), None);
assert_eq!(context.get_state::<i32>(0), Some(42));
}
#[test]
fn test_default_implementation() {
let context = HookContext::default();
assert_eq!(*context.current_hook.borrow(), 0);
assert!(context.states.borrow().is_empty());
}
}