diff --git a/rebar.config.script b/rebar.config.script index 52c1fdd..75d693c 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -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}, diff --git a/src/otpbp_json.erl b/src/otpbp_json.erl index 1af55d4..f3e8165 100644 --- a/src/otpbp_json.erl +++ b/src/otpbp_json.erl @@ -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). @@ -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). @@ -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). @@ -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">>; diff --git a/src/otpbp_pt.erl b/src/otpbp_pt.erl index 5e84486..558e776 100644 --- a/src/otpbp_pt.erl +++ b/src/otpbp_pt.erl @@ -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}, diff --git a/test/json_tests.erl b/test/json_tests.erl index d5419b4..a7d2818 100644 --- a/test/json_tests.erl +++ b/test/json_tests.erl @@ -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})),