blob: 6af15b5f96b5f42049d89205ff9cfe11c5d982c5 [file] [log] [blame]
% This file is part of Jiffy released under the MIT license.
% See the LICENSE file for more information.
-module(jiffy).
-export([decode/1, decode/2, encode/1, encode/2]).
-define(NOT_LOADED, not_loaded(?LINE)).
-compile([no_native]).
-on_load(init/0).
-type json_value() :: null
| true
| false
| json_string()
| json_number()
| json_object()
| json_array().
-type json_array() :: [json_value()].
-type json_string() :: atom() | binary().
-type json_number() :: integer() | float().
-ifdef(JIFFY_NO_MAPS).
-type json_object() :: {[{json_string(),json_value()}]}.
-else.
-type json_object() :: {[{json_string(),json_value()}]}
| #{json_string() => json_value()}.
-endif.
-type jiffy_decode_result() :: json_value()
| {has_trailer, json_value(), binary()}.
-type decode_option() :: return_maps
| use_nil
| return_trailer
| dedupe_keys
| copy_strings
| {null_term, any()}
| {bytes_per_iter, non_neg_integer()}
| {bytes_per_red, non_neg_integer()}.
-type encode_option() :: uescape
| pretty
| force_utf8
| use_nil
| escape_forward_slashes
| {bytes_per_iter, non_neg_integer()}
| {bytes_per_red, non_neg_integer()}.
-type decode_options() :: [decode_option()].
-type encode_options() :: [encode_option()].
-export_type([json_value/0, jiffy_decode_result/0]).
-spec decode(iolist() | binary()) -> jiffy_decode_result().
decode(Data) ->
decode(Data, []).
-spec decode(iolist() | binary(), decode_options()) -> jiffy_decode_result().
decode(Data, Opts) when is_binary(Data), is_list(Opts) ->
case nif_decode_init(Data, Opts) of
{error, Error} ->
error(Error);
{partial, EJson} ->
finish_decode(EJson);
{iter, {_, Decoder, Val, Objs, Curr}} ->
decode_loop(Data, Decoder, Val, Objs, Curr);
EJson ->
EJson
end;
decode(Data, Opts) when is_list(Data) ->
decode(iolist_to_binary(Data), Opts).
-spec encode(json_value()) -> iodata().
encode(Data) ->
encode(Data, []).
-spec encode(json_value(), encode_options()) -> iodata().
encode(Data, Options) ->
ForceUTF8 = lists:member(force_utf8, Options),
case nif_encode_init(Data, Options) of
{error, {invalid_string, _}} when ForceUTF8 == true ->
FixedData = jiffy_utf8:fix(Data),
encode(FixedData, Options -- [force_utf8]);
{error, {invalid_object_member_key, _}} when ForceUTF8 == true ->
FixedData = jiffy_utf8:fix(Data),
encode(FixedData, Options -- [force_utf8]);
{error, Error} ->
error(Error);
{partial, IOData} ->
finish_encode(IOData, []);
{iter, {Encoder, Stack, IOBuf}} ->
encode_loop(Data, Options, Encoder, Stack, IOBuf);
[Bin] when is_binary(Bin) ->
Bin;
RevIOData when is_list(RevIOData) ->
lists:reverse(RevIOData)
end.
finish_decode({bignum, Value}) ->
list_to_integer(binary_to_list(Value));
finish_decode({bignum_e, Value}) ->
{IVal, EVal} = case string:to_integer(binary_to_list(Value)) of
{I, [$e | ExpStr]} ->
{E, []} = string:to_integer(ExpStr),
{I, E};
{I, [$E | ExpStr]} ->
{E, []} = string:to_integer(ExpStr),
{I, E}
end,
try
IVal * math:pow(10, EVal)
catch
error:badarith ->
error({range, EVal})
end;
finish_decode({bigdbl, Value}) ->
try
list_to_float(binary_to_list(Value))
catch
error:badarg ->
error({range, Value})
end;
finish_decode({Pairs}) when is_list(Pairs) ->
finish_decode_obj(Pairs, []);
finish_decode(Vals) when is_list(Vals) ->
finish_decode_arr(Vals, []);
finish_decode({has_trailer, Value, Rest}) ->
{has_trailer, finish_decode(Value), Rest};
finish_decode(Val) ->
maybe_map(Val).
-ifndef(JIFFY_NO_MAPS).
maybe_map(Obj) when is_map(Obj) ->
maps:map(fun finish_decode_map/2, Obj);
maybe_map(Val) ->
Val.
finish_decode_map(_, V) ->
finish_decode(V).
-else.
maybe_map(Val) ->
Val.
-endif.
finish_decode_obj([], Acc) ->
{lists:reverse(Acc)};
finish_decode_obj([{K, V} | Pairs], Acc) ->
finish_decode_obj(Pairs, [{K, finish_decode(V)} | Acc]).
finish_decode_arr([], Acc) ->
lists:reverse(Acc);
finish_decode_arr([V | Vals], Acc) ->
finish_decode_arr(Vals, [finish_decode(V) | Acc]).
finish_encode([], Acc) ->
%% No reverse! The NIF returned us
%% the pieces in reverse order.
Acc;
finish_encode([<<_/binary>>=B | Rest], Acc) ->
finish_encode(Rest, [B | Acc]);
finish_encode([Val | Rest], Acc) when is_integer(Val) ->
Bin = list_to_binary(integer_to_list(Val)),
finish_encode(Rest, [Bin | Acc]);
finish_encode([InvalidEjson | _], _) ->
error({invalid_ejson, InvalidEjson});
finish_encode(_, _) ->
error(invalid_ejson).
init() ->
PrivDir = case code:priv_dir(?MODULE) of
{error, _} ->
EbinDir = filename:dirname(code:which(?MODULE)),
AppPath = filename:dirname(EbinDir),
filename:join(AppPath, "priv");
Path ->
Path
end,
erlang:load_nif(filename:join(PrivDir, "jiffy"), 0).
decode_loop(Data, Decoder, Val, Objs, Curr) ->
case nif_decode_iter(Data, Decoder, Val, Objs, Curr) of
{error, Error} ->
error(Error);
{partial, EJson} ->
finish_decode(EJson);
{iter, {_, NewDecoder, NewVal, NewObjs, NewCurr}} ->
decode_loop(Data, NewDecoder, NewVal, NewObjs, NewCurr);
EJson ->
EJson
end.
encode_loop(Data, Options, Encoder, Stack, IOBuf) ->
ForceUTF8 = lists:member(force_utf8, Options),
case nif_encode_iter(Encoder, Stack, IOBuf) of
{error, {invalid_string, _}} when ForceUTF8 == true ->
FixedData = jiffy_utf8:fix(Data),
encode(FixedData, Options -- [force_utf8]);
{error, {invalid_object_member_key, _}} when ForceUTF8 == true ->
FixedData = jiffy_utf8:fix(Data),
encode(FixedData, Options -- [force_utf8]);
{error, Error} ->
error(Error);
{partial, IOData} ->
finish_encode(IOData, []);
{iter, {NewEncoder, NewStack, NewIOBuf}} ->
encode_loop(Data, Options, NewEncoder, NewStack, NewIOBuf);
[Bin] when is_binary(Bin) ->
Bin;
RevIOData when is_list(RevIOData) ->
lists:reverse(RevIOData)
end.
not_loaded(Line) ->
erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}).
nif_decode_init(_Data, _Opts) ->
?NOT_LOADED.
nif_decode_iter(_Data, _Decoder, _, _, _) ->
?NOT_LOADED.
nif_encode_init(_Data, _Options) ->
?NOT_LOADED.
nif_encode_iter(_Encoder, _Stack, _IoList) ->
?NOT_LOADED.