1use core::fmt;
2
3use crate::{ContractSpecId, ContractSpecString};
4
5impl fmt::Display for ContractSpecId {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 write_lossy(f, &self.module_path)?;
26 f.write_str("::")?;
27 write_lossy(f, &self.declared)?;
28 f.write_str(" (")?;
29 write_lossy(f, &self.crate_name)?;
30 f.write_str("@")?;
31 write_lossy(f, &self.major)?;
32 f.write_str(")")
33 }
34}
35
36fn write_lossy(
39 f: &mut fmt::Formatter<'_>,
40 s: &ContractSpecString,
41) -> fmt::Result {
42 for chunk in s.as_slice().utf8_chunks() {
43 f.write_str(chunk.valid())?;
44 if !chunk.invalid().is_empty() {
45 f.write_str("\u{FFFD}")?;
46 }
47 }
48 Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53 extern crate std;
54
55 use super::*;
56 use std::format;
57 use std::string::{String, ToString};
58
59 fn s(bytes: &[u8]) -> ContractSpecString {
60 ContractSpecString::try_from(bytes).unwrap()
61 }
62
63 fn id(declared: &[u8]) -> ContractSpecId {
64 ContractSpecId {
65 crate_name: s(b"my-token"),
66 major: s(b"0.1"),
67 module_path: s(b"my_token::types"),
68 declared: s(declared),
69 }
70 }
71
72 #[test]
73 fn renders_path_then_crate_and_major() {
74 assert_eq!(
75 id(b"Meta").to_string(),
76 "my_token::types::Meta (my-token@0.1)"
77 );
78 assert_eq!(
81 id(b"A::B").to_string(),
82 "my_token::types::A::B (my-token@0.1)"
83 );
84 assert_eq!(
85 id("Café".as_bytes()).to_string(),
86 "my_token::types::Café (my-token@0.1)"
87 );
88 }
89
90 #[test]
91 fn invalid_utf8_renders_as_from_utf8_lossy_does() {
92 let cases: [&[u8]; 5] = [
93 b"Bad\xffName",
94 b"\xff\xfe",
95 b"cut\xe2\x82",
96 b"\xe2\x82\xac",
97 b"",
98 ];
99 for declared in cases {
100 assert_eq!(
101 id(declared).to_string(),
102 format!(
103 "my_token::types::{} (my-token@0.1)",
104 String::from_utf8_lossy(declared)
105 ),
106 "declared = {declared:?}",
107 );
108 }
109 }
110}