# resp.ex
RESP protocol parser and encoder for Elixir.

Zero-dependency implementation covering 100% of the Redis Serialization Protocol specification.
## Installation
```elixir
def deps do
[{:resp, "~> 0.1.0"}]
end
```
## Usage
### Parsing
```elixir
# Parse a RESP2 command (returns Resp.Array with Resp.Bulk args)
{:ok, arr, ""} = Resp.parse("*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n")
arr.count #=> 2
Enum.map(arr.data, & &1.data) #=> ["GET", "key"]
# Parse any RESP3 typed value
{:ok, val, ""} = Resp.parse_value("+OK\r\n") #=> %Resp.String{data: "OK"}
{:ok, val, ""} = Resp.parse_value("#t\r\n") #=> %Resp.Bool{data: true}
{:ok, val, ""} = Resp.parse_value("_\r\n") #=> %Resp.Null{version: :resp3}
# Continuation-based parsing for TCP streams
{:continuation, cont} = Resp.parse("*1\r\n$4\r\nPIN")
{:ok, cmd, ""} = cont.("G\r\n") # completes the PING
```
### Encoding
```elixir
Resp.encode_string("OK") #=> "+OK\r\n"
Resp.encode_error("ERR bad") #=> "-ERR bad\r\n"
Resp.encode_integer(42) #=> ":42\r\n"
Resp.encode_bulk("hello") #=> "$5\r\nhello\r\n"
Resp.encode_null() #=> "$-1\r\n" (RESP2)
Resp.encode_null3() #=> "_\r\n" (RESP3)
Resp.encode_bool(true) #=> "#t\r\n"
Resp.encode_double(3.14) #=> ",3.14\r\n"
Resp.encode_big_number("34928") #=> "(34928\r\n"
Resp.encode_array(2) #=> "*2\r\n" (header, follow with elements)
Resp.encode_map(1) #=> "%1\r\n" (header, follow with key-value pairs)
Resp.encode_verbatim_string("txt", "Some string")
#=> "=15\r\ntxt:Some string\r\n"
Resp.encode_blob_error("ERR") #=> "!3\r\nERR\r\n"
# Aggregate headers (follow with elements)
Resp.encode_set(2) #=> "~2\r\n"
Resp.encode_push(3) #=> ">3\r\n"
Resp.encode_attribute(1) #=> "|1\r\n"
# Raw passthrough
Resp.encode_raw("+OK\r\n") #=> "+OK\r\n"
# Type-dispatch encoding with mode awareness
Resp.encode_any("hello") #=> "$5\r\nhello\r\n" (defaults to RESP3)
Resp.encode_any(nil, :resp2) #=> "$-1\r\n"
Resp.encode_any(nil, :resp3) #=> "_\r\n"
Resp.encode_any(true, :resp2) #=> ":1\r\n"
Resp.encode_any(true, :resp3) #=> "#t\r\n"
Resp.encode_any(["a", "b"]) #=> "*2\r\n$1\r\na\r\n$1\r\nb\r\n"
Resp.encode_any(%{foo: 1}) #=> "%1\r\n$3\r\nfoo\r\n:1\r\n"
# Pack a command as RESP2 array of bulk strings
Resp.pack(["SET", "key", "value"])
#=> "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"
```
### Streaming (RESP3)
```elixir
# Streamed string
{:streamed, fun, ""} = Resp.parse_stream("$?\r\n")
{:chunk, "foo", ""} = fun.("$3\r\nfoo\r\n")
{:done, ""} = fun.(".\r\n")
# Pipelining with continuation
{:continuation, cont} = Resp.parse("*1\r\n$4\r\nPING\r\n*1\r\n$4\r\nPONG\r\n")
{:ok, cmd1, rest} = cont.("")
{:ok, cmd2, ""} = Resp.parse(rest)
```
### HELLO response
```elixir
Resp.hello(:resp2) #=> RESP2 array of server info
Resp.hello(:resp3) #=> RESP3 map of server info
```
## Supported Types
| Type | Code | Parse | Encode | Struct |
|------|------|-------|--------|--------|
| Simple String | `+` | ✓ | ✓ | `Resp.String` |
| Error | `-` | ✓ | ✓ | `Resp.Error` |
| Integer | `:` | ✓ | ✓ | `Resp.Integer` |
| Bulk String | `$` | ✓ | ✓ | `Resp.Bulk` |
| Null (RESP2) | `$-1` | ✓ | ✓ | `Resp.Null` |
| Null (RESP3) | `_` | ✓ | ✓ | `Resp.Null` |
| nil | `$-1` or `_` | — | — | literal `nil` |
| Array | `*` | ✓ | ✓ | `Resp.Array` |
| Boolean | `#` | ✓ | ✓ | `Resp.Bool` |
| Double | `,` | ✓ | ✓ | `Resp.Double` |
| Big Number | `(` | ✓ | ✓ | `Resp.BigNumber` |
| Blob Error | `!` | ✓ | ✓ | `Resp.BlobError` |
| Verbatim String | `=` | ✓ | ✓ | `Resp.VerbatimString` |
| Map | `%` | ✓ | ✓ | `Resp.Map` |
| Set | `~` | ✓ | ✓ | `Resp.Set` |
| Push | `>` | ✓ | ✓ | `Resp.Push` |
| Attribute | `\|` | ✓ | ✓ | `Resp.Attribute` |
| Streamed String | `$?` | ✓ | — | chunk function |
| Streamed Array | `*?` | ✓ | — | chunk function |
| Streamed Map | `%?` | ✓ | — | chunk function |
| Streamed Set | `~?` | ✓ | — | chunk function |
| Streamed Push | `>?` | ✓ | — | chunk function |
| Streamed Attr | `\|?` | ✓ | — | chunk function |
## Development
```bash
# Run all CI checks locally
just ci
# Or run individual steps
mix format --check-formatted
mix credo --strict
mix test
# Pull the latest Redis reference for validation
bash scripts/pull-redis-reference.sh
# Validate against the reference
mix test test/resp/reference_test.exs
```
## License
MIT