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