Packages

Erlang NIF JSON encoder/decoder using the glaze C++ library

Retired package: Deprecated - Deprecated

Current section

Files

Jump to
glazejson src glazejson.erl
Raw

src/glazejson.erl

-module(glazejson).
-moduledoc """
Fast JSON encoding and decoding using the glaze C++ library.
By default JSON `null` is represented as the atom `null`. To change it
application-wide, set the `null` env key in your config:
```
{glazejson, [{null, nil}]}.
```
See also [https://github.com/stephenberry/glaze]
""".
-export([decode/1, decode/2, encode/1, encode/2, minify/1, prettify/1,
encode_bigint/1, decode_bigint/1]).
-on_load(init/0).
-define(LIBNAME, glazejson).
-define(NOT_LOADED_ERROR,
erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, ?LINE}]})).
-type decode_opt() ::
return_maps
| object_as_tuple
| use_nil
| {null_term, atom()}
| {keys, atom | existing_atom | binary}.
-doc """
Decode options:
- `return_maps` - decode JSON objects as Erlang maps (default)
- `object_as_tuple` - decode JSON objects as `{[{K, V}]}` proplists
- `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)
""".
-type decode_opts() :: [decode_opt()].
-type encode_opt() ::
pretty
| uescape
| force_utf8
| use_nil
| {null_term, atom()}.
-doc """
Encode options:
- `pretty` - pretty-print the JSON output
- `uescape` - escape non-ASCII characters as \\uXXXX sequences
- `force_utf8` - fix invalid UTF-8 sequences before encoding
- `use_nil` - encode the atom `nil` as JSON `null`
- `{null_term, Atom}` - encode `Atom` as JSON `null`
""".
-type encode_opts() :: [encode_opt()].
-export_type([decode_opts/0, encode_opts/0]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
init() ->
NullVal = application:get_env(?LIBNAME, null, null),
is_atom(NullVal) orelse erlang:error("glazejson: option 'null' must be an atom"),
SoName =
case code:priv_dir(?LIBNAME) of
{error, bad_name} ->
case code:which(?MODULE) of
Filename when is_list(Filename) ->
Dir = filename:dirname(filename:dirname(Filename)),
filename:join([Dir, "priv", ?LIBNAME]);
_ ->
filename:join("../priv", ?LIBNAME)
end;
Dir ->
filename:join(Dir, ?LIBNAME)
end,
erlang:load_nif(SoName, [{null, NullVal}]).
-doc """
Decode a JSON binary or iolist to an Erlang term. JSON objects are returned as
maps (default).
""".
-spec decode(binary() | iolist()) -> term().
decode(_Input) ->
?NOT_LOADED_ERROR.
-doc "Decode a JSON binary or iolist to an Erlang term with options.".
-spec decode(binary() | iolist(), decode_opts()) -> term().
decode(_Input, _Opts) ->
?NOT_LOADED_ERROR.
-doc "Encode an Erlang term to a JSON binary.".
-spec encode(term()) -> binary().
encode(Data) ->
encode(Data, []).
-doc "Encode an Erlang term to a JSON binary with options.".
-spec encode(term(), encode_opts()) -> binary().
encode(_Data, _Opts) ->
?NOT_LOADED_ERROR.
-doc "Minify a JSON binary or iolist, removing all unnecessary whitespace.".
-spec minify(binary() | iolist()) -> {ok, binary()} | {error, binary()}.
minify(_Input) ->
?NOT_LOADED_ERROR.
-doc "Pretty-print a JSON binary or iolist with two-space indentation.".
-spec prettify(binary() | iolist()) -> {ok, binary()} | {error, binary()}.
prettify(_Input) ->
?NOT_LOADED_ERROR.
-doc "Encode a big integer to its JSON string representation.".
-spec encode_bigint(integer()) -> {ok, binary()} | {error, binary()}.
encode_bigint(_BigInt) ->
?NOT_LOADED_ERROR.
-doc "Decode a JSON number string to a big integer.".
-spec decode_bigint(binary() | iolist()) -> {ok, integer()} | {error, binary()}.
decode_bigint(_NumberString) ->
?NOT_LOADED_ERROR.
%%%----------------------------------------------------------------------------
%%% Tests
%%%----------------------------------------------------------------------------
-ifdef(EUNIT).
encode_test_() ->
[
?_assertEqual(<<"null">>, encode(null)),
?_assertEqual(<<"null">>, encode(nil, [use_nil])),
?_assertEqual(<<"true">>, encode(true)),
?_assertEqual(<<"false">>, encode(false)),
?_assertEqual(<<"1">>, encode(1)),
?_assertEqual(<<"1.5">>, encode(1.5)),
?_assertEqual(<<"\"hello\"">>, encode(<<"hello">>)),
?_assertEqual(<<"[1,2,3]">>, encode([1, 2, 3])),
?_assertEqual(<<"{}">>, encode(#{})),
?_assertMatch(<<"{", _/binary>>, encode(#{<<"a">> => 1}))
].
decode_test_() ->
[
?_assertEqual(null, decode(<<"null">>)),
?_assertEqual(nil, decode(<<"null">>, [use_nil])),
?_assertEqual(true, decode(<<"true">>)),
?_assertEqual(false, decode(<<"false">>)),
?_assertEqual(1, decode(<<"1">>)),
?_assertEqual(1.5, decode(<<"1.5">>)),
?_assertEqual(<<"hello">>, decode(<<"\"hello\"">>)),
?_assertEqual([1, 2, 3], decode(<<"[1,2,3]">>)),
?_assertEqual(#{<<"a">> => 1}, decode(<<"{\"a\":1}">>)),
?_assertEqual({[{<<"a">>, 1}]}, decode(<<"{\"a\":1}">>, [object_as_tuple])),
?_assertEqual(null, decode(<<"null">>, [{null_term, null}])),
?_assertEqual(my_null, decode(<<"null">>, [{null_term, my_null}]))
].
roundtrip_test_() ->
Vals = [null, true, false, 0, 1, -1, 1.5, <<"hello">>, [], [1, 2, 3],
#{<<"a">> => 1, <<"b">> => [1, 2]},
#{<<"nested">> => #{<<"x">> => true}}],
[?_assertEqual(V, decode(encode(V))) || V <- Vals].
minify_test_() ->
[
?_assertEqual({ok, <<"[1,2,3]">>}, minify(<<"[ 1, 2, 3 ]">>)),
?_assertEqual({ok, <<"{\"a\":1}">>}, minify(<<" { \"a\" : 1 } ">>))
].
prettify_test_() ->
[
?_assertMatch({ok, <<"[\n", _/binary>>}, prettify(<<"[1,2,3]">>)),
?_assertMatch({ok, <<"{\n", _/binary>>}, prettify(<<"{\"a\":1}">>))
].
keys_test_() ->
[
?_assertEqual(#{<<"a">> => 1}, decode(<<"{\"a\":1}">>)),
?_assertEqual(#{<<"a">> => 1}, decode(<<"{\"a\":1}">>, [{keys, binary}])),
?_assertEqual(#{a => 1}, decode(<<"{\"a\":1}">>, [{keys, atom}])),
?_assertEqual(#{a => 1}, decode(<<"{\"a\":1}">>, [{keys, existing_atom}])),
%% existing_atom falls back to a binary for keys with no matching atom
?_assertEqual(#{<<"no_such_atom_in_glazejson_test_suite_xyz">> => 1},
decode(<<"{\"no_such_atom_in_glazejson_test_suite_xyz\":1}">>,
[{keys, existing_atom}]))
].
uescape_test_() ->
[
%% U+00E9 (é), UTF-8: 0xC3 0xA9
?_assertEqual(<<"\"\\u00e9\"">>, encode(<<16#C3, 16#A9>>, [uescape])),
%% Without uescape, UTF-8 bytes pass through unescaped
?_assertEqual(<<"\"", 16#C3, 16#A9, "\"">>, encode(<<16#C3, 16#A9>>)),
%% U+1F600 (emoji, outside the BMP) encodes as a surrogate pair
?_assertEqual(<<"\"\\ud83d\\ude00\"">>, encode(<<16#F0,16#9F,16#98,16#80>>, [uescape])),
%% Round-trips back to the original UTF-8 binary
?_assertEqual(<<16#C3, 16#A9>>, decode(encode(<<16#C3, 16#A9>>, [uescape])))
].
force_utf8_test_() ->
[
%% Invalid byte sequences are sanitized to U+FFFD (UTF-8: EF BF BD)
?_assertEqual(<<"\"", 16#EF, 16#BF, 16#BD, 16#EF, 16#BF, 16#BD, "a\"">>,
encode(<<16#FF, 16#FE, $a>>, [force_utf8])),
%% Without force_utf8, invalid bytes pass through verbatim
?_assertEqual(<<"\"", 16#FF, 16#FE, "a\"">>, encode(<<16#FF, 16#FE, $a>>)),
%% Valid UTF-8 is left untouched
?_assertEqual(<<"\"", 16#C3, 16#A9, "\"">>, encode(<<16#C3, 16#A9>>, [force_utf8]))
].
pretty_test_() ->
[
?_assertEqual(<<"{\n \"a\": 1\n}">>, encode(#{<<"a">> => 1}, [pretty])),
?_assertEqual(<<"[\n 1,\n 2\n]">>, encode([1, 2], [pretty])),
?_assertEqual(#{<<"a">> => 1}, decode(encode(#{<<"a">> => 1}, [pretty])))
].
null_term_encode_test_() ->
[
?_assertEqual(<<"null">>, encode(null)),
?_assertEqual(<<"null">>, encode(nil, [{null_term, nil}])),
?_assertEqual(<<"null">>, encode(undefined, [{null_term, undefined}])),
?_assertEqual(<<"null">>, encode(null, [{null_term, undefined}]))
].
object_as_tuple_test_() ->
[
?_assertEqual({[]}, decode(<<"{}">>, [object_as_tuple])),
?_assertEqual({[{<<"a">>, 1}, {<<"b">>, 2}]},
decode(<<"{\"a\":1,\"b\":2}">>, [object_as_tuple])),
?_assertEqual({[{<<"a">>, {[{<<"b">>, true}]}}]},
decode(<<"{\"a\":{\"b\":true}}">>, [object_as_tuple])),
?_assertEqual({[{<<"a">>, 1}]},
decode(encode({[{<<"a">>, 1}]}), [object_as_tuple]))
].
numbers_test_() ->
[
?_assertEqual(0, decode(<<"0">>)),
?_assertEqual(-1, decode(<<"-1">>)),
?_assertEqual(-1.5, decode(<<"-1.5">>)),
?_assertEqual(1.0e10, decode(<<"1.0e10">>)),
?_assertEqual(1.0e-10, decode(<<"1.0e-10">>)),
?_assertEqual(<<"-1">>, encode(-1)),
?_assertEqual(<<"0">>, encode(0))
].
iolist_input_test_() ->
[
?_assertEqual(#{<<"a">> => 1}, decode([<<"{\"a\":">>, <<"1}">>])),
?_assertEqual(#{<<"a">> => 1}, decode([<<"{">>, [<<"\"a\":1">>], <<"}">>])),
?_assertEqual({ok, <<"{\"a\":1}">>}, minify([<<"{ \"a\"">>, <<": 1 }">>]))
].
decode_error_test_() ->
[
?_assertError(_, decode(<<"">>)),
?_assertError(_, decode(<<"{\"a\":}">>)),
?_assertError(_, decode(<<"{\"a\":1">>)),
?_assertError(_, decode(<<"[1, 2">>)),
?_assertError(_, decode(<<"not json">>))
].
bigint_test_() ->
Big = 123456789012345678901234567890,
Neg = -Big,
[
?_assertEqual({ok, <<"123456789012345678901234567890">>}, encode_bigint(Big)),
?_assertEqual({ok, <<"-123456789012345678901234567890">>}, encode_bigint(Neg)),
?_assertEqual({ok, Big}, decode_bigint(<<"123456789012345678901234567890">>)),
?_assertEqual({ok, Neg}, decode_bigint(<<"-123456789012345678901234567890">>)),
?_assertEqual({ok, 123}, decode_bigint(<<"123">>)),
?_assertEqual(Big, decode(<<"123456789012345678901234567890">>)),
?_assertEqual(<<"123456789012345678901234567890">>, encode(Big)),
?_assertEqual(Big, decode(encode(Big)))
].
-endif.