Skip to main content

khive_fold/
context.rs

1//! Fold context for parameterizing fold operations
2
3use std::ops::Deref;
4use std::sync::Arc;
5
6use chrono::{DateTime, Utc};
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use uuid::Uuid;
10
11/// Arc-backed shared JSON value; cheap to clone in hot paths.
12#[derive(Debug, Clone, Default, PartialEq)]
13pub struct SharedJson(Arc<serde_json::Value>);
14
15impl SharedJson {
16    /// Create a shared JSON wrapper from an owned JSON value.
17    #[must_use]
18    pub fn new(value: serde_json::Value) -> Self {
19        Self(Arc::new(value))
20    }
21
22    /// Borrow the inner JSON value.
23    #[must_use]
24    pub fn as_value(&self) -> &serde_json::Value {
25        self.0.as_ref()
26    }
27
28    /// Get mutable access to the JSON value, cloning only when needed.
29    pub fn make_mut(&mut self) -> &mut serde_json::Value {
30        Arc::make_mut(&mut self.0)
31    }
32
33    /// Convert back into an owned JSON value.
34    #[must_use]
35    pub fn into_inner(self) -> serde_json::Value {
36        match Arc::try_unwrap(self.0) {
37            Ok(value) => value,
38            Err(value) => value.as_ref().clone(),
39        }
40    }
41}
42
43impl Deref for SharedJson {
44    type Target = serde_json::Value;
45
46    fn deref(&self) -> &Self::Target {
47        self.as_value()
48    }
49}
50
51impl AsRef<serde_json::Value> for SharedJson {
52    fn as_ref(&self) -> &serde_json::Value {
53        self.as_value()
54    }
55}
56
57impl From<serde_json::Value> for SharedJson {
58    fn from(value: serde_json::Value) -> Self {
59        Self::new(value)
60    }
61}
62
63impl From<SharedJson> for serde_json::Value {
64    fn from(value: SharedJson) -> Self {
65        value.into_inner()
66    }
67}
68
69impl PartialEq<serde_json::Value> for SharedJson {
70    fn eq(&self, other: &serde_json::Value) -> bool {
71        self.as_value() == other
72    }
73}
74
75#[cfg(feature = "serde")]
76impl Serialize for SharedJson {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where
79        S: Serializer,
80    {
81        self.as_value().serialize(serializer)
82    }
83}
84
85#[cfg(feature = "serde")]
86impl<'de> Deserialize<'de> for SharedJson {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        serde_json::Value::deserialize(deserializer).map(Self::new)
92    }
93}
94
95/// Context for fold operations; `as_of` defaults to Unix epoch.
96#[derive(Debug, Clone, Default)]
97#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
98pub struct FoldContext {
99    /// Point in time to evaluate (for temporal queries)
100    pub as_of: DateTime<Utc>,
101
102    /// Correlation ID for tracing
103    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
104    pub correlation_id: Option<Uuid>,
105
106    /// Additional context as shared JSON.
107    #[cfg_attr(feature = "serde", serde(default))]
108    pub extra: SharedJson,
109}
110
111impl FoldContext {
112    /// Create a new context with the Unix epoch as `as_of`.
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Create a context for a specific point in time.
118    pub fn at(as_of: DateTime<Utc>) -> Self {
119        Self {
120            as_of,
121            ..Default::default()
122        }
123    }
124
125    /// Set the correlation ID.
126    pub fn with_correlation_id(mut self, id: Uuid) -> Self {
127        self.correlation_id = Some(id);
128        self
129    }
130
131    /// Set extra context.
132    pub fn with_extra(mut self, extra: impl Into<SharedJson>) -> Self {
133        self.extra = extra.into();
134        self
135    }
136
137    /// Borrow the extra context as a plain `serde_json::Value`.
138    #[must_use]
139    pub fn extra(&self) -> &serde_json::Value {
140        self.extra.as_value()
141    }
142
143    /// Mutably access the extra context, cloning the shared payload only if needed.
144    pub fn extra_mut(&mut self) -> &mut serde_json::Value {
145        self.extra.make_mut()
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_context_at_time() {
155        let past = Utc::now() - chrono::Duration::hours(1);
156        let ctx = FoldContext::at(past);
157        assert_eq!(ctx.as_of, past);
158    }
159
160    #[test]
161    fn test_context_new_is_epoch() {
162        let ctx = FoldContext::new();
163        assert_eq!(ctx.as_of, DateTime::<Utc>::default());
164    }
165
166    #[cfg(feature = "serde")]
167    #[test]
168    fn test_shared_json_round_trip() {
169        let ctx = FoldContext::new().with_extra(serde_json::json!({"count": 3, "flag": true}));
170        let encoded = serde_json::to_string(&ctx).unwrap();
171        let decoded: FoldContext = serde_json::from_str(&encoded).unwrap();
172        assert_eq!(decoded.extra, serde_json::json!({"count": 3, "flag": true}));
173    }
174
175    #[test]
176    fn test_shared_json_make_mut() {
177        let mut ctx = FoldContext::new().with_extra(serde_json::json!({"count": 1}));
178        let _clone = ctx.clone();
179        *ctx.extra_mut() = serde_json::json!({"count": 2});
180        assert_eq!(ctx.extra, serde_json::json!({"count": 2}));
181    }
182
183    #[test]
184    fn test_shared_json_clone_is_cheap_arc_refcount() {
185        let value = serde_json::json!({"large": "payload", "nested": {"a": 1, "b": 2}});
186        let shared = SharedJson::new(value.clone());
187        let clone = shared.clone();
188        assert_eq!(
189            shared.as_value() as *const _,
190            clone.as_value() as *const _,
191            "clone should share the same Arc allocation"
192        );
193        assert_eq!(*shared, *clone);
194        drop(clone);
195        let extracted = shared.into_inner();
196        assert_eq!(extracted, value);
197    }
198
199    #[test]
200    fn test_shared_json_extra_mut_creates_independent_copy() {
201        let original = FoldContext::new().with_extra(serde_json::json!({"x": 1}));
202        let mut mutated = original.clone();
203        assert_eq!(original.extra(), mutated.extra());
204        *mutated.extra_mut() = serde_json::json!({"x": 99});
205        assert_eq!(original.extra(), &serde_json::json!({"x": 1}));
206        assert_eq!(mutated.extra(), &serde_json::json!({"x": 99}));
207    }
208
209    #[test]
210    fn test_shared_json_from_value_transparent() {
211        let value = serde_json::json!([1, 2, 3]);
212        let shared: SharedJson = value.clone().into();
213        assert_eq!(shared.as_value(), &value);
214    }
215
216    #[test]
217    fn test_shared_json_default_is_null() {
218        let default = SharedJson::default();
219        assert_eq!(default.as_value(), &serde_json::Value::Null);
220    }
221}