Browse Source

encode to JSON canonical form

make sure that two JSON are encoded similarly whatever the order of the
keys in the object. This allows 2 json to be compared across machines.

The change follows the draft of this spec defined here:
https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00
Benoit Chesneau 8 years ago
parent
commit
0654e98bd5
3 changed files with 12 additions and 0 deletions
  1. 1 0
      src/jsone.erl
  2. 5 0
      src/jsone_encode.erl
  3. 6 0
      test/jsone_encode_tests.erl

+ 1 - 0
src/jsone.erl

@@ -184,6 +184,7 @@
 -type utc_offset_seconds() :: -86399..86399.
 
 -type encode_option() :: native_utf8
+                       | canonical_form
                        | {float_format, [float_format_option()]}
                        | {datetime_format, datetime_encode_format()}
                        | {object_key_type, string | scalar | value}

+ 5 - 0
src/jsone_encode.erl

@@ -63,6 +63,7 @@
 
 -record(encode_opt_v2, {
           native_utf8 = false :: boolean(),
+          canonical_form = false :: boolean(),
           float_format = [{scientific, 20}] :: [jsone:float_format_option()],
           datetime_format = {iso8601, 0} :: {jsone:datetime_format(), jsone:utc_offset_seconds()},
           object_key_type = string :: string | scalar | value,
@@ -306,6 +307,8 @@ array_values([],       Nexts, Buf, Opt) -> next(Nexts, <<(pp_newline(Buf, Nexts,
 array_values([X | Xs], Nexts, Buf, Opt) -> value(X, [{array_values, Xs} | Nexts], Buf, Opt).
 
 -spec object(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
+object(Members, Nexts, Buf, ?OPT{canonical_form = true}=Opt) ->
+  object_members(lists:keysort(1, Members), Nexts, pp_newline(<<Buf/binary, ${>>, Nexts, 1, Opt), Opt);
 object(Members, Nexts, Buf, Opt) ->
     object_members(Members, Nexts, pp_newline(<<Buf/binary, ${>>, Nexts, 1, Opt), Opt).
 
@@ -352,6 +355,8 @@ parse_options(Options) ->
 parse_option([], Opt) -> Opt;
 parse_option([native_utf8|T], Opt) ->
     parse_option(T, Opt?OPT{native_utf8=true});
+parse_option([canonical_form|T], Opt) ->
+    parse_option(T, Opt?OPT{canonical_form=true});
 parse_option([{float_format, F}|T], Opt) when is_list(F) ->
     parse_option(T, Opt?OPT{float_format = F});
 parse_option([{space, N}|T], Opt) when is_integer(N), N >= 0 ->

+ 6 - 0
test/jsone_encode_tests.erl

@@ -283,5 +283,11 @@ encode_test_() ->
      {"wrong option",
       fun () ->
               ?assertError(badarg, jsone_encode:encode(1, [{no_such_option, hoge}]))
+      end},
+     {"canonical_form",
+      fun () ->
+          Obj1 = maps:from_list( [{<<"key", (integer_to_binary(I))/binary >>, I} || I <- lists:seq(1000, 0, -1)] ),
+          Obj2 = maps:from_list( [{<<"key", (integer_to_binary(I))/binary >>, I} || I <- lists:seq(0, 1000, 1)] ),
+          ?assertEqual(jsone_encode:encode(Obj1, [canonical_form]), jsone_encode:encode(Obj2, [canonical_form]))
       end}
     ].