# `DrinkMeAlice.Json`

Bounded decoder/encoder boundary for SurrealDB JSON-RPC payloads.

Surreal replies are
ordinary JSON (floats may appear, integer tokens may exceed i64, object
key order is unspecified), so this decoder runs OTP `:json.decode/3`
behind explicit structural and decoded-term limits:

- input bytes bounded by the caller (the WebSocket frame guard already
  caps a message at `max_bytes`, default 1048576); checked again
  here so the bound does not depend on transport state;
- container depth <= 16 (RPC envelopes plus one level of record
  content stay far below this);
- object keys <= 128 per object, duplicate *decoded* keys rejected;
- aggregate document elements <= 65536;
- each decoded string <= 65536 bytes and valid UTF-8;
- integer tokens <= 40 digits *excluding the optional sign*
  (Erlang integers are exact at any width; the bound stops digit-string
  amplification);
- float tokens <= 64 bytes, decoded with `Float` semantics; the dialect may
  legally carry floats, so they are accepted and *counted* rather than
  rejected by shape.

Two enforcement layers, honestly labeled:

1. `preflight/1` — a single linear, allocation-free scan over the raw
   bytes *before* `:json.decode` runs. It counts every string
   literal's DECODED UTF-8 size (escape-aware: `\uXXXX` pairs count
   at their recombined width, so a 40 KiB value of `\"` escapes —
   whose raw span doubles its decoded size — passes while an oversized
   decoded value is rejected) and container nesting, rejecting
   violations before OTP has allocated anything for them. This is the
   pre-allocation bound — the string *callback* below is only a
   post-construction backstop, and is documented as such.
2. Decode callbacks — container start/push and every scalar enforce
   the bounds *while* decoding, aborting before an unbounded term is
   built. `term_words/1` and process-memory measurements in the tests
   supply the decode-amplification evidence the card requires.

Encoding (`encode/1`) accepts only canonical terms under the same
structural bounds, checked before OTP `:json.encode/1` runs.

# `json`

```elixir
@type json() :: map() | list() | binary() | integer() | float() | boolean() | nil
```

# `bounds`

```elixir
@spec bounds() :: map()
```

The declared structural/decoded-term bounds for this profile.

# `decode`

```elixir
@spec decode(binary()) :: {:ok, json()} | {:error, DrinkMeAlice.Error.t()}
```

Decode one Surreal-dialect JSON document under the bounded profile.

`{:ok, term}` | `{:error, %Error{}}` — `malformed_message` for syntax
violations, `bounded_limit_exceeded` for bound breaches, both with a
bounded tag and never an inspected input fragment.

# `encode`

```elixir
@spec encode(term()) :: {:ok, binary()} | {:error, DrinkMeAlice.Error.t()}
```

Encode an outbound message. Only canonical terms travel: string-keyed
maps, lists, valid UTF-8 binaries, i64 integers, booleans and `nil` —
no floats, atoms or structs (send those as strings with a SurrealQL
cast). The decode bounds apply (depth, keys, string bytes, elements),
checked before OTP `:json.encode/1` runs; output is at most
1048576 bytes.

# `max_bytes`

```elixir
@spec max_bytes() :: pos_integer()
```

Configured input byte bound (the 1 MiB message bound).

# `preflight`

```elixir
@spec preflight(binary()) :: :ok | {:error, DrinkMeAlice.Error.t()}
```

Lexical pre-pass over raw input before `:json.decode` allocates.
Returns `:ok` or `{:error, %Error{}}`. Enforces, allocation-free:

- every string literal's *decoded* UTF-8 size <= 65536 bytes —
  counted while scanning, so a 40 KiB value of `\"` escapes (whose
  raw span is double its decoded size) passes while an oversized
  decoded value is rejected before OTP allocates it. Escape widths:
  `\X` → 1 byte; `\uXXXX` → the code point's UTF-8 width (1–3),
  with a high surrogate held pending so a `\uD800..\uDBFF` +
  `\uDC00..\uDFFF` pair counts exactly 4. An unterminated string
  keeps counting to end of input — oversized decoded content is
  rejected at the bound even though the malformed literal would fail
  decode anyway;
- container nesting depth <= 16.

This is a bound check, not a validator — malformed JSON still reaches
the decoder, which rejects it through the same typed error surface.

# `term_words`

```elixir
@spec term_words(term()) :: non_neg_integer()
```

Heap size of a decoded term in machine words
(`:erts_debug.flat_size`, 8 bytes/word on this target). Honest scope:
this counts *heap* words only — off-heap binary bytes (>64B
refc binaries) are excluded by `flat_size`. RSS/binary-allocation
evidence is a separate Q01 measurement; this helper must not be cited
as covering it.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
