tirea_state/
op.rs

1//! Patch operations for modifying JSON documents.
2//!
3//! Each operation describes a single atomic change to apply to a document.
4
5use crate::Path;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// A numeric value that can be used in increment/decrement operations.
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum Number {
13    /// Integer value.
14    Int(i64),
15    /// Floating-point value.
16    Float(f64),
17}
18
19impl Number {
20    /// Create an integer number.
21    #[inline]
22    pub fn int(v: i64) -> Self {
23        Number::Int(v)
24    }
25
26    /// Create a floating-point number.
27    #[inline]
28    pub fn float(v: f64) -> Self {
29        Number::Float(v)
30    }
31
32    /// Convert to f64.
33    #[inline]
34    pub fn as_f64(&self) -> f64 {
35        match self {
36            Number::Int(i) => *i as f64,
37            Number::Float(f) => *f,
38        }
39    }
40
41    /// Convert to i64 (truncates floats).
42    #[inline]
43    pub fn as_i64(&self) -> i64 {
44        match self {
45            Number::Int(i) => *i,
46            Number::Float(f) => *f as i64,
47        }
48    }
49
50    /// Check if this is an integer.
51    #[inline]
52    pub fn is_int(&self) -> bool {
53        matches!(self, Number::Int(_))
54    }
55
56    /// Check if this is a float.
57    #[inline]
58    pub fn is_float(&self) -> bool {
59        matches!(self, Number::Float(_))
60    }
61}
62
63impl From<i64> for Number {
64    fn from(v: i64) -> Self {
65        Number::Int(v)
66    }
67}
68
69impl From<i32> for Number {
70    fn from(v: i32) -> Self {
71        Number::Int(v as i64)
72    }
73}
74
75impl From<u32> for Number {
76    fn from(v: u32) -> Self {
77        Number::Int(v as i64)
78    }
79}
80
81impl From<u64> for Number {
82    fn from(v: u64) -> Self {
83        Number::Int(v as i64)
84    }
85}
86
87impl From<f64> for Number {
88    fn from(v: f64) -> Self {
89        Number::Float(v)
90    }
91}
92
93impl From<f32> for Number {
94    fn from(v: f32) -> Self {
95        Number::Float(v as f64)
96    }
97}
98
99/// A single patch operation.
100///
101/// Operations are the atomic units of change. Each operation targets a specific
102/// path in the document and performs a specific mutation.
103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
104#[serde(tag = "op", rename_all = "snake_case")]
105pub enum Op {
106    /// Set a value at the path.
107    ///
108    /// Creates intermediate objects if they don't exist.
109    /// Returns error if array index is out of bounds.
110    Set {
111        /// Target path.
112        path: Path,
113        /// Value to set.
114        value: Value,
115    },
116
117    /// Delete the value at the path.
118    ///
119    /// No-op if the path doesn't exist.
120    Delete {
121        /// Target path.
122        path: Path,
123    },
124
125    /// Append a value to an array at the path.
126    ///
127    /// Creates the array if it doesn't exist.
128    /// Returns error if the target exists but is not an array.
129    Append {
130        /// Target path (must be an array or non-existent).
131        path: Path,
132        /// Value to append.
133        value: Value,
134    },
135
136    /// Merge an object into the object at the path.
137    ///
138    /// Creates the object if it doesn't exist.
139    /// Returns error if the target exists but is not an object.
140    MergeObject {
141        /// Target path (must be an object or non-existent).
142        path: Path,
143        /// Object to merge.
144        value: Value,
145    },
146
147    /// Increment a numeric value at the path.
148    ///
149    /// Returns error if the target is not a number.
150    Increment {
151        /// Target path (must be a number).
152        path: Path,
153        /// Amount to increment by.
154        amount: Number,
155    },
156
157    /// Decrement a numeric value at the path.
158    ///
159    /// Returns error if the target is not a number.
160    Decrement {
161        /// Target path (must be a number).
162        path: Path,
163        /// Amount to decrement by.
164        amount: Number,
165    },
166
167    /// Insert a value at a specific index in an array.
168    ///
169    /// Shifts elements to the right.
170    /// Returns error if index is out of bounds or target is not an array.
171    Insert {
172        /// Target path (must be an array).
173        path: Path,
174        /// Index to insert at.
175        index: usize,
176        /// Value to insert.
177        value: Value,
178    },
179
180    /// Remove the first occurrence of a value from an array.
181    ///
182    /// No-op if the value is not found.
183    /// Returns error if the target is not an array.
184    Remove {
185        /// Target path (must be an array).
186        path: Path,
187        /// Value to remove.
188        value: Value,
189    },
190
191    /// Merge a lattice delta into an existing value at path.
192    ///
193    /// When applied via `LatticeRegistry`, performs a proper lattice merge.
194    /// Without a registry, falls back to `Op::Set` semantics (writes the delta directly).
195    LatticeMerge {
196        /// Target path.
197        path: Path,
198        /// Delta value to merge.
199        value: Value,
200    },
201}
202
203impl Op {
204    // Convenience constructors
205
206    /// Create a Set operation.
207    #[inline]
208    pub fn set(path: Path, value: impl Into<Value>) -> Self {
209        Op::Set {
210            path,
211            value: value.into(),
212        }
213    }
214
215    /// Create a Delete operation.
216    #[inline]
217    pub fn delete(path: Path) -> Self {
218        Op::Delete { path }
219    }
220
221    /// Create an Append operation.
222    #[inline]
223    pub fn append(path: Path, value: impl Into<Value>) -> Self {
224        Op::Append {
225            path,
226            value: value.into(),
227        }
228    }
229
230    /// Create a MergeObject operation.
231    #[inline]
232    pub fn merge_object(path: Path, value: impl Into<Value>) -> Self {
233        Op::MergeObject {
234            path,
235            value: value.into(),
236        }
237    }
238
239    /// Create an Increment operation.
240    #[inline]
241    pub fn increment(path: Path, amount: impl Into<Number>) -> Self {
242        Op::Increment {
243            path,
244            amount: amount.into(),
245        }
246    }
247
248    /// Create a Decrement operation.
249    #[inline]
250    pub fn decrement(path: Path, amount: impl Into<Number>) -> Self {
251        Op::Decrement {
252            path,
253            amount: amount.into(),
254        }
255    }
256
257    /// Create an Insert operation.
258    #[inline]
259    pub fn insert(path: Path, index: usize, value: impl Into<Value>) -> Self {
260        Op::Insert {
261            path,
262            index,
263            value: value.into(),
264        }
265    }
266
267    /// Create a Remove operation.
268    #[inline]
269    pub fn remove(path: Path, value: impl Into<Value>) -> Self {
270        Op::Remove {
271            path,
272            value: value.into(),
273        }
274    }
275
276    /// Create a LatticeMerge operation.
277    #[inline]
278    pub fn lattice_merge(path: Path, value: impl Into<Value>) -> Self {
279        Op::LatticeMerge {
280            path,
281            value: value.into(),
282        }
283    }
284
285    /// Get the path this operation targets.
286    #[inline]
287    pub fn path(&self) -> &Path {
288        match self {
289            Op::Set { path, .. } => path,
290            Op::Delete { path } => path,
291            Op::Append { path, .. } => path,
292            Op::MergeObject { path, .. } => path,
293            Op::Increment { path, .. } => path,
294            Op::Decrement { path, .. } => path,
295            Op::Insert { path, .. } => path,
296            Op::Remove { path, .. } => path,
297            Op::LatticeMerge { path, .. } => path,
298        }
299    }
300
301    /// Get a mutable reference to the path.
302    #[inline]
303    pub fn path_mut(&mut self) -> &mut Path {
304        match self {
305            Op::Set { path, .. } => path,
306            Op::Delete { path } => path,
307            Op::Append { path, .. } => path,
308            Op::MergeObject { path, .. } => path,
309            Op::Increment { path, .. } => path,
310            Op::Decrement { path, .. } => path,
311            Op::Insert { path, .. } => path,
312            Op::Remove { path, .. } => path,
313            Op::LatticeMerge { path, .. } => path,
314        }
315    }
316
317    /// Get the operation name.
318    #[inline]
319    pub fn name(&self) -> &'static str {
320        match self {
321            Op::Set { .. } => "set",
322            Op::Delete { .. } => "delete",
323            Op::Append { .. } => "append",
324            Op::MergeObject { .. } => "merge_object",
325            Op::Increment { .. } => "increment",
326            Op::Decrement { .. } => "decrement",
327            Op::Insert { .. } => "insert",
328            Op::Remove { .. } => "remove",
329            Op::LatticeMerge { .. } => "lattice_merge",
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::path;
338    use serde_json::json;
339
340    #[test]
341    fn test_op_constructors() {
342        let set = Op::set(path!("a"), json!(1));
343        assert_eq!(set.name(), "set");
344        assert_eq!(set.path(), &path!("a"));
345
346        let del = Op::delete(path!("b"));
347        assert_eq!(del.name(), "delete");
348
349        let inc = Op::increment(path!("c"), 5i64);
350        assert_eq!(inc.name(), "increment");
351    }
352
353    #[test]
354    fn test_op_serde() {
355        let op = Op::set(path!("users", 0, "name"), json!("Alice"));
356        let json = serde_json::to_string(&op).unwrap();
357        let parsed: Op = serde_json::from_str(&json).unwrap();
358        assert_eq!(op, parsed);
359    }
360
361    #[test]
362    fn test_number_conversions() {
363        let n: Number = 42i64.into();
364        assert!(n.is_int());
365        assert_eq!(n.as_i64(), 42);
366
367        let n: Number = 1.5f64.into();
368        assert!(n.is_float());
369        assert!((n.as_f64() - 1.5).abs() < 0.001);
370    }
371}