Skip to main content

data_hook/
lib.rs

1//! # Data Hook
2//!
3//! A lightweight data collection facade similar to the `log` crate.
4//!
5//! The `data_hook` crate provides a simple API for collecting data that abstracts
6//! over the actual data processing implementation. Libraries can use the data collection
7//! API provided by this crate, and the consumer can choose the collector implementation
8//! that is most suitable for their use case.
9//!
10//! If no collector implementation is selected, the facade falls back to a "noop"
11//! implementation that ignores all data collection requests. The overhead in this case
12//! is very small - just an integer load, comparison and jump.
13//!
14//! # Usage
15//!
16//! The basic use of the data_hook crate is through the `data_hook!` macro:
17//!
18//! ```
19//! use data_hook::data_hook;
20//!
21//! data_hook!("user_action", "User clicked button: {}", "submit");
22//! data_hook!("performance", "Request took {} ms", 150);
23//! ```
24//!
25//! # Collectors
26//!
27//! Data hook supports pluggable collectors through the `Collect` trait.
28//! The consumer of libraries using data_hook should provide a collector implementation.
29
30use std::sync::OnceLock;
31
32/// A data record containing collected information
33#[derive(Debug, Clone)]
34pub struct Record {
35    /// Category or type of the data
36    pub category: String,
37    /// The actual data content
38    pub data: String,
39}
40
41impl Record {
42    /// Create a new data record
43    pub fn new(category: String, data: String) -> Self {
44        Self {
45            category,
46            data,
47        }
48    }
49}
50
51/// Trait for data collectors
52pub trait Collect: Send + Sync {
53    /// Process a collected data record
54    fn collect(&self, record: &Record);
55
56    /// Flush any buffered data (optional)
57    fn flush(&self) {}
58}
59
60/// A no-op collector that discards all data
61struct NoOpCollector;
62
63impl Collect for NoOpCollector {
64    fn collect(&self, _record: &Record) {
65        // Do nothing
66    }
67}
68
69/// Global collector instance
70static COLLECTOR: OnceLock<&'static dyn Collect> = OnceLock::new();
71
72/// Set the global data collector
73///
74/// This function should be called once at the start of the program to set up
75/// the data collection backend.
76pub fn set_collector(collector: &'static dyn Collect) {
77    let _ = COLLECTOR.set(collector);
78}
79
80/// Get the global collector, returning a no-op collector if none is set
81fn collector() -> &'static dyn Collect {
82    static NO_OP: NoOpCollector = NoOpCollector;
83    match COLLECTOR.get() {
84        Some(collector) => *collector,
85        None => &NO_OP,
86    }
87}
88
89/// Collect data using the global collector
90pub fn collect(record: Record) {
91    collector().collect(&record);
92}
93
94/// The main data collection macro
95///
96/// # Examples
97///
98/// Basic usage:
99/// ```
100/// use data_hook::data_hook;
101/// 
102/// data_hook!("user_action", "Button clicked");
103/// ```
104///
105/// With formatting:
106/// ```
107/// use data_hook::data_hook;
108/// 
109/// let user_id = 123;
110/// data_hook!("user_action", "User {} logged in", user_id);
111/// ```
112#[macro_export]
113macro_rules! data_hook {
114    ($category:expr, $($arg:tt)+) => {
115        $crate::collect($crate::Record::new(
116            $category.to_string(),
117            format!($($arg)+)
118        ))
119    };
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use std::sync::{Arc, Mutex};
126
127    struct TestCollector {
128        records: Arc<Mutex<Vec<Record>>>,
129    }
130
131    impl TestCollector {
132        fn new() -> (Self, Arc<Mutex<Vec<Record>>>) {
133            let records = Arc::new(Mutex::new(Vec::new()));
134            (Self { records: records.clone() }, records)
135        }
136    }
137
138    impl Collect for TestCollector {
139        fn collect(&self, record: &Record) {
140            if let Ok(mut records) = self.records.lock() {
141                records.push(record.clone());
142            }
143        }
144    }
145
146    #[test]
147    fn test_data_hook_macro() {
148        let (collector, records) = TestCollector::new();
149        let collector: &'static TestCollector = Box::leak(Box::new(collector));
150        set_collector(collector);
151
152        data_hook!("test", "Hello, world!");
153        data_hook!("test", "Number: {}", 42);
154
155        let records = records.lock().unwrap();
156        assert_eq!(records.len(), 2);
157        assert_eq!(records[0].category, "test");
158        assert_eq!(records[0].data, "Hello, world!");
159        assert_eq!(records[1].data, "Number: 42");
160    }
161
162    #[test]
163    fn test_record_creation() {
164        let record = Record::new("test_category".to_string(), "test data".to_string());
165        assert_eq!(record.category, "test_category");
166        assert_eq!(record.data, "test data");
167    }
168}