Skip to main content

README.md

# resp.ex

RESP protocol parser and encoder for Elixir.

![](https://images.unsplash.com/photo-1587654780291-39c9404d746b?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)

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