Browse Source

Support R16 or less (maps is disabled before the version OTP-17)

Takeru Ohta 9 years ago
parent
commit
ea18bc4270
7 changed files with 96 additions and 43 deletions
  1. 2 2
      doc/jsone.md
  2. 2 3
      rebar.config
  3. 9 3
      src/jsone.erl
  4. 14 2
      src/jsone_decode.erl
  5. 9 1
      src/jsone_encode.erl
  6. 27 9
      test/jsone_decode_tests.erl
  7. 33 23
      test/jsone_encode_tests.erl

+ 2 - 2
doc/jsone.md

@@ -68,11 +68,11 @@ decode_option() = {object_format, tuple | proplist | map} | {allow_ctrl_chars, b
 - `tuple`: An object is decoded as `{[]}` if it is empty, otherwise `{[{Key, Value}]}`. <br />
 - `proplist`: An object is decoded as `[{}]` if it is empty, otherwise `[{Key, Value}]`. <br />
 - `map`: An object is decoded as `#{}` if it is empty, otherwise `#{Key => Value}`. <br />
-- default: `map` <br />
+- default: `map` if OTP version is OTP-17 or more, `tuple` otherwise <br />
 
 `allow_ctrl_chars`: <br />
 - If the value is `true`, strings which contain ununescaped control characters will be regarded as a legal JSON string <br />
-- default: `false` <br />
+- default: `false`<br />
 
 
 

+ 2 - 3
rebar.config

@@ -1,11 +1,10 @@
 %% -*- erlang -*-
-{require_min_otp_vsn, "OTP17"}.
-
 {erl_opts, [
             warnings_as_errors,
             warn_export_all,
             warn_untyped_record,
-            inline
+            inline,
+            {platform_define, "^R[01][0-9]", 'NO_MAP_TYPE'}
            ]}.
 
 {xref_checks, [

+ 9 - 3
src/jsone.erl

@@ -69,10 +69,16 @@
                              | json_object_format_map().
 -type json_object_members() :: [{json_string(), json_value()}].
 
--type json_object_format_map() :: map().
 -type json_object_format_tuple() :: {json_object_members()}.
 -type json_object_format_proplist() :: [{}] | json_object_members().
 
+-ifdef('NO_MAP_TYPE').
+-opaque json_object_format_map() :: json_object_format_proplist().
+%% `maps' is not supported in this erts version
+-else.
+-type json_object_format_map() :: map().
+-endif.
+
 -type json_scalar() :: json_boolean() | json_number() | json_string().
 
 -type float_format_option() :: {scientific, Decimals :: 0..249}
@@ -174,11 +180,11 @@
 %% - `tuple': An object is decoded as `{[]}' if it is empty, otherwise `{[{Key, Value}]}'. <br />
 %% - `proplist': An object is decoded as `[{}]' if it is empty, otherwise `[{Key, Value}]'. <br />
 %% - `map': An object is decoded as `#{}' if it is empty, otherwise `#{Key => Value}'. <br />
-%% - default: `map' <br />
+%% - default: `map' if OTP version is OTP-17 or more, `tuple' otherwise <br />
 %%
 %% `allow_ctrl_chars': <br />
 %% - If the value is `true', strings which contain ununescaped control characters will be regarded as a legal JSON string <br />
-%% - default: `false' <br />
+%% - default: `false'<br />
 
 %%--------------------------------------------------------------------------------
 %% Exported Functions

+ 14 - 2
src/jsone_decode.erl

@@ -37,6 +37,14 @@
 %%--------------------------------------------------------------------------------
 -define(ERROR(Function, Args), {error, {badarg, [{?MODULE, Function, Args, [{line, ?LINE}]}]}}).
 
+-ifdef('NO_MAP_TYPE').
+-define(DEFAULT_OBJECT_FORMAT, tuple).
+-define(LIST_TO_MAP(X), error({this_erts_does_not_support_maps, X})).
+-else.
+-define(DEFAULT_OBJECT_FORMAT, map).
+-define(LIST_TO_MAP(X), maps:from_list(X)).
+-endif.
+
 -type next() :: {array_next, [jsone:json_value()]}
               | {object_value, jsone:json_object_members()}
               | {object_next, jsone:json_string(), jsone:json_object_members()}.
@@ -51,7 +59,11 @@
 
 -type decode_result() :: {ok, jsone:json_value(), Rest::binary()} | {error, {Reason::term(), [erlang:stack_item()]}}.
 
--record(decode_opt_v2, { object_format=map :: tuple | proplist | map, allow_ctrl_chars=false :: boolean()}).
+-record(decode_opt_v2,
+        {
+          object_format=?DEFAULT_OBJECT_FORMAT :: tuple | proplist | map,
+          allow_ctrl_chars=false :: boolean()
+        }).
 -define(OPT, #decode_opt_v2).
 -type opt() :: #decode_opt_v2{}.
 
@@ -267,7 +279,7 @@ number_exponation_part(Bin, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf,
 
 -spec make_object(jsone:json_object_members(), opt()) -> jsone:json_object().
 make_object(Members, ?OPT{object_format = tuple}) -> {lists:reverse(Members)};
-make_object(Members, ?OPT{object_format = map})   -> maps:from_list(Members);
+make_object(Members, ?OPT{object_format = map})   -> ?LIST_TO_MAP(Members);
 make_object([],      _)                           -> [{}];
 make_object(Members, _)                           -> lists:reverse(Members).
 

+ 9 - 1
src/jsone_encode.erl

@@ -44,6 +44,14 @@
 -define(IS_DATETIME(Y,M,D,H,Mi,S), (?IS_UINT(Y) andalso ?IS_UINT(M) andalso ?IS_UINT(D) andalso
                                     ?IS_UINT(H) andalso ?IS_UINT(Mi) andalso ?IS_UINT(S))).
 
+-ifdef('NO_MAP_TYPE').
+-define(IS_MAP(X), is_tuple(X)).
+-define(ENCODE_MAP(Value, Nexts, Buf, Opt), ?ERROR(value, [Value, Nexts, Buf, Opt])).
+-else.
+-define(IS_MAP(X), is_map(X)).
+-define(ENCODE_MAP(Value, Nexts, Buf, Opt), object(maps:to_list(Value), Nexts, Buf, Opt)).
+-endif.
+
 -type encode_result() :: {ok, binary()} | {error, {Reason::term(), [erlang:stack_item()]}}.
 -type next() :: {array_values, [jsone:json_value()]}
               | {object_value, jsone:json_value(), jsone:json_object_members()}
@@ -107,7 +115,7 @@ value({{_,_,_},{_,_,_}} = Value, Nexts, Buf, Opt)    -> datetime(Value, Nexts, B
 value({Value}, Nexts, Buf, Opt)                      -> object(Value, Nexts, Buf, Opt);
 value([{}], Nexts, Buf, Opt)                         -> object([], Nexts, Buf, Opt);
 value([{_, _}|_] = Value, Nexts, Buf, Opt)           -> object(Value, Nexts, Buf, Opt);
-value(Value, Nexts, Buf, Opt) when is_map(Value)     -> object(maps:to_list(Value), Nexts, Buf, Opt);
+value(Value, Nexts, Buf, Opt) when ?IS_MAP(Value)    -> ?ENCODE_MAP(Value, Nexts, Buf, Opt);
 value(Value, Nexts, Buf, Opt) when is_list(Value)    -> array(Value, Nexts, Buf, Opt);
 value(Value, Nexts, Buf, Opt)                        -> ?ERROR(value, [Value, Nexts, Buf, Opt]).
 

+ 27 - 9
test/jsone_decode_tests.erl

@@ -4,6 +4,18 @@
 
 -include_lib("eunit/include/eunit.hrl").
 
+-ifdef('NO_MAP_TYPE').
+-define(MAP_OBJECT_TYPE, tuple).
+-define(OBJ0, {[]}).
+-define(OBJ1(K, V), {[{K, V}]}).
+-define(OBJ2(K1, V1, K2, V2), {[{K1, V1}, {K2, V2}]}).
+-else.
+-define(MAP_OBJECT_TYPE, map).
+-define(OBJ0, #{}).
+-define(OBJ1(K, V), #{K => V}).
+-define(OBJ2(K1, V1, K2, V2), #{K1 => V1, K2 => V2}).
+-endif.
+
 decode_test_() ->
     [
      %% Symbols
@@ -178,9 +190,9 @@ decode_test_() ->
      {"simple object",
       fun () ->
               Input    = <<"{\"1\":2,\"key\":\"value\"}">>,
-              Expected = #{<<"1">> => 2, <<"key">> => <<"value">>},
+              Expected = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>),
               ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)), % `map' is the default format
-              ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, map}]))
+              ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}]))
       end},
      {"simple object: tuple or proplist",
       fun () ->
@@ -192,25 +204,31 @@ decode_test_() ->
      {"object: contains whitespaces",
       fun () ->
               Input    = <<"{  \"1\" :\t 2,\n\r\"key\" :   \n  \"value\"}">>,
-              Expected = #{<<"1">> => 2, <<"key">> => <<"value">>},
+              Expected = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>),
               ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input))
       end},
      {"empty object",
       fun () ->
-              ?assertEqual({ok, #{}, <<"">>}, jsone_decode:decode(<<"{}">>)),
-              ?assertEqual({ok, #{}, <<"">>}, jsone_decode:decode(<<"{ \t\r\n}">>)),
+              ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>)),
+              ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{ \t\r\n}">>)),
               ?assertEqual({ok, {[]}, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, tuple}])),
               ?assertEqual({ok, [{}], <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, proplist}]))
       end},
      {"empty object: map",
       fun () ->
-              ?assertEqual({ok, #{}, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, map}]))
+              ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, ?MAP_OBJECT_TYPE}]))
       end},
      {"duplicated members: map",
       fun () ->
               Input    = <<"{\"1\":\"first\",\"1\":\"second\"}">>,
-              Expected = #{<<"1">> => <<"first">>}, % the first (leftmost) value is used
-              ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, map}]))
+              case ?MAP_OBJECT_TYPE of
+                  map   ->
+                      Expected = ?OBJ1(<<"1">>, <<"first">>), % the first (leftmost) value is used
+                      ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}]));
+                  tuple ->
+                      Expected = ?OBJ2(<<"1">>, <<"first">>, <<"1">>, <<"second">>),
+                      ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}]))
+              end
       end},
      {"object: trailing comma is disallowed",
       fun () ->
@@ -248,7 +266,7 @@ decode_test_() ->
      {"compound data",
       fun () ->
               Input    = <<"  [true, {\"1\" : 2, \"array\":[[[[1]]], {\"ab\":\"cd\"}, false]}, null]   ">>,
-              Expected = [true, #{<<"1">> => 2, <<"array">> => [[[[1]]], #{<<"ab">> => <<"cd">>}, false]}, null],
+              Expected = [true, ?OBJ2(<<"1">>, 2, <<"array">>, [[[[1]]], ?OBJ1(<<"ab">>, <<"cd">>), false]), null],
               ?assertEqual({ok, Expected, <<"   ">>}, jsone_decode:decode(Input))
       end}
     ].

+ 33 - 23
test/jsone_encode_tests.erl

@@ -4,6 +4,16 @@
 
 -include_lib("eunit/include/eunit.hrl").
 
+-ifdef('NO_MAP_TYPE').
+-define(OBJ0, {[]}).
+-define(OBJ1(K, V), {[{K, V}]}).
+-define(OBJ2(K1, V1, K2, V2), {[{K1, V1}, {K2, V2}]}).
+-else.
+-define(OBJ0, #{}).
+-define(OBJ1(K, V), #{K => V}).
+-define(OBJ2(K1, V1, K2, V2), #{K1 => V1, K2 => V2}).
+-endif.
+
 encode_test_() ->
     [
      %% Symbols
@@ -145,13 +155,13 @@ encode_test_() ->
       end},
      {"simple object: map",
       fun () ->
-              Input = #{<<"key">> => <<"value">>, <<"1">> => 2},
+              Input = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>),
               Expected = <<"{\"1\":2,\"key\":\"value\"}">>,
               ?assertEqual({ok, Expected}, jsone_encode:encode(Input))
       end},
      {"empty object: map",
       fun () ->
-              Input = #{},
+              Input = ?OBJ0,
               Expected = <<"{}">>,
               ?assertEqual({ok, Expected}, jsone_encode:encode(Input))
       end},
@@ -163,29 +173,29 @@ encode_test_() ->
      {"object_key_type option",
       fun () ->
               %% key: atom
-              ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(#{a => 2}, [{object_key_type, string}])), % OK
-              ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(#{a => 2}, [{object_key_type, scalar}])), % OK
-              ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(#{a => 2}, [{object_key_type, value}])),  % OK
+              ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(?OBJ1(a, 2), [{object_key_type, string}])), % OK
+              ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(?OBJ1(a, 2), [{object_key_type, scalar}])), % OK
+              ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(?OBJ1(a, 2), [{object_key_type, value}])),  % OK
 
               %% key: number
-              ?assertMatch({error, {badarg, _}},  jsone_encode:encode(#{1 => 2}, [{object_key_type, string}])), % NG
-              ?assertEqual({ok, <<"{\"1\":2}">>}, jsone_encode:encode(#{1 => 2}, [{object_key_type, scalar}])), % OK
-              ?assertEqual({ok, <<"{\"1\":2}">>}, jsone_encode:encode(#{1 => 2}, [{object_key_type, value}])),  % OK
+              ?assertMatch({error, {badarg, _}},  jsone_encode:encode(?OBJ1(1, 2), [{object_key_type, string}])), % NG
+              ?assertEqual({ok, <<"{\"1\":2}">>}, jsone_encode:encode(?OBJ1(1, 2), [{object_key_type, scalar}])), % OK
+              ?assertEqual({ok, <<"{\"1\":2}">>}, jsone_encode:encode(?OBJ1(1, 2), [{object_key_type, value}])),  % OK
 
               %% key: datetime
-              ?assertMatch({error, {badarg, _}},  jsone_encode:encode(#{{{2000,1,1}, {0,0,0}} => 2}, [{object_key_type, string}])), % NG
-              ?assertEqual({ok, <<"{\"2000-01-01T00:00:00Z\":2}">>}, jsone_encode:encode(#{{{2000,1,1}, {0,0,0}} => 2}, [{object_key_type, scalar}])), % OK
-              ?assertEqual({ok, <<"{\"2000-01-01T00:00:00Z\":2}">>}, jsone_encode:encode(#{{{2000,1,1}, {0,0,0}} => 2}, [{object_key_type, value}])),  % OK
+              ?assertMatch({error, {badarg, _}},  jsone_encode:encode(?OBJ1({{2000,1,1}, {0,0,0}}, 2), [{object_key_type, string}])), % NG
+              ?assertEqual({ok, <<"{\"2000-01-01T00:00:00Z\":2}">>}, jsone_encode:encode(?OBJ1({{2000,1,1}, {0,0,0}}, 2), [{object_key_type, scalar}])), % OK
+              ?assertEqual({ok, <<"{\"2000-01-01T00:00:00Z\":2}">>}, jsone_encode:encode(?OBJ1({{2000,1,1}, {0,0,0}}, 2), [{object_key_type, value}])),  % OK
 
               %% key: array
-              ?assertMatch({error, {badarg, _}},    jsone_encode:encode(#{[1] => 2}, [{object_key_type, string}])), % NG
-              ?assertMatch({error, {badarg, _}},    jsone_encode:encode(#{[1] => 2}, [{object_key_type, scalar}])), % NG
-              ?assertEqual({ok, <<"{\"[1]\":2}">>}, jsone_encode:encode(#{[1] => 2}, [{object_key_type, value}])),  % OK
+              ?assertMatch({error, {badarg, _}},    jsone_encode:encode(?OBJ1([1], 2), [{object_key_type, string}])), % NG
+              ?assertMatch({error, {badarg, _}},    jsone_encode:encode(?OBJ1([1], 2), [{object_key_type, scalar}])), % NG
+              ?assertEqual({ok, <<"{\"[1]\":2}">>}, jsone_encode:encode(?OBJ1([1], 2), [{object_key_type, value}])),  % OK
 
               %% key: object
-              ?assertMatch({error, {badarg, _}},    jsone_encode:encode(#{#{} => 2}, [{object_key_type, string}])), % NG
-              ?assertMatch({error, {badarg, _}},    jsone_encode:encode(#{#{} => 2}, [{object_key_type, scalar}])), % NG
-              ?assertEqual({ok, <<"{\"{}\":2}">>}, jsone_encode:encode(#{#{} => 2}, [{object_key_type, value}]))    % OK
+              ?assertMatch({error, {badarg, _}},   jsone_encode:encode(?OBJ1(?OBJ0, 2), [{object_key_type, string}])), % NG
+              ?assertMatch({error, {badarg, _}},   jsone_encode:encode(?OBJ1(?OBJ0, 2), [{object_key_type, scalar}])), % NG
+              ?assertEqual({ok, <<"{\"{}\":2}">>}, jsone_encode:encode(?OBJ1(?OBJ0, 2), [{object_key_type, value}]))    % OK
       end},
      {"non binary object member key is disallowed",
       fun () ->
@@ -198,22 +208,22 @@ encode_test_() ->
       fun () ->
               ?assertEqual({ok, <<"[1, 2, 3]">>}, jsone_encode:encode([1,2,3], [{space, 1}])),
               ?assertEqual({ok, <<"[1,  2,  3]">>}, jsone_encode:encode([1,2,3], [{space, 2}])),
-              ?assertEqual({ok, <<"{\"a\": 1, \"b\": 2}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{space, 1}])),
-              ?assertEqual({ok, <<"{\"a\":  1,  \"b\":  2}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{space, 2}]))
+              ?assertEqual({ok, <<"{\"a\": 1, \"b\": 2}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{space, 1}])),
+              ?assertEqual({ok, <<"{\"a\":  1,  \"b\":  2}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{space, 2}]))
       end},
      {"indent",
       fun () ->
               ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 1}])),
               ?assertEqual({ok, <<"[\n  1,\n  2,\n  3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 2}])),
-              ?assertEqual({ok, <<"{\n \"a\":1,\n \"b\":2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 1}])),
-              ?assertEqual({ok, <<"{\n  \"a\":1,\n  \"b\":2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 2}]))
+              ?assertEqual({ok, <<"{\n \"a\":1,\n \"b\":2\n}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 1}])),
+              ?assertEqual({ok, <<"{\n  \"a\":1,\n  \"b\":2\n}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 2}]))
       end},
      {"indent+space",
       fun () ->
               ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 1}, {space, 1}])),
               ?assertEqual({ok, <<"[\n  1,\n  2,\n  3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 2}, {space, 2}])),
-              ?assertEqual({ok, <<"{\n \"a\": 1,\n \"b\": 2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 1}, {space, 1}])),
-              ?assertEqual({ok, <<"{\n  \"a\":  1,\n  \"b\":  2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 2}, {space, 2}]))
+              ?assertEqual({ok, <<"{\n \"a\": 1,\n \"b\": 2\n}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 1}, {space, 1}])),
+              ?assertEqual({ok, <<"{\n  \"a\":  1,\n  \"b\":  2\n}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 2}, {space, 2}]))
       end},
 
      %% Others