Skip to content

Commit

Permalink
Add json format functions for key-value lists
Browse files Browse the repository at this point in the history
Support formating key-value lists while preserve their ordering.
  • Loading branch information
Ledest committed Nov 3, 2024
1 parent f6dd2bc commit 0ccbd39
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 19 deletions.
2 changes: 2 additions & 0 deletions rebar.config.script
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ Funs = [{application, get_supervisor, 1},
{json, format, 1},
{json, format, 2},
{json, format, 3},
{json, format_key_value_list, 3},
{json, format_key_value_list_checked, 3},
{json, format_value, 3},
{lib, error_message, 2},
{lib, flush_receive, 0},
Expand Down
112 changes: 94 additions & 18 deletions src/otpbp_json.erl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@
% OTP 27.1
-export([format_value/3]).
-endif.
-ifndef(HAVE_json__format_key_value_list_3).
% OTP 27.2
-export([format_key_value_list/3]).
-endif.
-ifndef(HAVE_json__format_key_value_list_checked_3).
% OTP 27.2
-export([format_key_value_list_checked/3]).
-endif.
-ifdef(HAVE_json__encode_float_1).
-ifndef(HAVE_json__format_key_value_list_checked_3).
-endif.
-endif.

-ifndef(HAVE_json__encode_value_2).
-ifdef(HAVE_json__encode_atom_2).
Expand Down Expand Up @@ -121,6 +133,26 @@
-import(json, [encode_float/1]).
-endif.
-endif.
-ifndef(HAVE_json__format_value_3).
-ifdef(HAVE_json__format_key_value_list_3).
-import(json, [format_key_value_list/3]).
-endif.
-endif.
-ifndef(HAVE_json__format_key_value_list_3).
-ifndef(NEED_encode_float_1).
-define(NEED_encode_float_1, true).
-endif.
-endif.
-ifndef(HAVE_json__format_key_value_list_checked_3).
-ifndef(NEED_encode_float_1).
-define(NEED_encode_float_1, true).
-endif.
-endif.
-ifdef(NEED_encode_float_1).
-ifdef(HAVE_json__encode_float_1).
-import(json, [encode_float/1]).
-endif.
-endif.

-ifndef(HAVE_json__decode_1).
-ifndef(NEED_record__decode).
Expand Down Expand Up @@ -388,39 +420,64 @@ format_tail([Head|Tail], Enc, State, IndentAll, IndentRow) ->
[[[$,|IndentAll]|Enc(Head, Enc, State)]|format_tail(Tail, Enc, State, IndentAll, IndentRow)];
format_tail([], _, _, _, _) -> [].

-ifndef(NEED_indent_1).
-define(NEED_indent_1, true).
-endif.
-endif.

-ifndef(HAVE_json__format_key_value_list_3).
format_key_value_list(KVList, UserEnc, #{level := Level} = State) ->
{_, Indent} = indent(State),
NextState = State#{level := Level+1},
NextState = State#{level := Level + 1},
{KISize, KeyIndent} = indent(NextState),
EncKeyFun = fun(KeyVal, _Fun) -> UserEnc(KeyVal, UserEnc, NextState) end,
format_object(lists:map(fun({Key, Value}) ->
EncKey = key(Key, EncKeyFun),
ValState = NextState#{col := KISize + 2 + erlang:iolist_size(EncKey)},
ValState = NextState#{col := KISize + 2 + iolist_size(EncKey)},
[$,, KeyIndent, EncKey, ": "|UserEnc(Value, UserEnc, ValState)]
end,
KVList),
Indent).

format_object([], _) -> <<"{}">>;
format_object([[_Comma, KeyIndent|Entry]], Indent) ->
[_Key, _Colon|Value] = Entry,
{_, Rest} = string:take(Value, [$\s, $\n]),
[CP|_] = string:next_codepoint(Rest),
if
CP =:= ${; CP =:= $[ -> [${, KeyIndent, Entry, Indent, $}];
true -> ["{ ", Entry, " }"]
end;
format_object([[_Comma, KeyIndent|Entry]|Rest], Indent) -> [${, KeyIndent, Entry, Rest, Indent, $}].

indent(#{level := Level, indent := Indent}) ->
Steps = Level * Indent,
{Steps, steps(Steps)}.

steps(N) -> [$\n|lists:duplicate(N, $\s)].
-ifndef(NEED_indent_1).
-define(NEED_indent_1, true).
-endif.
-ifndef(NEED_key_2).
-define(NEED_key_2, true).
-endif.
-ifndef(NEED_format_object_2).
-define(NEED_format_object_2, true).
-endif.
-endif.

-ifndef(HAVE_json__format_key_value_list_checked_3).
format_key_value_list_checked([], UserEnc, State) when is_function(UserEnc, 3) ->
{_, Indent} = indent(State),
format_object([], Indent);
format_key_value_list_checked(KVList, UserEnc, #{level := Level} = State) when is_function(UserEnc, 3) ->
{_, Indent} = indent(State),
NextState = State#{level := Level + 1},
{KISize, KeyIndent} = indent(NextState),
EncKeyFun = fun(KeyVal, _Fun) -> UserEnc(KeyVal, UserEnc, NextState) end,
{EncKVList, _} = lists:foldl(fun({Key, Value}, {Acc, Visited0}) ->
EncKey = iolist_to_binary(key(Key, EncKeyFun)),
maps:is_key(EncKey, Visited0) andalso error({duplicate_key, Key}),
ValState = NextState#{col := KISize + 2 + byte_size(EncKey)},
{[[$, , KeyIndent, EncKey, ": "|UserEnc(Value, UserEnc, ValState)]|Acc],
Visited0#{EncKey => true}}
end,
{[], #{}}, KVList),
format_object(lists:reverse(EncKVList), Indent).

-ifndef(NEED_indent_1).
-define(NEED_indent_1, true).
-endif.
-ifndef(NEED_key_2).
-define(NEED_key_2, true).
-endif.
-ifndef(NEED_format_object_2).
-define(NEED_format_object_2, true).
-endif.
-endif.

-define(UTF8_ACCEPT, 0).
Expand Down Expand Up @@ -618,6 +675,25 @@ key(Key, _Encode) when is_integer(Key) -> [$", integer_to_binary(Key), $"];
key(Key, _Encode) when is_float(Key) -> [$", encode_float(Key), $"].
-endif.

-ifdef(NEED_format_object_2).
format_object([], _) -> <<"{}">>;
format_object([[_Comma, KeyIndent|Entry]], Indent) ->
[_Key, _Colon|Value] = Entry,
{_, Rest} = string:take(Value, [$\s, $\n]),
[CP|_] = string:next_codepoint(Rest),
if
CP =:= ${; CP =:= $[ -> [${, KeyIndent, Entry, Indent, $}];
true -> ["{ ", Entry, " }"]
end;
format_object([[_Comma, KeyIndent|Entry]|Rest], Indent) -> [${, KeyIndent, Entry, Rest, Indent, $}].
-endif.

-ifdef(NEED_indent_1).
indent(#{level := Level, indent := Indent}) ->
Steps = Level * Indent,
{Steps, [$\n|lists:duplicate(Steps, $\s)]}.
-endif.

-ifdef(NEED_escape_1).
escape($\x00) -> <<"\\u0000">>;
escape($\x01) -> <<"\\u0001">>;
Expand Down
2 changes: 1 addition & 1 deletion src/otpbp_pt.erl
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
{{json, [encode_atom, encode_list, encode_map, encode_map_checked, encode_value], 2}, otpbp_json},
{{json, [encode_key_value_list, encode_key_value_list_checked], 2}, otpbp_json},
{{json, format, [1, 2, 3]}, otpbp_json},
{{json, format_value, 3}, otpbp_json},
{{json, [format_key_value_list_checked, format_key_value_list, format_value], 3}, otpbp_json},
{{lib, [flush_receive, progname], 0}, otpbp_lib},
{{lib, nonl, 1}, otpbp_lib},
{{lib, [error_message, send, sendw], 2}, otpbp_lib},
Expand Down
136 changes: 136 additions & 0 deletions test/json_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,142 @@ format_list_test() ->
?assertEqual(ListString, format([<<"foo">>, <<"bar">>, <<"baz">>], #{indent => 3})),
ok.

format_proplist_test() ->
Formatter = fun({kvlist, KVList}, Fun, State) -> json:format_key_value_list(KVList, Fun, State);
({kvlist_checked, KVList}, Fun, State) -> json:format_key_value_list_checked(KVList, Fun, State);
(Other, Fun, State) -> json:format_value(Other, Fun, State)
end,
?assertEqual(<<
"{\n"
" \"a\": 1,\n"
" \"b\": \"str\"\n"
"}\n"
>>,
format({kvlist, [{a, 1}, {b, <<"str">>}]}, Formatter)),
?assertEqual(<<
"{\n"
" \"a\": 1,\n"
" \"b\": \"str\"\n"
"}\n"
>>,
format({kvlist_checked, [{a, 1}, {b, <<"str">>}]}, Formatter)),
?assertEqual(<<
"{\n"
" \"10\": 1.0,\n"
" \"1.0\": 10,\n"
" \"a\": \"αβ\",\n"
" \"αβ\": \"a\"\n"
"}\n"
/utf8>>,
format({kvlist, [{10, 1.0}, {1.0, 10}, {a, <<"αβ"/utf8>>}, {<<"αβ"/utf8>>, a}]}, Formatter)),
?assertEqual(<<
"{\n"
" \"10\": 1.0,\n"
" \"1.0\": 10,\n"
" \"a\": \"αβ\",\n"
" \"αβ\": \"a\"\n"
"}\n"
/utf8>>,
format({kvlist_checked, [{10, 1.0}, {1.0, 10}, {a, <<"αβ"/utf8>>}, {<<"αβ"/utf8>>, a}]}, Formatter)),
?assertEqual(<<
"{\n"
" \"a\": 1,\n"
" \"b\": {\n"
" \"aa\": 10,\n"
" \"bb\": 20\n"
" },\n"
" \"c\": \"str\"\n"
"}\n"
>>,
format({kvlist, [{a, 1}, {b, {kvlist, [{aa, 10}, {bb, 20}]}}, {c, <<"str">>}]}, Formatter)),
?assertEqual(<<
"[{\n"
" \"a1\": 1,\n"
" \"b1\": [{\n"
" \"a11\": 1,\n"
" \"b11\": 2\n"
" },{\n"
" \"a12\": 3,\n"
" \"b12\": 4\n"
" }],\n"
" \"c1\": \"str1\"\n"
" },\n"
" {\n"
" \"a2\": 2,\n"
" \"b2\": [{\n"
" \"a21\": 5,\n"
" \"b21\": 6\n"
" },{\n"
" \"a22\": 7,\n"
" \"b22\": 8\n"
" }],\n"
" \"c2\": \"str2\"\n"
" }]\n"
>>,
format([{kvlist,
[{a1, 1},
{b1, [{kvlist, [{a11, 1}, {b11, 2}]}, {kvlist, [{a12, 3}, {b12, 4}]}]},
{c1, <<"str1">>}]},
{kvlist,
[{a2, 2},
{b2, [{kvlist, [{a21, 5}, {b21, 6}]}, {kvlist, [{a22, 7}, {b22, 8}]}]},
{c2, <<"str2">>}]}],
Formatter)),
?assertEqual(<<
"{\n"
" \"a\": 1,\n"
" \"b\": {\n"
" \"aa\": 10,\n"
" \"bb\": 20\n"
" },\n"
" \"c\": \"str\"\n"
"}\n"
>>,
format({kvlist_checked, [{a, 1}, {b, {kvlist_checked, [{aa, 10}, {bb,20}]}}, {c, <<"str">>}]},
Formatter)),
?assertEqual(<<
"[{\n"
" \"a1\": 1,\n"
" \"b1\": [{\n"
" \"a11\": 1,\n"
" \"b11\": 2\n"
" },{\n"
" \"a12\": 3,\n"
" \"b12\": 4\n"
" }],\n"
" \"c1\": \"str1\"\n"
" },\n"
" {\n"
" \"a2\": 2,\n"
" \"b2\": [{\n"
" \"a21\": 5,\n"
" \"b21\": 6\n"
" },{\n"
" \"a22\": 7,\n"
" \"b22\": 8\n"
" }],\n"
" \"c2\": \"str2\"\n"
" }]\n"
>>,
format([{kvlist_checked,
[{a1, 1},
{b1, [{kvlist_checked, [{a11, 1}, {b11, 2}]}, {kvlist_checked, [{a12, 3}, {b12, 4}]}]},
{c1, <<"str1">>}]},
{kvlist_checked,
[{a2, 2},
{b2, [{kvlist_checked, [{a21, 5}, {b21, 6}]}, {kvlist_checked, [{a22, 7}, {b22, 8}]}]},
{c2, <<"str2">>}]}],
Formatter)),
?assertError({duplicate_key, a}, format({kvlist_checked, [{a, 1}, {b, <<"str">>}, {a, 2}]}, Formatter)),
%% on invalid input exact error is not specified
?assertError(_, format({kvlist, [{a, 1}, b]}, Formatter)),
?assertError(_, format({kvlist, x}, Formatter)),
?assertError(_, format({kvlist, [{#{}, 1}]}, Formatter)),
?assertError(_, format({kvlist_checked, [{a, 1}, b]}, Formatter)),
?assertError(_, format({kvlist_checked, x}, Formatter)),
?assertError(_, format({kvlist_checked, [{#{}, 1}]}, Formatter)),
ok.

format_map_test() ->
?assertEqual(<<"{}\n">>, format(#{})),
?assertEqual(<<"{ \"key\": \"val\" }\n">>, format(#{key => val})),
Expand Down

0 comments on commit 0ccbd39

Please sign in to comment.