SIN (Style Identifier Notation) implementation for Rust.
This crate implements the SIN Specification v1.0.0.
SIN is a compact, ASCII-only token format that encodes a player identity at the level of notation: the tuple (player side, player style). A single letter carries both — its case encodes the side, and the letter itself is the player-style abbreviation.
<abbr> e.g. W c J s
Because a player's side and style are both fixed for the duration of a match, a SIN token is a stable player identifier. SIN standardizes only the encoding: which letter denotes which style is left to the rule system — see the Game Protocol and Glossary.
| Property | Value | Rationale |
|---|---|---|
| Token length | 1 byte | ^[A-Za-z]$ per the specification |
| Closed domain | 52 tokens | 26 letters × 2 sides |
Identifier size |
2 bytes, Copy |
stored inline; parsing and encoding never allocate |
| Dependencies | none required | zero by default; serde is an optional, no_std add-on |
unsafe |
forbidden | the crate is built under a forbid-unsafe lint policy |
| MSRV | 1.81 | for core::error::Error without a std feature |
cargo add sashite-sinOr add it manually to Cargo.toml:
[dependencies]
sashite-sin = "1"serde(off by default) — implementsSerialize/DeserializeforIdentifier, (de)serializing it as its canonical token string (e.g."W"). Enabling it keeps the crateno_std.
[dependencies]
sashite-sin = { version = "1", features = ["serde"] }use sashite_sin::{Identifier, Side};
let western: Identifier = "W".parse()?; // via FromStr
let chinese = Identifier::parse("c")?; // via the inherent method
assert_eq!(western.letter().as_char(), 'W');
assert_eq!(western.side(), Side::First);
assert_eq!(chinese.side(), Side::Second);Construction is infallible: because each component type is valid by construction, every combination denotes a valid token.
use sashite_sin::{Identifier, Letter, Side};
let japanese = Identifier::new(Letter::try_from_char('J')?, Side::Second);
assert_eq!(japanese.encode().as_str(), "j");encode returns an allocation-free, fixed-buffer string view that dereferences
to str; to_char returns the single character; Display writes the same
canonical form.
use sashite_sin::Identifier;
let id = Identifier::parse("W")?;
assert_eq!(id.encode().as_str(), "W");
assert_eq!(id.to_char(), 'W');
assert_eq!(id.to_string(), "W"); // requires `alloc`/`std`use sashite_sin::Identifier;
assert!(Identifier::is_valid("S"));
assert!(!Identifier::is_valid("WW")); // a token is exactly one letterEvery transformation returns a new value (the type is Copy, so this is cheap):
use sashite_sin::{Identifier, Letter};
let western = Identifier::parse("W")?;
assert_eq!(western.flipped().encode().as_str(), "w");
assert_eq!(
western.with_letter(Letter::try_from_char('C')?).encode().as_str(),
"C",
);
assert!(western.is_first());The grammar (EBNF) is:
sin ::= abbr ;
abbr ::= "A"…"Z" | "a"…"z" ;A token maps to exactly two attributes:
| Component | Encodes | Values |
|---|---|---|
| letter case | side | uppercase → First, lowercase → Second |
| letter | player style | a single-letter abbreviation (A–Z) |
Letters are not reserved: the mapping from abbreviation to full style name is defined entirely by the rule system. See the examples page for the conventional mappings (Western, Chinese, Japanese, Siamese).
no_stdand allocation-free. Parsing borrows the input bytes; anIdentifieris a 2-byteCopyvalue andEncodedSinkeeps the single output byte in a fixed inline buffer. Nothing touches the heap.- No
unsafe, no regex engine. The parser matches raw bytes directly, eliminating ReDoS as an attack vector. - Bounded, panic-free parsing. Inputs longer than one byte are rejected on a
structural length check before any byte is inspected, and the public parsing
API returns a
Resultrather than panicking. const-friendly. Construction, parsing, validation, the accessors, and the transformations are allconst fn, so identifiers can be built and checked at compile time.- Total component construction. With valid-by-construction component types, building an identifier from its parts cannot fail.
The hot paths are tiny: parsing is a length check followed by a single byte
classification, and encoding writes one byte — expect single-digit nanoseconds
per call. Run cargo bench (see benches/parse.rs) for figures on your own
hardware.
- Game Protocol — the conceptual foundation
- SIN Specification v1.0.0 — the normative document
- SIN Examples — sample style mappings
Reference implementations in other languages are maintained by Sashité: Elixir, Ruby.
If a library's behavior appears to conflict with the specification, the specification is normative.
Available as open source under the terms of the Apache License 2.0.