morphix/
batch.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt::Debug;
4use std::mem::take;
5
6use crate::{Adapter, Mutation, MutationError, MutationKind};
7
8/// A batch collector for aggregating and optimizing multiple mutations.
9///
10/// `Batch` is used internally to collect multiple mutations and optimize them before creating the
11/// final mutation. It can merge consecutive append operations and eliminate redundant mutations.
12///
13/// ## Type Parameters
14///
15/// - `A` - The adapter type used for serialization
16///
17/// ## Example
18///
19/// ```
20/// use morphix::{Batch, JsonAdapter, Mutation, MutationKind};
21/// use serde_json::json;
22///
23/// let mut batch = Batch::<JsonAdapter>::new();
24///
25/// // Load multiple mutations
26/// batch.load(Mutation {
27///     path_rev: vec!["field".into()],
28///     operation: MutationKind::Replace(json!(1)),
29/// }).unwrap();
30///
31/// // Dump optimized mutations
32/// let optimized = batch.dump();
33/// ```
34pub struct Batch<A: Adapter> {
35    operation: Option<MutationKind<A>>,
36    children: BTreeMap<Cow<'static, str>, Self>,
37}
38
39impl<A: Adapter> Default for Batch<A> {
40    fn default() -> Self {
41        Self {
42            operation: None,
43            children: BTreeMap::new(),
44        }
45    }
46}
47
48impl<A: Adapter> Debug for Batch<A>
49where
50    A::Value: Debug,
51{
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("Batch")
54            .field("operation", &self.operation)
55            .field("children", &self.children)
56            .finish()
57    }
58}
59
60impl<A: Adapter> Batch<A> {
61    /// Creates a new empty batch.
62    pub fn new() -> Self {
63        Default::default()
64    }
65
66    /// Loads a [Mutation] into the batch, potentially merging with existing mutations.
67    ///
68    /// ## Arguments
69    ///
70    /// - `mutation` - mutation to add to the batch
71    ///
72    /// ## Errors
73    ///
74    /// - Returns an [MutationError] if the mutation cannot be applied.
75    pub fn load(&mut self, mutation: Mutation<A>) -> Result<(), MutationError> {
76        self.load_with_stack(mutation, &mut vec![])
77    }
78
79    fn load_with_stack(
80        &mut self,
81        mut mutation: Mutation<A>,
82        path_stack: &mut Vec<Cow<'static, str>>,
83    ) -> Result<(), MutationError> {
84        let mut batch = self;
85        if let Some(MutationKind::Replace(value)) = &mut batch.operation {
86            A::apply_mutation(value, mutation, path_stack)?;
87            return Ok(());
88        }
89        while let Some(key) = mutation.path_rev.pop() {
90            // We cannot avoid allocation here because `BTreeMap::entry` requires owned key.
91            path_stack.push(key.clone());
92            batch = batch.children.entry(key).or_default();
93            if let Some(MutationKind::Replace(value)) = &mut batch.operation {
94                A::apply_mutation(value, mutation, path_stack)?;
95                return Ok(());
96            }
97        }
98
99        match mutation.operation {
100            MutationKind::Replace(_) => {
101                batch.operation = Some(mutation.operation);
102                batch.children.clear();
103            }
104            MutationKind::Append(new_value) => match &mut batch.operation {
105                Some(MutationKind::Append(old_value)) => {
106                    A::merge_append(old_value, new_value, path_stack)?;
107                }
108                Some(_) => unreachable!(),
109                None => batch.operation = Some(MutationKind::Append(new_value)),
110            },
111            MutationKind::Batch(mutations) => {
112                let len = path_stack.len();
113                for mutation in mutations {
114                    batch.load_with_stack(mutation, path_stack)?;
115                    path_stack.truncate(len);
116                }
117            }
118        }
119
120        Ok(())
121    }
122
123    /// Dumps all accumulated mutations as a single optimized mutation.
124    ///
125    /// - Returns `None` if no mutations have been accumulated.
126    /// - Returns a single mutation if only one mutation exists.
127    /// - Returns a `Batch` mutation if multiple mutations exist.
128    pub fn dump(&mut self) -> Option<Mutation<A>> {
129        let mut mutations = vec![];
130        if let Some(operation) = self.operation.take() {
131            mutations.push(Mutation {
132                path_rev: vec![],
133                operation,
134            });
135        }
136        for (key, mut batch) in take(&mut self.children) {
137            if let Some(mut mutation) = batch.dump() {
138                mutation.path_rev.push(key);
139                mutations.push(mutation);
140            }
141        }
142        Self::build(mutations)
143    }
144
145    #[doc(hidden)]
146    pub fn build(mut mutations: Vec<Mutation<A>>) -> Option<Mutation<A>> {
147        match mutations.len() {
148            0 => None,
149            1 => Some(mutations.swap_remove(0)),
150            _ => Some(Mutation {
151                path_rev: vec![],
152                operation: MutationKind::Batch(mutations),
153            }),
154        }
155    }
156}
157
158#[cfg(test)]
159mod test {
160    use serde_json::json;
161
162    use super::*;
163    use crate::JsonAdapter;
164
165    #[test]
166    fn batch() {
167        let mut batch = Batch::<JsonAdapter>::new();
168        assert_eq!(batch.dump(), None);
169
170        let mut batch = Batch::<JsonAdapter>::new();
171        batch
172            .load(Mutation {
173                path_rev: vec!["bar".into(), "foo".into()],
174                operation: MutationKind::Replace(json!(1)),
175            })
176            .unwrap();
177        assert_eq!(
178            batch.dump(),
179            Some(Mutation {
180                path_rev: vec!["bar".into(), "foo".into()],
181                operation: MutationKind::Replace(json!(1))
182            }),
183        );
184
185        let mut batch = Batch::<JsonAdapter>::new();
186        batch
187            .load(Mutation {
188                path_rev: vec!["bar".into(), "foo".into()],
189                operation: MutationKind::Replace(json!(1)),
190            })
191            .unwrap();
192        batch
193            .load(Mutation {
194                path_rev: vec!["bar".into(), "foo".into()],
195                operation: MutationKind::Replace(json!(2)),
196            })
197            .unwrap();
198        assert_eq!(
199            batch.dump(),
200            Some(Mutation {
201                path_rev: vec!["bar".into(), "foo".into()],
202                operation: MutationKind::Replace(json!(2)),
203            }),
204        );
205
206        let mut batch = Batch::<JsonAdapter>::new();
207        batch
208            .load(Mutation {
209                path_rev: vec!["bar".into(), "foo".into()],
210                operation: MutationKind::Replace(json!({"qux": "1"})),
211            })
212            .unwrap();
213        batch
214            .load(Mutation {
215                path_rev: vec!["qux".into(), "bar".into(), "foo".into()],
216                operation: MutationKind::Append(json!("2")),
217            })
218            .unwrap();
219        assert_eq!(
220            batch.dump(),
221            Some(Mutation {
222                path_rev: vec!["bar".into(), "foo".into()],
223                operation: MutationKind::Replace(json!({"qux": "12"})),
224            }),
225        );
226
227        let mut batch = Batch::<JsonAdapter>::new();
228        batch
229            .load(Mutation {
230                path_rev: vec!["qux".into(), "bar".into(), "foo".into()],
231                operation: MutationKind::Append(json!("2")),
232            })
233            .unwrap();
234        batch
235            .load(Mutation {
236                path_rev: vec!["bar".into(), "foo".into()],
237                operation: MutationKind::Replace(json!({"qux": "1"})),
238            })
239            .unwrap();
240        assert_eq!(
241            batch.dump(),
242            Some(Mutation {
243                path_rev: vec!["bar".into(), "foo".into()],
244                operation: MutationKind::Replace(json!({"qux": "1"})),
245            }),
246        );
247
248        let mut batch = Batch::<JsonAdapter>::new();
249        batch
250            .load(Mutation {
251                path_rev: vec!["foo".into()],
252                operation: MutationKind::Batch(vec![
253                    Mutation {
254                        path_rev: vec!["bar".into()],
255                        operation: MutationKind::Append(json!("1")),
256                    },
257                    Mutation {
258                        path_rev: vec!["bar".into()],
259                        operation: MutationKind::Append(json!("2")),
260                    },
261                ]),
262            })
263            .unwrap();
264        assert_eq!(
265            batch.dump(),
266            Some(Mutation {
267                path_rev: vec!["bar".into(), "foo".into()],
268                operation: MutationKind::Append(json!("12")),
269            }),
270        );
271
272        let mut batch = Batch::<JsonAdapter>::new();
273        batch
274            .load(Mutation {
275                path_rev: vec!["bar".into()],
276                operation: MutationKind::Append(json!("2")),
277            })
278            .unwrap();
279        batch
280            .load(Mutation {
281                path_rev: vec!["qux".into()],
282                operation: MutationKind::Append(json!("1")),
283            })
284            .unwrap();
285        assert_eq!(
286            batch.dump(),
287            Some(Mutation {
288                path_rev: vec![],
289                operation: MutationKind::Batch(vec![
290                    Mutation {
291                        path_rev: vec!["bar".into()],
292                        operation: MutationKind::Append(json!("2")),
293                    },
294                    Mutation {
295                        path_rev: vec!["qux".into()],
296                        operation: MutationKind::Append(json!("1")),
297                    },
298                ]),
299            }),
300        );
301
302        let mut batch = Batch::<JsonAdapter>::new();
303        batch
304            .load(Mutation {
305                path_rev: vec!["bar".into(), "foo".into()],
306                operation: MutationKind::Append(json!("2")),
307            })
308            .unwrap();
309        batch
310            .load(Mutation {
311                path_rev: vec!["qux".into(), "foo".into()],
312                operation: MutationKind::Append(json!("1")),
313            })
314            .unwrap();
315        assert_eq!(
316            batch.dump(),
317            Some(Mutation {
318                path_rev: vec!["foo".into()],
319                operation: MutationKind::Batch(vec![
320                    Mutation {
321                        path_rev: vec!["bar".into()],
322                        operation: MutationKind::Append(json!("2")),
323                    },
324                    Mutation {
325                        path_rev: vec!["qux".into()],
326                        operation: MutationKind::Append(json!("1")),
327                    },
328                ]),
329            }),
330        );
331    }
332}