README.md

# qpi.ex

[![Hex.pm](https://img.shields.io/hexpm/v/sashite_qpi.svg)](https://hex.pm/packages/sashite_qpi)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/sashite_qpi)
[![License](https://img.shields.io/hexpm/l/sashite_qpi.svg)](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).