File 8331-Add-json-format-functions-for-key-value-lists.patch of Package erlang
From dd84bb57afd67134de78220a690da8416492e2d2 Mon Sep 17 00:00:00 2001
From: Svilen Ivanov <isvilen@applicata.bg>
Date: Wed, 2 Oct 2024 17:57:57 +0300
Subject: [PATCH] Add json format functions for key-value lists
Support formating key-value lists while preserve their ordering.
Co-authored-by: Viacheslav Katsuba <v.katsuba.dev@gmail.com>
Co-authored-by: Maria Scott <67057258+Maria-12648430@users.noreply.github.com>
---
lib/stdlib/src/json.erl | 66 ++++++++++--
lib/stdlib/test/json_SUITE.erl | 182 +++++++++++++++++++++++++++++++++
2 files changed, 241 insertions(+), 7 deletions(-)
diff --git a/lib/stdlib/src/json.erl b/lib/stdlib/src/json.erl
index d3437e0c4a..5654e47a3e 100644
--- a/lib/stdlib/src/json.erl
+++ b/lib/stdlib/src/json.erl
@@ -51,7 +51,9 @@ standards. The decoder is tested using [JSONTestSuite](https://github.com/nst/JS
-export([
format/1, format/2, format/3,
- format_value/3
+ format_value/3,
+ format_key_value_list/3,
+ format_key_value_list_checked/3
]).
-export_type([formatter/0]).
@@ -694,17 +696,47 @@ format_tail([Head|Tail], Enc, State, IndentAll, IndentRow) ->
format_tail([], _, _, _, _) ->
[].
+-spec format_key_value_list([{term(), term()}], Encode::formatter(), State::map()) -> iodata().
format_key_value_list(KVList, UserEnc, #{level := Level} = State) ->
{_,Indent} = indent(State),
NextState = State#{level := Level+1},
{KISize, KeyIndent} = indent(NextState),
EncKeyFun = fun(KeyVal, _Fun) -> UserEnc(KeyVal, UserEnc, NextState) end,
- Entry = fun(Key, Value) ->
- EncKey = key(Key, EncKeyFun),
- ValState = NextState#{col := KISize + 2 + erlang:iolist_size(EncKey)},
- [$, , KeyIndent, EncKey, ": " | UserEnc(Value, UserEnc, ValState)]
- end,
- format_object([Entry(Key,Value) || {Key, Value} <- KVList], Indent).
+ EntryFun = fun({Key, Value}) ->
+ EncKey = key(Key, EncKeyFun),
+ ValState = NextState#{col := KISize + 2 + erlang:iolist_size(EncKey)},
+ [$, , KeyIndent, EncKey, ": " | UserEnc(Value, UserEnc, ValState)]
+ end,
+ format_object(lists:map(EntryFun, KVList), Indent).
+
+-spec format_key_value_list_checked([{term(), term()}], Encoder::formatter(), State::map()) -> iodata().
+format_key_value_list_checked(KVList, UserEnc, State) when is_function(UserEnc, 3) ->
+ {_,Indent} = indent(State),
+ format_object(do_format_checked(KVList, UserEnc, State), Indent).
+
+do_format_checked([], _, _) ->
+ [];
+
+do_format_checked(KVList, UserEnc, #{level := Level} = State) ->
+ NextState = State#{level := Level + 1},
+ {KISize, KeyIndent} = indent(NextState),
+ EncKeyFun = fun(KeyVal, _Fun) -> UserEnc(KeyVal, UserEnc, NextState) end,
+ EncListFun =
+ fun({Key, Value}, {Acc, Visited0}) ->
+ EncKey = iolist_to_binary(key(Key, EncKeyFun)),
+ case is_map_key(EncKey, Visited0) of
+ true ->
+ error({duplicate_key, Key});
+ false ->
+ Visited1 = Visited0#{EncKey => true},
+ ValState = NextState#{col := KISize + 2 + erlang:iolist_size(EncKey)},
+ EncEntry = [$, , KeyIndent, EncKey, ": "
+ | UserEnc(Value, UserEnc, ValState)],
+ {[EncEntry | Acc], Visited1}
+ end
+ end,
+ {EncKVList, _} = lists:foldl(EncListFun, {[], #{}}, KVList),
+ lists:reverse(EncKVList).
format_object([], _) -> <<"{}">>;
format_object([[_Comma,KeyIndent|Entry]], Indent) ->
diff --git a/lib/stdlib/test/json_SUITE.erl b/lib/stdlib/test/json_SUITE.erl
index 3ac56f1ed2..70f92848fe 100644
--- a/lib/stdlib/test/json_SUITE.erl
+++ b/lib/stdlib/test/json_SUITE.erl
@@ -39,6 +39,7 @@
test_encode_proplist/1,
test_encode_escape_all/1,
test_format_list/1,
+ test_format_proplist/1,
test_format_map/1,
test_format_fun/1,
test_decode_atoms/1,
@@ -91,6 +92,7 @@ groups() ->
]},
{format, [parallel], [
test_format_list,
+ test_format_proplist,
test_format_map,
test_format_fun
]},
@@ -362,6 +364,178 @@ test_format_list(_Config) ->
?assertEqual(ListString, format([<<"foo">>, <<"bar">>, <<"baz">>], #{indent => 3})),
ok.
+test_format_proplist(_Config) ->
+ 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.
+
test_format_map(_Config) ->
?assertEqual(<<"{}\n>>, format(#{})),
?assertEqual(<<"{ \"key\": \"val\" }\n">>, format(#{key => val})),
--
2.43.0