Browse Source

Add object_key_type encoding option

Takeru Ohta 9 years ago
parent
commit
9ff5cdf332
5 changed files with 80 additions and 5 deletions
  1. 4 0
      README.md
  2. 19 1
      doc/jsone.md
  3. 13 1
      src/jsone.erl
  4. 22 3
      src/jsone_encode.erl
  5. 22 0
      test/jsone_encode_tests.erl

+ 4 - 0
README.md

@@ -104,6 +104,10 @@ Usage Example
                               [[{123,<<"value">>}],[],<<"{">>],
                               [[{123,<<"value">>}],[],<<"{">>],
                               [{line,138}]}]}}
                               [{line,138}]}]}}
 
 
+% 'object_key_type' option allows non-string object key
+> jsone:encode({[{123, <<"value">>}]}, [{object_key_type, scalar}]).
+<<"{\"123\":\"value\"}">>
+
 %% Pretty Print
 %% Pretty Print
 > Data = [true, #{<<"1">> => 2, <<"array">> => [[[[1]]], #{<<"ab">> => <<"cd">>}, false]}, null].
 > Data = [true, #{<<"1">> => 2, <<"array">> => [[[[1]]], #{<<"ab">> => <<"cd">>}, false]}, null].
 > io:format("~s\n", [jsone:encode(Data, [{indent, 1}, {space, 2}])]).
 > io:format("~s\n", [jsone:encode(Data, [{indent, 1}, {space, 2}])]).

+ 19 - 1
doc/jsone.md

@@ -35,7 +35,7 @@ decode_option() = {object_format, tuple | proplist | map}
 
 
 
 
 <pre><code>
 <pre><code>
-encode_option() = native_utf8 | {float_format, [<a href="#type-float_format_option">float_format_option()</a>]} | {space, non_neg_integer()} | {indent, non_neg_integer()}
+encode_option() = native_utf8 | {float_format, [<a href="#type-float_format_option">float_format_option()</a>]} | {object_key_type, string | scalar | value} | {space, non_neg_integer()} | {indent, non_neg_integer()}
 </code></pre>
 </code></pre>
 
 
 `native_utf8`: <br />
 `native_utf8`: <br />
@@ -45,6 +45,14 @@ encode_option() = native_utf8 | {float_format, [<a href="#type-float_format_opti
 - Encodes a `float()` value in the format which specified by `Options` <br />
 - Encodes a `float()` value in the format which specified by `Options` <br />
 - default: `[{scientific, 20}]` <br />
 - default: `[{scientific, 20}]` <br />
 
 
+`object_key_type`:
+- Allowable object key type <br />
+- `string`: Only string values are allowed (i.e. `json_string()` type) <br />
+- `scalar`: In addition to `string`, following values are allowed: nulls, booleans, numerics (i.e. `json_scalar()` type) <br />
+- `value`: Any json compatible values are allowed (i.e. `json_value()` type) <br />
+- default: `string` <br />
+- NOTE: Non `json_string()` value is automatically converted to a `binary()` value (e.g. `1` => `<<"1">>`, `#{}` => `<<"{}">>`) <br />
+
 `{space, N}`: <br />
 `{space, N}`: <br />
 - Inserts `N` spaces after every commna and colon <br />
 - Inserts `N` spaces after every commna and colon <br />
 - default: `0` <br />
 - default: `0` <br />
@@ -166,6 +174,16 @@ json_object_members() = [{<a href="#type-json_string">json_string()</a>, <a href
 
 
 
 
 
 
+### <a name="type-json_scalar">json_scalar()</a> ###
+
+
+<pre><code>
+json_scalar() = <a href="#type-json_boolean">json_boolean()</a> | <a href="#type-json_number">json_number()</a> | <a href="#type-json_string">json_string()</a>
+</code></pre>
+
+
+
+
 ### <a name="type-json_string">json_string()</a> ###
 ### <a name="type-json_string">json_string()</a> ###
 
 
 
 

+ 13 - 1
src/jsone.erl

@@ -47,6 +47,7 @@
               json_object_format_tuple/0,
               json_object_format_tuple/0,
               json_object_format_proplist/0,
               json_object_format_proplist/0,
               json_object_format_map/0,
               json_object_format_map/0,
+              json_scalar/0,
 
 
               encode_option/0,
               encode_option/0,
               decode_option/0,
               decode_option/0,
@@ -70,6 +71,8 @@
 -type json_object_format_tuple() :: {json_object_members()}.
 -type json_object_format_tuple() :: {json_object_members()}.
 -type json_object_format_proplist() :: [{}] | json_object_members().
 -type json_object_format_proplist() :: [{}] | json_object_members().
 
 
+-type json_scalar() :: json_boolean() | json_number() | json_string().
+
 -type float_format_option() :: {scientific, Decimals :: 0..249}
 -type float_format_option() :: {scientific, Decimals :: 0..249}
                              | {decimals, Decimals :: 0..253}
                              | {decimals, Decimals :: 0..253}
                              | compact.
                              | compact.
@@ -101,15 +104,24 @@
 
 
 -type encode_option() :: native_utf8
 -type encode_option() :: native_utf8
                        | {float_format, [float_format_option()]}
                        | {float_format, [float_format_option()]}
+                       | {object_key_type, string | scalar | value}
                        | {space, non_neg_integer()}
                        | {space, non_neg_integer()}
                        | {indent, non_neg_integer()}.
                        | {indent, non_neg_integer()}.
 %% `native_utf8': <br />
 %% `native_utf8': <br />
 %% - Encodes UTF-8 characters as a human-readable(non-escaped) string <br />
 %% - Encodes UTF-8 characters as a human-readable(non-escaped) string <br />
 %%
 %%
-%% `{float_format, Optoins}`:
+%% `{float_format, Optoins}':
 %% - Encodes a `float()` value in the format which specified by `Options' <br />
 %% - Encodes a `float()` value in the format which specified by `Options' <br />
 %% - default: `[{scientific, 20}]' <br />
 %% - default: `[{scientific, 20}]' <br />
 %%
 %%
+%% `object_key_type':
+%% - Allowable object key type <br />
+%% - `string': Only string values are allowed (i.e. `json_string()' type) <br />
+%% - `scalar': In addition to `string', following values are allowed: nulls, booleans, numerics (i.e. `json_scalar()' type) <br />
+%% - `value': Any json compatible values are allowed (i.e. `json_value()' type) <br />
+%% - default: `string' <br />
+%% - NOTE: Non `json_string()' value is automatically converted to a `binary()' value (e.g. `1' => `<<"1">>', `#{}' => `<<"{}">>') <br />
+%%
 %% `{space, N}': <br />
 %% `{space, N}': <br />
 %% - Inserts `N' spaces after every commna and colon <br />
 %% - Inserts `N' spaces after every commna and colon <br />
 %% - default: `0' <br />
 %% - default: `0' <br />

+ 22 - 3
src/jsone_encode.erl

@@ -44,11 +44,13 @@
 -type encode_result() :: {ok, binary()} | {error, {Reason::term(), [erlang:stack_item()]}}.
 -type encode_result() :: {ok, binary()} | {error, {Reason::term(), [erlang:stack_item()]}}.
 -type next() :: {array_values, [jsone:json_value()]}
 -type next() :: {array_values, [jsone:json_value()]}
               | {object_value, jsone:json_value(), jsone:json_object_members()}
               | {object_value, jsone:json_value(), jsone:json_object_members()}
-              | {object_members, jsone:json_object_members()}.
+              | {object_members, jsone:json_object_members()}
+              | {char, binary()}.
 
 
 -record(encode_opt_v1, {
 -record(encode_opt_v1, {
           native_utf8 = false :: boolean(),
           native_utf8 = false :: boolean(),
           float_format = [{scientific, 20}] :: [jsone:float_format_option()],
           float_format = [{scientific, 20}] :: [jsone:float_format_option()],
+          object_key_type = string :: string | scalar | value,
           space = 0 :: non_neg_integer(),
           space = 0 :: non_neg_integer(),
           indent = 0 :: non_neg_integer()
           indent = 0 :: non_neg_integer()
          }).
          }).
@@ -85,7 +87,9 @@ next(Level = [Next | Nexts], Buf, Opt) ->
             case Members of
             case Members of
                 [] -> object_members(Members, Nexts, Buf, Opt);
                 [] -> object_members(Members, Nexts, Buf, Opt);
                 _  -> object_members(Members, Nexts, pp_newline_or_space(<<Buf/binary, $,>>, Level, Opt), Opt)
                 _  -> object_members(Members, Nexts, pp_newline_or_space(<<Buf/binary, $,>>, Level, Opt), Opt)
-            end
+            end;
+        {char, C} ->
+            next(Nexts, <<Buf/binary, C>>, Opt)
     end.
     end.
 
 
 -spec value(jsone:json_value(), [next()], binary(), opt()) -> encode_result().
 -spec value(jsone:json_value(), [next()], binary(), opt()) -> encode_result().
@@ -108,6 +112,19 @@ string(<<Str/binary>>, Nexts, Buf, Opt) ->
 string(Str, Nexts, Buf, Opt) ->
 string(Str, Nexts, Buf, Opt) ->
     string(atom_to_binary(Str, utf8), Nexts, Buf, Opt).
     string(atom_to_binary(Str, utf8), Nexts, Buf, Opt).
 
 
+-spec object_key(jsone:json_value(), [next()], binary(), opt()) -> encode_result().
+object_key(Key, Nexts, Buf, Opt) when ?IS_STR(Key) ->
+    string(Key, Nexts, Buf, Opt);
+object_key(Key, Nexts, Buf, Opt = ?OPT{object_key_type = scalar}) when is_number(Key) ->
+    value(Key, [{char, $"} | Nexts], <<Buf/binary, $">>, Opt);
+object_key(Key, Nexts, Buf, Opt = ?OPT{object_key_type = value}) ->
+    case value(Key, [], <<>>, Opt) of
+        {error, Reason} -> {error, Reason};
+        {ok, BinaryKey} -> string(BinaryKey, Nexts, Buf, Opt)
+    end;
+object_key(Key, Nexts, Buf, Opt) ->
+    ?ERROR(object_key, [Key, Nexts, Buf, Opt]).
+
 -spec escape_string(binary(), [next()], binary(), opt()) -> encode_result().
 -spec escape_string(binary(), [next()], binary(), opt()) -> encode_result().
 escape_string(<<"">>,                   Nexts, Buf, Opt) -> next(Nexts, <<Buf/binary, $">>, Opt);
 escape_string(<<"">>,                   Nexts, Buf, Opt) -> next(Nexts, <<Buf/binary, $">>, Opt);
 escape_string(<<$", Str/binary>>,       Nexts, Buf, Opt) -> escape_string(Str, Nexts, <<Buf/binary, $\\, $">>, Opt);
 escape_string(<<$", Str/binary>>,       Nexts, Buf, Opt) -> escape_string(Str, Nexts, <<Buf/binary, $\\, $">>, Opt);
@@ -171,7 +188,7 @@ object(Members, Nexts, Buf, Opt) ->
 
 
 -spec object_members(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
 -spec object_members(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
 object_members([],                             Nexts, Buf, Opt)        -> next(Nexts, <<(pp_newline(Buf, Nexts, Opt))/binary, $}>>, Opt);
 object_members([],                             Nexts, Buf, Opt)        -> next(Nexts, <<(pp_newline(Buf, Nexts, Opt))/binary, $}>>, Opt);
-object_members([{Key, Value} | Xs], Nexts, Buf, Opt) when ?IS_STR(Key) -> string(Key, [{object_value, Value, Xs} | Nexts], Buf, Opt);
+object_members([{Key, Value} | Xs], Nexts, Buf, Opt)                   -> object_key(Key, [{object_value, Value, Xs} | Nexts], Buf, Opt);
 object_members(Arg, Nexts, Buf, Opt)                                   -> ?ERROR(object_members, [Arg, Nexts, Buf, Opt]).
 object_members(Arg, Nexts, Buf, Opt)                                   -> ?ERROR(object_members, [Arg, Nexts, Buf, Opt]).
 
 
 -spec object_value(jsone:json_value(), jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
 -spec object_value(jsone:json_value(), jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
@@ -210,5 +227,7 @@ parse_option([{space, N}|T], Opt) when is_integer(N), N >= 0 ->
     parse_option(T, Opt?OPT{space = N});
     parse_option(T, Opt?OPT{space = N});
 parse_option([{indent, N}|T], Opt) when is_integer(N), N >= 0 ->
 parse_option([{indent, N}|T], Opt) when is_integer(N), N >= 0 ->
     parse_option(T, Opt?OPT{indent = N});
     parse_option(T, Opt?OPT{indent = N});
+parse_option([{object_key_type, Type}|T], Opt) when Type =:= string; Type =:= scalar; Type =:= value ->
+    parse_option(T, Opt?OPT{object_key_type = Type});
 parse_option(List, Opt) ->
 parse_option(List, Opt) ->
     error(badarg, [List, Opt]).
     error(badarg, [List, Opt]).

+ 22 - 0
test/jsone_encode_tests.erl

@@ -142,6 +142,28 @@ encode_test_() ->
               Expected = <<"{\"key\":2}">>,
               Expected = <<"{\"key\":2}">>,
               ?assertEqual({ok, Expected}, jsone_encode:encode({[{key, 2}]}))
               ?assertEqual({ok, Expected}, jsone_encode:encode({[{key, 2}]}))
       end},
       end},
+     {"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
+
+              %% 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
+
+              %% 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
+
+              %% 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
+      end},
      {"non binary object member key is disallowed",
      {"non binary object member key is disallowed",
       fun () ->
       fun () ->
               ?assertMatch({error, {badarg, _}}, jsone_encode:encode({[{1, 2}]})),
               ?assertMatch({error, {badarg, _}}, jsone_encode:encode({[{1, 2}]})),