Skip to main content

algocline_core/
custom.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4/// KV store written from Lua via alc.stats.record(key, value).
5pub struct CustomMetrics {
6    entries: HashMap<String, serde_json::Value>,
7}
8
9impl CustomMetrics {
10    pub fn new() -> Self {
11        Self {
12            entries: HashMap::new(),
13        }
14    }
15
16    pub fn record(&mut self, key: String, value: serde_json::Value) {
17        self.entries.insert(key, value);
18    }
19
20    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
21        self.entries.get(key)
22    }
23
24    pub fn to_json(&self) -> serde_json::Value {
25        serde_json::to_value(&self.entries).unwrap_or(serde_json::Value::Null)
26    }
27}
28
29impl Default for CustomMetrics {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35/// Cheap, cloneable handle for custom metrics from the Lua bridge.
36///
37/// Wraps `Arc<Mutex<CustomMetrics>>` to match the Handle pattern
38/// used by `BudgetHandle` and `ProgressHandle`.
39///
40/// # Poison policy
41///
42/// Silently skips on poison. Custom metrics are observational —
43/// a missed record degrades stats but does not affect execution.
44#[derive(Clone)]
45pub struct CustomMetricsHandle {
46    inner: Arc<Mutex<CustomMetrics>>,
47}
48
49impl CustomMetricsHandle {
50    pub(crate) fn new(inner: Arc<Mutex<CustomMetrics>>) -> Self {
51        Self { inner }
52    }
53
54    /// Record a key-value pair. Silently skips on mutex poison.
55    pub fn record(&self, key: String, value: serde_json::Value) {
56        if let Ok(mut m) = self.inner.lock() {
57            m.record(key, value);
58        }
59    }
60
61    /// Get a value by key. Returns None on mutex poison or missing key.
62    pub fn get(&self, key: &str) -> Option<serde_json::Value> {
63        self.inner.lock().ok().and_then(|m| m.get(key).cloned())
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use serde_json::json;
71
72    #[test]
73    fn record_and_get() {
74        let mut cm = CustomMetrics::new();
75        cm.record("key".into(), json!(42));
76        assert_eq!(cm.get("key"), Some(&json!(42)));
77    }
78
79    #[test]
80    fn get_missing_returns_none() {
81        let cm = CustomMetrics::new();
82        assert_eq!(cm.get("missing"), None);
83    }
84
85    #[test]
86    fn record_overwrites() {
87        let mut cm = CustomMetrics::new();
88        cm.record("key".into(), json!(1));
89        cm.record("key".into(), json!(2));
90        assert_eq!(cm.get("key"), Some(&json!(2)));
91    }
92
93    #[test]
94    fn to_json_includes_all_entries() {
95        let mut cm = CustomMetrics::new();
96        cm.record("a".into(), json!(1));
97        cm.record("b".into(), json!("two"));
98        let json = cm.to_json();
99        assert_eq!(json.get("a").unwrap(), 1);
100        assert_eq!(json.get("b").unwrap(), "two");
101    }
102}
103
104#[cfg(test)]
105mod proptests {
106    use super::*;
107    use proptest::prelude::*;
108
109    proptest! {
110        #[test]
111        fn record_then_get_consistent(key in "[a-zA-Z_]{1,30}", val in any::<i64>()) {
112            let mut cm = CustomMetrics::new();
113            let json_val = serde_json::json!(val);
114            cm.record(key.clone(), json_val.clone());
115            prop_assert_eq!(cm.get(&key), Some(&json_val));
116        }
117
118        #[test]
119        fn last_write_wins(key in "[a-zA-Z_]{1,30}", v1 in any::<i64>(), v2 in any::<i64>()) {
120            let mut cm = CustomMetrics::new();
121            cm.record(key.clone(), serde_json::json!(v1));
122            cm.record(key.clone(), serde_json::json!(v2));
123            prop_assert_eq!(cm.get(&key), Some(&serde_json::json!(v2)));
124        }
125
126        #[test]
127        fn to_json_contains_all_recorded(
128            entries in proptest::collection::vec(
129                ("[a-z]{1,10}", any::<i64>()),
130                1..20,
131            )
132        ) {
133            let mut cm = CustomMetrics::new();
134            for (k, v) in &entries {
135                cm.record(k.clone(), serde_json::json!(v));
136            }
137            let json = cm.to_json();
138            // last-write-wins: check final value for each key
139            let mut expected = std::collections::HashMap::new();
140            for (k, v) in &entries {
141                expected.insert(k.clone(), serde_json::json!(v));
142            }
143            for (k, v) in &expected {
144                prop_assert_eq!(json.get(k), Some(v));
145            }
146        }
147    }
148}