Browse Source

Merge pull request #48 from russelldb/rdb/map-last-prop-wins

Add option to return last duplicate property
Takeru Ohta 5 years ago
parent
commit
c038a81f1f
3 changed files with 31 additions and 2 deletions
  1. 15 1
      src/jsone.erl
  2. 7 1
      src/jsone_decode.erl
  3. 9 0
      test/jsone_decode_tests.erl

+ 15 - 1
src/jsone.erl

@@ -234,6 +234,7 @@
                        | {allow_ctrl_chars, boolean()}
                        | reject_invalid_utf8
                        | {'keys', 'binary' | 'atom' | 'existing_atom' | 'attempt_atom'}
+                       | {duplicate_map_keys, first | last}
                        | common_option().
 %% `object_format': <br />
 %% - Decoded JSON object format <br />
@@ -260,7 +261,20 @@
 %% existing atom raises `badarg' exception. <br />
 %% - `attempt_atom': Returns existing atom as `existing_atom' but returns a
 %% binary string if fails find one.
-
+%%
+%% `duplicate_map_keys': <br />
+%% https://www.ietf.org/rfc/rfc4627.txt says that keys SHOULD be
+%% unique, but they don't have to be. Most JSON parsers will either
+%% give you the value of the first, or last duplicate property
+%% encountered. When `object_format' is `tuple' or `proplist' all
+%% duplicates are returned. When `object_format' is `map' by default
+%% the first instance of a duplicate is returned. Setting
+%% `duplicate_map_keys' to `last' will change this behaviour to return
+%% the last such instance.
+%% - If the value is `first' then the first duplicate key/value is returned.  <br />
+%% - If the value is `last' then the last duplicate key/value is returned.
+%% - default: `first'<br />
+%%
 
 -type stack_item() :: {Module :: module(),
                        Function :: atom(),

+ 7 - 1
src/jsone_decode.erl

@@ -69,7 +69,8 @@
           allow_ctrl_chars=false :: boolean(),
           reject_invalid_utf8=false :: boolean(),
           keys=binary :: 'binary' | 'atom' | 'existing_atom' | 'attempt_atom',
-          undefined_as_null=false :: boolean()
+          undefined_as_null=false :: boolean(),
+          duplicate_map_keys=first :: first | last
         }).
 -define(OPT, #decode_opt_v2).
 -type opt() :: #decode_opt_v2{}.
@@ -292,6 +293,8 @@ 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, duplicate_map_keys = last}) ->
+    ?LIST_TO_MAP(lists:reverse(Members));
 make_object(Members, ?OPT{object_format = map})   -> ?LIST_TO_MAP(Members);
 make_object([],      _)                           -> [{}];
 make_object(Members, _)                           -> lists:reverse(Members).
@@ -313,5 +316,8 @@ parse_option([{keys, K}|T], Opt)
     parse_option(T, Opt?OPT{keys = K});
 parse_option([undefined_as_null|T], Opt) ->
     parse_option(T, Opt?OPT{undefined_as_null = true});
+parse_option([{duplicate_map_keys, V} | T], Opt)
+  when V =:= first; V =:= last ->
+    parse_option(T, Opt?OPT{duplicate_map_keys=V});
 parse_option(List, Opt) ->
     error(badarg, [List, Opt]).

+ 9 - 0
test/jsone_decode_tests.erl

@@ -16,6 +16,7 @@
 -define(OBJ1(K, V), #{K => V}).
 -define(OBJ2(K1, V1, K2, V2), #{K1 => V1, K2 => V2}).
 -define(OBJ2_DUP_KEY(K1, V1, _K2, _V2), #{K1 => V1}). % the first (leftmost) value is used
+-define(OBJ2_DUP_KEY_LAST(_K1, _V1, K2, V2), #{K2 => V2}). % the last value is used
 -endif.
 
 decode_test_() ->
@@ -232,6 +233,14 @@ decode_test_() ->
               Expected = ?OBJ2_DUP_KEY(<<"1">>, <<"first">>, <<"1">>, <<"second">>),
               ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}]))
       end},
+     {"duplicated members last: map",
+      fun () ->
+              Input    = <<"{\"1\":\"first\",\"1\":\"second\"}">>,
+              Expected = ?OBJ2_DUP_KEY_LAST(<<"1">>, <<"first">>, <<"1">>, <<"second">>),
+              ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input,
+                                                                       [{object_format, ?MAP_OBJECT_TYPE},
+                                                                        {duplicate_map_keys, last}]))
+      end},
      {"object: trailing comma is disallowed",
       fun () ->
               Input = <<"{\"1\":2, \"key\":\"value\", }">>,