tirea_state/
writer.rs

1//! Generic JSON writer for building patches.
2//!
3//! `JsonWriter` provides a low-level API for building patches dynamically.
4//! For type-safe access, use derived `State` types instead.
5
6use crate::{Number, Op, Patch, Path};
7use serde_json::Value;
8
9/// Internal trait for writer operations.
10///
11/// This trait is implemented by all generated writers and `JsonWriter`.
12/// It provides a common interface for accessing and manipulating operations.
13#[doc(hidden)]
14pub trait WriterOps {
15    /// Get the accumulated operations.
16    fn ops(&self) -> &[Op];
17
18    /// Get mutable access to the operations.
19    fn ops_mut(&mut self) -> &mut Vec<Op>;
20
21    /// Take all operations, leaving the writer empty.
22    fn take_ops(&mut self) -> Vec<Op>;
23
24    /// Consume and build a patch.
25    fn into_patch(self) -> Patch;
26}
27
28/// A generic writer for building patches on arbitrary JSON structures.
29///
30/// `JsonWriter` is an escape hatch for cases where you need to work with
31/// dynamic or unknown JSON structures. For typed access, prefer using
32/// derived `State` types.
33///
34/// # Examples
35///
36/// ```
37/// use tirea_state::{JsonWriter, path};
38/// use serde_json::json;
39///
40/// let mut w = JsonWriter::new();
41/// w.set(path!("name"), json!("Alice"));
42/// w.set(path!("age"), json!(30));
43/// w.append(path!("tags"), json!("admin"));
44///
45/// let patch = w.build();
46/// assert_eq!(patch.len(), 3);
47/// ```
48#[derive(Debug, Clone)]
49pub struct JsonWriter {
50    base: Path,
51    ops: Vec<Op>,
52}
53
54impl JsonWriter {
55    /// Create a new writer at the document root.
56    #[inline]
57    pub fn new() -> Self {
58        Self {
59            base: Path::root(),
60            ops: Vec::new(),
61        }
62    }
63
64    /// Create a new writer at the specified base path.
65    #[inline]
66    pub fn at(base: Path) -> Self {
67        Self {
68            base,
69            ops: Vec::new(),
70        }
71    }
72
73    /// Get the base path of this writer.
74    #[inline]
75    pub fn base(&self) -> &Path {
76        &self.base
77    }
78
79    /// Create a nested writer at a relative path.
80    ///
81    /// The nested writer will have its own operations vector.
82    /// Use `merge` to combine its operations back into the parent.
83    #[inline]
84    pub fn nested(&self, path: Path) -> Self {
85        Self {
86            base: self.base.join(&path),
87            ops: Vec::new(),
88        }
89    }
90
91    /// Compute the full path by joining base with relative path.
92    #[inline]
93    fn full_path(&self, path: Path) -> Path {
94        if path.is_empty() {
95            self.base.clone()
96        } else {
97            self.base.join(&path)
98        }
99    }
100
101    /// Set a value at the specified path.
102    #[inline]
103    pub fn set(&mut self, path: Path, value: impl Into<Value>) -> &mut Self {
104        self.ops.push(Op::Set {
105            path: self.full_path(path),
106            value: value.into(),
107        });
108        self
109    }
110
111    /// Set a value at the base path.
112    #[inline]
113    pub fn set_root(&mut self, value: impl Into<Value>) -> &mut Self {
114        self.ops.push(Op::Set {
115            path: self.base.clone(),
116            value: value.into(),
117        });
118        self
119    }
120
121    /// Delete the value at the specified path.
122    #[inline]
123    pub fn delete(&mut self, path: Path) -> &mut Self {
124        self.ops.push(Op::Delete {
125            path: self.full_path(path),
126        });
127        self
128    }
129
130    /// Append a value to an array at the specified path.
131    #[inline]
132    pub fn append(&mut self, path: Path, value: impl Into<Value>) -> &mut Self {
133        self.ops.push(Op::Append {
134            path: self.full_path(path),
135            value: value.into(),
136        });
137        self
138    }
139
140    /// Merge an object into the object at the specified path.
141    #[inline]
142    pub fn merge_object(&mut self, path: Path, value: impl Into<Value>) -> &mut Self {
143        self.ops.push(Op::MergeObject {
144            path: self.full_path(path),
145            value: value.into(),
146        });
147        self
148    }
149
150    /// Increment a numeric value at the specified path.
151    #[inline]
152    pub fn increment(&mut self, path: Path, amount: impl Into<Number>) -> &mut Self {
153        self.ops.push(Op::Increment {
154            path: self.full_path(path),
155            amount: amount.into(),
156        });
157        self
158    }
159
160    /// Decrement a numeric value at the specified path.
161    #[inline]
162    pub fn decrement(&mut self, path: Path, amount: impl Into<Number>) -> &mut Self {
163        self.ops.push(Op::Decrement {
164            path: self.full_path(path),
165            amount: amount.into(),
166        });
167        self
168    }
169
170    /// Insert a value at a specific index in an array.
171    #[inline]
172    pub fn insert(&mut self, path: Path, index: usize, value: impl Into<Value>) -> &mut Self {
173        self.ops.push(Op::Insert {
174            path: self.full_path(path),
175            index,
176            value: value.into(),
177        });
178        self
179    }
180
181    /// Remove the first occurrence of a value from an array.
182    #[inline]
183    pub fn remove(&mut self, path: Path, value: impl Into<Value>) -> &mut Self {
184        self.ops.push(Op::Remove {
185            path: self.full_path(path),
186            value: value.into(),
187        });
188        self
189    }
190
191    /// Merge operations from another writer into this one.
192    #[inline]
193    pub fn merge<W: WriterOps>(&mut self, mut other: W) -> &mut Self {
194        self.ops.extend(other.take_ops());
195        self
196    }
197
198    /// Consume this writer and build a patch.
199    #[inline]
200    pub fn build(self) -> Patch {
201        Patch::with_ops(self.ops)
202    }
203
204    /// Check if this writer has any operations.
205    #[inline]
206    pub fn is_empty(&self) -> bool {
207        self.ops.is_empty()
208    }
209
210    /// Get the number of operations.
211    #[inline]
212    pub fn len(&self) -> usize {
213        self.ops.len()
214    }
215
216    /// Clear all operations.
217    #[inline]
218    pub fn clear(&mut self) {
219        self.ops.clear();
220    }
221}
222
223impl Default for JsonWriter {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229impl WriterOps for JsonWriter {
230    fn ops(&self) -> &[Op] {
231        &self.ops
232    }
233
234    fn ops_mut(&mut self) -> &mut Vec<Op> {
235        &mut self.ops
236    }
237
238    fn take_ops(&mut self) -> Vec<Op> {
239        std::mem::take(&mut self.ops)
240    }
241
242    fn into_patch(self) -> Patch {
243        self.build()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::path;
251    use serde_json::json;
252
253    #[test]
254    fn test_json_writer_basic() {
255        let mut w = JsonWriter::new();
256        w.set(path!("name"), json!("Alice"));
257        w.set(path!("age"), json!(30));
258
259        let patch = w.build();
260        assert_eq!(patch.len(), 2);
261    }
262
263    #[test]
264    fn test_json_writer_with_base() {
265        let mut w = JsonWriter::at(path!("user"));
266        w.set(path!("name"), json!("Bob"));
267
268        let patch = w.build();
269        assert_eq!(patch.ops()[0].path(), &path!("user", "name"));
270    }
271
272    #[test]
273    fn test_json_writer_nested() {
274        let w = JsonWriter::at(path!("data"));
275        let nested = w.nested(path!("items"));
276        assert_eq!(nested.base(), &path!("data", "items"));
277    }
278
279    #[test]
280    fn test_json_writer_merge() {
281        let mut w1 = JsonWriter::new();
282        w1.set(path!("a"), json!(1));
283
284        let mut w2 = JsonWriter::new();
285        w2.set(path!("b"), json!(2));
286
287        w1.merge(w2);
288        assert_eq!(w1.len(), 2);
289    }
290
291    #[test]
292    fn test_json_writer_all_ops() {
293        let mut w = JsonWriter::new();
294
295        w.set(path!("x"), json!(1));
296        w.delete(path!("y"));
297        w.append(path!("arr"), json!(1));
298        w.merge_object(path!("obj"), json!({"a": 1}));
299        w.increment(path!("count"), 1i64);
300        w.decrement(path!("score"), 5i64);
301        w.insert(path!("list"), 0, json!("first"));
302        w.remove(path!("tags"), json!("old"));
303
304        assert_eq!(w.len(), 8);
305    }
306}