# RESP3 Parser

```c
#define FIO_RESP3
#include "fio-stl.h"
```

A streaming, callback-based [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md) parser for Redis 6+ and compatible servers. Implemented in [`./004 resp3.h`](https://facil.io/0.8.x/api/004-resp3/). Depends on `FIO_ATOL`, included automatically.

The parser pushes values through a small internal stack, calling your callbacks for primitives, containers, and streaming strings. It does not allocate on its own.

### Configuration Macros

#### `FIO_RESP3_MAX_NESTING`

```c
#ifndef FIO_RESP3_MAX_NESTING
#define FIO_RESP3_MAX_NESTING 32
#endif
```

Maximum nesting depth. Valid range is 2 to 32,768. Determines the size of `fio_resp3_parser_s.stack`.

#### `FIO_RESP3_STREAM_THRESHOLD`

```c
#ifndef FIO_RESP3_STREAM_THRESHOLD
#define FIO_RESP3_STREAM_THRESHOLD 4096
#endif
```

Fixed-length blob strings (`$<len>`, `!<len>`, `=<len>`) larger than this threshold are **streamed incrementally** when the callback table provides the streaming string callbacks (`on_start_string`, `on_string_write`, `on_string_done`): the parser starts the string as soon as its header is parsed and feeds data in chunks as it arrives (`result.consumed` advances per chunk), instead of waiting for the whole blob to be contiguous in the read window. This allows blobs larger than any consumer read buffer.

Blobs at or below the threshold keep the wait-for-contiguity behavior (a single zero-copy `on_string_write`, even across split reads). Callback tables without streaming callbacks are unaffected (they always wait for contiguity).

### RESP3 Type Constants

```c
#define FIO_RESP3_SIMPLE_STR '+'   /* +<string>\r\n           */
#define FIO_RESP3_SIMPLE_ERR '-'   /* -<string>\r\n           */
#define FIO_RESP3_NUMBER     ':'   /* :<number>\r\n           */
#define FIO_RESP3_NULL       '_'   /* _\r\n                   */
#define FIO_RESP3_DOUBLE     ','   /* ,<double>\r\n           */
#define FIO_RESP3_BOOL       '#'   /* #t\r\n or #f\r\n       */
#define FIO_RESP3_BIGNUM     '('   /* (<big number>\r\n      */
#define FIO_RESP3_BLOB_STR   '$'   /* $<len>\r\n<bytes>\r\n  */
#define FIO_RESP3_BLOB_ERR   '!'   /* !<len>\r\n<bytes>\r\n  */
#define FIO_RESP3_VERBATIM   '='   /* =<len>\r\n<type:><bytes>\r\n */
#define FIO_RESP3_ARRAY      '*'   /* *<count>\r\n...         */
#define FIO_RESP3_MAP        '%'   /* %<count>\r\n...         */
#define FIO_RESP3_SET        '~'   /* ~<count>\r\n...         */
#define FIO_RESP3_PUSH       '>'   /* ><count>\r\n...         */
#define FIO_RESP3_ATTR       '|'   /* |<count>\r\n...         */
#define FIO_RESP3_STREAM_CHUNK ';' /* ;<len>\r\n<bytes>\r\n   */
#define FIO_RESP3_STREAM_END '.'   /* .\r\n                   */
```

### Types

#### `fio_resp3_frame_s`

```c
typedef struct {
  void *ctx;
  void *key;
  int64_t remaining;
  uint8_t type;
  uint8_t streaming;
  uint8_t expecting_value;
  uint8_t set_as_map;
} fio_resp3_frame_s;
```

Internal stack frame for a nested container. You do not need to touch this directly, but it is exposed in `fio_resp3_parser_s`.

#### `fio_resp3_parser_s`

```c
typedef struct {
  void *udata;
  uint32_t depth;
  uint8_t error;
  uint8_t streaming_string;
  uint8_t streaming_string_type;
  uint8_t streaming_blob_crlf;
  void *streaming_string_ctx;
  int64_t streaming_remaining;
  fio_resp3_frame_s stack[FIO_RESP3_MAX_NESTING];
} fio_resp3_parser_s;
```

Parser state. Initialize with `{.udata = my_context}` before the first call and reuse it for continuation after partial parses.

**Members:**
- `udata` - user data passed to all callbacks
- `depth` - current nesting depth
- `error` - set to non-zero after a protocol error; the parser will refuse further input
- `streaming_string` - non-zero while a streamed string (`$?` chunked, or a fixed-length blob above `FIO_RESP3_STREAM_THRESHOLD`) is in progress
- `streaming_string_type` - type of the streaming string in progress
- `streaming_blob_crlf` - trailing CRLF bytes pending for a streamed fixed-length blob (0...2)
- `streaming_string_ctx` - context returned by `on_start_string`
- `streaming_remaining` - data bytes remaining for a streamed fixed-length blob (0 when inactive or in `$?` chunked mode)
- `stack` - nested container frames

#### `fio_resp3_callbacks_s`

```c
typedef struct {
  /* primitives */
  void *(*on_null)(void *udata);
  void *(*on_bool)(void *udata, int is_true);
  void *(*on_number)(void *udata, int64_t num);
  void *(*on_double)(void *udata, double num);
  void *(*on_bignum)(void *udata, const void *data, size_t len);
  void *(*on_string)(void *udata, const void *data, size_t len, uint8_t type);
  void *(*on_error)(void *udata, const void *data, size_t len, uint8_t type);
  /* containers */
  void *(*on_array)(void *udata, void *parent_ctx, int64_t len);
  void *(*on_map)(void *udata, void *parent_ctx, int64_t len);
  void *(*on_set)(void *udata, void *parent_ctx, int64_t len);
  void *(*on_push)(void *udata, void *parent_ctx, int64_t len);
  void *(*on_attr)(void *udata, void *parent_ctx, int64_t len);
  /* push into containers */
  int (*array_push)(void *udata, void *ctx, void *value);
  int (*map_push)(void *udata, void *ctx, void *key, void *value);
  int (*set_push)(void *udata, void *ctx, void *value);
  int (*push_push)(void *udata, void *ctx, void *value);
  int (*attr_push)(void *udata, void *ctx, void *key, void *value);
  /* finalize containers */
  void *(*array_done)(void *udata, void *ctx);
  void *(*map_done)(void *udata, void *ctx);
  void *(*set_done)(void *udata, void *ctx);
  void *(*push_done)(void *udata, void *ctx);
  void *(*attr_done)(void *udata, void *ctx);
  /* errors */
  void (*free_unused)(void *udata, void *obj);
  void *(*on_error_protocol)(void *udata);
  /* streaming strings */
  void *(*on_start_string)(void *udata, size_t len, uint8_t type);
  int (*on_string_write)(void *udata, void *ctx, const void *data, size_t len);
  void *(*on_string_done)(void *udata, void *ctx, uint8_t type);
} fio_resp3_callbacks_s;
```

Callback table. Designed to be `static const`. Non-streaming callbacks left `NULL` are replaced with safe no-ops. Primitive no-ops return a non-NULL sentinel; container no-ops do the same; push no-ops return `0`; done no-ops return the context unchanged. The streaming-string callbacks (`on_start_string`, `on_string_write`, and `on_string_done`) are not filled with no-op fallbacks.

If `on_set`, `set_push`, and `set_done` are all `NULL` but map callbacks are provided, the parser treats RESP3 Sets as Maps, calling `map_push(ctx, value, value)` and `map_done`.

#### `fio_resp3_result_s`

```c
typedef struct {
  void *obj;
  size_t consumed;
  int err;
} fio_resp3_result_s;
```

Parse result.

**Members:**
- `obj` - top-level object, or `NULL` while incomplete / on error
- `consumed` - bytes consumed from the buffer
- `err` - non-zero if a protocol error occurred

### API Functions

#### `fio_resp3_parse`

```c
SFUNC fio_resp3_result_s fio_resp3_parse(fio_resp3_parser_s *parser,
                                         const fio_resp3_callbacks_s *callbacks,
                                         const void *buf,
                                         size_t len);
```

Parses as much RESP3 data as possible. State is preserved in `parser`, so you can call again with the remaining bytes after a partial read.

**Parameters:**
- `parser` - parser state
- `callbacks` - callback table (may be `NULL`; non-streaming callbacks become no-ops, while streaming-string callbacks remain unset)
- `buf` - input buffer
- `len` - input length

**Returns:** parse result.

**Note:** streamed strings (`$?`) require `on_start_string`, `on_string_write`, and `on_string_done` to be usable together. If `on_start_string` is missing or returns `NULL`, the parser sets `err` because it cannot buffer an unknown-length value. Once `on_start_string` returns a context, the parser enters streaming-string mode and later calls `on_string_write` and `on_string_done` directly, so those callbacks must also be supplied. For fixed-length blob, blob-error, and verbatim strings, `on_start_string` is optional; if it is missing or returns `NULL`, the parser falls back to `on_string` or `on_error`. If it returns a context for a fixed-length string, `on_string_write` and `on_string_done` are also called directly. Additionally, fixed-length blobs larger than `FIO_RESP3_STREAM_THRESHOLD` are always streamed incrementally when `on_start_string` is available (multiple `on_string_write` calls across parse calls possible), while smaller blobs complete in a single write once contiguous.

**Note:** top-level attributes (`|`) are delivered via callbacks but do not become the returned `obj`; parsing continues for the following reply.

### Example

```c
#define FIO_RESP3
#include "fio-stl.h"

static void *on_null(void *u) { (void)u; return (void *)1; }
static void *on_number(void *u, int64_t n) { (void)u; printf("num %lld\n", (long long)n); return (void *)1; }
static void *on_string(void *u, const void *d, size_t l, uint8_t t) {
  (void)u; (void)t;
  printf("str %.*s\n", (int)l, (const char *)d);
  return (void *)1;
}
static void *on_array(void *u, void *c, int64_t l) { (void)u; (void)c; (void)l; return (void *)2; }
static int array_push(void *u, void *c, void *v) { (void)u; (void)c; (void)v; return 0; }
static void *array_done(void *u, void *c) { (void)u; return c; }

int main(void) {
  static const fio_resp3_callbacks_s cb = {
    .on_null = on_null, .on_number = on_number, .on_string = on_string,
    .on_array = on_array, .array_push = array_push, .array_done = array_done,
  };
  const char *data = "*2\r\n:42\r\n$5\r\nhello\r\n";
  fio_resp3_parser_s p = {0};
  fio_resp3_result_s r = fio_resp3_parse(&p, &cb, data, strlen(data));
  printf("consumed=%zu err=%d obj=%p\n", r.consumed, r.err, r.obj);
  return 0;
}
```

------------------------------------------------------------
