% 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)). -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(). -type json_object() :: {[{json_string(),json_value()}]} | #{json_string() => json_value()}. -type json_preencoded() :: {json, iodata()}. -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([decode_options/0, encode_options/0]). -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); EJson -> EJson end; decode(Data, Opts) when is_list(Data) -> decode(iolist_to_binary(Data), Opts). -spec encode(json_value() | json_preencoded()) -> iodata(). encode(Data) -> encode(Data, []). -spec encode(json_value() | json_preencoded(), 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, []); [Bin] when is_binary(Bin) -> Bin; RevIOData when is_list(RevIOData) -> lists:reverse(RevIOData) end. finish_decode({bignum, Value}) -> binary_to_integer(Value); finish_decode({bigdbl, Value}) -> % Got something like a 1e400, 1.5e500, or 999...999.5. Split on e/E and if % we do have an explicit exponent use math:pow/2 to create the float since % Erlang/OTP's binary_to_float can't handle something like 1e10, it needs % the first number (the mantissa) to have '.' in it (it has to be 1.0e10). case binary:split(Value, [<<$e>>, <<$E>>]) of [IStr, EStr] -> EVal = binary_to_integer(EStr), IVal = case binary:match(IStr, <<$.>>) of nomatch -> binary_to_integer(IStr); _ -> binary_to_float(IStr) end, try IVal * math:pow(10, EVal) catch error:badarith -> error({range, Value}) end; [_] -> try binary_to_float(Value) catch error:badarg -> error({range, Value}) end 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). 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). 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) -> finish_encode(Rest, [integer_to_binary(Val) | Acc]); finish_encode([{json, Json} | Rest], Acc) -> %% Pre-encoded JSON spliced into the output as-is. This came from %% enc_unknown. finish_encode(Rest, [Json | 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). not_loaded(Line) -> erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). nif_decode_init(_Data, _Opts) -> ?NOT_LOADED. nif_encode_init(_Data, _Options) -> ?NOT_LOADED.