sbor/representations/display/
nested_string.rs

1use super::*;
2use crate::representations::*;
3use crate::rust::prelude::*;
4use crate::traversal::*;
5use crate::*;
6use TypedTraversalEvent::*;
7
8// TODO - This file could do with a minor refactor to commonize logic to print fields
9
10#[derive(Debug, Clone, Copy)]
11pub struct NestedStringDisplayContext<'s, 'a, E: FormattableCustomExtension> {
12    pub schema: &'s Schema<E::CustomSchema>,
13    pub custom_context: E::CustomDisplayContext<'a>,
14    pub print_mode: PrintMode,
15}
16
17impl From<fmt::Error> for FormattingError {
18    fn from(e: fmt::Error) -> Self {
19        FormattingError::Fmt(e)
20    }
21}
22
23pub fn format_payload_as_nested_string<F: fmt::Write, E: FormattableCustomExtension>(
24    f: &mut F,
25    context: &NestedStringDisplayContext<'_, '_, E>,
26    payload: &'_ [u8],
27    type_id: LocalTypeId,
28    depth_limit: usize,
29) -> Result<(), FormattingError> {
30    let mut traverser = traverse_payload_with_types(payload, context.schema, type_id, depth_limit);
31    if let PrintMode::MultiLine {
32        first_line_indent, ..
33    } = &context.print_mode
34    {
35        write!(f, "{:first_line_indent$}", "")?;
36    }
37    format_value_tree(f, &mut traverser, context)?;
38    consume_end_event(&mut traverser)?;
39    Ok(())
40}
41
42#[allow(clippy::too_many_arguments)]
43pub(crate) fn format_partial_payload_as_nested_string<
44    F: fmt::Write,
45    E: FormattableCustomExtension,
46>(
47    f: &mut F,
48    partial_payload: &[u8],
49    expected_start: ExpectedStart<E::CustomValueKind>,
50    check_exact_end: bool,
51    current_depth: usize,
52    context: &NestedStringDisplayContext<'_, '_, E>,
53    type_id: LocalTypeId,
54    depth_limit: usize,
55) -> Result<(), FormattingError> {
56    let mut traverser = traverse_partial_payload_with_types(
57        partial_payload,
58        expected_start,
59        check_exact_end,
60        current_depth,
61        context.schema,
62        type_id,
63        depth_limit,
64    );
65    if let PrintMode::MultiLine {
66        first_line_indent, ..
67    } = &context.print_mode
68    {
69        write!(f, "{:first_line_indent$}", "")?;
70    }
71    format_value_tree(f, &mut traverser, context)?;
72    if check_exact_end {
73        consume_end_event(&mut traverser)?;
74    }
75    Ok(())
76}
77
78fn consume_end_event<E: FormattableCustomExtension>(
79    traverser: &mut TypedTraverser<E>,
80) -> Result<(), FormattingError> {
81    traverser.consume_end_event().map_err(FormattingError::Sbor)
82}
83
84fn consume_container_end<E: FormattableCustomExtension>(
85    traverser: &mut TypedTraverser<E>,
86) -> Result<(), FormattingError> {
87    traverser
88        .consume_container_end_event()
89        .map_err(FormattingError::Sbor)
90}
91
92fn format_value_tree<F: fmt::Write, E: FormattableCustomExtension>(
93    f: &mut F,
94    traverser: &mut TypedTraverser<E>,
95    context: &NestedStringDisplayContext<'_, '_, E>,
96) -> Result<(), FormattingError> {
97    let typed_event = traverser.next_event();
98    match typed_event.event {
99        ContainerStart(type_id, container_header) => {
100            let parent_depth = typed_event.location.typed_container_path.len();
101            match container_header {
102                ContainerHeader::Tuple(header) => {
103                    format_tuple(f, traverser, context, type_id, header, parent_depth)
104                }
105                ContainerHeader::EnumVariant(header) => {
106                    format_enum_variant(f, traverser, context, type_id, header, parent_depth)
107                }
108                ContainerHeader::Array(header) => {
109                    format_array(f, traverser, context, type_id, header, parent_depth)
110                }
111                ContainerHeader::Map(header) => {
112                    format_map(f, traverser, context, type_id, header, parent_depth)
113                }
114            }
115        }
116        TerminalValue(type_id, value_ref) => format_terminal_value(f, context, type_id, value_ref),
117        _ => Err(FormattingError::Sbor(
118            typed_event
119                .display_as_unexpected_event("ContainerStart | TerminalValue", context.schema),
120        )),
121    }
122}
123
124fn format_tuple<F: fmt::Write, E: FormattableCustomExtension>(
125    f: &mut F,
126    traverser: &mut TypedTraverser<E>,
127    context: &NestedStringDisplayContext<'_, '_, E>,
128    type_id: LocalTypeId,
129    tuple_header: TupleHeader,
130    parent_depth: usize,
131) -> Result<(), FormattingError> {
132    let tuple_data = context
133        .schema
134        .resolve_matching_tuple_metadata(type_id, tuple_header.length);
135
136    if let Some(type_name) = tuple_data.name {
137        write!(f, "Tuple:{}(", type_name)?;
138    } else {
139        write!(f, "Tuple(")?;
140    }
141
142    let child_count = tuple_header.length;
143
144    match (child_count, context.print_mode) {
145        (0, _) => {
146            write!(f, ")")?;
147        }
148        (_, PrintMode::SingleLine) => {
149            match tuple_data.field_names {
150                Some(field_names) => {
151                    for i in 0..tuple_header.length {
152                        if i > 0 {
153                            write!(f, ", ")?;
154                        }
155                        write!(f, "{} = ", field_names.get(i).unwrap())?;
156                        format_value_tree(f, traverser, context)?;
157                    }
158                }
159                _ => {
160                    for i in 0..tuple_header.length {
161                        if i > 0 {
162                            write!(f, ", ")?;
163                        }
164                        format_value_tree(f, traverser, context)?;
165                    }
166                }
167            }
168            write!(f, ")")?;
169        }
170        (
171            _,
172            PrintMode::MultiLine {
173                indent_size: spaces_per_indent,
174                base_indent,
175                ..
176            },
177        ) => {
178            let child_indent_size = base_indent + spaces_per_indent * parent_depth;
179            let child_indent = " ".repeat(child_indent_size);
180            let parent_indent = &child_indent[0..child_indent_size - spaces_per_indent];
181            writeln!(f)?;
182            match tuple_data.field_names {
183                Some(field_names) => {
184                    for i in 0..tuple_header.length {
185                        write!(f, "{}{} = ", child_indent, field_names.get(i).unwrap())?;
186                        format_value_tree(f, traverser, context)?;
187                        writeln!(f, ",")?;
188                    }
189                }
190                _ => {
191                    for _ in 0..tuple_header.length {
192                        write!(f, "{}", child_indent)?;
193                        format_value_tree(f, traverser, context)?;
194                        writeln!(f, ",")?;
195                    }
196                }
197            }
198
199            write!(f, "{})", parent_indent)?;
200        }
201    }
202
203    consume_container_end(traverser)?;
204    Ok(())
205}
206
207fn format_enum_variant<F: fmt::Write, E: FormattableCustomExtension>(
208    f: &mut F,
209    traverser: &mut TypedTraverser<E>,
210    context: &NestedStringDisplayContext<'_, '_, E>,
211    type_id: LocalTypeId,
212    variant_header: EnumVariantHeader,
213    parent_depth: usize,
214) -> Result<(), FormattingError> {
215    let enum_data = context.schema.resolve_matching_enum_metadata(
216        type_id,
217        variant_header.variant,
218        variant_header.length,
219    );
220
221    if let Some(type_name) = enum_data.enum_name {
222        write!(f, "Enum:{}", type_name)?;
223    } else {
224        write!(f, "Enum")?;
225    }
226
227    if let Some(variant_name) = enum_data.variant_name {
228        write!(f, "<{}u8:{}>", variant_header.variant, variant_name)?;
229    } else {
230        write!(f, "<{}u8>", variant_header.variant)?;
231    }
232
233    write!(f, "(")?;
234    let field_length = variant_header.length;
235    match (field_length, context.print_mode) {
236        (0, _) | (_, PrintMode::SingleLine) => {
237            match enum_data.field_names {
238                Some(field_names) => {
239                    for i in 0..field_length {
240                        write!(
241                            f,
242                            "{}{} = ",
243                            (if i == 0 { "" } else { ", " }),
244                            field_names.get(i).unwrap()
245                        )?;
246                        format_value_tree(f, traverser, context)?;
247                    }
248                }
249                _ => {
250                    for i in 0..field_length {
251                        write!(f, "{}", (if i == 0 { "" } else { ", " }))?;
252                        format_value_tree(f, traverser, context)?;
253                    }
254                }
255            }
256            write!(f, ")")?;
257        }
258        (
259            _,
260            PrintMode::MultiLine {
261                indent_size: spaces_per_indent,
262                base_indent,
263                ..
264            },
265        ) => {
266            let child_indent_size = base_indent + spaces_per_indent * parent_depth;
267            let child_indent = " ".repeat(child_indent_size);
268            let parent_indent = &child_indent[0..child_indent_size - spaces_per_indent];
269            writeln!(f)?;
270            match enum_data.field_names {
271                Some(field_names) => {
272                    for i in 0..field_length {
273                        write!(f, "{}{} = ", child_indent, field_names.get(i).unwrap())?;
274                        format_value_tree(f, traverser, context)?;
275                        writeln!(f, ",")?;
276                    }
277                }
278                _ => {
279                    for _ in 0..field_length {
280                        write!(f, "{}", child_indent)?;
281                        format_value_tree(f, traverser, context)?;
282                        writeln!(f, ",")?;
283                    }
284                }
285            }
286
287            write!(f, "{})", parent_indent)?;
288        }
289    }
290
291    consume_container_end(traverser)?;
292    Ok(())
293}
294
295fn format_array<F: fmt::Write, E: FormattableCustomExtension>(
296    f: &mut F,
297    traverser: &mut TypedTraverser<E>,
298    context: &NestedStringDisplayContext<'_, '_, E>,
299    type_id: LocalTypeId,
300    array_header: ArrayHeader<E::CustomValueKind>,
301    parent_depth: usize,
302) -> Result<(), FormattingError> {
303    let array_data = context.schema.resolve_matching_array_metadata(type_id);
304
305    // Note - this is (purposefully) subtly different to the implementation in the manifest:
306    // We wrap bytes as Array<U8>(Hex("deadbeef")) instead of Bytes("deadbeef") to fit
307    // better with the type annotations.
308
309    match array_data.array_name {
310        Some(array_name) => {
311            write!(f, "Array:{}<", array_name)?;
312        }
313        None => {
314            write!(f, "Array<")?;
315        }
316    }
317
318    match array_data.element_name {
319        Some(element_name) => {
320            write!(f, "{}:{}>(", array_header.element_value_kind, element_name)?;
321        }
322        None => {
323            write!(f, "{}>(", array_header.element_value_kind)?;
324        }
325    }
326
327    let child_count = array_header.length;
328
329    match (
330        child_count,
331        context.print_mode,
332        array_header.element_value_kind,
333    ) {
334        (0, _, _) => {
335            write!(f, ")")?;
336        }
337        (_, _, ValueKind::U8) => {
338            write!(f, "Hex(\"")?;
339            let typed_event = traverser.next_event();
340            match typed_event.event {
341                TerminalValueBatch(_, TerminalValueBatchRef::U8(bytes)) => {
342                    f.write_str(&hex::encode(bytes))?;
343                }
344                _ => Err(FormattingError::Sbor(
345                    typed_event.display_as_unexpected_event("TerminalValueBatch", context.schema),
346                ))?,
347            };
348            write!(f, "\"))")?;
349        }
350        (child_count, PrintMode::SingleLine, _) => {
351            for i in 0..child_count {
352                if i > 0 {
353                    write!(f, ", ")?;
354                }
355                format_value_tree(f, traverser, context)?;
356            }
357            write!(f, ")")?;
358        }
359        (
360            child_count,
361            PrintMode::MultiLine {
362                indent_size: spaces_per_indent,
363                base_indent,
364                ..
365            },
366            _,
367        ) => {
368            let child_indent_size = base_indent + spaces_per_indent * parent_depth;
369            let child_indent = " ".repeat(child_indent_size);
370            let parent_indent = &child_indent[0..child_indent_size - spaces_per_indent];
371            writeln!(f)?;
372            for _ in 0..child_count {
373                write!(f, "{}", child_indent)?;
374                format_value_tree(f, traverser, context)?;
375                writeln!(f, ",")?;
376            }
377
378            write!(f, "{})", parent_indent)?;
379        }
380    }
381
382    consume_container_end(traverser)?;
383    Ok(())
384}
385
386fn format_map<F: fmt::Write, E: FormattableCustomExtension>(
387    f: &mut F,
388    traverser: &mut TypedTraverser<E>,
389    context: &NestedStringDisplayContext<'_, '_, E>,
390    type_id: LocalTypeId,
391    map_header: MapHeader<E::CustomValueKind>,
392    parent_depth: usize,
393) -> Result<(), FormattingError> {
394    let map_data = context.schema.resolve_matching_map_metadata(type_id);
395
396    match map_data.map_name {
397        Some(array_name) => {
398            write!(f, "Map:{}<", array_name)?;
399        }
400        None => {
401            write!(f, "Map<")?;
402        }
403    }
404
405    match map_data.key_name {
406        Some(key_name) => {
407            write!(f, "{}:{}, ", map_header.key_value_kind, key_name)?;
408        }
409        None => {
410            write!(f, "{}, ", map_header.key_value_kind)?;
411        }
412    }
413
414    match map_data.value_name {
415        Some(value_name) => {
416            write!(f, "{}:{}>(", map_header.value_value_kind, value_name)?;
417        }
418        None => {
419            write!(f, "{}>(", map_header.value_value_kind)?;
420        }
421    }
422
423    let child_count = map_header.length * 2;
424
425    match (child_count, context.print_mode) {
426        (0, _) => {
427            write!(f, ")")?;
428        }
429        (child_count, PrintMode::SingleLine) => {
430            for i in 0..child_count {
431                if i > 0 {
432                    write!(f, ", ")?;
433                }
434                format_value_tree(f, traverser, context)?;
435            }
436            write!(f, ")")?;
437        }
438        (
439            child_count,
440            PrintMode::MultiLine {
441                indent_size: spaces_per_indent,
442                base_indent,
443                ..
444            },
445        ) => {
446            let child_indent_size = base_indent + spaces_per_indent * parent_depth;
447            let child_indent = " ".repeat(child_indent_size);
448            let parent_indent = &child_indent[0..child_indent_size - spaces_per_indent];
449            writeln!(f)?;
450            for _ in 0..child_count {
451                write!(f, "{}", child_indent)?;
452                format_value_tree(f, traverser, context)?;
453                writeln!(f, ",")?;
454            }
455
456            write!(f, "{})", parent_indent)?;
457        }
458    }
459
460    consume_container_end(traverser)?;
461    Ok(())
462}
463
464fn format_terminal_value<F: fmt::Write, E: FormattableCustomExtension>(
465    f: &mut F,
466    context: &NestedStringDisplayContext<'_, '_, E>,
467    type_id: LocalTypeId,
468    value_ref: TerminalValueRef<E::CustomTraversal>,
469) -> Result<(), FormattingError> {
470    let type_name = context
471        .schema
472        .resolve_type_metadata(type_id)
473        .and_then(|m| m.get_name());
474    match value_ref {
475        TerminalValueRef::Bool(value) => write!(f, "{value}")?,
476        TerminalValueRef::I8(value) => write!(f, "{value}i8")?,
477        TerminalValueRef::I16(value) => write!(f, "{value}i16")?,
478        TerminalValueRef::I32(value) => write!(f, "{value}i32")?,
479        TerminalValueRef::I64(value) => write!(f, "{value}i64")?,
480        TerminalValueRef::I128(value) => write!(f, "{value}i128")?,
481        TerminalValueRef::U8(value) => write!(f, "{value}u8")?,
482        TerminalValueRef::U16(value) => write!(f, "{value}u16")?,
483        TerminalValueRef::U32(value) => write!(f, "{value}u32")?,
484        TerminalValueRef::U64(value) => write!(f, "{value}u64")?,
485        TerminalValueRef::U128(value) => write!(f, "{value}u128")?,
486        // Debug encode strings to use default debug rust escaping, and
487        // avoid control characters affecting the string representation.
488        // This makes the encoding tied to the Rust version; but this is
489        // OK - we don't guarantee nested string encoding is deterministic.
490        TerminalValueRef::String(value) => write!(f, "{value:?}")?,
491        TerminalValueRef::Custom(ref value) => {
492            match type_name {
493                Some(type_name) => {
494                    write!(f, "{}:{}(", value_ref.value_kind(), type_name)?;
495                }
496                None => {
497                    write!(f, "{}(", value_ref.value_kind())?;
498                }
499            }
500            E::display_string_content(f, &context.custom_context, value)?;
501            write!(f, ")")?;
502            return Ok(());
503        }
504    }
505    // Handle the normal terminal values which haven't returned already
506    // If the type has a type-name, append it after the :u8, eg "132u64:Epoch"
507    // This might arise from - eg - transparent wrapped types
508    if let Some(type_name) = type_name {
509        write!(f, ":{}", type_name)?;
510    }
511    Ok(())
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use radix_rust::*;
518
519    #[derive(Sbor, Hash, Eq, PartialEq)]
520    #[allow(clippy::enum_variant_names)]
521    enum TestEnum {
522        UnitVariant,
523        SingleFieldVariant { field: u8 },
524        DoubleStructVariant { field1: u8, field2: u8 },
525    }
526
527    #[derive(Sbor)]
528    struct MyUnitStruct;
529
530    #[derive(BasicSbor)]
531    struct MyComplexTupleStruct(
532        Vec<u16>,
533        Vec<u16>,
534        Vec<u8>,
535        Vec<u8>,
536        IndexMap<TestEnum, MyFieldStruct>,
537        BTreeMap<String, MyUnitStruct>,
538        TestEnum,
539        TestEnum,
540        TestEnum,
541        MyFieldStruct,
542        Vec<MyUnitStruct>,
543        BasicValue,
544    );
545
546    #[derive(Sbor)]
547    struct MyFieldStruct {
548        field1: u64,
549        field2: Vec<String>,
550    }
551
552    #[test]
553    fn complex_value_formatting() {
554        let (type_id, schema) =
555            generate_full_schema_from_single_type::<MyComplexTupleStruct, NoCustomSchema>();
556        let value = MyComplexTupleStruct(
557            vec![1, 2, 3],
558            vec![],
559            vec![],
560            vec![1, 2, 3],
561            indexmap! {
562                TestEnum::UnitVariant => MyFieldStruct { field1: 1, field2: vec!["hello".to_string()] },
563                TestEnum::SingleFieldVariant { field: 1 } => MyFieldStruct { field1: 2, field2: vec!["world".to_string()] },
564                TestEnum::DoubleStructVariant { field1: 1, field2: 2 } => MyFieldStruct { field1: 3, field2: vec!["!".to_string()] },
565            },
566            btreemap! {
567                "hello".to_string() => MyUnitStruct,
568                "world".to_string() => MyUnitStruct,
569            },
570            TestEnum::UnitVariant,
571            TestEnum::SingleFieldVariant { field: 1 },
572            TestEnum::DoubleStructVariant {
573                field1: 3,
574                field2: 5,
575            },
576            MyFieldStruct {
577                field1: 21,
578                field2: vec!["hello".to_string(), "world!".to_string()],
579            },
580            vec![MyUnitStruct, MyUnitStruct],
581            Value::Tuple {
582                fields: vec![
583                    Value::Enum {
584                        discriminator: 32,
585                        fields: vec![],
586                    },
587                    Value::Enum {
588                        discriminator: 21,
589                        fields: vec![Value::I32 { value: -3 }],
590                    },
591                ],
592            },
593        );
594        let payload = basic_encode(&value).unwrap();
595
596        let expected_annotated_single_line = r###"Tuple:MyComplexTupleStruct(Array<U16>(1u16, 2u16, 3u16), Array<U16>(), Array<U8>(), Array<U8>(Hex("010203")), Map<Enum:TestEnum, Tuple:MyFieldStruct>(Enum:TestEnum<0u8:UnitVariant>(), Tuple:MyFieldStruct(field1 = 1u64, field2 = Array<String>("hello")), Enum:TestEnum<1u8:SingleFieldVariant>(field = 1u8), Tuple:MyFieldStruct(field1 = 2u64, field2 = Array<String>("world")), Enum:TestEnum<2u8:DoubleStructVariant>(field1 = 1u8, field2 = 2u8), Tuple:MyFieldStruct(field1 = 3u64, field2 = Array<String>("!"))), Map<String, Tuple:MyUnitStruct>("hello", Tuple:MyUnitStruct(), "world", Tuple:MyUnitStruct()), Enum:TestEnum<0u8:UnitVariant>(), Enum:TestEnum<1u8:SingleFieldVariant>(field = 1u8), Enum:TestEnum<2u8:DoubleStructVariant>(field1 = 3u8, field2 = 5u8), Tuple:MyFieldStruct(field1 = 21u64, field2 = Array<String>("hello", "world!")), Array<Tuple:MyUnitStruct>(Tuple:MyUnitStruct(), Tuple:MyUnitStruct()), Tuple(Enum<32u8>(), Enum<21u8>(-3i32)))"###;
597        let display_context = ValueDisplayParameters::Annotated {
598            display_mode: DisplayMode::NestedString,
599            print_mode: PrintMode::SingleLine,
600            schema: schema.v1(),
601            custom_context: Default::default(),
602            type_id,
603            depth_limit: 64,
604        };
605        let actual_annotated_single_line =
606            BasicRawPayload::new_from_valid_slice_with_checks(&payload)
607                .unwrap()
608                .to_string(display_context);
609        // println!("{}", actual_annotated_single_line);
610        assert_eq!(
611            &actual_annotated_single_line,
612            expected_annotated_single_line,
613        );
614
615        let expected_annotated_multi_line = r###"Tuple:MyComplexTupleStruct(
616            Array<U16>(
617                1u16,
618                2u16,
619                3u16,
620            ),
621            Array<U16>(),
622            Array<U8>(),
623            Array<U8>(Hex("010203")),
624            Map<Enum:TestEnum, Tuple:MyFieldStruct>(
625                Enum:TestEnum<0u8:UnitVariant>(),
626                Tuple:MyFieldStruct(
627                    field1 = 1u64,
628                    field2 = Array<String>(
629                        "hello",
630                    ),
631                ),
632                Enum:TestEnum<1u8:SingleFieldVariant>(
633                    field = 1u8,
634                ),
635                Tuple:MyFieldStruct(
636                    field1 = 2u64,
637                    field2 = Array<String>(
638                        "world",
639                    ),
640                ),
641                Enum:TestEnum<2u8:DoubleStructVariant>(
642                    field1 = 1u8,
643                    field2 = 2u8,
644                ),
645                Tuple:MyFieldStruct(
646                    field1 = 3u64,
647                    field2 = Array<String>(
648                        "!",
649                    ),
650                ),
651            ),
652            Map<String, Tuple:MyUnitStruct>(
653                "hello",
654                Tuple:MyUnitStruct(),
655                "world",
656                Tuple:MyUnitStruct(),
657            ),
658            Enum:TestEnum<0u8:UnitVariant>(),
659            Enum:TestEnum<1u8:SingleFieldVariant>(
660                field = 1u8,
661            ),
662            Enum:TestEnum<2u8:DoubleStructVariant>(
663                field1 = 3u8,
664                field2 = 5u8,
665            ),
666            Tuple:MyFieldStruct(
667                field1 = 21u64,
668                field2 = Array<String>(
669                    "hello",
670                    "world!",
671                ),
672            ),
673            Array<Tuple:MyUnitStruct>(
674                Tuple:MyUnitStruct(),
675                Tuple:MyUnitStruct(),
676            ),
677            Tuple(
678                Enum<32u8>(),
679                Enum<21u8>(
680                    -3i32,
681                ),
682            ),
683        )"###;
684        let display_context = ValueDisplayParameters::Annotated {
685            display_mode: DisplayMode::NestedString,
686            print_mode: PrintMode::MultiLine {
687                indent_size: 4,
688                base_indent: 8,
689                first_line_indent: 0,
690            },
691            schema: schema.v1(),
692            custom_context: Default::default(),
693            type_id,
694            depth_limit: 64,
695        };
696        let actual_annotated_multi_line =
697            BasicRawPayload::new_from_valid_slice_with_checks(&payload)
698                .unwrap()
699                .to_string(display_context);
700        // println!("{}", actual_annotated_multi_line);
701        assert_eq!(&actual_annotated_multi_line, expected_annotated_multi_line,);
702    }
703}