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}