Skip to main content

sashite_sin/
encode.rs

1//! Allocation-free string encoding of a SIN token.
2
3use crate::identifier::Identifier;
4
5/// The canonical string form of an [`Identifier`], stored inline.
6///
7/// A token occupies exactly one byte, so `EncodedSin` keeps it in a fixed
8/// one-byte buffer with no heap allocation. It is produced by
9/// [`Identifier::encode`] and dereferences to [`str`], so it can be used
10/// wherever a string slice is expected.
11///
12/// # Examples
13///
14/// ```
15/// # fn main() -> Result<(), sashite_sin::ParseError> {
16/// use sashite_sin::Identifier;
17///
18/// let enc = Identifier::parse("W")?.encode();
19/// assert_eq!(enc.as_str(), "W");
20/// assert_eq!(&*enc, "W"); // via Deref<Target = str>
21/// assert_eq!(enc.len(), 1); // str method reached through Deref
22/// assert_eq!(enc, "W"); // direct comparison via PartialEq<&str>
23/// assert_eq!("W", enc); // and the reverse direction
24/// # Ok(())
25/// # }
26/// ```
27#[derive(Clone, Copy)]
28pub struct EncodedSin {
29    buf: [u8; 1],
30}
31
32impl EncodedSin {
33    /// Encodes an identifier into its canonical token form: the single
34    /// abbreviation letter, cased according to the side.
35    #[must_use]
36    pub(crate) const fn from_identifier(id: Identifier) -> Self {
37        Self {
38            buf: [id.letter().to_ascii(id.side())],
39        }
40    }
41
42    /// Returns the encoded token as a string slice.
43    #[must_use]
44    pub fn as_str(&self) -> &str {
45        debug_assert!(
46            self.buf.is_ascii(),
47            "EncodedSin must contain only ASCII bytes"
48        );
49        // ASCII is always valid UTF-8, so this conversion cannot fail; the empty
50        // fallback is unreachable and exists only to avoid `unsafe`.
51        core::str::from_utf8(&self.buf).unwrap_or("")
52    }
53}
54
55impl core::ops::Deref for EncodedSin {
56    type Target = str;
57
58    fn deref(&self) -> &str {
59        self.as_str()
60    }
61}
62
63impl AsRef<str> for EncodedSin {
64    fn as_ref(&self) -> &str {
65        self.as_str()
66    }
67}
68
69impl core::fmt::Display for EncodedSin {
70    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71        // `pad`, not `write_str`: a width, fill, alignment or precision in the
72        // format spec must be honoured, the way `str` and `char` honour it —
73        // and the way `Identifier`'s own `Display` does, so the two agree.
74        f.pad(self.as_str())
75    }
76}
77
78impl core::fmt::Debug for EncodedSin {
79    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80        write!(f, "EncodedSin({:?})", self.as_str())
81    }
82}
83
84// A token is a single byte, so the derived comparisons are the byte's. These
85// are additive: before them, two `EncodedSin` values could be compared to a
86// `str` but not to each other, so `assert_eq!(a.encode(), b.encode())` did not
87// compile.
88impl PartialEq for EncodedSin {
89    fn eq(&self, other: &Self) -> bool {
90        self.buf == other.buf
91    }
92}
93
94impl Eq for EncodedSin {}
95
96impl core::hash::Hash for EncodedSin {
97    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
98        self.buf.hash(state);
99    }
100}
101
102impl PartialOrd for EncodedSin {
103    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
104        Some(self.cmp(other))
105    }
106}
107
108impl Ord for EncodedSin {
109    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
110        self.buf.cmp(&other.buf)
111    }
112}
113
114impl PartialEq<str> for EncodedSin {
115    fn eq(&self, other: &str) -> bool {
116        self.as_str() == other
117    }
118}
119
120impl PartialEq<&str> for EncodedSin {
121    fn eq(&self, other: &&str) -> bool {
122        self.as_str() == *other
123    }
124}
125
126impl PartialEq<EncodedSin> for str {
127    fn eq(&self, other: &EncodedSin) -> bool {
128        self == other.as_str()
129    }
130}
131
132impl PartialEq<EncodedSin> for &str {
133    fn eq(&self, other: &EncodedSin) -> bool {
134        *self == other.as_str()
135    }
136}