sashite-sin 1.0.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
//! Value-semantics and real-world usage tests.
//!
//! Boolean queries are covered in `transformations.rs`. This file checks the
//! attributes decoded from the documented style mappings of the spec's examples
//! page, and the behaviour of the derived `Eq` / `Hash` / `Ord` implementations
//! when identifiers are used in collections.

use sashite_sin::{Identifier, Letter, Side};
use std::collections::{HashMap, HashSet};

/// Builds all 52 identifiers in canonical (ascending) order.
fn all_ids() -> Vec<Identifier> {
    let mut ids = Vec::with_capacity(52);
    for letter in Letter::ALL {
        for side in [Side::First, Side::Second] {
            ids.push(Identifier::new(letter, side));
        }
    }
    ids
}

/// Tokens drawn from the spec's examples page (the conventional Sashité style
/// mapping `W`/`C`/`J`/`S`), with the attributes they must decode to. SIN
/// reserves no letters; these assignments are a context convention.
const STYLE_CASES: &[(&str, char, Side)] = &[
    ("W", 'W', Side::First),  // Western, first player
    ("w", 'W', Side::Second), // Western, second player
    ("C", 'C', Side::First),  // Chinese, first player
    ("c", 'C', Side::Second), // Chinese, second player
    ("J", 'J', Side::First),  // Japanese, first player
    ("j", 'J', Side::Second), // Japanese, second player
    ("S", 'S', Side::First),  // Siamese, first player
    ("s", 'S', Side::Second), // Siamese, second player
];

#[test]
fn documented_styles_decode_as_expected() {
    for &(token, letter, side) in STYLE_CASES {
        let id = Identifier::parse(token).unwrap_or_else(|e| panic!("{token:?}: {e:?}"));
        assert_eq!(id.letter().as_char(), letter, "{token:?}");
        assert_eq!(id.side(), side, "{token:?}");
        assert_eq!(id.to_char().to_string(), token, "{token:?}");
        assert_eq!(id.encode().as_str(), token, "{token:?} round-trip");
    }
}

#[test]
fn equality_distinguishes_every_attribute() {
    let base = Identifier::parse("W").unwrap();
    assert_eq!(base, Identifier::parse("W").unwrap()); // reflexive across parses
    assert_ne!(base, Identifier::parse("w").unwrap()); // side differs
    assert_ne!(base, Identifier::parse("C").unwrap()); // letter differs
}

#[test]
fn parses_from_bytes() {
    // `TryFrom<&[u8]>` parses raw bytes and agrees with the string path.
    assert_eq!(
        Identifier::try_from(&b"W"[..]).unwrap(),
        Identifier::parse("W").unwrap(),
    );
    assert!(Identifier::try_from(&b"WW"[..]).is_err());
    // Non-UTF-8 bytes are rejected gracefully, not via a panic.
    assert!(Identifier::try_from(&[0xFF_u8, 0xFE][..]).is_err());
}

#[test]
fn all_identifiers_are_distinct_and_hashable() {
    let ids = all_ids();
    let set: HashSet<Identifier> = ids.iter().copied().collect();
    assert_eq!(
        set.len(),
        52,
        "all 52 identifiers must be distinct under Hash + Eq"
    );

    // Re-inserting existing values does not grow the set.
    let mut reinserted = set;
    reinserted.extend(ids.iter().copied());
    assert_eq!(reinserted.len(), 52);
}

#[test]
fn usable_as_map_keys() {
    let mut map: HashMap<Identifier, &str> = HashMap::new();
    map.insert(Identifier::parse("W").unwrap(), "first western");
    map.insert(Identifier::parse("c").unwrap(), "second chinese");

    assert_eq!(
        map.get(&Identifier::parse("W").unwrap()),
        Some(&"first western")
    );
    // A different side is a different key.
    assert_eq!(map.get(&Identifier::parse("w").unwrap()), None);
    assert_eq!(map.get(&Identifier::parse("C").unwrap()), None);
}

#[test]
fn sorting_yields_canonical_order() {
    let canonical = all_ids(); // generated ascending: letter, then side
    assert!(
        canonical.windows(2).all(|w| w[0] < w[1]),
        "generation order must be strictly ascending and duplicate-free",
    );

    let mut scrambled = canonical.clone();
    scrambled.reverse();
    scrambled.sort();
    assert_eq!(
        scrambled, canonical,
        "sorting must reproduce canonical order"
    );
}

#[test]
fn debug_output_is_informative() {
    let id = Identifier::parse("w").unwrap();
    let rendered = format!("{id:?}");
    assert!(rendered.contains("Letter('W')"), "{rendered}");
    assert!(rendered.contains("Second"), "{rendered}");

    // EncodedSin's Debug shows the token text.
    assert_eq!(format!("{:?}", id.encode()), "EncodedSin(\"w\")");
}