Skip to main content

platform_value/btreemap_extensions/
btreemap_field_replacement.rs

1use crate::inner_value_at_path::is_array_path;
2use crate::value_map::ValueMapHelper;
3use crate::{Error, Value};
4use std::collections::BTreeMap;
5
6use base64::prelude::BASE64_STANDARD;
7use base64::Engine;
8use std::iter::Peekable;
9use std::vec::IntoIter;
10
11#[derive(Debug, Clone, Copy)]
12pub enum IntegerReplacementType {
13    U128,
14    I128,
15    U64,
16    I64,
17    U32,
18    I32,
19    U16,
20    I16,
21    U8,
22    I8,
23}
24
25impl IntegerReplacementType {
26    pub fn replace_for_value(&self, value: Value) -> Result<Value, Error> {
27        Ok(match self {
28            IntegerReplacementType::U128 => Value::U128(value.try_into()?),
29            IntegerReplacementType::I128 => Value::I128(value.try_into()?),
30            IntegerReplacementType::U64 => Value::U64(value.try_into()?),
31            IntegerReplacementType::I64 => Value::I64(value.try_into()?),
32            IntegerReplacementType::U32 => Value::U32(value.try_into()?),
33            IntegerReplacementType::I32 => Value::I32(value.try_into()?),
34            IntegerReplacementType::U16 => Value::U16(value.try_into()?),
35            IntegerReplacementType::I16 => Value::I16(value.try_into()?),
36            IntegerReplacementType::U8 => Value::U8(value.try_into()?),
37            IntegerReplacementType::I8 => Value::I8(value.try_into()?),
38        })
39    }
40}
41
42#[derive(Debug, Clone, Copy)]
43pub enum ReplacementType {
44    Identifier,
45    BinaryBytes,
46    TextBase58,
47    TextBase64,
48}
49
50impl ReplacementType {
51    pub fn replace_for_bytes(&self, bytes: Vec<u8>) -> Result<Value, Error> {
52        match self {
53            ReplacementType::Identifier => {
54                Ok(Value::Identifier(bytes.try_into().map_err(|_| {
55                    Error::ByteLengthNot32BytesError(String::from(
56                        "Trying to replace into an identifier, but not 32 bytes long",
57                    ))
58                })?))
59            }
60            ReplacementType::BinaryBytes => Ok(Value::Bytes(bytes)),
61            ReplacementType::TextBase58 => Ok(Value::Text(bs58::encode(bytes).into_string())),
62            ReplacementType::TextBase64 => Ok(Value::Text(BASE64_STANDARD.encode(bytes))),
63        }
64    }
65
66    pub fn replace_for_bytes_20(&self, bytes: [u8; 20]) -> Result<Value, Error> {
67        match self {
68            ReplacementType::BinaryBytes => Ok(Value::Bytes20(bytes)),
69            ReplacementType::TextBase58 => Ok(Value::Text(bs58::encode(bytes).into_string())),
70            ReplacementType::TextBase64 => Ok(Value::Text(BASE64_STANDARD.encode(bytes))),
71            _ => Err(Error::ByteLengthNot36BytesError(
72                "trying to replace 36 bytes into an identifier".to_string(),
73            )),
74        }
75    }
76
77    pub fn replace_for_bytes_32(&self, bytes: [u8; 32]) -> Result<Value, Error> {
78        match self {
79            ReplacementType::Identifier => Ok(Value::Identifier(bytes)),
80            ReplacementType::BinaryBytes => Ok(Value::Bytes32(bytes)),
81            ReplacementType::TextBase58 => Ok(Value::Text(bs58::encode(bytes).into_string())),
82            ReplacementType::TextBase64 => Ok(Value::Text(BASE64_STANDARD.encode(bytes))),
83        }
84    }
85
86    pub fn replace_for_bytes_36(&self, bytes: [u8; 36]) -> Result<Value, Error> {
87        match self {
88            ReplacementType::BinaryBytes => Ok(Value::Bytes36(bytes)),
89            ReplacementType::TextBase58 => Ok(Value::Text(bs58::encode(bytes).into_string())),
90            ReplacementType::TextBase64 => Ok(Value::Text(BASE64_STANDARD.encode(bytes))),
91            _ => Err(Error::ByteLengthNot36BytesError(
92                "trying to replace 36 bytes into an identifier".to_string(),
93            )),
94        }
95    }
96
97    pub fn replace_consume_value(&self, value: Value) -> Result<Value, Error> {
98        let bytes = value.into_identifier_bytes()?;
99        self.replace_for_bytes(bytes)
100    }
101
102    pub fn replace_value_in_place(&self, value: &mut Value) -> Result<(), Error> {
103        let bytes = value.take().into_identifier_bytes()?;
104        *value = self.replace_for_bytes(bytes)?;
105        Ok(())
106    }
107}
108
109pub trait BTreeValueMapReplacementPathHelper {
110    fn replace_at_path(
111        &mut self,
112        path: &str,
113        replacement_type: ReplacementType,
114    ) -> Result<(), Error>;
115    fn replace_at_paths<'a, I: IntoIterator<Item = &'a String>>(
116        &mut self,
117        paths: I,
118        replacement_type: ReplacementType,
119    ) -> Result<(), Error>;
120}
121
122/// Replaces one value in place with its `replacement_type` form. The
123/// fixed-size byte values keep their width; anything else is read as the
124/// bytes it holds in whatever encoding it currently has. Shared by the
125/// `Value` and the `BTreeMap<String, Value>` path replacers, so both give
126/// the same value kind for the same input.
127pub(crate) fn replace_leaf(
128    value: &mut Value,
129    replacement_type: ReplacementType,
130) -> Result<(), Error> {
131    match value {
132        Value::Bytes20(bytes) => {
133            *value = replacement_type.replace_for_bytes_20(*bytes)?;
134        }
135        Value::Bytes32(bytes) => {
136            *value = replacement_type.replace_for_bytes_32(*bytes)?;
137        }
138        Value::Bytes36(bytes) => {
139            *value = replacement_type.replace_for_bytes_36(*bytes)?;
140        }
141        _ => {
142            let bytes = match replacement_type {
143                ReplacementType::Identifier | ReplacementType::TextBase58 => {
144                    value.to_identifier_bytes()
145                }
146                ReplacementType::BinaryBytes | ReplacementType::TextBase64 => {
147                    value.to_binary_bytes()
148                }
149            }?;
150            *value = replacement_type.replace_for_bytes(bytes)?;
151        }
152    }
153    Ok(())
154}
155
156/// The members a `list[]` or `list[3]` component addresses in a list value:
157/// every element, or the one at the index.
158fn list_members(list: &mut Value, index: Option<usize>) -> Result<Vec<&mut Value>, Error> {
159    let list = list.to_array_mut()?;
160    match index {
161        Some(index) => match list.get_mut(index) {
162            Some(member) => Ok(vec![member]),
163            None => Err(Error::StructureError(format!(
164                "element at position {index} in array does not exist"
165            ))),
166        },
167        None => Ok(list.iter_mut().collect()),
168    }
169}
170
171fn replace_down(
172    mut current_values: Vec<&mut Value>,
173    mut split: Peekable<IntoIter<&str>>,
174    replacement_type: ReplacementType,
175) -> Result<(), Error> {
176    if let Some(path_component) = split.next() {
177        let is_last_component = split.peek().is_none();
178        let next_values = current_values
179            .iter_mut()
180            .map(|current_value| {
181                if current_value.is_map() {
182                    let map = current_value.as_map_mut_ref()?;
183                    // `list[]` names the members of a list: the values to
184                    // replace when it ends the path, the values to descend
185                    // into otherwise. An absent list is nothing to replace.
186                    if let Some((list_name, index)) = is_array_path(path_component)? {
187                        let Some(list) = map.get_optional_key_mut(list_name) else {
188                            return Ok(None);
189                        };
190                        let members = list_members(list, index)?;
191                        if is_last_component {
192                            for member in members {
193                                replace_leaf(member, replacement_type)?;
194                            }
195                            return Ok(None);
196                        }
197                        return Ok(Some(members));
198                    }
199                    let Some(new_value) = map.get_optional_key_mut(path_component) else {
200                        return Ok(None);
201                    };
202                    if is_last_component {
203                        replace_leaf(new_value, replacement_type)?;
204                        Ok(None)
205                    } else {
206                        Ok(Some(vec![new_value]))
207                    }
208                } else if current_value.is_array() {
209                    // if it's an array we apply to all members
210                    let array = current_value.to_array_mut()?.iter_mut().collect();
211                    Ok(Some(array))
212                } else {
213                    Err(Error::PathError("path was not an array or map".to_string()))
214                }
215            })
216            .collect::<Result<Vec<_>, Error>>()?
217            .into_iter()
218            .flatten()
219            .flatten()
220            .collect();
221        replace_down(next_values, split, replacement_type)
222    } else {
223        Ok(())
224    }
225}
226
227impl BTreeValueMapReplacementPathHelper for BTreeMap<String, Value> {
228    fn replace_at_path(
229        &mut self,
230        path: &str,
231        replacement_type: ReplacementType,
232    ) -> Result<(), Error> {
233        let mut split: Vec<_> = path.split('.').collect();
234        let first = split.first();
235        let Some(first_path_component) = first else {
236            return Err(Error::PathError("path was empty".to_string()));
237        };
238        // `list[]` first: the members of a top-level list
239        if let Some((list_name, index)) = is_array_path(first_path_component)? {
240            let Some(list) = self.get_mut(list_name) else {
241                return Ok(());
242            };
243            let members = list_members(list, index)?;
244            if split.len() == 1 {
245                for member in members {
246                    replace_leaf(member, replacement_type)?;
247                }
248                return Ok(());
249            }
250            split.remove(0);
251            return replace_down(members, split.into_iter().peekable(), replacement_type);
252        }
253        let Some(current_value) = self.get_mut(first_path_component.to_owned()) else {
254            return Ok(());
255        };
256        if split.len() == 1 {
257            replace_leaf(current_value, replacement_type)
258        } else {
259            split.remove(0);
260            let current_values = vec![current_value];
261            //todo: make this non recursive
262            replace_down(
263                current_values,
264                split.into_iter().peekable(),
265                replacement_type,
266            )
267        }
268    }
269
270    fn replace_at_paths<'a, I: IntoIterator<Item = &'a String>>(
271        &mut self,
272        paths: I,
273        replacement_type: ReplacementType,
274    ) -> Result<(), Error> {
275        paths
276            .into_iter()
277            .try_for_each(|path| self.replace_at_path(path.as_str(), replacement_type))
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::string_encoding::Encoding;
285    use crate::value_map::ValueMapHelper;
286    use crate::{Error, Identifier, Value};
287    use base64::prelude::BASE64_STANDARD;
288    use base64::Engine;
289    use std::collections::BTreeMap;
290
291    // -----------------------------------------------------------------------
292    // IntegerReplacementType::replace_for_value — each variant
293    // -----------------------------------------------------------------------
294
295    #[test]
296    fn integer_replacement_u8() {
297        let result = IntegerReplacementType::U8
298            .replace_for_value(Value::U64(200))
299            .unwrap();
300        assert_eq!(result, Value::U8(200));
301    }
302
303    #[test]
304    fn integer_replacement_i8() {
305        let result = IntegerReplacementType::I8
306            .replace_for_value(Value::I64(-100))
307            .unwrap();
308        assert_eq!(result, Value::I8(-100));
309    }
310
311    #[test]
312    fn integer_replacement_u16() {
313        let result = IntegerReplacementType::U16
314            .replace_for_value(Value::U64(60000))
315            .unwrap();
316        assert_eq!(result, Value::U16(60000));
317    }
318
319    #[test]
320    fn integer_replacement_i16() {
321        let result = IntegerReplacementType::I16
322            .replace_for_value(Value::I64(-30000))
323            .unwrap();
324        assert_eq!(result, Value::I16(-30000));
325    }
326
327    #[test]
328    fn integer_replacement_u32() {
329        let result = IntegerReplacementType::U32
330            .replace_for_value(Value::U64(3_000_000))
331            .unwrap();
332        assert_eq!(result, Value::U32(3_000_000));
333    }
334
335    #[test]
336    fn integer_replacement_i32() {
337        let result = IntegerReplacementType::I32
338            .replace_for_value(Value::I64(-3_000_000))
339            .unwrap();
340        assert_eq!(result, Value::I32(-3_000_000));
341    }
342
343    #[test]
344    fn integer_replacement_u64() {
345        let result = IntegerReplacementType::U64
346            .replace_for_value(Value::U64(u64::MAX))
347            .unwrap();
348        assert_eq!(result, Value::U64(u64::MAX));
349    }
350
351    #[test]
352    fn integer_replacement_i64() {
353        let result = IntegerReplacementType::I64
354            .replace_for_value(Value::I64(i64::MIN))
355            .unwrap();
356        assert_eq!(result, Value::I64(i64::MIN));
357    }
358
359    #[test]
360    fn integer_replacement_u128() {
361        let result = IntegerReplacementType::U128
362            .replace_for_value(Value::U64(42))
363            .unwrap();
364        assert_eq!(result, Value::U128(42));
365    }
366
367    #[test]
368    fn integer_replacement_i128() {
369        let result = IntegerReplacementType::I128
370            .replace_for_value(Value::I64(-42))
371            .unwrap();
372        assert_eq!(result, Value::I128(-42));
373    }
374
375    #[test]
376    fn integer_replacement_overflow_error() {
377        // Trying to fit a large u64 into u8 should error
378        let result = IntegerReplacementType::U8.replace_for_value(Value::U64(300));
379        assert!(result.is_err());
380    }
381
382    #[test]
383    fn integer_replacement_non_integer_error() {
384        // Non-integer value should fail
385        let result =
386            IntegerReplacementType::U64.replace_for_value(Value::Text("not a number".into()));
387        assert!(result.is_err());
388    }
389
390    // -----------------------------------------------------------------------
391    // ReplacementType::replace_for_bytes
392    // -----------------------------------------------------------------------
393
394    #[test]
395    fn replace_for_bytes_identifier_32_bytes_ok() {
396        let bytes = vec![0xABu8; 32];
397        let result = ReplacementType::Identifier
398            .replace_for_bytes(bytes.clone())
399            .unwrap();
400        let expected: [u8; 32] = bytes.try_into().unwrap();
401        assert_eq!(result, Value::Identifier(expected));
402    }
403
404    #[test]
405    fn replace_for_bytes_identifier_wrong_size() {
406        let bytes = vec![0xABu8; 31]; // not 32 bytes
407        let result = ReplacementType::Identifier.replace_for_bytes(bytes);
408        assert!(matches!(result, Err(Error::ByteLengthNot32BytesError(_))));
409    }
410
411    #[test]
412    fn replace_for_bytes_identifier_too_long() {
413        let bytes = vec![0xABu8; 33];
414        let result = ReplacementType::Identifier.replace_for_bytes(bytes);
415        assert!(matches!(result, Err(Error::ByteLengthNot32BytesError(_))));
416    }
417
418    #[test]
419    fn replace_for_bytes_binary_bytes() {
420        let bytes = vec![1, 2, 3, 4, 5];
421        let result = ReplacementType::BinaryBytes
422            .replace_for_bytes(bytes.clone())
423            .unwrap();
424        assert_eq!(result, Value::Bytes(bytes));
425    }
426
427    #[test]
428    fn replace_for_bytes_text_base58() {
429        let bytes = vec![0x01, 0x02, 0x03];
430        let expected = bs58::encode(&bytes).into_string();
431        let result = ReplacementType::TextBase58
432            .replace_for_bytes(bytes)
433            .unwrap();
434        assert_eq!(result, Value::Text(expected));
435    }
436
437    #[test]
438    fn replace_for_bytes_text_base64() {
439        let bytes = vec![0xDE, 0xAD, 0xBE, 0xEF];
440        let expected = BASE64_STANDARD.encode(&bytes);
441        let result = ReplacementType::TextBase64
442            .replace_for_bytes(bytes)
443            .unwrap();
444        assert_eq!(result, Value::Text(expected));
445    }
446
447    // -----------------------------------------------------------------------
448    // replace_for_bytes_20: correct size and wrong replacement type
449    // -----------------------------------------------------------------------
450
451    #[test]
452    fn replace_for_bytes_20_binary() {
453        let bytes = [0xFFu8; 20];
454        let result = ReplacementType::BinaryBytes
455            .replace_for_bytes_20(bytes)
456            .unwrap();
457        assert_eq!(result, Value::Bytes20(bytes));
458    }
459
460    #[test]
461    fn replace_for_bytes_20_text_base58() {
462        let bytes = [0x01u8; 20];
463        let expected = bs58::encode(bytes).into_string();
464        let result = ReplacementType::TextBase58
465            .replace_for_bytes_20(bytes)
466            .unwrap();
467        assert_eq!(result, Value::Text(expected));
468    }
469
470    #[test]
471    fn replace_for_bytes_20_text_base64() {
472        let bytes = [0x02u8; 20];
473        let expected = BASE64_STANDARD.encode(bytes);
474        let result = ReplacementType::TextBase64
475            .replace_for_bytes_20(bytes)
476            .unwrap();
477        assert_eq!(result, Value::Text(expected));
478    }
479
480    #[test]
481    fn replace_for_bytes_20_identifier_error() {
482        let bytes = [0xAAu8; 20];
483        let result = ReplacementType::Identifier.replace_for_bytes_20(bytes);
484        assert!(matches!(result, Err(Error::ByteLengthNot36BytesError(_))));
485    }
486
487    // -----------------------------------------------------------------------
488    // replace_for_bytes_32: correct size and all replacement types
489    // -----------------------------------------------------------------------
490
491    #[test]
492    fn replace_for_bytes_32_identifier() {
493        let bytes = [0xBBu8; 32];
494        let result = ReplacementType::Identifier
495            .replace_for_bytes_32(bytes)
496            .unwrap();
497        assert_eq!(result, Value::Identifier(bytes));
498    }
499
500    #[test]
501    fn replace_for_bytes_32_binary() {
502        let bytes = [0xCCu8; 32];
503        let result = ReplacementType::BinaryBytes
504            .replace_for_bytes_32(bytes)
505            .unwrap();
506        assert_eq!(result, Value::Bytes32(bytes));
507    }
508
509    #[test]
510    fn replace_for_bytes_32_text_base58() {
511        let bytes = [0x01u8; 32];
512        let expected = bs58::encode(bytes).into_string();
513        let result = ReplacementType::TextBase58
514            .replace_for_bytes_32(bytes)
515            .unwrap();
516        assert_eq!(result, Value::Text(expected));
517    }
518
519    #[test]
520    fn replace_for_bytes_32_text_base64() {
521        let bytes = [0x02u8; 32];
522        let expected = BASE64_STANDARD.encode(bytes);
523        let result = ReplacementType::TextBase64
524            .replace_for_bytes_32(bytes)
525            .unwrap();
526        assert_eq!(result, Value::Text(expected));
527    }
528
529    // -----------------------------------------------------------------------
530    // replace_for_bytes_36: correct size and wrong replacement type
531    // -----------------------------------------------------------------------
532
533    #[test]
534    fn replace_for_bytes_36_binary() {
535        let bytes = [0xDDu8; 36];
536        let result = ReplacementType::BinaryBytes
537            .replace_for_bytes_36(bytes)
538            .unwrap();
539        assert_eq!(result, Value::Bytes36(bytes));
540    }
541
542    #[test]
543    fn replace_for_bytes_36_text_base58() {
544        let bytes = [0x03u8; 36];
545        let expected = bs58::encode(bytes).into_string();
546        let result = ReplacementType::TextBase58
547            .replace_for_bytes_36(bytes)
548            .unwrap();
549        assert_eq!(result, Value::Text(expected));
550    }
551
552    #[test]
553    fn replace_for_bytes_36_text_base64() {
554        let bytes = [0x04u8; 36];
555        let expected = BASE64_STANDARD.encode(bytes);
556        let result = ReplacementType::TextBase64
557            .replace_for_bytes_36(bytes)
558            .unwrap();
559        assert_eq!(result, Value::Text(expected));
560    }
561
562    #[test]
563    fn replace_for_bytes_36_identifier_error() {
564        let bytes = [0xEEu8; 36];
565        let result = ReplacementType::Identifier.replace_for_bytes_36(bytes);
566        assert!(matches!(result, Err(Error::ByteLengthNot36BytesError(_))));
567    }
568
569    // -----------------------------------------------------------------------
570    // replace_at_path — single segment
571    // -----------------------------------------------------------------------
572
573    #[test]
574    fn replace_at_path_single_segment_bytes32() {
575        let bytes = [0xABu8; 32];
576        let mut map = BTreeMap::new();
577        map.insert("id".to_string(), Value::Bytes32(bytes));
578
579        map.replace_at_path("id", ReplacementType::Identifier)
580            .unwrap();
581        assert_eq!(map.get("id"), Some(&Value::Identifier(bytes)));
582    }
583
584    #[test]
585    fn replace_at_path_single_segment_bytes20() {
586        let bytes = [0x11u8; 20];
587        let mut map = BTreeMap::new();
588        map.insert("addr".to_string(), Value::Bytes20(bytes));
589
590        map.replace_at_path("addr", ReplacementType::BinaryBytes)
591            .unwrap();
592        assert_eq!(map.get("addr"), Some(&Value::Bytes20(bytes)));
593    }
594
595    #[test]
596    fn replace_at_path_single_segment_bytes36() {
597        let bytes = [0x22u8; 36];
598        let mut map = BTreeMap::new();
599        map.insert("outpoint".to_string(), Value::Bytes36(bytes));
600
601        map.replace_at_path("outpoint", ReplacementType::BinaryBytes)
602            .unwrap();
603        assert_eq!(map.get("outpoint"), Some(&Value::Bytes36(bytes)));
604    }
605
606    #[test]
607    fn replace_at_path_single_segment_identifier_to_base58() {
608        let bytes = [0xCCu8; 32];
609        let mut map = BTreeMap::new();
610        map.insert("id".to_string(), Value::Identifier(bytes));
611
612        map.replace_at_path("id", ReplacementType::TextBase58)
613            .unwrap();
614        let expected = bs58::encode(bytes).into_string();
615        assert_eq!(map.get("id"), Some(&Value::Text(expected)));
616    }
617
618    // -----------------------------------------------------------------------
619    // replace_at_path — multi-segment nested path
620    // -----------------------------------------------------------------------
621
622    #[test]
623    fn replace_at_path_nested() {
624        let bytes = [0xFFu8; 32];
625        let inner_map = vec![(Value::Text("nested_id".into()), Value::Bytes32(bytes))];
626        let mut map = BTreeMap::new();
627        map.insert("parent".to_string(), Value::Map(inner_map));
628
629        map.replace_at_path("parent.nested_id", ReplacementType::Identifier)
630            .unwrap();
631
632        let parent = map.get("parent").unwrap();
633        if let Value::Map(inner) = parent {
634            let val = inner.get_optional_key("nested_id").unwrap();
635            assert_eq!(*val, Value::Identifier(bytes));
636        } else {
637            panic!("expected Map");
638        }
639    }
640
641    #[test]
642    fn replace_at_path_deep_nested() {
643        let bytes = [0xAAu8; 32];
644        let level2 = vec![(Value::Text("deep_id".into()), Value::Bytes32(bytes))];
645        let level1 = vec![(Value::Text("level2".into()), Value::Map(level2))];
646        let mut map = BTreeMap::new();
647        map.insert("level1".to_string(), Value::Map(level1));
648
649        map.replace_at_path("level1.level2.deep_id", ReplacementType::Identifier)
650            .unwrap();
651
652        let l1 = map.get("level1").unwrap();
653        if let Value::Map(l1_map) = l1 {
654            let l2 = l1_map.get_optional_key("level2").unwrap();
655            if let Value::Map(l2_map) = l2 {
656                let val = l2_map.get_optional_key("deep_id").unwrap();
657                assert_eq!(*val, Value::Identifier(bytes));
658            } else {
659                panic!("expected Map at level2");
660            }
661        } else {
662            panic!("expected Map at level1");
663        }
664    }
665
666    // -----------------------------------------------------------------------
667    // replace_at_path — array traversal
668    // -----------------------------------------------------------------------
669
670    #[test]
671    fn replace_at_path_through_array_applies_to_elements() {
672        // When replace_down encounters an array at a non-terminal path component,
673        // it expands the array elements into the next recursion level. The path
674        // component consumed at the array level is effectively discarded (since
675        // arrays don't have named keys). The NEXT component is then applied to
676        // each array element.
677        //
678        // Structure:
679        //   top-level BTreeMap: "wrapper" -> Map { "arr" -> Array [ Map{"id": Bytes32}, ... ] }
680        // Path: "wrapper.arr.placeholder.id"
681        //   - "wrapper" handled by replace_at_path (first component)
682        //   - replace_down gets ["arr", "placeholder", "id"]
683        //   - "arr" consumed: looks up in wrapper map, finds Array, returns it
684        //   - "placeholder" consumed: current is Array, expands to array items (Maps)
685        //   - "id" consumed: terminal component, looks up in each item Map, performs replacement
686        let bytes1 = [0x11u8; 32];
687        let bytes2 = [0x22u8; 32];
688        let item1 = Value::Map(vec![(Value::Text("id".into()), Value::Bytes32(bytes1))]);
689        let item2 = Value::Map(vec![(Value::Text("id".into()), Value::Bytes32(bytes2))]);
690        let wrapper_map = vec![(Value::Text("arr".into()), Value::Array(vec![item1, item2]))];
691        let mut map = BTreeMap::new();
692        map.insert("wrapper".to_string(), Value::Map(wrapper_map));
693
694        // "placeholder" is consumed by the array level and discarded
695        map.replace_at_path("wrapper.arr.placeholder.id", ReplacementType::Identifier)
696            .unwrap();
697
698        if let Value::Map(wrapper) = map.get("wrapper").unwrap() {
699            let arr_val = wrapper.get_optional_key("arr").unwrap();
700            if let Value::Array(arr) = arr_val {
701                assert_eq!(arr.len(), 2);
702                for (i, item) in arr.iter().enumerate() {
703                    if let Value::Map(m) = item {
704                        let val = m.get_optional_key("id").unwrap();
705                        let expected_bytes = if i == 0 { bytes1 } else { bytes2 };
706                        assert_eq!(*val, Value::Identifier(expected_bytes));
707                    } else {
708                        panic!("expected Map in array");
709                    }
710                }
711            } else {
712                panic!("expected Array");
713            }
714        } else {
715            panic!("expected Map at wrapper");
716        }
717    }
718
719    // -----------------------------------------------------------------------
720    // replace_at_path: `list[]` names the members of a list
721    // -----------------------------------------------------------------------
722
723    fn base58_id(seed: u8) -> Value {
724        Value::Text(Identifier::from([seed; 32]).to_string(Encoding::Base58))
725    }
726
727    #[test]
728    fn should_replace_the_members_of_a_top_level_list() {
729        let mut map = BTreeMap::new();
730        map.insert(
731            "ids".to_string(),
732            Value::Array(vec![base58_id(1), Value::Bytes32([2u8; 32])]),
733        );
734        map.replace_at_path("ids[]", ReplacementType::Identifier)
735            .unwrap();
736        assert_eq!(
737            map.get("ids").unwrap(),
738            &Value::Array(vec![
739                Value::Identifier([1u8; 32]),
740                Value::Identifier([2u8; 32])
741            ])
742        );
743
744        // One member by index; an index past the end is an error
745        map.insert(
746            "more".to_string(),
747            Value::Array(vec![base58_id(3), Value::Text("keep".into())]),
748        );
749        map.replace_at_path("more[0]", ReplacementType::Identifier)
750            .unwrap();
751        assert_eq!(
752            map.get("more").unwrap(),
753            &Value::Array(vec![
754                Value::Identifier([3u8; 32]),
755                Value::Text("keep".into())
756            ])
757        );
758        assert!(map
759            .replace_at_path("more[2]", ReplacementType::Identifier)
760            .is_err());
761    }
762
763    #[test]
764    fn should_replace_the_members_of_a_nested_list_and_descend_into_a_list_of_maps() {
765        let inner = Value::Map(vec![(
766            Value::Text("ids".into()),
767            Value::Array(vec![base58_id(4)]),
768        )]);
769        let mut map = BTreeMap::new();
770        map.insert("meta".to_string(), inner);
771        map.replace_at_path("meta.ids[]", ReplacementType::Identifier)
772            .unwrap();
773        let Some(Value::Map(meta)) = map.get("meta") else {
774            panic!("expected the map");
775        };
776        assert_eq!(
777            meta.get_optional_key("ids").unwrap(),
778            &Value::Array(vec![Value::Identifier([4u8; 32])])
779        );
780
781        let item = Value::Map(vec![(Value::Text("id".into()), base58_id(5))]);
782        map.insert("items".to_string(), Value::Array(vec![item]));
783        map.replace_at_path("items[].id", ReplacementType::Identifier)
784            .unwrap();
785        let Some(Value::Array(items)) = map.get("items") else {
786            panic!("expected the list");
787        };
788        let Value::Map(item) = &items[0] else {
789            panic!("expected a map member");
790        };
791        assert_eq!(
792            item.get_optional_key("id").unwrap(),
793            &Value::Identifier([5u8; 32])
794        );
795    }
796
797    #[test]
798    fn should_treat_an_absent_top_level_list_as_nothing_to_replace() {
799        let mut map = BTreeMap::new();
800        map.insert("a".to_string(), Value::U32(1));
801        assert!(map
802            .replace_at_path("ids[]", ReplacementType::Identifier)
803            .is_ok());
804        // Below a scalar there is no list to look in
805        assert!(map
806            .replace_at_path("a.ids[]", ReplacementType::Identifier)
807            .is_err());
808    }
809
810    // -----------------------------------------------------------------------
811    // Error paths
812    // -----------------------------------------------------------------------
813
814    #[test]
815    fn replace_at_path_empty_path_error() {
816        let mut map = BTreeMap::new();
817        map.insert("key".to_string(), Value::U64(1));
818        let result = map.replace_at_path("", ReplacementType::Identifier);
819        // Empty string splits to [""] which is a single component, not truly empty
820        // The path "" will try to look up key "" in the map, which doesn't exist
821        // So it returns Ok(()) because missing key is not an error
822        assert!(result.is_ok());
823    }
824
825    #[test]
826    fn replace_at_path_missing_key_returns_ok() {
827        let mut map = BTreeMap::new();
828        map.insert("key".to_string(), Value::U64(1));
829        // Nonexistent key -> returns Ok(())
830        let result = map.replace_at_path("nonexistent", ReplacementType::BinaryBytes);
831        assert!(result.is_ok());
832    }
833
834    #[test]
835    fn replace_at_path_non_map_value_in_nested_path_error() {
836        let mut map = BTreeMap::new();
837        map.insert("key".to_string(), Value::U64(42));
838        // Trying to traverse into a non-map/non-array value
839        let result = map.replace_at_path("key.sub", ReplacementType::BinaryBytes);
840        assert!(matches!(result, Err(Error::PathError(_))));
841    }
842
843    // -----------------------------------------------------------------------
844    // replace_at_paths — multiple paths
845    // -----------------------------------------------------------------------
846
847    #[test]
848    fn replace_at_paths_multiple() {
849        let bytes1 = [0xAAu8; 32];
850        let bytes2 = [0xBBu8; 32];
851        let mut map = BTreeMap::new();
852        map.insert("id1".to_string(), Value::Bytes32(bytes1));
853        map.insert("id2".to_string(), Value::Bytes32(bytes2));
854
855        let paths = vec!["id1".to_string(), "id2".to_string()];
856        map.replace_at_paths(&paths, ReplacementType::Identifier)
857            .unwrap();
858
859        assert_eq!(map.get("id1"), Some(&Value::Identifier(bytes1)));
860        assert_eq!(map.get("id2"), Some(&Value::Identifier(bytes2)));
861    }
862
863    // -----------------------------------------------------------------------
864    // replace_consume_value and replace_value_in_place
865    // -----------------------------------------------------------------------
866
867    #[test]
868    fn replace_consume_value_identifier_to_base58() {
869        let bytes = [0xCCu8; 32];
870        let val = Value::Identifier(bytes);
871        let result = ReplacementType::TextBase58
872            .replace_consume_value(val)
873            .unwrap();
874        let expected = bs58::encode(bytes).into_string();
875        assert_eq!(result, Value::Text(expected));
876    }
877
878    #[test]
879    fn replace_value_in_place_identifier_to_binary() {
880        let bytes = [0xDDu8; 32];
881        let mut val = Value::Identifier(bytes);
882        ReplacementType::BinaryBytes
883            .replace_value_in_place(&mut val)
884            .unwrap();
885        assert_eq!(val, Value::Bytes(bytes.to_vec()));
886    }
887}