Skip to main content

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}