Skip to main content

src/glazer_json.erl

-module(glazer_json).
-moduledoc """
Fast JSON encoding and decoding using the glaze C++ library.

By default `null`s are represented as the atom `null`. To change it
application-wide, set the `null` env key in your config:
```
{glazer, [{null, nil}]}.
```

## Features

- Decoding straight to Erlang terms: maps, lists, binaries, integers
  (including bignums), floats, booleans, and `null`
- Encoding Erlang terms straight to JSON, including big integers
- Incremental/streaming decoding of partial input (e.g. NDJSON over a
  socket) via `stream_decoder/0,1`, `stream_feed/2`, `stream_eof/1`
- Configurable representation of JSON `null` and JSON object keys
- `minify/1` and `prettify/1` helpers
- `read_file/1,2` and `write_file/2,3` helpers for decoding/encoding
  directly to/from a file
- `query/2,3`: run a [jq](https://jqlang.org/) filter over a JSON
  document, returning decoded Erlang terms (requires `glazer` to be built
  with `libjq` available)
""".
-export([decode/1, decode/2, try_decode/1, try_decode/2,
         encode/1, encode/2, minify/1, prettify/1,
         query/2, query/3,
         scan/1, scan/2,
         read_file/1, read_file/2, write_file/2, write_file/3,
         stream_decoder/0, stream_decoder/1, stream_feed/2, stream_eof/1,
         'decode!'/1, 'encode!'/1, 'encode_to_iodata!'/1]).

-type decode_opt() ::
    object_as_tuple
  | use_nil
  | {null_term, atom()}
  | {keys, atom | existing_atom | binary}
  | dedupe_keys
  | copy_strings
  | return_trailer
  | validate_utf8
  | skip_utf8_validation.

-doc """
Decode options:

- `object_as_tuple`       - decode JSON objects as `{[{K, V}]}` proplists rather than maps
- `use_nil`               - use the atom `nil` for JSON null
- `{null_term, Atom}`     - use `Atom` for JSON null
- `{keys, atom}`          - decode object keys as atoms
- `{keys, existing_atom}` - decode keys as existing atoms, fall back to binary
- `{keys, binary}`        - decode keys as binaries (default)
- `dedupe_keys`           - with `object_as_tuple`, eliminate duplicate object
  keys from the resulting proplist, keeping the last occurrence's value
  (and position). Has no effect when objects are decoded as maps (the
  default) or with `{keys, atom | existing_atom}`: a JSON object with
  duplicate keys is always deduped (last value wins) when decoded to a map,
  since maps cannot represent duplicate keys.
- `copy_strings`          - always allocate a fresh binary for each decoded
  string value, rather than returning a sub-binary that references the
  original input. By default (without this option) unescaped strings are
  zero-copy sub-binaries of the input, which is faster but keeps the entire
  input binary alive in memory as long as any decoded string referencing it
  is reachable. Use `copy_strings` when decoded strings are long-lived and
  the input is large, to allow the GC to reclaim the input buffer
  independently.
- `return_trailer`        - allow (instead of rejecting) trailing
  non-whitespace data after the decoded value. On a match, the successful
  result is `{has_trailer, Term, Rest}` where `Rest` is the unconsumed
  remainder of the input as a zero-copy sub-binary. Without this option,
  trailing non-whitespace data after a complete value is a parse error.
  Useful for decoding one JSON value off the front of a buffer (e.g. a
  newline-delimited stream) without a separate `scan/1,2` pass to find
  where the value ends first.
- `validate_utf8`         - enable UTF-8 validation for JSON strings.
  When enabled, invalid UTF-8 sequences in string values or object keys
  cause a parse error instead of being silently accepted, matching the
  behavior of OTP's json module and other JSON parsers. By default,
  UTF-8 validation is disabled for backward compatibility and performance.
- `skip_utf8_validation`  - explicitly disable UTF-8 validation for JSON
  strings (this is the default behavior). This option is provided for
  clarity when UTF-8 validation behavior might be changed in future versions.
""".
-type decode_opts() :: [decode_opt()].

-type encode_opt() ::
    pretty
  | uescape
  | force_utf8
  | escape_fwd_slash
  | use_nil
  | {null_term, atom()}.

-doc """
Encode options:

- `pretty`            - pretty-print the JSON output
- `uescape`           - escape non-ASCII characters as \\uXXXX sequences
- `force_utf8`        - replace invalid UTF-8 byte sequences with the
  Unicode replacement character (U+FFFD) before encoding. Without this
  option, invalid bytes in binaries are copied into the output verbatim,
  which can produce a result that is not valid UTF-8/JSON. A pre-existing
  literal U+FFFD in the input is left untouched (not double-replaced). When
  combined with `uescape`, the replacement character is further escaped to
  `\\ufffd`. This is an *encode*-only option: for UTF-8 validation during
  decoding, use `validate_utf8` in decode options (disabled by default)
- `escape_fwd_slash`  - escape forward slashes (`/`) as `\\/` in JSON strings,
  which is valid per RFC 8259 §7. By default, forward slashes are not escaped
- `use_nil`           - encode the atom `nil` as JSON `null`
- `{null_term, Atom}` - encode `Atom` as JSON `null`
""".
-type encode_opts() :: [encode_opt()].

-type query_reason() ::
    enomem
  | jq_not_available
  | jq_decode_error
  | {jq_compile_error, binary()}
  | invalid_input
  | binary().

-export_type([decode_opt/0, decode_opts/0, encode_opt/0, encode_opts/0, query_reason/0,
               scan_state/0, stream_decoder/0]).

-type scan_state() :: tuple().

-record(stream_decoder, {
  opts   = []        :: decode_opts(),
  buffer = <<>>      :: binary(),
  state  = undefined :: scan_state() | undefined
}).

-opaque stream_decoder() :: #stream_decoder{}.

-doc """
Decode a JSON binary or iolist to an Erlang term. JSON objects are returned as
maps (default). Raises `{parse_error, Msg}` on invalid input.

## Examples

```erlang
1> glazer_json:decode(<<"{\"a\":1,\"b\":[true,null,3.5]}">>).
#{<<"a">> => 1, <<"b">> => [true, null, 3.5]}

2> glazer_json:decode(<<"not json">>).
** exception error: {parse_error,<<"...">>}
```
""".
-spec decode(binary() | iolist()) -> term().
decode(Input) ->
  case try_decode(Input) of
    {ok,    Term}   -> Term;
    {error, Reason} -> error({parse_error, Reason})
  end.

-doc """
Decode a JSON binary or iolist to an Erlang term with options
(see `t:decode_opts/0`). Raises `{parse_error, Reason}` on invalid input.

## Examples

```erlang
%% Object keys as atoms
1> glazer_json:decode(<<"{\"a\":1}">>, [{keys, atom}]).
#{a => 1}

%% JSON null as the atom `nil`
2> glazer_json:decode(<<"{\"a\":null}">>, [use_nil]).
#{<<"a">> => nil}

%% Objects as jiffy-style proplist tuples
3> glazer_json:decode(<<"{\"a\":1}">>, [object_as_tuple]).
{[{<<"a">>, 1}]}

%% Trailing data returned alongside the decoded value instead of erroring
4> glazer_json:decode(<<"1 2">>, [return_trailer]).
{has_trailer, 1, <<"2">>}
```
""".
-spec decode(binary() | iolist(), decode_opts()) -> term().
decode(Input, Opts) ->
  case try_decode(Input, Opts) of
    {ok,    Term}   -> Term;
    {error, Reason} -> error({parse_error, Reason})
  end.

-doc """
Decode a JSON binary to an Erlang term. Equivalent to `decode/1`, provided
for API parity with Elixir's `JSON.decode!/1`. Raises `{parse_error,
Reason}` on invalid input.

## Examples

```erlang
1> glazer_json:'decode!'(<<"{\"a\":1}">>).
#{<<"a">> => 1}
```
""".
-spec 'decode!'(binary() | iolist()) -> term().
'decode!'(Input) ->
  decode(Input, [use_nil]).

-doc """
Decode a JSON binary or iolist, returning `{ok, Term}` or
`{error, Reason}` instead of raising.

## Examples

```erlang
1> glazer_json:try_decode(<<"{\"a\":1}">>).
{ok, #{<<"a">> => 1}}

2> glazer_json:try_decode(<<"not json">>).
{error, <<"...">>}
```
""".
-spec try_decode(binary() | iolist()) -> {ok, term()} | {error, binary()}.
try_decode(Input) ->
  glazer:json_try_decode(Input).

-doc """
Decode a JSON binary or iolist with options (see `t:decode_opts/0`),
returning `{ok, Term}` or `{error, Reason}` instead of raising.

## Examples

```erlang
1> glazer_json:try_decode(<<"{\"a\":1}">>, [{keys, atom}]).
{ok, #{a => 1}}

2> glazer_json:try_decode(<<"not json">>, [{keys, atom}]).
{error, <<"...">>}

%% Trailing data returned alongside the decoded value instead of erroring
3> glazer_json:try_decode(<<"1 2">>, [return_trailer]).
{ok, {has_trailer, 1, <<"2">>}}
```
""".
-spec try_decode(binary() | iolist(), decode_opts()) -> {ok, term()} | {error, binary()}.
try_decode(Input, Opts) ->
  glazer:json_try_decode(Input, Opts).

-doc """
Encode an Erlang term to a JSON binary.

Raises `{encode_error, {Msg, Term}}` if `Data` contains a value that
cannot be represented as JSON (e.g. an improper list, a pid, or an
unsupported tuple).

## Examples

```erlang
1> glazer_json:encode(#{<<"a">> => 1, <<"b">> => [true, null, 3.5]}).
<<"{\"a\":1,\"b\":[true,null,3.5]}">>

2> glazer_json:encode(<<"hello">>).
<<"\"hello\"">>

3> glazer_json:encode(123456789012345678901234567890).
<<"123456789012345678901234567890">>

4> glazer_json:encode([1|2]).
** exception error: {encode_error,{<<"improper list">>,2}}
```
""".
-spec encode(term()) -> binary().
encode(Data) ->
  glazer:json_encode(Data).

-doc """
Encode an Erlang term to a JSON binary with options (see `t:encode_opts/0`).

Raises `{encode_error, {Msg, Term}}` if `Data` contains a value that
cannot be represented as JSON.

## Examples

```erlang
%% Pretty-print with two-space indentation
1> glazer_json:encode(#{a => 1}, [pretty]).
<<"{\n  \"a\": 1\n}">>

%% Escape non-ASCII characters as \\uXXXX
2> glazer_json:encode(<<"héllo"/utf8>>, [uescape]).
<<"\"h\\u00e9llo\"">>

%% Encode the atom `nil` as JSON null
3> glazer_json:encode(#{<<"a">> => nil}, [use_nil]).
<<"{\"a\":null}">>

%% A binary with an invalid UTF-8 byte (0x80 is a lone continuation byte)
%% is copied through verbatim by default, yielding output that is not
%% valid UTF-8/JSON
4> glazer_json:encode(<<"a", 128, "b">>).
<<"\"a", 128, "b\"">>

%% force_utf8 replaces the invalid byte with U+FFFD (encoded as 0xEF 0xBF 0xBD)
5> glazer_json:encode(<<"a", 128, "b">>, [force_utf8]).
<<"\"a", 239, 191, 189, "b\"">>

%% force_utf8 + uescape further escapes the replacement character
6> glazer_json:encode(<<"a", 128, "b">>, [force_utf8, uescape]).
<<"\"a\\ufffdb\"">>

%% Escape forward slashes in JSON strings
7> glazer_json:encode(<<"https://example.com/path">>, [escape_fwd_slash]).
<<"\"https:\\/\\/example.com\\/path\"">>
```
""".
-spec encode(term(), encode_opts()) -> binary().
encode(Data, Opts) ->
  glazer:json_encode(Data, Opts).

-doc """
Encode an Erlang term to a JSON binary. Equivalent to `encode/1`, provided
for API parity with Elixir's `JSON.encode!/1`. Raises `{encode_error, Msg}`
if `Data` cannot be encoded.

## Examples

```erlang
1> glazer_json:'encode!'(#{<<"a">> => 1}).
<<"{\"a\":1}">>
```
""".
-spec 'encode!'(term()) -> binary().
'encode!'(Data) ->
  encode(Data, [use_nil]).

-doc """
Encode an Erlang term to JSON as iodata. Equivalent to `encode/1` (which
already returns a binary, itself valid iodata), provided for API parity with
Elixir's `JSON.encode_to_iodata!/1`. Raises `{encode_error, Msg}` if `Data`
cannot be encoded.

## Examples

```erlang
1> glazer_json:'encode_to_iodata!'(#{<<"a">> => 1}).
<<"{\"a\":1}">>
```
""".
-spec 'encode_to_iodata!'(term()) -> iodata().
'encode_to_iodata!'(Data) ->
  encode(Data, [use_nil]).

-doc """
Minify a JSON binary or iolist, removing all unnecessary whitespace.

## Examples

```erlang
1> glazer_json:minify(<<"{\n  \"a\": 1,\n  \"b\": [1, 2, 3]\n}">>).
<<"{\"a\":1,\"b\":[1,2,3]}">>
```
""".
-spec minify(binary() | iolist()) -> binary().
minify(Input) ->
  glazer:json_minify(Input).

-doc """
Pretty-print a JSON binary or iolist with indentation.

## Examples

```erlang
1> glazer_json:prettify(<<"{\"a\":1,\"b\":[1,2,3]}">>).
<<"{\n   \"a\": 1,\n   \"b\": [\n      1,\n      2,\n      3\n   ]\n}">>

2> glazer_json:prettify(<<"{}">>).
<<"{}">>
```
""".
-spec prettify(binary() | iolist()) -> binary().
prettify(Input) ->
  glazer:json_prettify(Input).

-doc """
Read `Filename` and decode its contents as JSON.

Raises `{parse_error, Reason}` if the file's contents aren't valid JSON, or
a binary `"Filename: Reason"` message (see `file:format_error/1`) if the
file can't be read.

## Example

```erlang
1> glazer_json:read_file("data.json").
#{<<"a">> => 1}
```
""".
-spec read_file(file:name_all()) -> term().
read_file(Filename) ->
  read_file(Filename, []).

-doc """
Read `Filename` and decode its contents as JSON, with decode options
(see `decode/2`).
""".
-spec read_file(file:name_all(), decode_opts()) -> term().
read_file(Filename, Opts) ->
  case file:read_file(Filename) of
    {ok, Bin}       -> decode(Bin, Opts);
    {error, Reason} -> error(glazer:format_error("~ts: ~ts", [Filename,file:format_error(Reason)]))
  end.

-doc """
Encode `Data` to JSON and write it to `Filename`, overwriting any existing
file.

Raises a binary `"Filename: Reason"` message (see `file:format_error/1`)
if the file can't be written.

## Example

```erlang
1> glazer_json:write_file("data.json", #{<<"a">> => 1}).
ok
```
""".
-spec write_file(file:name_all(), term()) -> ok.
write_file(Filename, Data) ->
  write_file(Filename, Data, []).

-doc """
Encode `Data` to JSON with encode options (see `encode/2`) and write it to
`Filename`, overwriting any existing file.
""".
-spec write_file(file:name_all(), term(), encode_opts()) -> ok.
write_file(Filename, Data, Opts) ->
  case file:write_file(Filename, encode(Data, Opts)) of
    ok              -> ok;
    {error, Reason} -> error(glazer:format_error("~ts: ~ts", [Filename, file:format_error(Reason)]))
  end.

-doc """
Run a [jq](https://jqlang.org/) `Filter` program against a JSON binary or
iolist `Input`, returning one Erlang term per value produced by the filter
(in the order they are emitted by jq).

Requires `glazer` to have been built against `libjq`; if `libjq` was not
available at build time, this returns `{error, jq_not_available}`.

A runtime error raised by the filter itself (e.g. via jq's `error/0,1`) is
returned as `{error, Msg}` where `Msg` is the binary message produced by jq.

## Examples

```erlang
1> glazer_json:query(<<"{\"a\":[1,2,3]}">>, <<".a[]">>).
{ok,[1,2,3]}

2> glazer_json:query(<<"{\"a\":1}">>, <<".b">>).
{ok,[null]}

3> glazer_json:query(<<"not json">>, <<".">>).
{error, invalid_input}
```
""".
-spec query(binary() | iolist(), binary() | iolist()) ->
  {ok, [term()]} | {error, query_reason()}.
query(Input, Filter) ->
  glazer:json_query(Input, Filter).

-doc """
Like `query/2`, but decodes each result term using `JSONDecodeOpts`
(see `decode/2`).

## Examples

```erlang
1> glazer_json:query(<<"{\"a\":[1,2,3]}">>, <<".a">>, [{keys, atom}]).
{ok, [[1,2,3]]}

2> glazer_json:query(<<"{\"a\":null}">>, <<".a">>, [use_nil]).
{ok, [nil]}
```
""".
-spec query(binary() | iolist(), binary() | iolist(), decode_opts()) ->
  {ok, [term()]} | {error, query_reason()}.
query(Input, Filter, JSONDecodeOpts) ->
  glazer:json_query(Input, Filter, JSONDecodeOpts).

-doc """
Locate the end of the next complete top-level JSON value in `Bin`, without
decoding it.

Returns:

- `{complete, EndOffset}` - a complete value spans `binary:part(Bin, 0,
  EndOffset)`; the rest of `Bin` (if any) is left over for the next call
- `{incomplete, ScanState}` - `Bin` doesn't yet contain a complete value;
  feed more data via `scan/2` once it's available, passing the *entire
  unconsumed remainder* (this `Bin`, with new bytes appended) plus
  `ScanState`

This is the low-level primitive behind [`stream_feed/2`](`stream_feed/2`);
most callers should use the `stream_*` API instead.

## Example

Slicing off complete values from a buffer of concatenated JSON:

```erlang
1> Buf0 = <<"{\"a\":1} {\"b\":2}">>,
2> {complete, End1} = glazer_json:scan(Buf0).
{complete, 7}
3> <<Val1:End1/binary, Buf1/binary>> = Buf0,
4> Val1.
<<"{\"a\":1}">>
5> Buf1.
<<" {\"b\":2}">>
6> {complete, End2} = glazer_json:scan(Buf1).
{complete, 8}
```

Resuming a scan once more bytes arrive:

```erlang
1> {incomplete, S0} = glazer_json:scan(<<"{\"a\":">>).
{incomplete, {6,1,false,false,true,false}}
2> glazer_json:scan(<<"{\"a\":1}">>, S0).
{complete, 7}
```
""".
-spec scan(binary() | iolist()) ->
  {complete, non_neg_integer()} | {incomplete, scan_state()}.
scan(Bin) ->
  glazer:json_scan(Bin).

-doc """
Resume scanning `Bin` (the unconsumed remainder plus newly-appended bytes)
from `ScanState`.

## Examples

```erlang
1> {incomplete, S0} = glazer_json:scan(<<"[1, 2,">>).
{incomplete, {6,1,false,false,true,false}}
2> glazer_json:scan(<<"[1, 2, 3]">>, S0).
{complete, 9}
```
""".
-spec scan(binary() | iolist(), scan_state()) ->
  {complete, non_neg_integer()} | {incomplete, scan_state()}.
scan(Bin, ScanState) ->
  glazer:json_scan(Bin, ScanState).

%%%----------------------------------------------------------------------------
%%% Streaming / incremental decode
%%%----------------------------------------------------------------------------

-doc """
Create a new incremental decoder for feeding JSON in chunks (e.g. from a
socket or file), useful when a complete document isn't available up front
or when a stream contains a sequence of concatenated/whitespace-separated
JSON values (e.g. newline-delimited JSON).

Decoding itself is **not** incremental — each complete top-level value is
still decoded in a single pass via [`decode/2`](`decode/2`) using the
library's fast whole-buffer decoder. Only the *boundary detection* (finding
where one value ends and the next begins) is incremental, via a small
byte-scanner that tracks nesting/string state across chunks.

## Example

```erlang
1> D0 = glazer_json:stream_decoder(),
2> {Vals1, D1} = glazer_json:stream_feed(D0, <<"{\"a\":1} {\"b\":">>),
3> Vals1.
[#{<<"a">> => 1}]
4> {Vals2, _D2} = glazer_json:stream_feed(D1, <<"2}">>),
5> Vals2.
[#{<<"b">> => 2}]
```
""".
-spec stream_decoder() -> stream_decoder().
stream_decoder() ->
  stream_decoder([]).

-doc """
Create a new incremental decoder, passing `Opts` through to every
[`decode/2`](`decode/2`) call.
""".
-spec stream_decoder(decode_opts()) -> stream_decoder().
stream_decoder(Opts) when is_list(Opts) ->
  #stream_decoder{opts = Opts}.

-doc """
Feed a chunk of bytes into the decoder, returning any complete JSON values
found so far (in order) along with the updated decoder.

Raises the same exceptions as [`decode/2`](`decode/2`) (e.g.
`Reason`) if a value that the scanner deemed complete fails
to decode.

## Example

Call `stream_feed/2` for each chunk received from the source while more
data may still arrive, and [`stream_eof/1`](`stream_eof/1`) once the source
is exhausted to flush any trailing value:

```erlang
loop(Socket, D0) ->
  case gen_tcp:recv(Socket, 0) of
    {ok, Chunk} ->
      {Vals, D1} = glazer_json:stream_feed(D0, Chunk),
      handle_values(Vals),
      loop(Socket, D1);
    {error, closed} ->
      case glazer_json:stream_eof(D0) of
        {ok, Trailing}  -> handle_values(Trailing);
        {error, Reason} -> handle_truncated_stream(Reason)
      end
  end.
```

The same decoder fits naturally into a `gen_server` driving an
active-mode socket: keep the `stream_decoder()` in the process state,
feed it from `handle_info({tcp, ...})`, and flush it on
`{tcp_closed, ...}`:

```erlang
-module(json_conn).
-behaviour(gen_server).
-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).

-record(state, {socket, decoder}).

start_link(Socket) ->
  gen_server:start_link(?MODULE, Socket, []).

init(Socket) ->
  inet:setopts(Socket, [{active, once}]),
  {ok, #state{socket = Socket, decoder = glazer_json:stream_decoder()}}.

handle_info({tcp, Socket, Data}, #state{socket = Socket, decoder = D0} = State) ->
  {Vals, D1} = glazer_json:stream_feed(D0, Data),
  lists:foreach(fun handle_value/1, Vals),
  inet:setopts(Socket, [{active, once}]),
  {noreply, State#state{decoder = D1}};

handle_info({tcp_closed, Socket}, #state{socket = Socket, decoder = D0} = State) ->
  case glazer_json:stream_eof(D0) of
    {ok, Trailing}  -> lists:foreach(fun handle_value/1, Trailing);
    {error, Reason} -> handle_truncated_stream(Reason)
  end,
  {stop, normal, State};

handle_info({tcp_error, Socket, Reason}, #state{socket = Socket} = State) ->
  {stop, Reason, State}.

handle_call(_Request, _From, State) -> {reply, ok, State}.
handle_cast(_Request, State)        -> {noreply, State}.

handle_value(Val) ->
  io:format("received: ~p~n", [Val]).
```
""".
-spec stream_feed(stream_decoder(), binary() | iolist()) -> {[term()], stream_decoder()}.
stream_feed(#stream_decoder{buffer = Buf} = D, Chunk) ->
  NewBuf = iolist_to_binary([Buf, Chunk]),
  stream_drain(D#stream_decoder{buffer = NewBuf, state = undefined}, []).

stream_drain(#stream_decoder{buffer = Buf, opts = Opts} = D, Acc) ->
  case scan(Buf) of
    {complete, End} ->
      <<ValueBin:End/binary, Rest/binary>> = Buf,
      Term = decode(ValueBin, Opts),
      stream_drain(D#stream_decoder{buffer = Rest, state = undefined}, [Term | Acc]);
    {incomplete, NewSt} ->
      {lists:reverse(Acc), D#stream_decoder{state = NewSt}}
  end.

-doc """
Signal end-of-stream: decode any remaining buffered bytes as a final value
(useful for a trailing bare scalar, e.g. a lone number or `true`/`null`,
which the scanner can't otherwise distinguish from a value that's still
being written to mid-chunk).

Returns `{ok, [Term]}` with zero or one trailing value, or `{error,
Reason}` if the remaining bytes don't form a complete value.

## Example

```erlang
1> D0 = glazer_json:stream_decoder(),
2> {Vals1, D1} = glazer_json:stream_feed(D0, <<"123">>),
3> Vals1.
[]
4> glazer_json:stream_eof(D1).
{ok, [123]}
```

A stream that ends mid-value (e.g. a dropped connection) yields an error
instead of silently dropping the partial data:

```erlang
1> D0 = glazer_json:stream_decoder(),
2> {Vals1, D1} = glazer_json:stream_feed(D0, <<"{\"a\":1, \"b\":">>),
3> Vals1.
[]
4> glazer_json:stream_eof(D1).
{error, _Reason}
```
""".
-spec stream_eof(stream_decoder()) -> {ok, [term()]} | {error, term()}.
stream_eof(#stream_decoder{buffer = Buf, opts = Opts}) ->
  case is_blank(Buf) of
    true  -> {ok, []};
    false ->
      try decode(Buf, Opts) of
        Term -> {ok, [Term]}
      catch
        error:Reason -> {error, Reason}
      end
  end.

%% True if `Bin` is empty or contains only JSON whitespace (space, tab, CR, LF).
is_blank(Bin) ->
  lists:all(fun(B) -> B =:= $\s orelse B =:= $\t orelse B =:= $\r orelse B =:= $\n end,
            binary_to_list(Bin)).