# qpi.ex
[](https://hex.pm/packages/sashite_qpi)
[](https://hexdocs.pm/sashite_qpi)
[](https://github.com/sashite/qpi.ex/blob/main/LICENSE)
> **QPI** (Qualified Piece Identifier) implementation for Elixir.
## Overview
This library implements the [QPI Specification v1.0.0](https://sashite.dev/specs/qpi/1.0.0/).
QPI provides complete piece identification by combining two primitive notations:
- [SIN](https://sashite.dev/specs/sin/1.0.0/) (Style Identifier Notation) — identifies the piece style
- [PIN](https://sashite.dev/specs/pin/1.0.0/) (Piece Identifier Notation) — identifies the piece attributes
A QPI identifier is a **pair of (SIN, PIN)** that encodes complete **Piece Identity**.
## Installation
Add `sashite_qpi` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:sashite_qpi, "~> 1.0"}
]
end
```
## Dependencies
```elixir
{:sashite_sin, "~> 2.0"} # Style Identifier Notation
{:sashite_pin, "~> 2.0"} # Piece Identifier Notation
```
## Usage
### Parsing (String → Identifier)
Convert a QPI string into an `Identifier` struct.
```elixir
# Standard parsing (returns {:ok, identifier} or {:error, reason})
{:ok, qpi} = Sashite.Qpi.parse("C:K^")
qpi.sin.style # => :C (Piece Style)
qpi.pin.type # => :K (Piece Name)
qpi.pin.side # => :first (Piece Side)
qpi.pin.state # => :normal (Piece State)
qpi.pin.terminal # => true (Terminal Status)
# Components are full SIN and PIN structs
Sashite.Sin.Identifier.first_player?(qpi.sin) # => true
Sashite.Pin.Identifier.enhanced?(qpi.pin) # => false
# Bang version (raises on error)
qpi = Sashite.Qpi.parse!("C:K^")
# Invalid input
{:error, :empty_input} = Sashite.Qpi.parse("")
{:error, :missing_separator} = Sashite.Qpi.parse("CK")
Sashite.Qpi.parse!("invalid") # => raises ArgumentError
```
### Formatting (Identifier → String)
Convert an `Identifier` back to a QPI string.
```elixir
alias Sashite.Qpi.Identifier
# From components
sin = Sashite.Sin.parse!("C")
pin = Sashite.Pin.parse!("K^")
qpi = Identifier.new(sin, pin)
Identifier.to_string(qpi) # => "C:K^"
# With attributes
sin = Sashite.Sin.parse!("s")
pin = Sashite.Pin.parse!("+r")
qpi = Identifier.new(sin, pin)
Identifier.to_string(qpi) # => "s:+r"
```
### Validation
```elixir
# Boolean check
Sashite.Qpi.valid?("C:K^") # => true
Sashite.Qpi.valid?("s:+r") # => true
Sashite.Qpi.valid?("invalid") # => false
Sashite.Qpi.valid?("C:") # => false
Sashite.Qpi.valid?(":K") # => false
```
### Accessing Components
```elixir
{:ok, qpi} = Sashite.Qpi.parse("S:+R^")
# Get components (struct fields)
qpi.sin # => %Sashite.Sin.Identifier{style: :S, side: :first}
qpi.pin # => %Sashite.Pin.Identifier{type: :R, side: :first, state: :enhanced, terminal: true}
# Serialize components
Sashite.Sin.Identifier.to_string(qpi.sin) # => "S"
Sashite.Pin.Identifier.to_string(qpi.pin) # => "+R^"
Sashite.Qpi.Identifier.to_string(qpi) # => "S:+R^"
```
### Five Piece Identity Attributes
All attributes come directly from the components:
```elixir
{:ok, qpi} = Sashite.Qpi.parse("S:+R^")
# From SIN component
qpi.sin.style # => :S (Piece Style)
# From PIN component
qpi.pin.type # => :R (Piece Name)
qpi.pin.side # => :first (Piece Side)
qpi.pin.state # => :enhanced (Piece State)
qpi.pin.terminal # => true (Terminal Status)
```
### Native and Derived Relationship
QPI defines a deterministic relationship based on case comparison between SIN and PIN letters.
```elixir
alias Sashite.Qpi.Identifier
{:ok, qpi} = Sashite.Qpi.parse("C:K^")
# Access the relationship
qpi.sin.side # => :first (derived from SIN letter case)
Identifier.native?(qpi) # => true (sin.side == pin.side)
Identifier.derived?(qpi) # => false
# Native: SIN case matches PIN case
Sashite.Qpi.parse!("C:K") |> Identifier.native?() # => true (both uppercase/first)
Sashite.Qpi.parse!("c:k") |> Identifier.native?() # => true (both lowercase/second)
# Derived: SIN case differs from PIN case
Sashite.Qpi.parse!("C:k") |> Identifier.derived?() # => true (uppercase vs lowercase)
Sashite.Qpi.parse!("c:K") |> Identifier.derived?() # => true (lowercase vs uppercase)
```
### Transformations
All transformations return new immutable structs.
```elixir
alias Sashite.Qpi.Identifier
qpi = Sashite.Qpi.parse!("C:K^")
# Replace SIN component
new_sin = Sashite.Sin.parse!("S")
Identifier.with_sin(qpi, new_sin) |> Identifier.to_string() # => "S:K^"
# Replace PIN component
new_pin = Sashite.Pin.parse!("+Q^")
Identifier.with_pin(qpi, new_pin) |> Identifier.to_string() # => "C:+Q^"
# Flip both components (change player)
Identifier.flip(qpi) |> Identifier.to_string() # => "c:k^"
# Native/Derived transformations
qpi = Sashite.Qpi.parse!("C:r")
Identifier.native(qpi) |> Identifier.to_string() # => "C:R" (PIN case aligned with SIN case)
Identifier.derive(qpi) |> Identifier.to_string() # => "C:r" (already derived, unchanged)
qpi = Sashite.Qpi.parse!("C:R")
Identifier.native(qpi) |> Identifier.to_string() # => "C:R" (already native, unchanged)
Identifier.derive(qpi) |> Identifier.to_string() # => "C:r" (PIN case differs from SIN case)
```
### Transform via Components
```elixir
alias Sashite.Qpi.Identifier
alias Sashite.Sin.Identifier, as: SinId
alias Sashite.Pin.Identifier, as: PinId
qpi = Sashite.Qpi.parse!("C:K^")
# Transform SIN via component
Identifier.with_sin(qpi, SinId.with_style(qpi.sin, :S)) |> Identifier.to_string() # => "S:K^"
# Transform PIN via component
Identifier.with_pin(qpi, PinId.with_type(qpi.pin, :Q)) |> Identifier.to_string() # => "C:Q^"
Identifier.with_pin(qpi, PinId.with_state(qpi.pin, :enhanced)) |> Identifier.to_string() # => "C:+K^"
Identifier.with_pin(qpi, PinId.with_terminal(qpi.pin, false)) |> Identifier.to_string() # => "C:K"
```
### Component Queries
Since QPI is a composition, use the component APIs directly:
```elixir
alias Sashite.Sin.Identifier, as: SinId
alias Sashite.Pin.Identifier, as: PinId
{:ok, qpi} = Sashite.Qpi.parse("S:+P^")
# SIN queries (style and side)
qpi.sin.style # => :S
qpi.sin.side # => :first
SinId.first_player?(qpi.sin) # => true
SinId.letter(qpi.sin) # => "S"
# PIN queries (type, state, terminal)
qpi.pin.type # => :P
qpi.pin.state # => :enhanced
qpi.pin.terminal # => true
PinId.enhanced?(qpi.pin) # => true
PinId.letter(qpi.pin) # => "P"
PinId.prefix(qpi.pin) # => "+"
PinId.suffix(qpi.pin) # => "^"
# Compare QPIs via components
{:ok, other} = Sashite.Qpi.parse("C:+P^")
SinId.same_style?(qpi.sin, other.sin) # => false (S vs C)
PinId.same_type?(qpi.pin, other.pin) # => true (both P)
SinId.same_side?(qpi.sin, other.sin) # => true (both first)
PinId.same_state?(qpi.pin, other.pin) # => true (both enhanced)
```
## API Reference
### Types
```elixir
# Identifier represents a parsed QPI with complete Piece Identity.
%Sashite.Qpi.Identifier{
sin: %Sashite.Sin.Identifier{}, # SIN component
pin: %Sashite.Pin.Identifier{} # PIN component
}
# Create an Identifier from SIN and PIN components.
# Raises ArgumentError if components are invalid.
@spec Sashite.Qpi.Identifier.new(Sin.Identifier.t(), Pin.Identifier.t()) :: t()
```
### Parsing
```elixir
# Parses a QPI string into an Identifier.
# Returns {:ok, identifier} or {:error, reason}.
@spec Sashite.Qpi.parse(String.t()) :: {:ok, Identifier.t()} | {:error, atom()}
# Parses a QPI string into an Identifier.
# Raises ArgumentError if the string is not valid.
@spec Sashite.Qpi.parse!(String.t()) :: Identifier.t()
```
### Validation
```elixir
# Reports whether string is a valid QPI.
@spec Sashite.Qpi.valid?(term()) :: boolean()
```
### Transformations
All transformations return new `%Sashite.Qpi.Identifier{}` structs:
```elixir
# Component replacement
@spec with_sin(t(), Sin.Identifier.t()) :: t()
@spec with_pin(t(), Pin.Identifier.t()) :: t()
# Flip transformation (transforms both components)
@spec flip(t()) :: t()
# Native/Derived transformations
@spec native(t()) :: t()
@spec derive(t()) :: t()
```
### Queries
```elixir
# Native/Derived queries
@spec native?(t()) :: boolean()
@spec derived?(t()) :: boolean()
```
### Errors
Parsing returns `{:error, reason}` tuples with these atoms:
| Reason | Cause |
|--------|-------|
| `:empty_input` | String length is 0 |
| `:missing_separator` | No `:` found in string |
| `:missing_sin` | Nothing before `:` |
| `:missing_pin` | Nothing after `:` |
| `:invalid_sin` | SIN parsing failed |
| `:invalid_pin` | PIN parsing failed |
## Piece Identity Mapping
QPI encodes complete **Piece Identity** as defined in the [Glossary](https://sashite.dev/glossary/):
| Piece Attribute | QPI Access | Encoding |
|---------------------|----------------------|--------------------------------------------------------|
| **Piece Style** | `qpi.sin.style` | SIN letter (case-insensitive identity) |
| **Piece Name** | `qpi.pin.type` | PIN letter (case-insensitive identity) |
| **Piece Side** | `qpi.pin.side` | PIN letter case (uppercase = first, lowercase = second)|
| **Piece State** | `qpi.pin.state` | PIN modifier (`+` = enhanced, `-` = diminished) |
| **Terminal Status** | `qpi.pin.terminal` | PIN marker (`^` = terminal) |
Additionally, QPI provides a **Native/Derived relationship** via `native?/1`, `derived?/1`, `native/1`, and `derive/1`.
## Design Principles
- **Pure composition**: QPI composes SIN and PIN without reimplementing features
- **Minimal API**: Core functions (`native?`, `derived?`, `native`, `derive`, `to_string`) plus transformations
- **Component transparency**: Access components directly via struct fields
- **QPI-specific conveniences**: `flip/1`, `native/1`, `derive/1` (operations that span both components)
- **Functional style**: Pure functions, immutable structs
- **Elixir idioms**: `{:ok, _}` / `{:error, _}` tuples, `parse!` bang variant
- **Pipe-friendly**: Transformations designed for `|>` operator
- **No duplication**: Delegates to `sashite_sin` and `sashite_pin`
## Related Specifications
- [Game Protocol](https://sashite.dev/game-protocol/) — Conceptual foundation
- [QPI Specification](https://sashite.dev/specs/qpi/1.0.0/) — Official specification
- [QPI Examples](https://sashite.dev/specs/qpi/1.0.0/examples/) — Usage examples
- [SIN Specification](https://sashite.dev/specs/sin/1.0.0/) — Style component
- [PIN Specification](https://sashite.dev/specs/pin/1.0.0/) — Piece component
## License
Available as open source under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).