sashite-epin 1.0.0

Extended Piece Identifier Notation (EPIN): a compact, ASCII-only, no_std token format that extends PIN with a native/derived style marker for abstract strategy board games.
Documentation
//! Allocation-free string encoding of an EPIN token.

use crate::identifier::Identifier;

/// The derivation marker appended for derived style status.
const DERIVATION_MARKER: u8 = b'\'';

/// The canonical string form of an [`Identifier`], stored inline.
///
/// An EPIN token occupies at most four bytes — a PIN token of up to three bytes
/// plus an optional derivation marker — so `EncodedEpin` keeps them in a fixed
/// buffer with no heap allocation. It is produced by [`Identifier::encode`] and
/// dereferences to [`str`], so it can be used wherever a string slice is
/// expected.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), sashite_epin::ParseError> {
/// use sashite_epin::Identifier;
///
/// let enc = Identifier::parse("+K^'")?.encode();
/// assert_eq!(enc.as_str(), "+K^'");
/// assert_eq!(&*enc, "+K^'"); // via Deref<Target = str>
/// assert_eq!(enc.len(), 4); // str method reached through Deref
/// assert_eq!(enc, "+K^'"); // direct comparison via PartialEq<&str>
/// assert_eq!("+K^'", enc); // and the reverse direction
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Copy)]
pub struct EncodedEpin {
    buf: [u8; 4],
    len: u8,
}

impl EncodedEpin {
    /// Encodes an identifier into its canonical token form.
    ///
    /// The PIN core is encoded by `sashite-pin`; this only copies those bytes
    /// and appends the derivation marker when the style status is derived.
    pub(crate) fn from_identifier(id: Identifier) -> Self {
        let mut buf = [0u8; 4];
        let mut len: u8 = 0;

        // The PIN core (one to three ASCII bytes).
        let pin = id.pin().encode();
        for &byte in pin.as_str().as_bytes() {
            buf[usize::from(len)] = byte;
            len += 1;
        }

        // Optional derivation-marker suffix.
        if id.is_derived() {
            buf[usize::from(len)] = DERIVATION_MARKER;
            len += 1;
        }

        Self { buf, len }
    }

    /// Returns the encoded token as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        let bytes = &self.buf[..usize::from(self.len)];
        debug_assert!(
            bytes.is_ascii(),
            "EncodedEpin must contain only ASCII bytes"
        );
        // ASCII is always valid UTF-8, so this conversion cannot fail; the empty
        // fallback is unreachable and exists only to avoid `unsafe`.
        core::str::from_utf8(bytes).unwrap_or("")
    }
}

impl core::ops::Deref for EncodedEpin {
    type Target = str;

    fn deref(&self) -> &str {
        self.as_str()
    }
}

impl AsRef<str> for EncodedEpin {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl core::fmt::Display for EncodedEpin {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl core::fmt::Debug for EncodedEpin {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "EncodedEpin({:?})", self.as_str())
    }
}

impl PartialEq<str> for EncodedEpin {
    fn eq(&self, other: &str) -> bool {
        self.as_str() == other
    }
}

impl PartialEq<&str> for EncodedEpin {
    fn eq(&self, other: &&str) -> bool {
        self.as_str() == *other
    }
}

impl PartialEq<EncodedEpin> for str {
    fn eq(&self, other: &EncodedEpin) -> bool {
        self == other.as_str()
    }
}

impl PartialEq<EncodedEpin> for &str {
    fn eq(&self, other: &EncodedEpin) -> bool {
        *self == other.as_str()
    }
}