diff --git a/README.md b/README.md index f4e27ce..3883274 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ see below | `datetime()` * arrays json arrays are represented with erlang lists of json values as described - in this section + in this section, or by tuples with the `tuples_to_lists` option * objects @@ -272,7 +272,8 @@ json_term() = [json_term()] | float() | binary() | atom() - | datetime() + | datetime() + | tuple() ``` the erlang representation of json. binaries should be `utf8` encoded, or close @@ -318,6 +319,7 @@ option() = dirty_strings | return_tail | uescape | unescaped_jsonp + | tuples_to_lists strict_option() = comments | trailing_commas @@ -495,7 +497,7 @@ encode(Term, Opts) -> JSON Term = json_term() JSON = json_text() - Opts = [option() | space | {space, N} | indent | {indent, N}] + Opts = [option() | space | {space, N} | indent | {indent, N} | tuples_to_lists] N = pos_integer() ``` diff --git a/src/jsx_config.erl b/src/jsx_config.erl index ba1d872..7acc11f 100644 --- a/src/jsx_config.erl +++ b/src/jsx_config.erl @@ -60,6 +60,8 @@ | {indent, non_neg_integer()} | {depth, non_neg_integer()} | {newline, binary()} + | {tuples_to_lists, boolean()} + | {disable_timestamp_heuristics, boolean()} | legacy_option() | {legacy_option(), boolean()}. -type legacy_option() :: strict_comments @@ -114,6 +116,10 @@ parse_config([multi_term|Rest], Config) -> parse_config(Rest, Config#config{multi_term=true}); parse_config([return_tail|Rest], Config) -> parse_config(Rest, Config#config{return_tail=true}); +parse_config([tuples_to_lists|Rest], Config) -> + parse_config(Rest, Config#config{tuples_to_lists = true}); +parse_config([disable_timestamp_heuristics|Rest], Config) -> + parse_config(Rest, Config#config{disable_timestamp_heuristics = true}); %% retained for backwards compat, now does nothing however parse_config([repeat_keys|Rest], Config) -> parse_config(Rest, Config); @@ -215,7 +221,9 @@ valid_flags() -> stream, uescape, error_handler, - incomplete_handler + incomplete_handler, + tuples_to_lists, + disable_timestamp_heuristics ]. @@ -259,7 +267,9 @@ config_test_() -> strict_escapes = true, strict_control_codes = true, stream = true, - uescape = true + uescape = true, + tuples_to_lists = true, + disable_timestamp_heuristics = true }, parse_config([dirty_strings, escaped_forward_slashes, @@ -270,7 +280,9 @@ config_test_() -> repeat_keys, strict, stream, - uescape + uescape, + tuples_to_lists, + disable_timestamp_heuristics ]) ) }, @@ -342,6 +354,8 @@ config_to_list_test_() -> stream, uescape, unescaped_jsonp, + tuples_to_lists, + disable_timestamp_heuristics, strict ], config_to_list( @@ -356,7 +370,9 @@ config_to_list_test_() -> strict_escapes = true, strict_control_codes = true, stream = true, - uescape = true + uescape = true, + tuples_to_lists = true, + disable_timestamp_heuristics = true } ) )}, diff --git a/src/jsx_config.hrl b/src/jsx_config.hrl index c89963c..a03fd75 100644 --- a/src/jsx_config.hrl +++ b/src/jsx_config.hrl @@ -14,5 +14,7 @@ uescape = false :: boolean(), unescaped_jsonp = false :: boolean(), error_handler = false :: false | jsx_config:handler(), - incomplete_handler = false :: false | jsx_config:handler() + incomplete_handler = false :: false | jsx_config:handler(), + tuples_to_lists = false :: boolean(), + disable_timestamp_heuristics = false :: boolean() }). diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index a1242a7..6961d4a 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -22,58 +22,66 @@ -module(jsx_encoder). - --export([encoder/3, encode/1, encode/2]). +-include("jsx_config.hrl"). +-export([encoder/3, encode/2, encode/3]). -spec encoder(Handler::module(), State::any(), Config::jsx_config:options()) -> jsx:encoder(). encoder(Handler, State, Config) -> Parser = jsx:parser(Handler, State, Config), - fun(Term) -> Parser(encode(Term) ++ [end_json]) end. + fun(Term) -> Parser(encode(Term, jsx_config:parse_config(Config)) ++ [end_json]) end. --spec encode(Term::any()) -> [any(), ...]. +-spec encode(Term::any(), Config::jsx_config:options()) -> [any(), ...]. -encode(Term) -> encode(Term, ?MODULE). +encode(Term, Config) -> encode(Term, ?MODULE, Config). --spec encode(Term::any(), EntryPoint::module()) -> [any(), ...]. +-spec encode(Term::any(), EntryPoint::module(), Config::jsx_config:options()) -> [any(), ...]. -encode(Map, _EntryPoint) when is_map(Map), map_size(Map) < 1 -> +encode(Map, _EntryPoint, _Config) when is_map(Map), map_size(Map) < 1 -> [start_object, end_object]; -encode(Term, EntryPoint) when is_map(Term) -> - [start_object] ++ unpack(Term, EntryPoint); -encode(Term, EntryPoint) -> encode_(Term, EntryPoint). +encode(Term, EntryPoint, Config) when is_map(Term) -> + [start_object] ++ unpack(Term, EntryPoint, Config); +encode(Term, EntryPoint, Config) -> encode_(Term, EntryPoint, Config). -encode_([], _EntryPoint) -> [start_array, end_array]; -encode_([{}], _EntryPoint) -> [start_object, end_object]; +encode_([], _EntryPoint, _Config) -> [start_array, end_array]; +encode_([{}], _EntryPoint,#config{tuples_to_lists = false}) -> [start_object, end_object]; %% datetime special case -encode_([{{_,_,_},{_,_,_}} = DateTime|Rest], EntryPoint) -> - [start_array] ++ [DateTime] ++ unhitch(Rest, EntryPoint); -encode_([{_, _}|_] = Term, EntryPoint) -> - [start_object] ++ unzip(Term, EntryPoint); -encode_(Term, EntryPoint) when is_list(Term) -> - [start_array] ++ unhitch(Term, EntryPoint); +encode_([{{_,_,_},{_,_,_}} = DateTime|Rest], EntryPoint, #config{tuples_to_lists = false} = Config) -> + [start_array] ++ [DateTime] ++ unhitch(Rest, EntryPoint, Config); +encode_({{A,B,C},{D,E,F}} = DateTime, _EntryPoint, #config{tuples_to_lists = true, disable_timestamp_heuristics = false} = _Config) + when is_integer(A), is_integer(B), is_integer(C), is_integer(D), is_integer(E), is_integer(F) -> + [DateTime]; +encode_({A,B,C} = Timestamp, _EntryPoint, #config{tuples_to_lists = true, disable_timestamp_heuristics = false} = _Config) + when is_integer(A), is_integer(B), is_integer(C) -> + [Timestamp]; -encode_(Else, _EntryPoint) -> [Else]. +encode_([{_, _}|_] = Term, EntryPoint, #config{tuples_to_lists = false} = Config) -> + [start_object] ++ unzip(Term, EntryPoint, Config); +encode_(Term, EntryPoint, Config) when is_list(Term) -> + [start_array] ++ unhitch(Term, EntryPoint, Config); +encode_(T, EntryPoint, #config{tuples_to_lists = true} = Config) when is_tuple(T)-> + encode_(tuple_to_list(T), EntryPoint, Config); +encode_(Else, _EntryPoint, _Config) -> [Else]. -unzip([{K, V}|Rest], EntryPoint) when is_integer(K); is_binary(K); is_atom(K) -> - [K] ++ EntryPoint:encode(V, EntryPoint) ++ unzip(Rest, EntryPoint); -unzip([], _) -> [end_object]; -unzip(_, _) -> erlang:error(badarg). +unzip([{K, V}|Rest], EntryPoint, Config) when is_integer(K); is_binary(K); is_atom(K) -> + [K] ++ EntryPoint:encode(V, EntryPoint, Config) ++ unzip(Rest, EntryPoint, Config); +unzip([], _, _) -> [end_object]; +unzip(_, _, _) -> erlang:error(badarg). -unhitch([V|Rest], EntryPoint) -> - EntryPoint:encode(V, EntryPoint) ++ unhitch(Rest, EntryPoint); -unhitch([], _) -> [end_array]. +unhitch([V|Rest], EntryPoint, Config) -> + EntryPoint:encode(V, EntryPoint, Config) ++ unhitch(Rest, EntryPoint, Config); +unhitch([], _, _) -> [end_array]. -unpack(Map, EntryPoint) -> unpack(Map, maps:keys(Map), EntryPoint). +unpack(Map, EntryPoint, Config) -> unpack(Map, maps:keys(Map), EntryPoint, Config). -unpack(Map, [K|Rest], EntryPoint) when is_integer(K); is_binary(K); is_atom(K) -> - [K] ++ EntryPoint:encode(maps:get(K, Map), EntryPoint) ++ unpack(Map, Rest, EntryPoint); -unpack(_, [], _) -> [end_object]. +unpack(Map, [K|Rest], EntryPoint, Config) when is_integer(K); is_binary(K); is_atom(K) -> + [K] ++ EntryPoint:encode(maps:get(K, Map), EntryPoint, Config) ++ unpack(Map, Rest, EntryPoint, Config); +unpack(_, [], _, _) -> [end_object]. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -105,11 +113,11 @@ improper_lists_test_() -> [ {"improper proplist", ?_assertError( badarg, - encode([{<<"key">>, <<"value">>}, false]) + encode([{<<"key">>, <<"value">>}, false], #config{}) )}, {"improper list", ?_assertError( badarg, - encode([{literal, true}, false, null]) + encode([{literal, true}, false, null], #config{}) )} ]. diff --git a/src/jsx_to_json.erl b/src/jsx_to_json.erl index d20add6..1e585f2 100644 --- a/src/jsx_to_json.erl +++ b/src/jsx_to_json.erl @@ -33,7 +33,9 @@ space = 0, indent = 0, depth = 0, - newline = <<$\n>> + newline = <<$\n>>, + tuples_to_lists = false, + disable_timestamp_heuristics = false }). -type config() :: proplists:proplist(). @@ -62,6 +64,8 @@ parse_config([{indent, Val}|Rest], Config) when is_integer(Val), Val > 0 -> parse_config(Rest, Config#config{indent = Val}); parse_config([indent|Rest], Config) -> parse_config(Rest, Config#config{indent = 1}); +parse_config([tuples_to_lists|Rest], Config) -> + parse_config(Rest, Config#config{tuples_to_lists = true}); parse_config([{newline, Val}|Rest], Config) when is_binary(Val) -> parse_config(Rest, Config#config{newline = Val}); parse_config([{K, _}|Rest] = Options, Config) ->