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