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}