Browse Source

Merge pull request #15 from altworx/atom_keys

Add jsx compatible keys/labels option for object keys atom conversion
Takeru Ohta 8 years ago
parent
commit
8c7f923c7d
4 changed files with 59 additions and 4 deletions
  1. 13 1
      doc/jsone.md
  2. 14 1
      src/jsone.erl
  3. 16 2
      src/jsone_decode.erl
  4. 16 0
      test/jsone_decode_tests.erl

+ 13 - 1
doc/jsone.md

@@ -60,7 +60,7 @@ datetime_format() = iso8601
 
 
 <pre><code>
-decode_option() = {object_format, tuple | proplist | map} | {allow_ctrl_chars, boolean()}
+decode_option() = {object_format, tuple | proplist | map} | {allow_ctrl_chars, boolean()} | {keys, binary | atom | existing_atom | attempt_atom}
 </code></pre>
 
 `object_format`: <br />
@@ -74,6 +74,18 @@ decode_option() = {object_format, tuple | proplist | map} | {allow_ctrl_chars, b
 - If the value is `true`, strings which contain ununescaped control characters will be regarded as a legal JSON string <br />
 - default: `false`<br />
 
+`keys`: <br />
+Defines way how object keys are decoded. The default value is `binary`.
+The option is compatible with `labels` option in `jsx`. <br />
+- `binary`: The key is left as a string which is encoded as binary. It's default
+and backward compatible behaviour. <br />
+- `atom`: The key is converted to an atom. Results in `badarg` if Key value
+regarded as UTF-8 is not a valid atom. <br />
+- `existing_atom`: Returns existing atom. Any key value which is not
+existing atom raises `badarg` exception. <br />
+- `attempt_atom`: Returns existing atom as `existing_atom` but returns a
+binary string if fails find one.
+
 
 
 ### <a name="type-encode_option">encode_option()</a> ###

+ 14 - 1
src/jsone.erl

@@ -217,7 +217,8 @@
 %% - default: `0' <br />
 
 -type decode_option() :: {object_format, tuple | proplist | map}
-                       | {allow_ctrl_chars, boolean()}.
+                       | {allow_ctrl_chars, boolean()}
+                       | {'keys', 'binary' | 'atom' | 'existing_atom' | 'attempt_atom'}.
 %% `object_format': <br />
 %% - Decoded JSON object format <br />
 %% - `tuple': An object is decoded as `{[]}' if it is empty, otherwise `{[{Key, Value}]}'. <br />
@@ -228,6 +229,18 @@
 %% `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 />
+%%
+%% `keys': <br />
+%% Defines way how object keys are decoded. The default value is `binary'.
+%% The option is compatible with `labels' option in `jsx'. <br />
+%% - `binary': The key is left as a string which is encoded as binary. It's default
+%% and backward compatible behaviour. <br />
+%% - `atom': The key is converted to an atom. Results in `badarg' if Key value
+%% regarded as UTF-8 is not a valid atom. <br />
+%% - `existing_atom': Returns existing atom. Any key value which is not
+%% existing atom raises `badarg' exception. <br />
+%% - `attempt_atom': Returns existing atom as `existing_atom' but returns a
+%% binary string if fails find one.
 
 %%--------------------------------------------------------------------------------
 %% Exported Functions

+ 16 - 2
src/jsone_decode.erl

@@ -66,7 +66,8 @@
 -record(decode_opt_v2,
         {
           object_format=?DEFAULT_OBJECT_FORMAT :: tuple | proplist | map,
-          allow_ctrl_chars=false :: boolean()
+          allow_ctrl_chars=false :: boolean(),
+          keys=binary :: 'binary' | 'atom' | 'existing_atom' | 'attempt_atom'
         }).
 -define(OPT, #decode_opt_v2).
 -type opt() :: #decode_opt_v2{}.
@@ -139,9 +140,19 @@ object_key(<<$", Bin/binary>>, Members, Nexts, Buf, Opt) -> string(Bin, byte_siz
 object_key(<<Bin/binary>>, Members, Nexts, Buf, Opt)     -> ?ERROR(object_key, [Bin, Members, Nexts, Buf, Opt]).
 
 -spec object_value(binary(), jsone:json_string(), jsone:json_object_members(), [next()], binary(), opt()) -> decode_result().
-object_value(<<$:, Bin/binary>>, Key, Members, Nexts, Buf, Opt) -> whitespace(Bin, value, [{object_next, Key, Members} | Nexts], Buf, Opt);
+object_value(<<$:, Bin/binary>>, Key, Members, Nexts, Buf, Opt) ->
+    whitespace(Bin, value, [{object_next, object_key(Key, Opt), Members} | Nexts], Buf, Opt);
 object_value(Bin,                Key, Members, Nexts, Buf, Opt) -> ?ERROR(object_value, [Bin, Key, Members, Nexts, Buf, Opt]).
 
+-compile({inline, [object_key/2]}).
+object_key(Key, ?OPT{keys = binary}) -> Key;
+object_key(Key, ?OPT{keys = atom}) -> binary_to_atom(Key, utf8);
+object_key(Key, ?OPT{keys = existing_atom}) -> binary_to_existing_atom(Key, utf8);
+object_key(Key, ?OPT{keys = attempt_atom}) ->
+    try binary_to_existing_atom(Key, utf8)
+    catch error:badarg -> Key
+    end.
+
 -spec object_next(binary(), jsone:json_object_members(), [next()], binary(), opt()) -> decode_result().
 object_next(<<$}, Bin/binary>>, Members, Nexts, Buf, Opt) -> next(Bin, make_object(Members, Opt), Nexts, Buf, Opt);
 object_next(<<$,, Bin/binary>>, Members, Nexts, Buf, Opt) -> whitespace(Bin, {object_key, Members}, Nexts, Buf, Opt);
@@ -280,5 +291,8 @@ parse_option([{object_format,F}|T], Opt) when F =:= tuple; F =:= proplist; F =:=
     parse_option(T, Opt?OPT{object_format=F});
 parse_option([{allow_ctrl_chars,B}|T], Opt) when is_boolean(B) ->
     parse_option(T, Opt?OPT{allow_ctrl_chars=B});
+parse_option([{keys, K}|T], Opt)
+  when K =:= binary; K =:= atom; K =:= existing_atom; K =:= attempt_atom ->
+    parse_option(T, Opt?OPT{keys = K});
 parse_option(List, Opt) ->
     error(badarg, [List, Opt]).

+ 16 - 0
test/jsone_decode_tests.erl

@@ -261,6 +261,22 @@ decode_test_() ->
               Input = <<"{\"1\":2 \"key\":\"value\"">>,
               ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
       end},
+     {"atom keys",
+      fun () ->
+              KeyOpt = fun(Keys) -> [{keys, Keys}, {object_format, proplist}]
+                       end,
+              Input = <<"{\"foo\":\"ok\"}">>,
+              ?assertEqual([{<<"foo">>, <<"ok">>}], jsone:decode(Input, KeyOpt(binary))),
+              ?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(atom))),
+              ?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(existing_atom))),
+              ?assertError(badarg, jsone:decode(<<"{\"@#$%^!\":\"ok\"}">>, KeyOpt(existing_atom))),
+              ?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(attempt_atom))),
+              ?assertEqual([{<<"@#$%^!">>, <<"ok">>}], jsone:decode(<<"{\"@#$%^!\":\"ok\"}">>, KeyOpt(attempt_atom))),
+              Value = integer_to_binary(rand:uniform(9999)),
+              % do not make atom in test code
+              [{Atom,  <<"ok">>}] = jsone:decode(<<"{\"", Value/binary, "\":\"ok\"}">>, KeyOpt(atom)),
+              ?assertEqual(Value, atom_to_binary(Atom, latin1))
+      end},
 
      %% Others
      {"compound data",