Skip to main content

thruput_spec/
impls.rs

1use core::fmt;
2
3use crate::{ContractSpecId, ContractSpecString};
4
5/// `module_path::declared (crate_name@major)`: an id the way a person reading
6/// a diagnostic wants it.
7///
8/// Not the joined `CRATE@MAJOR::module::path::Name` form: a reader wants to
9/// see which segment is which, and `module_path` already leads with the crate,
10/// so the crate and major trail in parentheses rather than doubling up in the
11/// path.
12///
13/// Only for messages. The rendering is one-way -- `declared` may itself carry
14/// `::`, so where the module path ends cannot be recovered from the text --
15/// and ids are compared and keyed whole, never by this.
16///
17/// Lossy: nothing on the wire makes a segment UTF-8, and a diagnostic is the
18/// wrong place to fail, so an invalid sequence renders as U+FFFD, exactly as
19/// `String::from_utf8_lossy` has it. Segments are decoded rather than written
20/// through `ContractSpecString`'s own `Display`, which hex-escapes every
21/// non-ASCII byte, because a Rust identifier may be non-ASCII. Validate the
22/// segments first wherever their bytes matter.
23impl 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
36/// Write `s` as text the way `String::from_utf8_lossy` would, without
37/// allocating.
38fn 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        // A `::` in `declared` passes through verbatim, which is what makes
79        // the rendering one-way.
80        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}