sashite-feen 0.2.0

Field Expression Encoding Notation (FEEN): a compact, ASCII-only, no_std, zero-allocation validator and encoder for board-game positions in abstract strategy games, built on EPIN and SIN.
Documentation
//! Errors produced when parsing or validating a FEEN string.

/// The reason a string is not a valid, canonical FEEN position.
///
/// Returned by [`crate::Feen::parse`]. The taxonomy mirrors the specification's
/// validation requirements (§11) and the reference implementations: each
/// variant pins down one class of failure so the error mapping cannot drift.
///
/// This enum is `#[non_exhaustive]`: future revisions may add variants without
/// a breaking change, so downstream `match` expressions should include a
/// wildcard arm.
///
/// # Examples
///
/// ```
/// use sashite_feen::{Feen, ParseError};
///
/// assert_eq!(Feen::parse("nope").unwrap_err(), ParseError::FieldCount);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ParseError {
    // --- Input-level ---
    /// The input exceeds [`crate::MAX_STRING_LENGTH`] bytes.
    InputTooLong,
    /// The input contains a non-ASCII byte (FEEN is ASCII-only).
    NonAscii,
    /// The input is not exactly three space-separated fields.
    FieldCount,

    // --- Field 1: piece placement ---
    /// The piece-placement field is empty.
    PlacementEmpty,
    /// The piece-placement field starts with a `/` separator.
    PlacementStartsWithSeparator,
    /// The piece-placement field ends with a `/` separator.
    PlacementEndsWithSeparator,
    /// A segment between separators is empty.
    ///
    /// This is a defensive guard rather than a reachable outcome: the
    /// start/end-separator checks, plus the fact that a run of `/` is always
    /// consumed maximally, mean every segment starts on a non-`/` byte and every
    /// placement token contributes at least one cell. No input has ever produced
    /// it, and a test asserts as much. It is kept so that a future change to the
    /// scanner fails with a typed error instead of a silent zero-cell segment.
    EmptySegment,
    /// An empty-count token is zero or has a leading zero.
    InvalidEmptyCount,
    /// A piece token is not a valid EPIN identifier.
    InvalidPieceToken,

    // --- Field 1: board geometry ---
    /// The board is not regular: ranks differ in size within a dimension.
    BoardNotRegular,
    /// A separator group of depth `N` encloses a segment lacking depth `N-1`.
    DimensionalCoherence,
    /// The board has more than [`crate::MAX_DIMENSIONS`] dimensions.
    TooManyDimensions,
    /// A dimension has more than [`crate::MAX_DIMENSION_SIZE`] cells.
    DimensionTooLarge,

    // --- Field 2: hands ---
    /// The hands field does not contain exactly one `/` delimiter.
    InvalidHandsDelimiter,
    /// A hand multiplicity is `0`, `1`, or has a leading zero.
    InvalidHandCount,
    /// Identical EPIN tokens in a hand are not aggregated into one item.
    HandNotAggregated,
    /// Hand items are not in canonical order.
    HandNotCanonical,

    // --- Field 3: style–turn ---
    /// The style–turn field does not contain exactly one `/` delimiter.
    InvalidStyleTurnDelimiter,
    /// A style token is not a valid SIN identifier.
    InvalidStyleToken,
    /// The two style tokens are not of opposite case.
    StylesSameCase,

    // --- Cross-field cardinality ---
    /// The board has more than [`MAX_SQUARE_COUNT`](crate::MAX_SQUARE_COUNT)
    /// squares.
    TooManySquares,
    /// The position has more pieces than the board has squares.
    TooManyPieces,
}

impl core::fmt::Display for ParseError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let message = match self {
            Self::InputTooLong => "FEEN string exceeds the maximum length",
            Self::NonAscii => "FEEN string contains a non-ASCII byte",
            Self::FieldCount => "FEEN string must have exactly three space-separated fields",
            Self::PlacementEmpty => "the piece-placement field is empty",
            Self::PlacementStartsWithSeparator => {
                "the piece-placement field must not start with '/'"
            }
            Self::PlacementEndsWithSeparator => "the piece-placement field must not end with '/'",
            Self::EmptySegment => "the piece-placement field has an empty segment",
            Self::InvalidEmptyCount => "an empty-count is zero or has a leading zero",
            Self::InvalidPieceToken => "the piece-placement field contains an invalid piece token",
            Self::BoardNotRegular => "the board is not regular (ranks of differing sizes)",
            Self::DimensionalCoherence => "the board violates dimensional coherence",
            Self::TooManyDimensions => "the board has too many dimensions",
            Self::DimensionTooLarge => "a board dimension exceeds the maximum size",
            Self::InvalidHandsDelimiter => "the hands field must contain exactly one '/'",
            Self::InvalidHandCount => "a hand multiplicity is 0, 1, or has a leading zero",
            Self::HandNotAggregated => "identical hand pieces are not aggregated",
            Self::HandNotCanonical => "hand items are not in canonical order",
            Self::InvalidStyleTurnDelimiter => "the style-turn field must contain exactly one '/'",
            Self::InvalidStyleToken => "the style-turn field contains an invalid style token",
            Self::StylesSameCase => "the two style tokens must be of opposite case",
            Self::TooManySquares => "the board has more squares than the maximum",
            Self::TooManyPieces => "the position has more pieces than squares",
        };
        // `pad`, not `write_str`: a width, fill, alignment or precision in the
        // format spec must be honoured, the way `str` and `char` honour it.
        // (`Feen`'s own `Display` deliberately does not — see `encode.rs`; a
        // whole position can run to 4 KiB, which is not a stack buffer this
        // crate is willing to take on a bare-metal target.)
        f.pad(message)
    }
}

impl core::error::Error for ParseError {}

/// The reason an owned position cannot be written as a FEEN string.
///
/// Returned by [`encode`](crate::encode) and [`write_feen`](crate::write_feen).
/// A [`Qi`](sashite_qi::Qi) is a strictly more permissive model than FEEN — it
/// admits geometries and sizes FEEN has no spelling for — so serializing one is
/// a fallible operation. Available with the `alloc` feature.
///
/// A position obtained from [`Feen::to_qi`](crate::Feen::to_qi) can never
/// produce any of these: parsing already established everything the encoder
/// needs.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "alloc")] {
/// use sashite_feen::sashite_epin::Identifier as Piece;
/// use sashite_feen::sashite_qi::Qi;
/// use sashite_feen::sashite_sin::Identifier as Style;
/// use sashite_feen::{encode, EncodeError};
///
/// let style = |c| Style::try_from(c).expect("a valid SIN token");
///
/// // Two layers of one rank each: FEEN would have to write `3//3`, and a `//`
/// // may only separate structures that themselves contain a `/`.
/// let thin = Qi::<Piece, Style>::new(&[2, 1, 3], style("C"), style("c"))
///     .expect("a valid Qi position");
/// assert_eq!(encode(&thin), Err(EncodeError::ShapeNotRepresentable));
///
/// // Widen the middle dimension and it is spellable.
/// let ok = Qi::<Piece, Style>::new(&[2, 2, 3], style("C"), style("c"))
///     .expect("a valid Qi position");
/// assert_eq!(encode(&ok).as_deref(), Ok("3/3//3/3 / C/c"));
/// # }
/// ```
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EncodeError {
    /// The board's geometry has no FEEN spelling.
    ///
    /// FEEN encodes dimensionality through the *depth* of its `/` separators, so
    /// a dimension exists in the notation only if it is actually split: a 2-D
    /// board needs at least two ranks to carry a `/`, and a 3-D board needs at
    /// least two layers to carry a `//` and at least two ranks per layer to
    /// carry the `/` inside it (specification §7.4, dimensional coherence).
    ///
    /// The rule is therefore: **every dimension except the innermost must hold
    /// at least two elements.** `[8, 8]`, `[3, 1]` and `[2, 2, 2]` are fine;
    /// `[1, 8]`, `[2, 1, 3]` and `[1, 2, 3]` are not.
    ShapeNotRepresentable,
    /// The canonical form would be longer than
    /// [`MAX_STRING_LENGTH`](crate::MAX_STRING_LENGTH) bytes.
    ///
    /// [`Qi`](sashite_qi::Qi) bounds a position by its square count; this crate
    /// also bounds a FEEN string by its byte length, and a densely occupied
    /// board reaches the second bound long before the first. Rather than hand
    /// back a string [`Feen::parse`](crate::Feen::parse) would reject, the
    /// encoder refuses.
    TooLong,
    /// The sink returned an error.
    ///
    /// Only [`write_feen`](crate::write_feen) can produce this;
    /// [`encode`](crate::encode) writes into a `String`, which never fails.
    /// Output already written to the sink is left in place.
    Sink,
}

#[cfg(feature = "alloc")]
impl core::fmt::Display for EncodeError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let message = match self {
            Self::ShapeNotRepresentable => {
                "the board geometry has no FEEN spelling (every dimension but \
                 the innermost must hold at least two elements)"
            }
            Self::TooLong => "the canonical FEEN string would exceed the maximum length",
            Self::Sink => "the sink returned an error",
        };
        f.pad(message)
    }
}

#[cfg(feature = "alloc")]
impl core::error::Error for EncodeError {}