# sashite-sin
[](https://crates.io/crates/sashite-sin)
[](https://docs.rs/sashite-sin)
[](https://github.com/sashite/sin.rs/actions/workflows/ci.yml)
[](https://github.com/sashite/sin.rs/blob/main/LICENSE)
> **SIN** (Style Identifier Notation) implementation for Rust.
## Overview
This crate implements the [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.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.
```text
<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](https://sashite.dev/game-protocol/) and [Glossary](https://sashite.dev/glossary/).
### Implementation constraints
| 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 |
## Installation
```sh
cargo add sashite-sin
```
Or add it manually to `Cargo.toml`:
```toml
[dependencies]
sashite-sin = "1"
```
## Cargo features
- **`serde`** *(off by default)* — implements `Serialize` / `Deserialize` for
`Identifier`, (de)serializing it as its canonical token string (e.g. `"W"`).
Enabling it keeps the crate `no_std`.
```toml
[dependencies]
sashite-sin = { version = "1", features = ["serde"] }
```
## Usage
### Parsing
```rust
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);
```
### Building from typed components
Construction is **infallible**: because each component type is valid by
construction, every combination denotes a valid token.
```rust
use sashite_sin::{Identifier, Letter, Side};
let japanese = Identifier::new(Letter::try_from_char('J')?, Side::Second);
assert_eq!(japanese.encode().as_str(), "j");
```
### Encoding and formatting
`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.
```rust
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`
```
### Validation
```rust
use sashite_sin::Identifier;
assert!(Identifier::is_valid("S"));
assert!(!Identifier::is_valid("WW")); // a token is exactly one letter
```
### Transformations and queries
Every transformation returns a new value (the type is `Copy`, so this is cheap):
```rust
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());
```
## Token format
The grammar (EBNF) is:
```ebnf
sin ::= abbr ;
A token maps to exactly two attributes:
| 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](https://sashite.dev/specs/sin/1.0.0/examples/) for the
conventional mappings (Western, Chinese, Japanese, Siamese).
## Design and guarantees
- **`no_std` and allocation-free.** Parsing borrows the input bytes; an
`Identifier` is a 2-byte `Copy` value and `EncodedSin` keeps 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 `Result` rather than panicking.
- **`const`-friendly.** Construction, parsing, validation, the accessors, and
the transformations are all `const 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.
### Performance
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.
## Related specifications
- [Game Protocol](https://sashite.dev/game-protocol/) — the conceptual foundation
- [SIN Specification v1.0.0](https://sashite.dev/specs/sin/1.0.0/) — the normative document
- [SIN Examples](https://sashite.dev/specs/sin/1.0.0/examples/) — sample style mappings
Reference implementations in other languages are maintained by Sashité:
[Elixir](https://github.com/sashite/sin.ex),
[Ruby](https://github.com/sashite/sin.rb).
If a library's behavior appears to conflict with the specification, the
specification is normative.
## License
Available as open source under the terms of the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).