sashite_sin/identifier.rs
1//! The central SIN identifier type.
2
3use crate::encode::EncodedSin;
4use crate::error::ParseError;
5use crate::letter::Letter;
6use crate::side::Side;
7
8/// A parsed SIN token: a player's identity at the level of notation.
9///
10/// An `Identifier` bundles the two attributes a token encodes — a [`Letter`]
11/// abbreviation and a [`Side`] — into a single 2-byte `Copy` value.
12/// Construction from typed components via [`Identifier::new`] is total: every
13/// combination is a valid token, so it cannot fail.
14///
15/// The derived total ordering compares attributes in the order letter → side.
16///
17/// # Examples
18///
19/// ```
20/// # fn main() -> Result<(), sashite_sin::ParseError> {
21/// use sashite_sin::{Identifier, Side};
22///
23/// let chinese: Identifier = "c".parse()?;
24/// assert_eq!(chinese.letter().as_char(), 'C');
25/// assert_eq!(chinese.side(), Side::Second);
26/// assert_eq!(chinese.to_char(), 'c');
27///
28/// // Transformations are cheap and infallible; the value is `Copy`.
29/// assert_eq!(chinese.flipped().to_char(), 'C');
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
34pub struct Identifier {
35 letter: Letter,
36 side: Side,
37}
38
39impl Identifier {
40 /// Builds an identifier from its two typed components.
41 ///
42 /// This is infallible: because each component type is valid by
43 /// construction, every combination denotes a valid SIN token.
44 ///
45 /// # Examples
46 ///
47 /// ```
48 /// use sashite_sin::{Identifier, Letter, Side};
49 ///
50 /// let p = Identifier::new(Letter::try_from_char('C').unwrap(), Side::Second);
51 /// assert_eq!(p.encode().as_str(), "c");
52 /// ```
53 #[must_use]
54 pub const fn new(letter: Letter, side: Side) -> Self {
55 Self { letter, side }
56 }
57
58 /// Parses a string slice into an identifier.
59 ///
60 /// The whole input must be the token: there is no leading or trailing
61 /// slack, so surrounding whitespace, a trailing line break or any second
62 /// character is a rejection rather than something to be trimmed away. The
63 /// 52 tokens accepted here are the entire domain.
64 ///
65 /// # Errors
66 ///
67 /// Returns [`ParseError::Empty`] for an empty input, [`ParseError::TooLong`]
68 /// if `input` is two bytes or longer, and [`ParseError::InvalidLetter`] if
69 /// it is a single byte that is not an ASCII letter. Length is counted in
70 /// bytes, so a lone non-ASCII character is `TooLong`; see [`ParseError`] for
71 /// why that split is where it is.
72 ///
73 /// # Examples
74 ///
75 /// ```
76 /// # fn main() -> Result<(), sashite_sin::ParseError> {
77 /// use sashite_sin::Identifier;
78 ///
79 /// let western = Identifier::parse("W")?;
80 /// assert!(western.is_first());
81 /// assert_eq!(western.letter().as_char(), 'W');
82 ///
83 /// // Nothing is trimmed, and a line break is never absorbed.
84 /// assert!(Identifier::parse(" W").is_err());
85 /// assert!(Identifier::parse("W\n").is_err());
86 /// # Ok(())
87 /// # }
88 /// ```
89 pub const fn parse(input: &str) -> Result<Self, ParseError> {
90 crate::parse::parse(input)
91 }
92
93 /// Reports whether `input` is a valid SIN token, without allocating or
94 /// constructing an identifier on the caller's side.
95 ///
96 /// This answers exactly the question [`Identifier::parse`] answers — it is
97 /// defined as that call succeeding — so the two can never disagree about
98 /// what a token is.
99 ///
100 /// # Examples
101 ///
102 /// ```
103 /// use sashite_sin::Identifier;
104 ///
105 /// assert!(Identifier::is_valid("S"));
106 /// assert!(Identifier::is_valid("s"));
107 /// assert!(!Identifier::is_valid("SS"));
108 /// assert!(!Identifier::is_valid(""));
109 /// ```
110 #[must_use]
111 pub const fn is_valid(input: &str) -> bool {
112 Self::parse(input).is_ok()
113 }
114
115 /// Returns the canonical, allocation-free string encoding of this token.
116 #[must_use]
117 pub const fn encode(self) -> EncodedSin {
118 EncodedSin::from_identifier(self)
119 }
120
121 /// Returns the token as its single cased character: uppercase for
122 /// [`Side::First`], lowercase for [`Side::Second`].
123 ///
124 /// # Examples
125 ///
126 /// ```
127 /// # fn main() -> Result<(), sashite_sin::ParseError> {
128 /// use sashite_sin::Identifier;
129 ///
130 /// assert_eq!(Identifier::parse("J")?.to_char(), 'J');
131 /// assert_eq!(Identifier::parse("j")?.to_char(), 'j');
132 /// # Ok(())
133 /// # }
134 /// ```
135 #[must_use]
136 pub const fn to_char(self) -> char {
137 self.letter.to_ascii(self.side) as char
138 }
139
140 // --- Accessors ---
141
142 /// Returns the player-style abbreviation (always uppercase).
143 #[must_use]
144 pub const fn letter(self) -> Letter {
145 self.letter
146 }
147
148 /// Returns the side the player belongs to.
149 #[must_use]
150 pub const fn side(self) -> Side {
151 self.side
152 }
153
154 // --- Side queries ---
155
156 /// Reports whether the side is [`Side::First`].
157 #[must_use]
158 pub const fn is_first(self) -> bool {
159 matches!(self.side, Side::First)
160 }
161
162 /// Reports whether the side is [`Side::Second`].
163 #[must_use]
164 pub const fn is_second(self) -> bool {
165 matches!(self.side, Side::Second)
166 }
167
168 // --- Transformations (return a new value; the type is `Copy`) ---
169
170 /// Returns a copy with the abbreviation replaced.
171 #[must_use]
172 pub const fn with_letter(self, letter: Letter) -> Self {
173 Self::new(letter, self.side)
174 }
175
176 /// Returns a copy with the side replaced.
177 #[must_use]
178 pub const fn with_side(self, side: Side) -> Self {
179 Self::new(self.letter, side)
180 }
181
182 /// Returns a copy belonging to the opposite [`Side`].
183 #[must_use]
184 pub const fn flipped(self) -> Self {
185 self.with_side(self.side.flip())
186 }
187}
188
189impl core::fmt::Display for Identifier {
190 /// Writes the canonical token — the single cased abbreviation letter.
191 ///
192 /// The token goes out through [`core::fmt::Formatter::pad`] rather than
193 /// straight to the underlying buffer, so width, fill, alignment and
194 /// precision behave exactly as they do for the equivalent [`str`]. Writing
195 /// directly would silently discard those options, which matters as soon as
196 /// a token is placed in a fixed-width column.
197 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
198 f.pad(self.encode().as_str())
199 }
200}
201
202impl core::str::FromStr for Identifier {
203 type Err = ParseError;
204
205 /// Parses a token, so `"W".parse::<Identifier>()` works.
206 ///
207 /// # Errors
208 ///
209 /// Identical to [`Identifier::parse`], which this defers to; the two cannot
210 /// drift apart.
211 fn from_str(s: &str) -> Result<Self, Self::Err> {
212 Self::parse(s)
213 }
214}
215
216impl TryFrom<&str> for Identifier {
217 type Error = ParseError;
218
219 /// Parses a token.
220 ///
221 /// # Errors
222 ///
223 /// Identical to [`Identifier::parse`], which this defers to.
224 fn try_from(s: &str) -> Result<Self, Self::Error> {
225 Self::parse(s)
226 }
227}
228
229impl TryFrom<&[u8]> for Identifier {
230 type Error = ParseError;
231
232 /// Parses a token straight from raw bytes, for callers holding a network
233 /// or file buffer rather than a `&str`.
234 ///
235 /// No UTF-8 validation happens, and none is needed: a token is a single
236 /// ASCII byte, so ill-formed input simply fails the same length and letter
237 /// checks every other entry point applies. This never disagrees with
238 /// [`Identifier::parse`] on bytes that *are* valid UTF-8, and it accepts
239 /// nothing that `parse` would reject.
240 ///
241 /// # Errors
242 ///
243 /// The same variants as [`Identifier::parse`], decided the same way. Being
244 /// ill-formed is not itself a reason and gets no variant of its own; the
245 /// input is judged on its length exactly as text would be. A lone `0xFF` is
246 /// [`ParseError::InvalidLetter`] because it is one byte that is not a
247 /// letter, and any two bytes are [`ParseError::TooLong`] whether or not
248 /// they decode.
249 ///
250 /// # Examples
251 ///
252 /// ```
253 /// use sashite_sin::{Identifier, ParseError};
254 ///
255 /// assert_eq!(Identifier::try_from(&b"W"[..]).unwrap().to_char(), 'W');
256 ///
257 /// // Not UTF-8, and rejected without a panic or a separate error kind.
258 /// assert_eq!(Identifier::try_from(&[0xFF][..]), Err(ParseError::InvalidLetter));
259 /// assert_eq!(Identifier::try_from(&[0xC3][..]), Err(ParseError::InvalidLetter));
260 /// assert_eq!(Identifier::try_from(&[0xC3, 0xA9][..]), Err(ParseError::TooLong));
261 /// ```
262 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
263 crate::parse::parse_bytes(bytes)
264 }
265}