Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/store/
value_editor.rs

1//! ValueEditor for mutable access to DocStore values.
2
3use crate::{Result, Store, crdt::doc::Value, store::errors::StoreError};
4
5use super::DocStore;
6
7/// An editor for a `Value` obtained from a `DocStore`.
8///
9/// This provides a mutable lens into a value, allowing modifications
10/// to be staged and then saved back to the DocStore.
11pub struct ValueEditor<'a> {
12    pub(super) kv_store: &'a DocStore,
13    pub(super) keys: Vec<String>,
14}
15
16impl<'a> ValueEditor<'a> {
17    pub fn new<K>(kv_store: &'a DocStore, keys: K) -> Self
18    where
19        K: Into<Vec<String>>,
20    {
21        Self {
22            kv_store,
23            keys: keys.into(),
24        }
25    }
26
27    /// Uses the stored keys to traverse the nested data structure and retrieve the value.
28    ///
29    /// This method starts from the fully merged view of the DocStore's subtree (local
30    /// Transaction changes layered on top of backend state) and navigates using the path
31    /// specified by `self.keys`. If `self.keys` is empty, it retrieves the root
32    /// of the DocStore's subtree.
33    ///
34    /// Returns `Error::NotFound` if any part of the path does not exist, or if the
35    /// final value is a tombstone (`Value::Deleted`).
36    /// Returns `Error::Io` with `ErrorKind::InvalidData` if a non-map value is encountered
37    /// during path traversal where a map was expected.
38    pub async fn get(&self) -> Result<Value> {
39        self.kv_store.get_at_path(&self.keys).await
40    }
41
42    /// Sets a `Value` at the path specified by `self.keys` within the `DocStore`'s `Transaction`.
43    ///
44    /// This method modifies the local data associated with the `Transaction`. The changes
45    /// are not persisted to the backend until `Transaction::commit()` is called.
46    /// If the path specified by `self.keys` does not exist, it will be created.
47    /// Intermediate non-map values in the path will be overwritten by maps as needed.
48    /// If `self.keys` is empty (editor points to root), the provided `value` must
49    /// be a `Value::Doc`.
50    ///
51    /// Returns `Error::InvalidOperation` if setting the root and `value` is not a node.
52    pub async fn set(&self, value: Value) -> Result<()> {
53        self.kv_store.set_at_path(&self.keys, value).await
54    }
55
56    /// Returns a nested value by appending `key` to the current editor's path.
57    ///
58    /// This is a convenience method that uses `self.get()` to find the map at the current
59    /// editor's path, and then retrieves `key` from that map.
60    pub async fn get_value(&self, key: impl AsRef<str>) -> Result<Value> {
61        let key = key.as_ref();
62        if self.keys.is_empty() {
63            // If the base path is empty, trying to get a sub-key implies trying to get a top-level key.
64            return self.kv_store.get_at_path([key]).await;
65        }
66
67        let mut path_to_value = self.keys.clone();
68        path_to_value.push(key.to_string());
69        self.kv_store.get_at_path(&path_to_value).await
70    }
71
72    /// Constructs a new `ValueEditor` for a path one level deeper.
73    ///
74    /// The new editor's path will be `self.keys` with `key` appended.
75    pub fn get_value_mut(&self, key: impl Into<String>) -> ValueEditor<'a> {
76        let mut new_keys = self.keys.clone();
77        new_keys.push(key.into());
78        ValueEditor::new(self.kv_store, new_keys)
79    }
80
81    /// Marks the value at the editor's current path as deleted.
82    /// This is achieved by setting its value to `Value::Deleted`.
83    /// The change is staged in the `Transaction` and needs to be committed.
84    pub async fn delete_self(&self) -> Result<()> {
85        self.set(Value::Deleted).await
86    }
87
88    /// Marks the value at the specified child `key` (relative to the editor's current path) as deleted.
89    /// This is achieved by setting its value to `Value::Deleted`.
90    /// The change is staged in the `Transaction` and needs to be committed.
91    ///
92    /// If the editor points to the root (empty path), this will delete the top-level `key`.
93    pub async fn delete_child(&self, key: impl Into<String>) -> Result<()> {
94        let mut path_to_delete = self.keys.clone();
95        path_to_delete.push(key.into());
96        self.kv_store
97            .set_at_path(&path_to_delete, Value::Deleted)
98            .await
99    }
100}
101
102impl DocStore {
103    /// Gets a mutable editor for a value associated with the given key.
104    ///
105    /// If the key does not exist, the editor will be initialized with an empty map,
106    /// allowing immediate use of map-modifying methods. The type can be changed
107    /// later using `ValueEditor::set()`.
108    ///
109    /// Changes made via the `ValueEditor` are staged in the `Transaction` by its `set` method
110    /// and must be committed via `Transaction::commit()` to be persisted to the `Doc`'s backend.
111    pub fn get_value_mut(&self, key: impl Into<String>) -> ValueEditor<'_> {
112        ValueEditor::new(self, vec![key.into()])
113    }
114
115    /// Gets a mutable editor for the root of this Doc's subtree.
116    ///
117    /// Changes made via the `ValueEditor` are staged in the `Transaction` by its `set` method
118    /// and must be committed via `Transaction::commit()` to be persisted to the `Doc`'s backend.
119    pub fn get_root_mut(&self) -> ValueEditor<'_> {
120        ValueEditor::new(self, Vec::new())
121    }
122
123    /// Retrieves a `Value` from the Doc using a specified path.
124    ///
125    /// The path is a slice of strings, where each string is a key in the
126    /// nested map structure. If the path is empty, it retrieves the entire
127    /// content of this Doc's named subtree as a `Value::Doc`.
128    ///
129    /// This method operates on the fully merged view of the Doc's data,
130    /// including any local changes from the current `Transaction` layered on top
131    /// of the backend state.
132    ///
133    /// # Arguments
134    ///
135    /// * `path`: A slice of `String` representing the path to the desired value.
136    ///
137    /// # Errors
138    ///
139    /// * `Error::NotFound` if any segment of the path does not exist (for non-empty paths),
140    ///   or if the final value or an intermediate value is a `Value::Deleted` (tombstone).
141    /// * `Error::Io` with `ErrorKind::InvalidData` if a non-map value is
142    ///   encountered during path traversal where a map was expected.
143    pub async fn get_at_path<S, P>(&self, path: P) -> Result<Value>
144    where
145        S: AsRef<str>,
146        P: AsRef<[S]>,
147    {
148        let path_slice = path.as_ref();
149        if path_slice.is_empty() {
150            // Requesting the root of this Doc's named subtree
151            return Ok(Value::Doc(self.get_all().await?));
152        }
153
154        let mut current_value_view = Value::Doc(self.get_all().await?);
155
156        for key_segment_s in path_slice.iter() {
157            match current_value_view {
158                Value::Doc(node) => match node.get(key_segment_s.as_ref()) {
159                    Some(next_value) => {
160                        current_value_view = next_value.clone();
161                    }
162                    None => {
163                        return Err(StoreError::KeyNotFound {
164                            store: self.name.clone(),
165                            key: path_slice
166                                .iter()
167                                .map(|s| s.as_ref())
168                                .collect::<Vec<_>>()
169                                .join("."),
170                        }
171                        .into());
172                    }
173                },
174                Value::Deleted => {
175                    // A tombstone encountered in the path means the path doesn't lead to a value.
176                    return Err(StoreError::KeyNotFound {
177                        store: self.name.clone(),
178                        key: path_slice
179                            .iter()
180                            .map(|s| s.as_ref())
181                            .collect::<Vec<_>>()
182                            .join("."),
183                    }
184                    .into());
185                }
186                _ => {
187                    // Expected a node to continue traversal, but found something else.
188                    return Err(StoreError::TypeMismatch {
189                        store: self.name.clone(),
190                        expected: "Doc".to_string(),
191                        actual: "non-node value".to_string(),
192                    }
193                    .into());
194                }
195            }
196        }
197
198        // Check if the final resolved value is a tombstone.
199        match current_value_view {
200            Value::Deleted => Err(StoreError::KeyNotFound {
201                store: self.name.clone(),
202                key: path_slice
203                    .iter()
204                    .map(|s| s.as_ref())
205                    .collect::<Vec<_>>()
206                    .join("."),
207            }
208            .into()),
209            _ => Ok(current_value_view),
210        }
211    }
212
213    /// Sets a `Value` at a specified path within the `Doc`'s `Transaction`.
214    ///
215    /// The path is a slice of strings, where each string is a key in the
216    /// nested map structure.
217    ///
218    /// This method modifies the local data associated with the `Transaction`. The changes
219    /// are not persisted to the backend until `Transaction::commit()` is called.
220    /// If the path does not exist, it will be created. Intermediate non-map values
221    /// in the path will be overwritten by maps as needed to complete the path.
222    ///
223    /// # Arguments
224    ///
225    /// * `path`: A slice of `String` representing the path where the value should be set.
226    /// * `value`: The `Value` to set at the specified path.
227    ///
228    /// # Errors
229    ///
230    /// * `Error::InvalidOperation` if the `path` is empty and `value` is not a `Value::Doc`.
231    /// * `Error::Serialize` if the updated subtree data cannot be serialized to JSON.
232    /// * Potentially other errors from `Transaction::update_subtree`.
233    pub async fn set_at_path<S, P>(&self, path: P, value: Value) -> Result<()>
234    where
235        S: Into<String> + Clone,
236        P: AsRef<[S]>,
237    {
238        let path_slice = path.as_ref();
239        if path_slice.is_empty() {
240            // Setting the root of this Doc's named subtree.
241            // The value must be a node.
242            if let Value::Doc(node) = value {
243                let serialized_data = serde_json::to_string(&node)?;
244                return self.txn.update_subtree(&self.name, &serialized_data).await;
245            } else {
246                return Err(StoreError::TypeMismatch {
247                    store: self.name.clone(),
248                    expected: "Doc".to_string(),
249                    actual: "non-doc value".to_string(),
250                }
251                .into());
252            }
253        }
254
255        let mut subtree_data = self.local_data()?.unwrap_or_default();
256
257        // Build the dot-separated path string
258        let path_str: String = path_slice
259            .iter()
260            .map(|s| {
261                let s_string: String = s.clone().into();
262                s_string
263            })
264            .collect::<Vec<_>>()
265            .join(".");
266
267        // Use Doc::set which now creates intermediate nodes automatically
268        subtree_data.set(&path_str, value);
269
270        let serialized_data = serde_json::to_string(&subtree_data)?;
271        self.txn.update_subtree(&self.name, &serialized_data).await
272    }
273}