sashite-sin 1.1.0

Style Identifier Notation (SIN): a compact, ASCII-only, no_std token encoding a player's side and style in abstract strategy board games.
Documentation
//! The central SIN identifier type.

use crate::encode::EncodedSin;
use crate::error::ParseError;
use crate::letter::Letter;
use crate::side::Side;

/// A parsed SIN token: a player's identity at the level of notation.
///
/// An `Identifier` bundles the two attributes a token encodes — a [`Letter`]
/// abbreviation and a [`Side`] — into a single 2-byte `Copy` value.
/// Construction from typed components via [`Identifier::new`] is total: every
/// combination is a valid token, so it cannot fail.
///
/// The derived total ordering compares attributes in the order letter → side.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), sashite_sin::ParseError> {
/// use sashite_sin::{Identifier, Side};
///
/// let chinese: Identifier = "c".parse()?;
/// assert_eq!(chinese.letter().as_char(), 'C');
/// assert_eq!(chinese.side(), Side::Second);
/// assert_eq!(chinese.to_char(), 'c');
///
/// // Transformations are cheap and infallible; the value is `Copy`.
/// assert_eq!(chinese.flipped().to_char(), 'C');
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Identifier {
    letter: Letter,
    side: Side,
}

impl Identifier {
    /// Builds an identifier from its two typed components.
    ///
    /// This is infallible: because each component type is valid by
    /// construction, every combination denotes a valid SIN token.
    ///
    /// # Examples
    ///
    /// ```
    /// use sashite_sin::{Identifier, Letter, Side};
    ///
    /// let p = Identifier::new(Letter::try_from_char('C').unwrap(), Side::Second);
    /// assert_eq!(p.encode().as_str(), "c");
    /// ```
    #[must_use]
    pub const fn new(letter: Letter, side: Side) -> Self {
        Self { letter, side }
    }

    /// Parses a string slice into an identifier.
    ///
    /// The whole input must be the token: there is no leading or trailing
    /// slack, so surrounding whitespace, a trailing line break or any second
    /// character is a rejection rather than something to be trimmed away. The
    /// 52 tokens accepted here are the entire domain.
    ///
    /// # Errors
    ///
    /// Returns [`ParseError::Empty`] for an empty input, [`ParseError::TooLong`]
    /// if `input` is two bytes or longer, and [`ParseError::InvalidLetter`] if
    /// it is a single byte that is not an ASCII letter. Length is counted in
    /// bytes, so a lone non-ASCII character is `TooLong`; see [`ParseError`] for
    /// why that split is where it is.
    ///
    /// # Examples
    ///
    /// ```
    /// # fn main() -> Result<(), sashite_sin::ParseError> {
    /// use sashite_sin::Identifier;
    ///
    /// let western = Identifier::parse("W")?;
    /// assert!(western.is_first());
    /// assert_eq!(western.letter().as_char(), 'W');
    ///
    /// // Nothing is trimmed, and a line break is never absorbed.
    /// assert!(Identifier::parse(" W").is_err());
    /// assert!(Identifier::parse("W\n").is_err());
    /// # Ok(())
    /// # }
    /// ```
    pub const fn parse(input: &str) -> Result<Self, ParseError> {
        crate::parse::parse(input)
    }

    /// Reports whether `input` is a valid SIN token, without allocating or
    /// constructing an identifier on the caller's side.
    ///
    /// This answers exactly the question [`Identifier::parse`] answers — it is
    /// defined as that call succeeding — so the two can never disagree about
    /// what a token is.
    ///
    /// # Examples
    ///
    /// ```
    /// use sashite_sin::Identifier;
    ///
    /// assert!(Identifier::is_valid("S"));
    /// assert!(Identifier::is_valid("s"));
    /// assert!(!Identifier::is_valid("SS"));
    /// assert!(!Identifier::is_valid(""));
    /// ```
    #[must_use]
    pub const fn is_valid(input: &str) -> bool {
        Self::parse(input).is_ok()
    }

    /// Returns the canonical, allocation-free string encoding of this token.
    #[must_use]
    pub const fn encode(self) -> EncodedSin {
        EncodedSin::from_identifier(self)
    }

    /// Returns the token as its single cased character: uppercase for
    /// [`Side::First`], lowercase for [`Side::Second`].
    ///
    /// # Examples
    ///
    /// ```
    /// # fn main() -> Result<(), sashite_sin::ParseError> {
    /// use sashite_sin::Identifier;
    ///
    /// assert_eq!(Identifier::parse("J")?.to_char(), 'J');
    /// assert_eq!(Identifier::parse("j")?.to_char(), 'j');
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub const fn to_char(self) -> char {
        self.letter.to_ascii(self.side) as char
    }

    // --- Accessors ---

    /// Returns the player-style abbreviation (always uppercase).
    #[must_use]
    pub const fn letter(self) -> Letter {
        self.letter
    }

    /// Returns the side the player belongs to.
    #[must_use]
    pub const fn side(self) -> Side {
        self.side
    }

    // --- Side queries ---

    /// Reports whether the side is [`Side::First`].
    #[must_use]
    pub const fn is_first(self) -> bool {
        matches!(self.side, Side::First)
    }

    /// Reports whether the side is [`Side::Second`].
    #[must_use]
    pub const fn is_second(self) -> bool {
        matches!(self.side, Side::Second)
    }

    // --- Transformations (return a new value; the type is `Copy`) ---

    /// Returns a copy with the abbreviation replaced.
    #[must_use]
    pub const fn with_letter(self, letter: Letter) -> Self {
        Self::new(letter, self.side)
    }

    /// Returns a copy with the side replaced.
    #[must_use]
    pub const fn with_side(self, side: Side) -> Self {
        Self::new(self.letter, side)
    }

    /// Returns a copy belonging to the opposite [`Side`].
    #[must_use]
    pub const fn flipped(self) -> Self {
        self.with_side(self.side.flip())
    }
}

impl core::fmt::Display for Identifier {
    /// Writes the canonical token — the single cased abbreviation letter.
    ///
    /// The token goes out through [`core::fmt::Formatter::pad`] rather than
    /// straight to the underlying buffer, so width, fill, alignment and
    /// precision behave exactly as they do for the equivalent [`str`]. Writing
    /// directly would silently discard those options, which matters as soon as
    /// a token is placed in a fixed-width column.
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.pad(self.encode().as_str())
    }
}

impl core::str::FromStr for Identifier {
    type Err = ParseError;

    /// Parses a token, so `"W".parse::<Identifier>()` works.
    ///
    /// # Errors
    ///
    /// Identical to [`Identifier::parse`], which this defers to; the two cannot
    /// drift apart.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

impl TryFrom<&str> for Identifier {
    type Error = ParseError;

    /// Parses a token.
    ///
    /// # Errors
    ///
    /// Identical to [`Identifier::parse`], which this defers to.
    fn try_from(s: &str) -> Result<Self, Self::Error> {
        Self::parse(s)
    }
}

impl TryFrom<&[u8]> for Identifier {
    type Error = ParseError;

    /// Parses a token straight from raw bytes, for callers holding a network
    /// or file buffer rather than a `&str`.
    ///
    /// No UTF-8 validation happens, and none is needed: a token is a single
    /// ASCII byte, so ill-formed input simply fails the same length and letter
    /// checks every other entry point applies. This never disagrees with
    /// [`Identifier::parse`] on bytes that *are* valid UTF-8, and it accepts
    /// nothing that `parse` would reject.
    ///
    /// # Errors
    ///
    /// The same variants as [`Identifier::parse`], decided the same way. Being
    /// ill-formed is not itself a reason and gets no variant of its own; the
    /// input is judged on its length exactly as text would be. A lone `0xFF` is
    /// [`ParseError::InvalidLetter`] because it is one byte that is not a
    /// letter, and any two bytes are [`ParseError::TooLong`] whether or not
    /// they decode.
    ///
    /// # Examples
    ///
    /// ```
    /// use sashite_sin::{Identifier, ParseError};
    ///
    /// assert_eq!(Identifier::try_from(&b"W"[..]).unwrap().to_char(), 'W');
    ///
    /// // Not UTF-8, and rejected without a panic or a separate error kind.
    /// assert_eq!(Identifier::try_from(&[0xFF][..]), Err(ParseError::InvalidLetter));
    /// assert_eq!(Identifier::try_from(&[0xC3][..]), Err(ParseError::InvalidLetter));
    /// assert_eq!(Identifier::try_from(&[0xC3, 0xA9][..]), Err(ParseError::TooLong));
    /// ```
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
        crate::parse::parse_bytes(bytes)
    }
}