platform_value/btreemap_extensions/
btreemap_field_replacement.rs

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