Skip to main content

platform_value/
replace.rs

1use crate::btreemap_extensions::btreemap_field_replacement::replace_leaf;
2use crate::btreemap_extensions::btreemap_field_replacement::IntegerReplacementType;
3use crate::inner_value_at_path::is_array_path;
4use crate::{Error, ReplacementType, Value, ValueMapHelper};
5use std::collections::HashSet;
6
7impl Value {
8    /// If the `Value` is a `Map`, replaces the value at the path inside the map.
9    /// This is used to set inner values as Identifiers or BinaryData, or from Identifiers or
10    /// BinaryData to base58 or base64 strings.
11    /// Either returns `Err(Error::Structure("reason"))` or `Err(Error::ByteLengthNot32BytesError))`
12    /// if the replacement can not happen.
13    ///
14    /// ```
15    /// # use platform_value::{Error, Identifier, ReplacementType, Value};
16    /// #
17    /// let mut inner_value = Value::Map(
18    ///     vec![
19    ///         (Value::Text(String::from("food_id")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
20    ///     ]
21    /// );
22    /// let mut value = Value::Map(
23    ///     vec![
24    ///         (Value::Text(String::from("foods")), inner_value),
25    ///     ]
26    /// );
27    ///
28    /// value.replace_at_path("foods.food_id", ReplacementType::Identifier).expect("expected to replace at path with identifier");
29    ///
30    /// assert_eq!(value.get_value_at_path("foods.food_id"), Ok(&Value::Identifier([86, 35, 118, 67, 167, 43, 101, 109, 72, 97, 35, 99, 0, 254, 108, 154, 254, 154, 190, 40, 237, 25, 58, 246, 111, 19, 44, 215, 141, 140, 156, 117])));
31    ///
32    /// let mut tangerine_value = Value::Map(
33    ///     vec![
34    ///         (Value::Text(String::from("food_id")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
35    ///     ]
36    /// );
37    /// let mut mandarin_value = Value::Map(
38    ///     vec![
39    ///         (Value::Text(String::from("food_id")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
40    ///     ]
41    /// );
42    /// let mut oranges_value = Value::Array(
43    ///     vec![
44    ///         tangerine_value,
45    ///         mandarin_value
46    ///     ]
47    /// );
48    /// let mut value = Value::Map(
49    ///     vec![
50    ///         (Value::Text(String::from("foods")), oranges_value),
51    ///     ]
52    /// );
53    ///
54    /// value.replace_at_path("foods[].food_id", ReplacementType::Identifier).expect("expected to replace at path with identifier");
55    ///
56    /// assert_eq!(value.get_value_at_path("foods[0].food_id"), Ok(&Value::Identifier([86, 35, 118, 67, 167, 43, 101, 109, 72, 97, 35, 99, 0, 254, 108, 154, 254, 154, 190, 40, 237, 25, 58, 246, 111, 19, 44, 215, 141, 140, 156, 117])));
57    ///
58    /// ```
59    pub fn replace_at_path(
60        &mut self,
61        path: &str,
62        replacement_type: ReplacementType,
63    ) -> Result<(), Error> {
64        let mut split = path.split('.').peekable();
65        let mut current_values = vec![self];
66        while let Some(path_component) = split.next() {
67            if let Some((string_part, number_part)) = is_array_path(path_component)? {
68                let is_last_component = split.peek().is_none();
69                current_values = current_values
70                    .into_iter()
71                    .filter_map(|current_value| {
72                        let map = match current_value.to_map_mut() {
73                            Ok(map) => map,
74                            Err(err) => return Some(Err(err)),
75                        };
76                        // An absent list is an absent optional property, as an
77                        // absent key is below: nothing to replace
78                        let array_value = map.get_optional_key_mut(string_part)?;
79                        let array = match array_value.to_array_mut() {
80                            Ok(array) => array,
81                            Err(err) => return Some(Err(err)),
82                        };
83                        match number_part {
84                            Some(number_part) => match array.get_mut(number_part) {
85                                Some(member) => Some(Ok(vec![member])),
86                                None => Some(Err(Error::StructureError(format!(
87                                    "element at position {number_part} in array does not exist"
88                                )))),
89                            },
90                            // we are replacing all members in array
91                            None => Some(Ok(array.iter_mut().collect())),
92                        }
93                    })
94                    .collect::<Result<Vec<Vec<&mut Value>>, Error>>()?
95                    .into_iter()
96                    .flatten()
97                    .collect();
98                if is_last_component {
99                    // `list[]` or `list[3]` ends the path: the members are the
100                    // values to replace
101                    for member in current_values {
102                        replace_leaf(member, replacement_type)?;
103                    }
104                    return Ok(());
105                }
106            } else {
107                current_values = current_values
108                    .into_iter()
109                    .filter_map(|current_value| {
110                        let map = match current_value.as_map_mut_ref() {
111                            Ok(map) => map,
112                            Err(err) => return Some(Err(err)),
113                        };
114
115                        let new_value = map.get_optional_key_mut(path_component)?;
116
117                        if split.peek().is_none() {
118                            return replace_leaf(new_value, replacement_type).err().map(Err);
119                        }
120                        Some(Ok(new_value))
121                    })
122                    .collect::<Result<Vec<&mut Value>, Error>>()?;
123            }
124        }
125        Ok(())
126    }
127
128    /// Calls replace_at_path for every path in a given array.
129    /// Either returns `Err(Error::Structure("reason"))` or `Err(Error::ByteLengthNot32BytesError))`
130    /// if the replacement can not happen.
131    ///
132    /// ```
133    /// # use platform_value::{Error, Identifier, ReplacementType, Value};
134    /// #
135    /// let mut inner_value = Value::Map(
136    ///     vec![
137    ///         (Value::Text(String::from("grapes")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
138    ///         (Value::Text(String::from("oranges")), Value::Array(vec![Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101)])),
139    ///     ]
140    /// );
141    /// let mut value = Value::Map(
142    ///     vec![
143    ///         (Value::Text(String::from("foods")), inner_value),
144    ///     ]
145    /// );
146    ///
147    /// let paths = vec!["foods.grapes", "foods.oranges"];
148    ///
149    /// value.replace_at_paths(paths, ReplacementType::Identifier).expect("expected to replace at paths with identifier");
150    ///
151    /// assert_eq!(value.get_value_at_path("foods.grapes"), Ok(&Value::Identifier([86, 35, 118, 67, 167, 43, 101, 109, 72, 97, 35, 99, 0, 254, 108, 154, 254, 154, 190, 40, 237, 25, 58, 246, 111, 19, 44, 215, 141, 140, 156, 117])));
152    /// assert_eq!(value.get_value_at_path("foods.oranges"), Ok(&Value::Identifier([104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101])));
153    ///
154    /// ```
155    pub fn replace_at_paths<'a, I: IntoIterator<Item = &'a str>>(
156        &mut self,
157        paths: I,
158        replacement_type: ReplacementType,
159    ) -> Result<(), Error> {
160        paths
161            .into_iter()
162            .try_for_each(|path| self.replace_at_path(path, replacement_type))
163    }
164
165    /// If the `Value` is a `Map`, replaces the value at the path inside the map.
166    /// This is used to set inner values as Identifiers or BinaryData, or from Identifiers or
167    /// BinaryData to base58 or base64 strings.
168    /// Either returns `Err(Error::Structure("reason"))` or `Err(Error::ByteLengthNot32BytesError))`
169    /// if the replacement can not happen.
170    ///
171    /// ```
172    /// # use platform_value::{Error, Identifier, IntegerReplacementType, Value};
173    /// #
174    /// let mut inner_value = Value::Map(
175    ///     vec![
176    ///         (Value::Text(String::from("food_id")), Value::U8(5)),
177    ///     ]
178    /// );
179    /// let mut value = Value::Map(
180    ///     vec![
181    ///         (Value::Text(String::from("foods")), inner_value),
182    ///     ]
183    /// );
184    ///
185    /// value.replace_integer_type_at_path("foods.food_id", IntegerReplacementType::U32).expect("expected to replace at path with identifier");
186    ///
187    /// assert_eq!(value.get_value_at_path("foods.food_id"), Ok(&Value::U32(5)));
188    ///
189    /// let mut tangerine_value = Value::Map(
190    ///     vec![
191    ///         (Value::Text(String::from("food_id")), Value::U128(8)),
192    ///     ]
193    /// );
194    /// let mut mandarin_value = Value::Map(
195    ///     vec![
196    ///         (Value::Text(String::from("food_id")), Value::U32(2)),
197    ///     ]
198    /// );
199    /// let mut oranges_value = Value::Array(
200    ///     vec![
201    ///         tangerine_value,
202    ///         mandarin_value
203    ///     ]
204    /// );
205    /// let mut value = Value::Map(
206    ///     vec![
207    ///         (Value::Text(String::from("foods")), oranges_value),
208    ///     ]
209    /// );
210    ///
211    /// value.replace_integer_type_at_path("foods[].food_id", IntegerReplacementType::U16).expect("expected to replace at path with identifier");
212    ///
213    /// assert_eq!(value.get_value_at_path("foods[0].food_id"), Ok(&Value::U16(8)));
214    ///
215    /// ```
216    pub fn replace_integer_type_at_path(
217        &mut self,
218        path: &str,
219        replacement_type: IntegerReplacementType,
220    ) -> Result<(), Error> {
221        let mut split = path.split('.').peekable();
222        let mut current_values = vec![self];
223        while let Some(path_component) = split.next() {
224            if let Some((string_part, number_part)) = is_array_path(path_component)? {
225                current_values = current_values
226                    .into_iter()
227                    .map(|current_value| {
228                        let map = current_value.to_map_mut()?;
229                        let array_value = map.get_key_mut(string_part)?;
230                        let array = array_value.to_array_mut()?;
231                        if let Some(number_part) = number_part {
232                            if array.len() < number_part {
233                                //this already exists
234                                Ok(vec![array.get_mut(number_part).unwrap()])
235                            } else {
236                                Err(Error::StructureError(format!(
237                                    "element at position {number_part} in array does not exist"
238                                )))
239                            }
240                        } else {
241                            // we are replacing all members in array
242                            Ok(array.iter_mut().collect())
243                        }
244                    })
245                    .collect::<Result<Vec<Vec<&mut Value>>, Error>>()?
246                    .into_iter()
247                    .flatten()
248                    .collect()
249            } else {
250                current_values = current_values
251                    .into_iter()
252                    .filter_map(|current_value| {
253                        let map = match current_value.as_map_mut_ref() {
254                            Ok(map) => map,
255                            Err(err) => return Some(Err(err)),
256                        };
257
258                        let new_value = map.get_optional_key_mut(path_component)?;
259
260                        if split.peek().is_none() {
261                            *new_value = match replacement_type.replace_for_value(new_value.clone())
262                            {
263                                Ok(value) => value,
264                                Err(err) => return Some(Err(err)),
265                            };
266                            return None;
267                        }
268                        Some(Ok(new_value))
269                    })
270                    .collect::<Result<Vec<&mut Value>, Error>>()?;
271            }
272        }
273        Ok(())
274    }
275
276    /// Calls replace_at_path for every path in a given array.
277    /// Either returns `Err(Error::Structure("reason"))` or `Err(Error::ByteLengthNot32BytesError))`
278    /// if the replacement can not happen.
279    ///
280    /// ```
281    /// # use platform_value::{Error, Identifier, IntegerReplacementType, ReplacementType, Value};
282    /// #
283    /// let mut inner_value = Value::Map(
284    ///     vec![
285    ///         (Value::Text(String::from("grapes")), Value::U16(5)),
286    ///         (Value::Text(String::from("oranges")), Value::I32(6)),
287    ///     ]
288    /// );
289    /// let mut value = Value::Map(
290    ///     vec![
291    ///         (Value::Text(String::from("foods")), inner_value),
292    ///     ]
293    /// );
294    ///
295    /// let paths = vec!["foods.grapes", "foods.oranges"];
296    ///
297    /// value.replace_integer_type_at_paths(paths, IntegerReplacementType::U32).expect("expected to replace at paths with identifier");
298    ///
299    /// assert_eq!(value.get_value_at_path("foods.grapes"), Ok(&Value::U32(5)));
300    /// assert_eq!(value.get_value_at_path("foods.oranges"), Ok(&Value::U32(6)));
301    ///
302    /// ```
303    pub fn replace_integer_type_at_paths<'a, I: IntoIterator<Item = &'a str>>(
304        &mut self,
305        paths: I,
306        replacement_type: IntegerReplacementType,
307    ) -> Result<(), Error> {
308        paths
309            .into_iter()
310            .try_for_each(|path| self.replace_integer_type_at_path(path, replacement_type))
311    }
312
313    /// `replace_to_binary_types_when_setting_with_path` will replace a value with a corresponding
314    /// binary type (Identifier or Binary Data) if that data is in one of the given paths.
315    /// Paths can either be terminal, or can represent an object or an array (with values) where
316    /// all subvalues must be set to the binary type.
317    /// Either returns `Err(Error::Structure("reason"))` or `Err(Error::ByteLengthNot32BytesError))`
318    /// if the replacement can not happen.
319    ///
320    /// ```
321    /// # use std::collections::HashSet;
322    /// use platform_value::{Error, Identifier, ReplacementType, Value};
323    /// #
324    /// let mut inner_inner_value = Value::Map(
325    ///     vec![
326    ///         (Value::Text(String::from("mandarins")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
327    ///         (Value::Text(String::from("tangerines")), Value::Array(vec![Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101)])),
328    ///     ]
329    /// );
330    /// let mut inner_value = Value::Map(
331    ///     vec![
332    ///         (Value::Text(String::from("grapes")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
333    ///         (Value::Text(String::from("oranges")), inner_inner_value),
334    ///     ]
335    /// );
336    /// let mut value = Value::Map(
337    ///     vec![
338    ///         (Value::Text(String::from("foods")), inner_value),
339    ///     ]
340    /// );
341    ///
342    ///
343    /// let identifier_paths = HashSet::from(["foods.oranges.tangerines"]);
344    ///
345    /// value.replace_to_binary_types_of_root_value_when_setting_at_path("foods.oranges", identifier_paths, HashSet::new()).expect("expected to replace at paths with identifier");
346    ///
347    /// assert_eq!(value.get_value_at_path("foods.oranges.tangerines"), Ok(&Value::Identifier([104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101])));
348    ///
349    /// ```
350    pub fn replace_to_binary_types_of_root_value_when_setting_at_path(
351        &mut self,
352        path: &str,
353        identifier_paths: HashSet<&str>,
354        binary_paths: HashSet<&str>,
355    ) -> Result<(), Error> {
356        if identifier_paths.contains(path) {
357            ReplacementType::Identifier.replace_value_in_place(self)?;
358        } else if binary_paths.contains(path) {
359            ReplacementType::BinaryBytes.replace_value_in_place(self)?;
360        } else {
361            identifier_paths
362                .into_iter()
363                .try_for_each(|identifier_path| {
364                    if identifier_path.starts_with(path) {
365                        self.replace_at_path(identifier_path, ReplacementType::Identifier)
366                            .map(|_| ())
367                    } else {
368                        Ok(())
369                    }
370                })?;
371
372            binary_paths.into_iter().try_for_each(|binary_path| {
373                if binary_path.starts_with(path) {
374                    self.replace_at_path(binary_path, ReplacementType::BinaryBytes)
375                        .map(|_| ())
376                } else {
377                    Ok(())
378                }
379            })?;
380        }
381        Ok(())
382    }
383
384    /// `replace_to_binary_types_when_setting_with_path` will replace a value with a corresponding
385    /// binary type (Identifier or Binary Data) if that data is in one of the given paths.
386    /// Paths can either be terminal, or can represent an object or an array (with values) where
387    /// all subvalues must be set to the binary type.
388    /// Either returns `Err(Error::Structure("reason"))` or `Err(Error::ByteLengthNot32BytesError))`
389    /// if the replacement can not happen.
390    ///
391    /// ```
392    /// # use std::collections::HashSet;
393    /// use platform_value::{Error, Identifier, ReplacementType, Value};
394    /// #
395    /// let mut inner_inner_value = Value::Map(
396    ///     vec![
397    ///         (Value::Text(String::from("mandarins")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
398    ///         (Value::Text(String::from("tangerines")), Value::Array(vec![Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101), Value::U8(108),Value::U8(104), Value::U8(101)])),
399    ///     ]
400    /// );
401    /// let mut inner_value = Value::Map(
402    ///     vec![
403    ///         (Value::Text(String::from("grapes")), Value::Text("6oFRdsUNiAtXscRn52atKYCiF8RBnH9vbUzhtzY3d83e".to_string())),
404    ///         (Value::Text(String::from("oranges")), inner_inner_value),
405    ///     ]
406    /// );
407    /// let mut value = Value::Map(
408    ///     vec![
409    ///         (Value::Text(String::from("foods")), inner_value),
410    ///     ]
411    /// );
412    ///
413    ///
414    /// let identifier_paths = HashSet::from(["foods.oranges.tangerines"]);
415    ///
416    /// let oranges = value.get_mut_value_at_path("foods.oranges").unwrap();
417    /// oranges.replace_to_binary_types_when_setting_with_path("foods.oranges", identifier_paths, HashSet::new()).expect("expected to replace at paths with identifier");
418    ///
419    /// assert_eq!(value.get_value_at_path("foods.oranges.tangerines"), Ok(&Value::Identifier([104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101, 108, 104, 101])));
420    ///
421    /// ```
422    pub fn replace_to_binary_types_when_setting_with_path(
423        &mut self,
424        path: &str,
425        identifier_paths: HashSet<&str>,
426        binary_paths: HashSet<&str>,
427    ) -> Result<(), Error> {
428        if identifier_paths.contains(path) {
429            ReplacementType::Identifier.replace_value_in_place(self)?;
430        } else if binary_paths.contains(path) {
431            ReplacementType::BinaryBytes.replace_value_in_place(self)?;
432        } else {
433            let mut path = path.to_string();
434            path.push('.');
435            identifier_paths
436                .into_iter()
437                .try_for_each(|identifier_path| {
438                    if let Some(suffix) = identifier_path.strip_prefix(path.as_str()) {
439                        self.replace_at_path(suffix, ReplacementType::Identifier)
440                            .map(|_| ())
441                    } else {
442                        Ok(())
443                    }
444                })?;
445            binary_paths.into_iter().try_for_each(|binary_path| {
446                if let Some(suffix) = binary_path.strip_prefix(path.as_str()) {
447                    self.replace_at_path(suffix, ReplacementType::BinaryBytes)
448                        .map(|_| ())
449                } else {
450                    Ok(())
451                }
452            })?;
453        }
454        Ok(())
455    }
456
457    /// Cleans all values and removes null inner values at any depth.
458    /// if the replacement can not happen.
459    ///
460    /// ```
461    /// # use platform_value::{Error, Identifier, IntegerReplacementType, ReplacementType, Value};
462    /// #
463    /// let mut inner_value = Value::Map(
464    ///     vec![
465    ///         (Value::Text(String::from("grapes")), Value::Null),
466    ///         (Value::Text(String::from("oranges")), Value::I32(6)),
467    ///     ]
468    /// );
469    /// let mut value = Value::Map(
470    ///     vec![
471    ///         (Value::Text(String::from("foods")), inner_value),
472    ///     ]
473    /// );
474    ///
475    /// value = value.clean_recursive().unwrap();
476    ///
477    /// assert_eq!(value.get_optional_value_at_path("foods.grapes"), Ok(None));
478    ///
479    pub fn clean_recursive(self) -> Result<Value, Error> {
480        Ok(Value::Map(
481            self.into_map()?
482                .into_iter()
483                .filter_map(|(key, value)| {
484                    if value.is_null() {
485                        None
486                    } else if value.is_map() {
487                        match value.clean_recursive() {
488                            Ok(value) => Some(Ok((key, value))),
489                            Err(e) => Some(Err(e)),
490                        }
491                    } else {
492                        Some(Ok((key, value)))
493                    }
494                })
495                .collect::<Result<Vec<_>, Error>>()?,
496        ))
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::{IntegerReplacementType, ReplacementType, Value};
504
505    // ---------------------------------------------------------------
506    // Helper: builds a 32-byte base58-encoded string from a seed byte
507    // ---------------------------------------------------------------
508    fn base58_of_32_bytes(seed: u8) -> String {
509        bs58::encode([seed; 32]).into_string()
510    }
511
512    fn make_32_u8_array(seed: u8) -> Vec<Value> {
513        vec![Value::U8(seed); 32]
514    }
515
516    // ===============================================================
517    // replace_at_path — single segment, ReplacementType::Identifier
518    // ===============================================================
519
520    #[test]
521    fn replace_at_path_single_segment_identifier_from_text() {
522        let b58 = base58_of_32_bytes(1);
523        let mut value = Value::Map(vec![(Value::Text("id".into()), Value::Text(b58))]);
524        value
525            .replace_at_path("id", ReplacementType::Identifier)
526            .unwrap();
527        assert_eq!(
528            value.get_value_at_path("id").unwrap(),
529            &Value::Identifier([1u8; 32])
530        );
531    }
532
533    #[test]
534    fn replace_at_path_single_segment_identifier_from_u8_array() {
535        let mut value = Value::Map(vec![(
536            Value::Text("id".into()),
537            Value::Array(make_32_u8_array(7)),
538        )]);
539        value
540            .replace_at_path("id", ReplacementType::Identifier)
541            .unwrap();
542        assert_eq!(
543            value.get_value_at_path("id").unwrap(),
544            &Value::Identifier([7u8; 32])
545        );
546    }
547
548    // ===============================================================
549    // replace_at_path — single segment, ReplacementType::BinaryBytes
550    // ===============================================================
551
552    #[test]
553    fn replace_at_path_single_segment_binary_bytes_from_base64() {
554        use base64::prelude::*;
555        let raw = vec![10u8, 20, 30];
556        let b64 = BASE64_STANDARD.encode(&raw);
557        let mut value = Value::Map(vec![(Value::Text("data".into()), Value::Text(b64))]);
558        value
559            .replace_at_path("data", ReplacementType::BinaryBytes)
560            .unwrap();
561        assert_eq!(value.get_value_at_path("data").unwrap(), &Value::Bytes(raw));
562    }
563
564    // ===============================================================
565    // replace_at_path — single segment, ReplacementType::TextBase58
566    // ===============================================================
567
568    #[test]
569    fn replace_at_path_single_segment_text_base58() {
570        let mut value = Value::Map(vec![(
571            Value::Text("id".into()),
572            Value::Bytes(vec![1, 2, 3]),
573        )]);
574        value
575            .replace_at_path("id", ReplacementType::TextBase58)
576            .unwrap();
577        let expected_b58 = bs58::encode(vec![1u8, 2, 3]).into_string();
578        assert_eq!(
579            value.get_value_at_path("id").unwrap(),
580            &Value::Text(expected_b58)
581        );
582    }
583
584    // ===============================================================
585    // replace_at_path — single segment, ReplacementType::TextBase64
586    // ===============================================================
587
588    #[test]
589    fn replace_at_path_single_segment_text_base64() {
590        use base64::prelude::*;
591        let raw = vec![1u8, 2, 3];
592        let mut value = Value::Map(vec![(Value::Text("bin".into()), Value::Bytes(raw.clone()))]);
593        value
594            .replace_at_path("bin", ReplacementType::TextBase64)
595            .unwrap();
596        let expected_b64 = BASE64_STANDARD.encode(&raw);
597        assert_eq!(
598            value.get_value_at_path("bin").unwrap(),
599            &Value::Text(expected_b64)
600        );
601    }
602
603    // ===============================================================
604    // replace_at_path — multi-segment nested path
605    // ===============================================================
606
607    #[test]
608    fn replace_at_path_multi_segment_nested() {
609        let b58 = base58_of_32_bytes(5);
610        let inner = Value::Map(vec![(Value::Text("owner_id".into()), Value::Text(b58))]);
611        let mut value = Value::Map(vec![(Value::Text("doc".into()), inner)]);
612
613        value
614            .replace_at_path("doc.owner_id", ReplacementType::Identifier)
615            .unwrap();
616        assert_eq!(
617            value.get_value_at_path("doc.owner_id").unwrap(),
618            &Value::Identifier([5u8; 32])
619        );
620    }
621
622    // ===============================================================
623    // replace_at_path — array path with [] (all members)
624    // ===============================================================
625
626    #[test]
627    fn replace_at_path_array_all_members() {
628        let b58 = base58_of_32_bytes(3);
629        let elem1 = Value::Map(vec![(Value::Text("id".into()), Value::Text(b58.clone()))]);
630        let elem2 = Value::Map(vec![(Value::Text("id".into()), Value::Text(b58))]);
631        let arr = Value::Array(vec![elem1, elem2]);
632        let mut value = Value::Map(vec![(Value::Text("items".into()), arr)]);
633
634        value
635            .replace_at_path("items[].id", ReplacementType::Identifier)
636            .unwrap();
637        assert_eq!(
638            value.get_value_at_path("items[0].id").unwrap(),
639            &Value::Identifier([3u8; 32])
640        );
641        assert_eq!(
642            value.get_value_at_path("items[1].id").unwrap(),
643            &Value::Identifier([3u8; 32])
644        );
645    }
646
647    // ===============================================================
648    // replace_at_path: a path ending in a list names its members
649    // ===============================================================
650
651    #[test]
652    fn should_replace_every_member_when_a_list_ends_the_path() {
653        let b58 = base58_of_32_bytes(7);
654        let list = Value::Array(vec![Value::Text(b58), Value::Bytes32([8u8; 32])]);
655        let mut value = Value::Map(vec![(Value::Text("ids".into()), list)]);
656
657        value
658            .replace_at_path("ids[]", ReplacementType::Identifier)
659            .unwrap();
660        assert_eq!(
661            value.get_value_at_path("ids").unwrap(),
662            &Value::Array(vec![
663                Value::Identifier([7u8; 32]),
664                Value::Identifier([8u8; 32])
665            ])
666        );
667    }
668
669    #[test]
670    fn should_replace_one_member_by_index_and_refuse_an_index_past_the_end() {
671        let b58 = base58_of_32_bytes(9);
672        let list = Value::Array(vec![Value::Text("keep".into()), Value::Text(b58)]);
673        let mut value = Value::Map(vec![(Value::Text("ids".into()), list)]);
674
675        value
676            .replace_at_path("ids[1]", ReplacementType::Identifier)
677            .unwrap();
678        assert_eq!(
679            value.get_value_at_path("ids").unwrap(),
680            &Value::Array(vec![
681                Value::Text("keep".into()),
682                Value::Identifier([9u8; 32])
683            ])
684        );
685        // An error, not the panic the inverted bounds check used to reach
686        assert!(value
687            .replace_at_path("ids[2]", ReplacementType::Identifier)
688            .is_err());
689    }
690
691    #[test]
692    fn should_keep_the_fixed_size_kind_of_a_replaced_member() {
693        // The same helper serves the map replacer: a 32-byte value replaced
694        // as binary bytes stays `Bytes32` on both paths
695        let list = Value::Array(vec![Value::Bytes32([8u8; 32])]);
696        let mut value = Value::Map(vec![(Value::Text("digests".into()), list)]);
697
698        value
699            .replace_at_path("digests[]", ReplacementType::BinaryBytes)
700            .unwrap();
701        assert_eq!(
702            value.get_value_at_path("digests").unwrap(),
703            &Value::Array(vec![Value::Bytes32([8u8; 32])])
704        );
705    }
706
707    #[test]
708    fn should_treat_an_absent_list_as_nothing_to_replace() {
709        let mut value = Value::Map(vec![(Value::Text("a".into()), Value::U32(42))]);
710        assert!(value
711            .replace_at_path("ids[]", ReplacementType::Identifier)
712            .is_ok());
713        assert!(value
714            .replace_at_path("ids[].inner", ReplacementType::Identifier)
715            .is_ok());
716    }
717
718    // ===============================================================
719    // replace_at_path — optional key missing returns Ok
720    // ===============================================================
721
722    #[test]
723    fn replace_at_path_missing_key_returns_ok() {
724        let mut value = Value::Map(vec![(Value::Text("a".into()), Value::U32(42))]);
725        // "b" does not exist — filter_map filters it out
726        let result = value.replace_at_path("b", ReplacementType::Identifier);
727        assert!(result.is_ok());
728    }
729
730    // ===============================================================
731    // replace_at_path — non-map at root gives error
732    // ===============================================================
733
734    #[test]
735    fn replace_at_path_on_non_map_errors() {
736        let mut value = Value::U32(42);
737        let result = value.replace_at_path("key", ReplacementType::Identifier);
738        assert!(result.is_err());
739    }
740
741    // ===============================================================
742    // replace_at_path — non-32-byte data for Identifier errors
743    // ===============================================================
744
745    #[test]
746    fn replace_at_path_identifier_wrong_length_errors() {
747        // base58-encode only 10 bytes, not 32
748        let b58 = bs58::encode([1u8; 10]).into_string();
749        let mut value = Value::Map(vec![(Value::Text("id".into()), Value::Text(b58))]);
750        let result = value.replace_at_path("id", ReplacementType::Identifier);
751        assert!(result.is_err());
752    }
753
754    // ===============================================================
755    // replace_at_paths — multiple paths
756    // ===============================================================
757
758    #[test]
759    fn replace_at_paths_replaces_multiple() {
760        let b58 = base58_of_32_bytes(9);
761        let inner = Value::Map(vec![
762            (Value::Text("a".into()), Value::Text(b58.clone())),
763            (Value::Text("b".into()), Value::Text(b58)),
764        ]);
765        let mut value = Value::Map(vec![(Value::Text("root".into()), inner)]);
766        value
767            .replace_at_paths(vec!["root.a", "root.b"], ReplacementType::Identifier)
768            .unwrap();
769        assert_eq!(
770            value.get_value_at_path("root.a").unwrap(),
771            &Value::Identifier([9u8; 32])
772        );
773        assert_eq!(
774            value.get_value_at_path("root.b").unwrap(),
775            &Value::Identifier([9u8; 32])
776        );
777    }
778
779    // ===============================================================
780    // replace_integer_type_at_path — single segment, U32
781    // ===============================================================
782
783    #[test]
784    fn replace_integer_type_single_segment_u32() {
785        let mut value = Value::Map(vec![(Value::Text("count".into()), Value::U8(5))]);
786        value
787            .replace_integer_type_at_path("count", IntegerReplacementType::U32)
788            .unwrap();
789        assert_eq!(value.get_value_at_path("count").unwrap(), &Value::U32(5));
790    }
791
792    // ===============================================================
793    // replace_integer_type_at_path — single segment, various types
794    // ===============================================================
795
796    #[test]
797    fn replace_integer_type_single_segment_u16() {
798        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::U128(100))]);
799        value
800            .replace_integer_type_at_path("v", IntegerReplacementType::U16)
801            .unwrap();
802        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::U16(100));
803    }
804
805    #[test]
806    fn replace_integer_type_single_segment_u64() {
807        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::U8(42))]);
808        value
809            .replace_integer_type_at_path("v", IntegerReplacementType::U64)
810            .unwrap();
811        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::U64(42));
812    }
813
814    #[test]
815    fn replace_integer_type_single_segment_i32() {
816        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::I8(-3))]);
817        value
818            .replace_integer_type_at_path("v", IntegerReplacementType::I32)
819            .unwrap();
820        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::I32(-3));
821    }
822
823    #[test]
824    fn replace_integer_type_single_segment_u128() {
825        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::U64(999))]);
826        value
827            .replace_integer_type_at_path("v", IntegerReplacementType::U128)
828            .unwrap();
829        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::U128(999));
830    }
831
832    #[test]
833    fn replace_integer_type_single_segment_i128() {
834        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::I64(-500))]);
835        value
836            .replace_integer_type_at_path("v", IntegerReplacementType::I128)
837            .unwrap();
838        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::I128(-500));
839    }
840
841    #[test]
842    fn replace_integer_type_single_segment_u8() {
843        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::U16(7))]);
844        value
845            .replace_integer_type_at_path("v", IntegerReplacementType::U8)
846            .unwrap();
847        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::U8(7));
848    }
849
850    #[test]
851    fn replace_integer_type_single_segment_i8() {
852        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::I16(-2))]);
853        value
854            .replace_integer_type_at_path("v", IntegerReplacementType::I8)
855            .unwrap();
856        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::I8(-2));
857    }
858
859    #[test]
860    fn replace_integer_type_single_segment_i16() {
861        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::I32(-100))]);
862        value
863            .replace_integer_type_at_path("v", IntegerReplacementType::I16)
864            .unwrap();
865        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::I16(-100));
866    }
867
868    #[test]
869    fn replace_integer_type_single_segment_i64() {
870        let mut value = Value::Map(vec![(Value::Text("v".into()), Value::I128(-9999))]);
871        value
872            .replace_integer_type_at_path("v", IntegerReplacementType::I64)
873            .unwrap();
874        assert_eq!(value.get_value_at_path("v").unwrap(), &Value::I64(-9999));
875    }
876
877    // ===============================================================
878    // replace_integer_type_at_path — multi-segment
879    // ===============================================================
880
881    #[test]
882    fn replace_integer_type_multi_segment() {
883        let inner = Value::Map(vec![(Value::Text("level".into()), Value::U8(10))]);
884        let mut value = Value::Map(vec![(Value::Text("nested".into()), inner)]);
885        value
886            .replace_integer_type_at_path("nested.level", IntegerReplacementType::U32)
887            .unwrap();
888        assert_eq!(
889            value.get_value_at_path("nested.level").unwrap(),
890            &Value::U32(10)
891        );
892    }
893
894    // ===============================================================
895    // replace_integer_type_at_path — array path
896    // ===============================================================
897
898    #[test]
899    fn replace_integer_type_array_all_members() {
900        let elem1 = Value::Map(vec![(Value::Text("n".into()), Value::U128(8))]);
901        let elem2 = Value::Map(vec![(Value::Text("n".into()), Value::U32(2))]);
902        let arr = Value::Array(vec![elem1, elem2]);
903        let mut value = Value::Map(vec![(Value::Text("items".into()), arr)]);
904        value
905            .replace_integer_type_at_path("items[].n", IntegerReplacementType::U16)
906            .unwrap();
907        assert_eq!(
908            value.get_value_at_path("items[0].n").unwrap(),
909            &Value::U16(8)
910        );
911        assert_eq!(
912            value.get_value_at_path("items[1].n").unwrap(),
913            &Value::U16(2)
914        );
915    }
916
917    // ===============================================================
918    // replace_integer_type_at_path — missing key returns Ok
919    // ===============================================================
920
921    #[test]
922    fn replace_integer_type_missing_key_returns_ok() {
923        let mut value = Value::Map(vec![(Value::Text("a".into()), Value::U32(1))]);
924        let result = value.replace_integer_type_at_path("missing", IntegerReplacementType::U32);
925        assert!(result.is_ok());
926    }
927
928    // ===============================================================
929    // replace_integer_type_at_path — non-map errors
930    // ===============================================================
931
932    #[test]
933    fn replace_integer_type_on_non_map_errors() {
934        let mut value = Value::U32(42);
935        let result = value.replace_integer_type_at_path("key", IntegerReplacementType::U32);
936        assert!(result.is_err());
937    }
938
939    // ===============================================================
940    // replace_integer_type_at_paths — multiple paths
941    // ===============================================================
942
943    #[test]
944    fn replace_integer_type_at_paths_replaces_multiple() {
945        let inner = Value::Map(vec![
946            (Value::Text("x".into()), Value::U16(5)),
947            (Value::Text("y".into()), Value::I32(6)),
948        ]);
949        let mut value = Value::Map(vec![(Value::Text("root".into()), inner)]);
950        value
951            .replace_integer_type_at_paths(vec!["root.x", "root.y"], IntegerReplacementType::U32)
952            .unwrap();
953        assert_eq!(value.get_value_at_path("root.x").unwrap(), &Value::U32(5));
954        assert_eq!(value.get_value_at_path("root.y").unwrap(), &Value::U32(6));
955    }
956
957    // ===============================================================
958    // replace_to_binary_types_of_root_value_when_setting_at_path
959    // — identifier match (exact path in identifier_paths)
960    // ===============================================================
961
962    #[test]
963    fn replace_root_binary_types_identifier_exact_match() {
964        let b58 = base58_of_32_bytes(2);
965        let mut value = Value::Text(b58);
966        let identifier_paths = HashSet::from(["my_id"]);
967        value
968            .replace_to_binary_types_of_root_value_when_setting_at_path(
969                "my_id",
970                identifier_paths,
971                HashSet::new(),
972            )
973            .unwrap();
974        assert_eq!(value, Value::Identifier([2u8; 32]));
975    }
976
977    // ===============================================================
978    // replace_to_binary_types_of_root_value_when_setting_at_path
979    // — binary match (exact path in binary_paths)
980    // ===============================================================
981
982    #[test]
983    fn replace_root_binary_types_binary_exact_match() {
984        let b58 = base58_of_32_bytes(4);
985        let mut value = Value::Text(b58);
986        let binary_paths = HashSet::from(["my_data"]);
987        value
988            .replace_to_binary_types_of_root_value_when_setting_at_path(
989                "my_data",
990                HashSet::new(),
991                binary_paths,
992            )
993            .unwrap();
994        // BinaryBytes uses into_identifier_bytes (base58 decode) then replace_for_bytes -> Value::Bytes
995        assert_eq!(value, Value::Bytes([4u8; 32].to_vec()));
996    }
997
998    // ===============================================================
999    // replace_to_binary_types_of_root_value_when_setting_at_path
1000    // — prefix-based partial replacement (path starts_with)
1001    // ===============================================================
1002
1003    #[test]
1004    fn replace_root_binary_types_prefix_based() {
1005        let b58 = base58_of_32_bytes(6);
1006        let inner = Value::Map(vec![(Value::Text("sub_id".into()), Value::Text(b58))]);
1007        let mut value = Value::Map(vec![(Value::Text("nested".into()), inner)]);
1008        let identifier_paths = HashSet::from(["root.nested.sub_id"]);
1009        value
1010            .replace_to_binary_types_of_root_value_when_setting_at_path(
1011                "root",
1012                identifier_paths,
1013                HashSet::new(),
1014            )
1015            .unwrap();
1016        // The identifier_path "root.nested.sub_id" starts_with "root", so
1017        // replace_at_path("root.nested.sub_id", Identifier) is called on self.
1018        // But self is the map starting at "nested", so the full path from self's
1019        // perspective is "root.nested.sub_id" which includes the "root" prefix --
1020        // this means it tries to find "root" key in self. Since our value doesn't
1021        // have a "root" key, the replacement is silently skipped.
1022        // This is the actual behavior of the method.
1023    }
1024
1025    #[test]
1026    fn replace_root_binary_types_prefix_replaces_sub_path() {
1027        let b58 = base58_of_32_bytes(6);
1028        let inner = Value::Map(vec![(Value::Text("sub_id".into()), Value::Text(b58))]);
1029        let mut value = Value::Map(vec![(Value::Text("nested".into()), inner)]);
1030        // The identifier path starts with the path prefix
1031        let identifier_paths = HashSet::from(["doc.nested.sub_id"]);
1032        value
1033            .replace_to_binary_types_of_root_value_when_setting_at_path(
1034                "doc",
1035                identifier_paths,
1036                HashSet::new(),
1037            )
1038            .unwrap();
1039        // "doc.nested.sub_id".starts_with("doc") is true, so
1040        // self.replace_at_path("doc.nested.sub_id", Identifier) is called.
1041        // self doesn't have key "doc", so the filter_map returns empty vec.
1042        // This is the expected silent skip behavior.
1043    }
1044
1045    // ===============================================================
1046    // replace_to_binary_types_of_root_value_when_setting_at_path
1047    // — no match at all, returns Ok
1048    // ===============================================================
1049
1050    #[test]
1051    fn replace_root_binary_types_no_match_returns_ok() {
1052        let mut value = Value::Map(vec![(Value::Text("a".into()), Value::U32(1))]);
1053        let result = value.replace_to_binary_types_of_root_value_when_setting_at_path(
1054            "unrelated",
1055            HashSet::from(["other.path"]),
1056            HashSet::new(),
1057        );
1058        assert!(result.is_ok());
1059    }
1060
1061    // ===============================================================
1062    // replace_to_binary_types_when_setting_with_path — identifier
1063    // ===============================================================
1064
1065    #[test]
1066    fn replace_when_setting_with_path_identifier_exact() {
1067        let b58 = base58_of_32_bytes(11);
1068        let mut value = Value::Text(b58);
1069        let identifier_paths = HashSet::from(["doc.owner"]);
1070        value
1071            .replace_to_binary_types_when_setting_with_path(
1072                "doc.owner",
1073                identifier_paths,
1074                HashSet::new(),
1075            )
1076            .unwrap();
1077        assert_eq!(value, Value::Identifier([11u8; 32]));
1078    }
1079
1080    // ===============================================================
1081    // replace_to_binary_types_when_setting_with_path — strip prefix
1082    // ===============================================================
1083
1084    #[test]
1085    fn replace_when_setting_with_path_strip_prefix() {
1086        let b58 = base58_of_32_bytes(15);
1087        let mut value = Value::Map(vec![(Value::Text("sub_id".into()), Value::Text(b58))]);
1088        let identifier_paths = HashSet::from(["container.sub_id"]);
1089        value
1090            .replace_to_binary_types_when_setting_with_path(
1091                "container",
1092                identifier_paths,
1093                HashSet::new(),
1094            )
1095            .unwrap();
1096        assert_eq!(
1097            value.get_value_at_path("sub_id").unwrap(),
1098            &Value::Identifier([15u8; 32])
1099        );
1100    }
1101
1102    // ===============================================================
1103    // replace_to_binary_types_when_setting_with_path — binary strip prefix
1104    // ===============================================================
1105
1106    #[test]
1107    fn replace_when_setting_with_path_binary_strip_prefix() {
1108        use base64::prelude::*;
1109        let raw = vec![1u8, 2, 3, 4, 5];
1110        let b64 = BASE64_STANDARD.encode(&raw);
1111        let mut value = Value::Map(vec![(Value::Text("blob".into()), Value::Text(b64))]);
1112        let binary_paths = HashSet::from(["container.blob"]);
1113        value
1114            .replace_to_binary_types_when_setting_with_path(
1115                "container",
1116                HashSet::new(),
1117                binary_paths,
1118            )
1119            .unwrap();
1120        assert_eq!(value.get_value_at_path("blob").unwrap(), &Value::Bytes(raw));
1121    }
1122
1123    // ===============================================================
1124    // replace_to_binary_types_when_setting_with_path — no match
1125    // ===============================================================
1126
1127    #[test]
1128    fn replace_when_setting_with_path_no_match_ok() {
1129        let mut value = Value::Map(vec![(Value::Text("a".into()), Value::U32(1))]);
1130        let result = value.replace_to_binary_types_when_setting_with_path(
1131            "container",
1132            HashSet::from(["other.path"]),
1133            HashSet::new(),
1134        );
1135        assert!(result.is_ok());
1136    }
1137
1138    // ===============================================================
1139    // clean_recursive — removes nulls from nested maps
1140    // ===============================================================
1141
1142    #[test]
1143    fn clean_recursive_removes_nulls() {
1144        let inner = Value::Map(vec![
1145            (Value::Text("keep".into()), Value::U32(1)),
1146            (Value::Text("drop".into()), Value::Null),
1147        ]);
1148        let value = Value::Map(vec![
1149            (Value::Text("inner".into()), inner),
1150            (Value::Text("also_drop".into()), Value::Null),
1151        ]);
1152        let cleaned = value.clean_recursive().unwrap();
1153        let map = cleaned.to_map().unwrap();
1154        assert_eq!(map.len(), 1);
1155        let inner_map = map.get_key("inner").unwrap().to_map().unwrap();
1156        assert_eq!(inner_map.len(), 1);
1157        assert!(inner_map.get_optional_key("keep").is_some());
1158        assert!(inner_map.get_optional_key("drop").is_none());
1159    }
1160
1161    #[test]
1162    fn clean_recursive_deeply_nested() {
1163        let deep = Value::Map(vec![
1164            (Value::Text("a".into()), Value::Null),
1165            (Value::Text("b".into()), Value::U8(1)),
1166        ]);
1167        let mid = Value::Map(vec![
1168            (Value::Text("deep".into()), deep),
1169            (Value::Text("c".into()), Value::Null),
1170        ]);
1171        let value = Value::Map(vec![(Value::Text("mid".into()), mid)]);
1172        let cleaned = value.clean_recursive().unwrap();
1173        let mid_map = cleaned.get_value_at_path("mid").unwrap().to_map().unwrap();
1174        assert_eq!(mid_map.len(), 1); // only "deep" remains
1175        let deep_map = cleaned
1176            .get_value_at_path("mid.deep")
1177            .unwrap()
1178            .to_map()
1179            .unwrap();
1180        assert_eq!(deep_map.len(), 1); // only "b" remains
1181    }
1182
1183    #[test]
1184    fn clean_recursive_preserves_non_null_non_map_values() {
1185        let value = Value::Map(vec![
1186            (Value::Text("num".into()), Value::U64(42)),
1187            (Value::Text("text".into()), Value::Text("hello".into())),
1188            (
1189                Value::Text("arr".into()),
1190                Value::Array(vec![Value::Null, Value::U8(1)]),
1191            ),
1192        ]);
1193        let cleaned = value.clean_recursive().unwrap();
1194        let map = cleaned.to_map().unwrap();
1195        assert_eq!(map.len(), 3);
1196        // Arrays with Null inside are NOT cleaned (clean_recursive only filters map entries)
1197        let arr = map.get_key("arr").unwrap().to_array_ref().unwrap();
1198        assert_eq!(arr.len(), 2);
1199    }
1200
1201    #[test]
1202    fn clean_recursive_all_null() {
1203        let value = Value::Map(vec![
1204            (Value::Text("a".into()), Value::Null),
1205            (Value::Text("b".into()), Value::Null),
1206        ]);
1207        let cleaned = value.clean_recursive().unwrap();
1208        let map = cleaned.to_map().unwrap();
1209        assert_eq!(map.len(), 0);
1210    }
1211
1212    #[test]
1213    fn clean_recursive_on_non_map_errors() {
1214        let value = Value::U32(42);
1215        let result = value.clean_recursive();
1216        assert!(result.is_err());
1217    }
1218
1219    // ===============================================================
1220    // replace_at_path — intermediate non-map value errors
1221    // ===============================================================
1222
1223    #[test]
1224    fn replace_at_path_intermediate_non_map_errors() {
1225        let mut value = Value::Map(vec![(Value::Text("a".into()), Value::U32(42))]);
1226        // "a" is U32, not a map, so traversing "a.b" should error
1227        let result = value.replace_at_path("a.b", ReplacementType::Identifier);
1228        assert!(result.is_err());
1229    }
1230}