Files
fluxer/fluxer_gateway/src/utils/list_ops.erl
Hampus Kraft 2f557eda8c initial commit
2026-01-01 21:05:54 +00:00

648 lines
21 KiB
Erlang

%% Copyright (C) 2026 Fluxer Contributors
%%
%% This file is part of Fluxer.
%%
%% Fluxer is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% Fluxer is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
-module(list_ops).
-export([
replace_by_id/3,
remove_by_id/2,
replace_by_user_id/3,
remove_by_user_id/2,
bulk_update/2,
extract_user_id/1
]).
-type item() :: map() | term().
-type id() :: binary() | integer().
-type item_list() :: [item()].
-spec replace_by_id(item_list(), id(), item()) -> item_list().
replace_by_id(Items, Id, NewItem) when is_list(Items) ->
lists:map(
fun
(Item) when is_map(Item) ->
case maps:get(<<"id">>, Item, undefined) of
Id -> NewItem;
_ -> Item
end;
(Item) ->
Item
end,
Items
);
replace_by_id(_, _, _) ->
[].
-spec remove_by_id(item_list(), id()) -> item_list().
remove_by_id(Items, Id) when is_list(Items) ->
lists:filter(
fun
(Item) when is_map(Item) ->
maps:get(<<"id">>, Item, undefined) =/= Id;
(_Item) ->
true
end,
Items
);
remove_by_id(_, _) ->
[].
-spec replace_by_user_id(item_list(), integer(), item()) -> item_list().
replace_by_user_id(Items, UserId, NewItem) when is_list(Items), is_integer(UserId) ->
lists:map(
fun
(Item) when is_map(Item) ->
ItemUserId = extract_user_id(Item),
case ItemUserId =:= UserId of
true -> NewItem;
false -> Item
end;
(Item) ->
Item
end,
Items
);
replace_by_user_id(_, _, _) ->
[].
-spec remove_by_user_id(item_list(), integer()) -> item_list().
remove_by_user_id(Items, UserId) when is_list(Items), is_integer(UserId) ->
lists:filter(
fun
(Item) when is_map(Item) ->
ItemUserId = extract_user_id(Item),
ItemUserId =/= UserId;
(_Item) ->
true
end,
Items
);
remove_by_user_id(_, _) ->
[].
-spec bulk_update(item_list(), item_list()) -> item_list().
bulk_update(Items, Updates) when is_list(Items), is_list(Updates) ->
UpdateMap = lists:foldl(
fun
(Item, Acc) when is_map(Item) ->
case maps:get(<<"id">>, Item, undefined) of
undefined -> Acc;
ItemId -> maps:put(ItemId, Item, Acc)
end;
(_, Acc) ->
Acc
end,
#{},
Updates
),
lists:map(
fun
(Item) when is_map(Item) ->
ItemId = maps:get(<<"id">>, Item, undefined),
case maps:get(ItemId, UpdateMap, undefined) of
undefined -> Item;
UpdatedItem -> UpdatedItem
end;
(Item) ->
Item
end,
Items
);
bulk_update(Items, _) when is_list(Items) ->
Items;
bulk_update(_, _) ->
[].
-spec extract_user_id(map() | term()) -> integer().
extract_user_id(Item) ->
UserMap = map_utils:ensure_map(map_utils:get_safe(Item, <<"user">>, #{})),
case maps:find(<<"id">>, UserMap) of
error ->
0;
{ok, RawId} ->
case type_conv:to_integer(RawId) of
undefined -> undefined;
Value -> Value
end
end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
make_item_with_id(Id) ->
#{<<"id">> => Id, <<"data">> => <<"test">>}.
make_item_with_user_id(UserId) ->
#{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"data">> => <<"member">>
}.
replace_by_id_success_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>),
make_item_with_id(<<"3">>)
],
NewItem = #{<<"id">> => <<"2">>, <<"data">> => <<"updated">>},
Result = replace_by_id(Items, <<"2">>, NewItem),
?assertEqual(3, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(NewItem, lists:nth(2, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(3, Result)).
replace_by_id_no_match_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
],
NewItem = #{<<"id">> => <<"99">>, <<"data">> => <<"new">>},
Result = replace_by_id(Items, <<"99">>, NewItem),
?assertEqual(Items, Result).
replace_by_id_empty_list_test() ->
Result = replace_by_id([], <<"1">>, #{<<"id">> => <<"1">>}),
?assertEqual([], Result).
replace_by_id_mixed_list_test() ->
Items = [
make_item_with_id(<<"1">>),
<<"non_map_item">>,
make_item_with_id(<<"2">>),
{tuple, item},
make_item_with_id(<<"3">>)
],
NewItem = #{<<"id">> => <<"2">>, <<"data">> => <<"replaced">>},
Result = replace_by_id(Items, <<"2">>, NewItem),
?assertEqual(5, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(<<"non_map_item">>, lists:nth(2, Result)),
?assertEqual(NewItem, lists:nth(3, Result)),
?assertEqual({tuple, item}, lists:nth(4, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(5, Result)).
replace_by_id_integer_id_test() ->
Items = [
#{<<"id">> => 1, <<"data">> => <<"a">>},
#{<<"id">> => 2, <<"data">> => <<"b">>}
],
NewItem = #{<<"id">> => 2, <<"data">> => <<"updated">>},
Result = replace_by_id(Items, 2, NewItem),
?assertEqual(2, length(Result)),
?assertEqual(#{<<"id">> => 1, <<"data">> => <<"a">>}, lists:nth(1, Result)),
?assertEqual(NewItem, lists:nth(2, Result)).
replace_by_id_invalid_input_test() ->
?assertEqual([], replace_by_id(not_a_list, <<"1">>, #{})),
?assertEqual([], replace_by_id(#{}, <<"1">>, #{})),
?assertEqual([], replace_by_id(undefined, <<"1">>, #{})).
replace_by_id_item_without_id_test() ->
Items = [
#{<<"id">> => <<"1">>},
#{<<"name">> => <<"no_id">>},
#{<<"id">> => <<"2">>}
],
NewItem = #{<<"id">> => <<"2">>, <<"updated">> => true},
Result = replace_by_id(Items, <<"2">>, NewItem),
?assertEqual(3, length(Result)),
?assertEqual(#{<<"id">> => <<"1">>}, lists:nth(1, Result)),
?assertEqual(#{<<"name">> => <<"no_id">>}, lists:nth(2, Result)),
?assertEqual(NewItem, lists:nth(3, Result)).
remove_by_id_success_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>),
make_item_with_id(<<"3">>)
],
Result = remove_by_id(Items, <<"2">>),
?assertEqual(2, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(2, Result)).
remove_by_id_no_match_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
],
Result = remove_by_id(Items, <<"99">>),
?assertEqual(Items, Result).
remove_by_id_multiple_matches_test() ->
Items = [
make_item_with_id(<<"1">>),
#{<<"id">> => <<"2">>, <<"version">> => 1},
#{<<"id">> => <<"2">>, <<"version">> => 2},
make_item_with_id(<<"3">>)
],
Result = remove_by_id(Items, <<"2">>),
?assertEqual(2, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(2, Result)).
remove_by_id_empty_list_test() ->
Result = remove_by_id([], <<"1">>),
?assertEqual([], Result).
remove_by_id_mixed_list_test() ->
Items = [
make_item_with_id(<<"1">>),
<<"non_map">>,
make_item_with_id(<<"2">>),
[list, item],
make_item_with_id(<<"3">>)
],
Result = remove_by_id(Items, <<"2">>),
?assertEqual(4, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(<<"non_map">>, lists:nth(2, Result)),
?assertEqual([list, item], lists:nth(3, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(4, Result)).
remove_by_id_invalid_input_test() ->
?assertEqual([], remove_by_id(not_a_list, <<"1">>)),
?assertEqual([], remove_by_id(undefined, <<"1">>)),
?assertEqual([], remove_by_id(123, <<"1">>)).
remove_by_id_all_items_match_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"1">>),
make_item_with_id(<<"1">>)
],
Result = remove_by_id(Items, <<"1">>),
?assertEqual([], Result).
replace_by_user_id_success_test() ->
Items = [
make_item_with_user_id(100),
make_item_with_user_id(200),
make_item_with_user_id(300)
],
NewItem = #{
<<"user">> => #{<<"id">> => <<"200">>},
<<"data">> => <<"updated">>
},
Result = replace_by_user_id(Items, 200, NewItem),
?assertEqual(3, length(Result)),
?assertEqual(make_item_with_user_id(100), lists:nth(1, Result)),
?assertEqual(NewItem, lists:nth(2, Result)),
?assertEqual(make_item_with_user_id(300), lists:nth(3, Result)).
replace_by_user_id_no_match_test() ->
Items = [
make_item_with_user_id(100),
make_item_with_user_id(200)
],
NewItem = make_item_with_user_id(999),
Result = replace_by_user_id(Items, 999, NewItem),
?assertEqual(Items, Result).
replace_by_user_id_empty_list_test() ->
Result = replace_by_user_id([], 100, make_item_with_user_id(100)),
?assertEqual([], Result).
replace_by_user_id_nested_extraction_test() ->
Items = [
#{
<<"user">> => #{<<"id">> => <<"123">>, <<"name">> => <<"alice">>},
<<"role">> => <<"admin">>
},
#{<<"user">> => #{<<"id">> => <<"456">>, <<"name">> => <<"bob">>}, <<"role">> => <<"user">>}
],
NewItem = #{<<"user">> => #{<<"id">> => <<"456">>}, <<"role">> => <<"moderator">>},
Result = replace_by_user_id(Items, 456, NewItem),
?assertEqual(2, length(Result)),
?assertEqual(lists:nth(1, Items), lists:nth(1, Result)),
?assertEqual(NewItem, lists:nth(2, Result)).
replace_by_user_id_mixed_list_test() ->
Items = [
make_item_with_user_id(100),
<<"string_item">>,
make_item_with_user_id(200),
{tuple},
#{<<"other">> => <<"map">>}
],
NewItem = make_item_with_user_id(200),
Result = replace_by_user_id(Items, 200, NewItem),
?assertEqual(5, length(Result)),
?assertEqual(make_item_with_user_id(100), lists:nth(1, Result)),
?assertEqual(<<"string_item">>, lists:nth(2, Result)),
?assertEqual(NewItem, lists:nth(3, Result)),
?assertEqual({tuple}, lists:nth(4, Result)),
?assertEqual(#{<<"other">> => <<"map">>}, lists:nth(5, Result)).
replace_by_user_id_invalid_structure_test() ->
Items = [
#{<<"user">> => <<"not_a_map">>, <<"data">> => <<"x">>},
#{<<"no_user_key">> => <<"y">>},
make_item_with_user_id(100)
],
NewItem = make_item_with_user_id(100),
Result = replace_by_user_id(Items, 100, NewItem),
?assertEqual(3, length(Result)),
?assertEqual(lists:nth(1, Items), lists:nth(1, Result)),
?assertEqual(lists:nth(2, Items), lists:nth(2, Result)),
?assertEqual(NewItem, lists:nth(3, Result)).
replace_by_user_id_invalid_input_test() ->
?assertEqual([], replace_by_user_id(not_a_list, 100, #{})),
?assertEqual([], replace_by_user_id(undefined, 100, #{})),
?assertEqual([], replace_by_user_id([make_item_with_user_id(100)], <<"not_integer">>, #{})).
remove_by_user_id_success_test() ->
Items = [
make_item_with_user_id(100),
make_item_with_user_id(200),
make_item_with_user_id(300)
],
Result = remove_by_user_id(Items, 200),
?assertEqual(2, length(Result)),
?assertEqual(make_item_with_user_id(100), lists:nth(1, Result)),
?assertEqual(make_item_with_user_id(300), lists:nth(2, Result)).
remove_by_user_id_no_match_test() ->
Items = [
make_item_with_user_id(100),
make_item_with_user_id(200)
],
Result = remove_by_user_id(Items, 999),
?assertEqual(Items, Result).
remove_by_user_id_multiple_matches_test() ->
Items = [
make_item_with_user_id(100),
#{<<"user">> => #{<<"id">> => <<"200">>}, <<"version">> => 1},
#{<<"user">> => #{<<"id">> => <<"200">>}, <<"version">> => 2},
make_item_with_user_id(300)
],
Result = remove_by_user_id(Items, 200),
?assertEqual(2, length(Result)),
?assertEqual(make_item_with_user_id(100), lists:nth(1, Result)),
?assertEqual(make_item_with_user_id(300), lists:nth(2, Result)).
remove_by_user_id_empty_list_test() ->
Result = remove_by_user_id([], 100),
?assertEqual([], Result).
remove_by_user_id_mixed_list_test() ->
Items = [
make_item_with_user_id(100),
<<"non_map">>,
make_item_with_user_id(200),
[list],
#{<<"invalid">> => <<"structure">>}
],
Result = remove_by_user_id(Items, 200),
?assertEqual(4, length(Result)),
?assertEqual(make_item_with_user_id(100), lists:nth(1, Result)),
?assertEqual(<<"non_map">>, lists:nth(2, Result)),
?assertEqual([list], lists:nth(3, Result)),
?assertEqual(#{<<"invalid">> => <<"structure">>}, lists:nth(4, Result)).
remove_by_user_id_invalid_nested_structure_test() ->
Items = [
#{<<"user">> => <<"not_a_map">>},
#{<<"no_user">> => <<"field">>},
#{<<"user">> => #{<<"no_id">> => <<"field">>}},
make_item_with_user_id(100)
],
Result = remove_by_user_id(Items, 0),
?assertEqual(1, length(Result)),
?assertEqual(make_item_with_user_id(100), lists:nth(1, Result)).
remove_by_user_id_invalid_input_test() ->
?assertEqual([], remove_by_user_id(not_a_list, 100)),
?assertEqual([], remove_by_user_id(undefined, 100)),
?assertEqual([], remove_by_user_id([make_item_with_user_id(100)], <<"not_integer">>)).
bulk_update_multiple_updates_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>),
make_item_with_id(<<"3">>),
make_item_with_id(<<"4">>)
],
Updates = [
#{<<"id">> => <<"2">>, <<"data">> => <<"updated_2">>},
#{<<"id">> => <<"4">>, <<"data">> => <<"updated_4">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(4, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(#{<<"id">> => <<"2">>, <<"data">> => <<"updated_2">>}, lists:nth(2, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(3, Result)),
?assertEqual(#{<<"id">> => <<"4">>, <<"data">> => <<"updated_4">>}, lists:nth(4, Result)).
bulk_update_partial_updates_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>),
make_item_with_id(<<"3">>)
],
Updates = [
#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(3, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}, lists:nth(2, Result)),
?assertEqual(make_item_with_id(<<"3">>), lists:nth(3, Result)).
bulk_update_no_matches_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
],
Updates = [
#{<<"id">> => <<"99">>, <<"data">> => <<"new">>},
#{<<"id">> => <<"98">>, <<"data">> => <<"new2">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(Items, Result).
bulk_update_empty_lists_test() ->
?assertEqual([], bulk_update([], [])),
?assertEqual([], bulk_update([], [make_item_with_id(<<"1">>)])),
Items = [make_item_with_id(<<"1">>)],
?assertEqual(Items, bulk_update(Items, [])).
bulk_update_updates_without_id_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
],
Updates = [
#{<<"name">> => <<"no_id">>},
#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(2, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}, lists:nth(2, Result)).
bulk_update_mixed_items_list_test() ->
Items = [
make_item_with_id(<<"1">>),
<<"non_map_item">>,
make_item_with_id(<<"2">>),
{tuple, item}
],
Updates = [
#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(4, length(Result)),
?assertEqual(make_item_with_id(<<"1">>), lists:nth(1, Result)),
?assertEqual(<<"non_map_item">>, lists:nth(2, Result)),
?assertEqual(#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}, lists:nth(3, Result)),
?assertEqual({tuple, item}, lists:nth(4, Result)).
bulk_update_mixed_updates_list_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
],
Updates = [
<<"non_map">>,
#{<<"id">> => <<"1">>, <<"data">> => <<"updated">>},
{tuple},
#{<<"no_id">> => <<"field">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(2, length(Result)),
?assertEqual(#{<<"id">> => <<"1">>, <<"data">> => <<"updated">>}, lists:nth(1, Result)),
?assertEqual(make_item_with_id(<<"2">>), lists:nth(2, Result)).
bulk_update_duplicate_ids_in_updates_test() ->
Items = [
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
],
Updates = [
#{<<"id">> => <<"1">>, <<"data">> => <<"first_update">>},
#{<<"id">> => <<"1">>, <<"data">> => <<"second_update">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(2, length(Result)),
?assertEqual(#{<<"id">> => <<"1">>, <<"data">> => <<"second_update">>}, lists:nth(1, Result)),
?assertEqual(make_item_with_id(<<"2">>), lists:nth(2, Result)).
bulk_update_invalid_input_test() ->
Items = [make_item_with_id(<<"1">>)],
?assertEqual(Items, bulk_update(Items, not_a_list)),
?assertEqual(Items, bulk_update(Items, undefined)),
?assertEqual(Items, bulk_update(Items, #{})),
?assertEqual([], bulk_update(not_a_list, [make_item_with_id(<<"1">>)])),
?assertEqual([], bulk_update(undefined, [])).
bulk_update_item_without_id_preserved_test() ->
Items = [
make_item_with_id(<<"1">>),
#{<<"name">> => <<"no_id_item">>},
make_item_with_id(<<"2">>)
],
Updates = [
#{<<"id">> => <<"1">>, <<"data">> => <<"updated">>}
],
Result = bulk_update(Items, Updates),
?assertEqual(3, length(Result)),
?assertEqual(#{<<"id">> => <<"1">>, <<"data">> => <<"updated">>}, lists:nth(1, Result)),
?assertEqual(#{<<"name">> => <<"no_id_item">>}, lists:nth(2, Result)),
?assertEqual(make_item_with_id(<<"2">>), lists:nth(3, Result)).
extract_user_id_valid_structure_test() ->
Item = #{<<"user">> => #{<<"id">> => <<"12345">>}},
?assertEqual(12345, extract_user_id(Item)).
extract_user_id_missing_user_test() ->
Item = #{<<"other">> => <<"field">>},
?assertEqual(0, extract_user_id(Item)).
extract_user_id_missing_id_test() ->
Item = #{<<"user">> => #{<<"name">> => <<"alice">>}},
?assertEqual(0, extract_user_id(Item)).
extract_user_id_non_map_test() ->
?assertEqual(0, extract_user_id(<<"string">>)),
?assertEqual(0, extract_user_id([list])),
?assertEqual(0, extract_user_id({tuple})),
?assertEqual(0, extract_user_id(undefined)),
?assertEqual(0, extract_user_id(123)).
extract_user_id_user_not_map_test() ->
Item = #{<<"user">> => <<"not_a_map">>},
?assertEqual(0, extract_user_id(Item)).
extract_user_id_nested_structure_test() ->
Item = #{
<<"user">> => #{
<<"id">> => <<"999">>,
<<"name">> => <<"bob">>,
<<"extra">> => #{<<"nested">> => <<"data">>}
},
<<"role">> => <<"admin">>
},
?assertEqual(999, extract_user_id(Item)).
extract_user_id_empty_id_test() ->
Item = #{<<"user">> => #{<<"id">> => <<>>}},
?assertEqual(undefined, extract_user_id(Item)).
extract_user_id_empty_user_map_test() ->
Item = #{<<"user">> => #{}},
?assertEqual(0, extract_user_id(Item)).
extract_user_id_invalid_id_format_test() ->
Item = #{<<"user">> => #{<<"id">> => <<"not_a_number">>}},
?assertEqual(undefined, extract_user_id(Item)).
-endif.