chore(gateway): delete modules with no callers (#2494)

This commit is contained in:
Hampus
2026-09-06 15:26:08 +02:00
committed by GitHub
parent d8f2aa3184
commit 7c3e722085
9 changed files with 0 additions and 1442 deletions
@@ -1,306 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(fluxer_gateway_crypto).
-typing([eqwalizer]).
-export([
init/0,
decrypt/2,
encrypt/2,
derive_shared_secret/2,
generate_keypair/0,
get_public_key/0,
new_crypto_state/1,
is_encrypted_frame/1,
unwrap_encrypted_frame/1,
wrap_encrypted_frame/1
]).
-export_type([keypair/0, crypto_state/0]).
-define(KEYPAIR_KEY, {?MODULE, instance_keypair}).
-define(ENCRYPTED_FRAME_PREFIX, 16#FE).
-define(NONCE_SIZE, 12).
-define(TAG_SIZE, 16).
-define(KEY_SIZE, 32).
-type keypair() :: #{public := binary(), private := binary()}.
-type crypto_state() :: #{
shared_secret := binary(),
send_counter := non_neg_integer(),
recv_counter := non_neg_integer()
}.
-spec init() -> ok.
init() ->
case persistent_term:get(?KEYPAIR_KEY, undefined) of
undefined ->
Keypair = generate_keypair(),
persistent_term:put(?KEYPAIR_KEY, Keypair),
ok;
_ ->
ok
end.
-spec generate_keypair() -> keypair().
generate_keypair() ->
{Public0, Private0} = crypto:generate_key(ecdh, x25519),
Public = key_material(Public0),
Private = key_material(Private0),
#{public => Public, private => Private}.
-spec key_material(term()) -> binary().
key_material(Value) when is_binary(Value) ->
Value;
key_material(Value) ->
error({invalid_key_material, Value}).
-spec get_public_key() -> binary() | undefined.
get_public_key() ->
case persistent_term:get(?KEYPAIR_KEY, undefined) of
undefined -> undefined;
#{public := Public} -> Public
end.
-spec derive_shared_secret(binary(), keypair()) -> {ok, binary()} | {error, term()}.
derive_shared_secret(PeerPublic, #{private := Private}) when
byte_size(PeerPublic) =:= ?KEY_SIZE
->
try
SharedSecret = crypto:compute_key(ecdh, PeerPublic, Private, x25519),
{ok, SharedSecret}
catch
error:Reason ->
{error, {key_exchange_failed, Reason}}
end;
derive_shared_secret(PeerPublic, _Keypair) ->
{error, {invalid_peer_key_size, byte_size(PeerPublic)}}.
-spec new_crypto_state(binary()) -> crypto_state().
new_crypto_state(SharedSecret) when byte_size(SharedSecret) =:= ?KEY_SIZE ->
#{
shared_secret => SharedSecret,
send_counter => 0,
recv_counter => 0
}.
-spec encrypt(binary(), crypto_state()) -> {ok, binary(), crypto_state()} | {error, term()}.
encrypt(Plaintext, #{shared_secret := Key, send_counter := Counter} = State) ->
try
Nonce = counter_to_nonce(Counter),
AAD = <<>>,
{Ciphertext, Tag} = crypto:crypto_one_time_aead(
aes_256_gcm,
Key,
Nonce,
Plaintext,
AAD,
?TAG_SIZE,
true
),
Encrypted = <<Nonce/binary, Tag/binary, Ciphertext/binary>>,
NewState = State#{send_counter => Counter + 1},
{ok, Encrypted, NewState}
catch
error:Reason ->
{error, {encrypt_failed, Reason}}
end.
-spec decrypt(binary(), crypto_state()) -> {ok, binary(), crypto_state()} | {error, term()}.
decrypt(Data, #{shared_secret := Key, recv_counter := Counter} = State) ->
MinSize = ?NONCE_SIZE + ?TAG_SIZE,
case byte_size(Data) > MinSize of
false ->
{error, {invalid_encrypted_data, too_short}};
true ->
decrypt_sized_data(Data, Key, Counter, State)
end.
-spec decrypt_sized_data(binary(), binary(), non_neg_integer(), crypto_state()) ->
{ok, binary(), crypto_state()} | {error, term()}.
decrypt_sized_data(Data, Key, Counter, State) ->
<<Nonce:?NONCE_SIZE/binary, Tag:?TAG_SIZE/binary, Ciphertext/binary>> = Data,
ExpectedNonce = counter_to_nonce(Counter),
case validate_nonce(Nonce, ExpectedNonce, Counter) of
{ok, ActualCounter} ->
do_decrypt(Ciphertext, Key, Nonce, Tag, State, ActualCounter);
{error, Reason} ->
{error, Reason}
end.
-spec do_decrypt(binary(), binary(), binary(), binary(), crypto_state(), non_neg_integer()) ->
{ok, binary(), crypto_state()} | {error, term()}.
do_decrypt(Ciphertext, Key, Nonce, Tag, State, ActualCounter) ->
AAD = <<>>,
try
case
crypto:crypto_one_time_aead(
aes_256_gcm,
Key,
Nonce,
Ciphertext,
AAD,
Tag,
false
)
of
Plaintext when is_binary(Plaintext) ->
NewState = State#{recv_counter => ActualCounter + 1},
{ok, Plaintext, NewState};
error ->
{error, authentication_failed}
end
catch
error:Reason ->
{error, {decrypt_failed, Reason}}
end.
-spec counter_to_nonce(non_neg_integer()) -> binary().
counter_to_nonce(Counter) ->
<<0:32, Counter:64/big-unsigned-integer>>.
-spec validate_nonce(binary(), binary(), non_neg_integer()) ->
{ok, non_neg_integer()} | {error, term()}.
validate_nonce(Nonce, ExpectedNonce, Counter) when Nonce =:= ExpectedNonce ->
{ok, Counter};
validate_nonce(Nonce, _ExpectedNonce, Counter) ->
<<_Prefix:4/binary, ReceivedCounter:64/big-unsigned-integer>> = Nonce,
MaxWindow = 32,
case ReceivedCounter > Counter andalso ReceivedCounter =< Counter + MaxWindow of
true ->
{ok, ReceivedCounter};
false ->
{error, {nonce_mismatch, Counter, ReceivedCounter}}
end.
-spec is_encrypted_frame(binary()) -> boolean().
is_encrypted_frame(<<?ENCRYPTED_FRAME_PREFIX, _Rest/binary>>) ->
true;
is_encrypted_frame(_) ->
false.
-spec unwrap_encrypted_frame(binary()) -> {ok, binary()} | {error, not_encrypted}.
unwrap_encrypted_frame(<<?ENCRYPTED_FRAME_PREFIX, Data/binary>>) ->
{ok, Data};
unwrap_encrypted_frame(_) ->
{error, not_encrypted}.
-spec wrap_encrypted_frame(binary()) -> binary().
wrap_encrypted_frame(Data) ->
<<?ENCRYPTED_FRAME_PREFIX, Data/binary>>.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
generate_keypair_test() ->
Keypair = generate_keypair(),
?assert(is_map(Keypair)),
?assertEqual(?KEY_SIZE, byte_size(maps:get(public, Keypair))),
?assertEqual(?KEY_SIZE, byte_size(maps:get(private, Keypair))).
derive_shared_secret_test() ->
Keypair1 = generate_keypair(),
Keypair2 = generate_keypair(),
{ok, Secret1} = derive_shared_secret(maps:get(public, Keypair2), Keypair1),
{ok, Secret2} = derive_shared_secret(maps:get(public, Keypair1), Keypair2),
?assertEqual(Secret1, Secret2),
?assertEqual(?KEY_SIZE, byte_size(Secret1)).
derive_shared_secret_invalid_key_test() ->
Keypair = generate_keypair(),
Result = derive_shared_secret(<<"short">>, Keypair),
?assertMatch({error, {invalid_peer_key_size, _}}, Result).
new_crypto_state_test() ->
Secret = crypto:strong_rand_bytes(?KEY_SIZE),
State = new_crypto_state(Secret),
?assertEqual(Secret, maps:get(shared_secret, State)),
?assertEqual(0, maps:get(send_counter, State)),
?assertEqual(0, maps:get(recv_counter, State)).
encrypt_decrypt_roundtrip_test() ->
Secret = crypto:strong_rand_bytes(?KEY_SIZE),
State = new_crypto_state(Secret),
Plaintext = <<"hello world">>,
{ok, Ciphertext, State2} = encrypt(Plaintext, State),
?assert(byte_size(Ciphertext) > byte_size(Plaintext)),
?assertEqual(1, maps:get(send_counter, State2)),
{ok, Decrypted, State3} = decrypt(Ciphertext, State),
?assertEqual(Plaintext, Decrypted),
?assertEqual(1, maps:get(recv_counter, State3)).
encrypt_multiple_messages_test() ->
Secret = crypto:strong_rand_bytes(?KEY_SIZE),
SendState = new_crypto_state(Secret),
RecvState = new_crypto_state(Secret),
Messages = [<<"msg1">>, <<"msg2">>, <<"msg3">>],
{FinalSendState, FinalRecvState, DecryptedMsgs} = lists:foldl(
fun(Msg, {SS, RS, Acc}) ->
{ok, Cipher, SS2} = encrypt(Msg, SS),
{ok, Plain, RS2} = decrypt(Cipher, RS),
{SS2, RS2, [Plain | Acc]}
end,
{SendState, RecvState, []},
Messages
),
?assertEqual(3, maps:get(send_counter, FinalSendState)),
?assertEqual(3, maps:get(recv_counter, FinalRecvState)),
?assertEqual(Messages, lists:reverse(DecryptedMsgs)).
decrypt_tampered_test() ->
Secret = crypto:strong_rand_bytes(?KEY_SIZE),
State = new_crypto_state(Secret),
{ok, Ciphertext, _} = encrypt(<<"hello">>, State),
Rest = binary:part(Ciphertext, 1, byte_size(Ciphertext) - 1),
Tampered = <<(binary:first(Ciphertext) bxor 1), Rest/binary>>,
Result = decrypt(Tampered, State),
?assertMatch({error, _}, Result).
decrypt_too_short_test() ->
Secret = crypto:strong_rand_bytes(?KEY_SIZE),
State = new_crypto_state(Secret),
Result = decrypt(<<"short">>, State),
?assertMatch({error, {invalid_encrypted_data, too_short}}, Result).
is_encrypted_frame_test() ->
?assertEqual(true, is_encrypted_frame(<<16#FE, "data">>)),
?assertEqual(false, is_encrypted_frame(<<"data">>)),
?assertEqual(false, is_encrypted_frame(<<16#FF, "data">>)),
?assertEqual(false, is_encrypted_frame(<<>>)).
unwrap_encrypted_frame_test() ->
?assertEqual({ok, <<"data">>}, unwrap_encrypted_frame(<<16#FE, "data">>)),
?assertEqual({error, not_encrypted}, unwrap_encrypted_frame(<<"data">>)).
wrap_encrypted_frame_test() ->
?assertEqual(<<16#FE, "data">>, wrap_encrypted_frame(<<"data">>)).
counter_to_nonce_test() ->
Nonce0 = counter_to_nonce(0),
?assertEqual(?NONCE_SIZE, byte_size(Nonce0)),
?assertEqual(<<0:32, 0:64>>, Nonce0),
Nonce1 = counter_to_nonce(1),
?assertEqual(<<0:32, 1:64>>, Nonce1).
init_creates_keypair_test() ->
persistent_term:erase(?KEYPAIR_KEY),
ok = init(),
Public = get_public_key(),
case Public of
PublicBin when is_binary(PublicBin) ->
?assertEqual(?KEY_SIZE, byte_size(PublicBin));
undefined ->
?assert(false)
end,
persistent_term:erase(?KEYPAIR_KEY).
init_idempotent_test() ->
persistent_term:erase(?KEYPAIR_KEY),
ok = init(),
Public1 = get_public_key(),
ok = init(),
Public2 = get_public_key(),
?assertEqual(Public1, Public2),
persistent_term:erase(?KEYPAIR_KEY).
-endif.
-145
View File
@@ -1,145 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(guild_common).
-typing([eqwalizer]).
-export([
safe_call/3,
parse_event_data/1,
relay_upsert_voice_state/2,
strip_members/1
]).
-spec safe_call(pid(), term(), timeout()) -> term().
safe_call(Pid, Msg, Timeout) when is_pid(Pid) ->
try gen_server:call(Pid, Msg, Timeout) of
Reply -> Reply
catch
exit:{timeout, _} -> {error, timeout};
exit:{noproc, _} -> {error, noproc};
exit:{normal, _} -> {error, noproc};
_:Reason -> {error, Reason}
end.
-spec parse_event_data
(binary()) -> term();
(map()) -> map().
parse_event_data(EventData) when is_binary(EventData) ->
json:decode(EventData);
parse_event_data(EventData) when is_map(EventData) ->
EventData.
-spec relay_upsert_voice_state(term(), map()) -> map().
relay_upsert_voice_state(VoiceState, State) when is_map(VoiceState) ->
ConnectionId = maps:get(<<"connection_id">>, VoiceState, undefined),
case ConnectionId of
undefined ->
State;
_ ->
upsert_voice_state(ConnectionId, VoiceState, State)
end;
relay_upsert_voice_state(_, State) ->
State.
-spec upsert_voice_state(term(), map(), map()) -> map().
upsert_voice_state(ConnectionId, VoiceState, State) ->
VoiceStates0 = maps:get(voice_states, State, #{}),
ChannelId = maps:get(<<"channel_id">>, VoiceState, null),
VoiceStates =
case ChannelId of
null -> maps:remove(ConnectionId, VoiceStates0);
_ -> VoiceStates0#{ConnectionId => VoiceState}
end,
State#{voice_states => VoiceStates}.
-spec strip_members(term()) -> term().
strip_members(Data) when is_map(Data) ->
Data1 = maps:remove(<<"members">>, Data),
maps:remove(<<"member_role_index">>, Data1);
strip_members(Data) ->
Data.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
slow_gen_call_handler() ->
receive
{'$gen_call', _From, _Msg} ->
ok = gateway_retry_timer:wait(5000)
after 30000 ->
ok
end.
safe_call_timeout_test() ->
Pid = spawn(fun slow_gen_call_handler/0),
Result = safe_call(Pid, ping, 50),
?assertEqual({error, timeout}, Result),
exit(Pid, kill),
ok.
safe_call_noproc_test() ->
Pid = spawn(fun() -> ok end),
ok = gateway_retry_timer:wait(50),
Result = safe_call(Pid, ping, 100),
?assertMatch({error, _}, Result),
ok.
parse_event_data_binary_test() ->
Binary = <<"{\"key\":\"value\"}">>,
Result = parse_event_data(Binary),
?assertEqual(#{<<"key">> => <<"value">>}, Result).
parse_event_data_map_test() ->
Map = #{<<"key">> => <<"value">>},
Result = parse_event_data(Map),
?assertEqual(Map, Result).
relay_upsert_voice_state_adds_state_test() ->
VoiceState = #{
<<"connection_id">> => <<"conn-1">>,
<<"channel_id">> => <<"100">>,
<<"user_id">> => <<"42">>
},
State0 = #{voice_states => #{}},
State1 = relay_upsert_voice_state(VoiceState, State0),
VoiceStates = maps:get(voice_states, State1),
?assertEqual(VoiceState, maps:get(<<"conn-1">>, VoiceStates)).
relay_upsert_voice_state_removes_on_null_channel_test() ->
Existing = #{<<"connection_id">> => <<"conn-1">>, <<"channel_id">> => <<"100">>},
State0 = #{voice_states => #{<<"conn-1">> => Existing}},
RemoveState = #{<<"connection_id">> => <<"conn-1">>, <<"channel_id">> => null},
State1 = relay_upsert_voice_state(RemoveState, State0),
VoiceStates = maps:get(voice_states, State1),
?assertEqual(false, maps:is_key(<<"conn-1">>, VoiceStates)).
relay_upsert_voice_state_no_connection_id_test() ->
State0 = #{voice_states => #{}},
State1 = relay_upsert_voice_state(#{<<"channel_id">> => <<"100">>}, State0),
?assertEqual(State0, State1).
relay_upsert_voice_state_non_map_test() ->
State0 = #{voice_states => #{}},
State1 = relay_upsert_voice_state(not_a_map, State0),
?assertEqual(State0, State1).
strip_members_test() ->
Data = #{
<<"members">> => [#{<<"user">> => #{<<"id">> => <<"1">>}}],
<<"member_role_index">> => #{1 => [<<"role1">>]},
<<"channels">> => [#{<<"id">> => <<"10">>}],
<<"roles">> => [#{<<"id">> => <<"role1">>}]
},
Stripped = #{} = strip_members(Data),
?assertEqual(false, maps:is_key(<<"members">>, Stripped)),
?assertEqual(false, maps:is_key(<<"member_role_index">>, Stripped)),
?assertEqual([#{<<"id">> => <<"10">>}], maps:get(<<"channels">>, Stripped)),
?assertEqual([#{<<"id">> => <<"role1">>}], maps:get(<<"roles">>, Stripped)).
strip_members_empty_test() ->
?assertEqual(#{}, strip_members(#{})).
strip_members_non_map_test() ->
?assertEqual(not_a_map, strip_members(not_a_map)).
-endif.
-30
View File
@@ -1,30 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(push_core).
-typing([eqwalizer]).
-export([handle_message_create/1]).
-export([sync_user_guild_settings/3]).
-export([sync_user_blocked_ids/2]).
-export([invalidate_user_subscriptions/1]).
-spec handle_message_create(map()) -> ok.
handle_message_create(Params) ->
case fluxer_gateway_env:get(push_enabled) of
true ->
push:handle_message_create(Params);
false ->
ok
end.
-spec sync_user_guild_settings(integer(), integer(), map()) -> ok.
sync_user_guild_settings(UserId, GuildId, UserGuildSettings) ->
push:sync_user_guild_settings(UserId, GuildId, UserGuildSettings).
-spec sync_user_blocked_ids(integer(), [integer()]) -> ok.
sync_user_blocked_ids(UserId, BlockedIds) ->
push:sync_user_blocked_ids(UserId, BlockedIds).
-spec invalidate_user_subscriptions(integer()) -> ok.
invalidate_user_subscriptions(UserId) ->
push:invalidate_user_subscriptions(UserId).
@@ -1,308 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(gateway_runtime_probe).
-typing([eqwalizer]).
-export([
snapshot/0,
top_processes/2,
sample_processes/2,
top_guilds/1,
sample_guilds/2,
guild_probe/1,
logger_status/0,
gc_logger/0
]).
-define(MAX_LIMIT, 100).
-define(DEFAULT_LIMIT, 20).
-define(MAX_SAMPLE_MS, 5000).
-define(DEFAULT_SAMPLE_MS, 250).
-define(MIN_SAMPLE_MS, 10).
-define(STATE_TIMEOUT_MS, 50).
-define(MAX_PROCESS_SCAN, 5000).
-define(MAX_GUILD_PID_SCAN, 5000).
-define(LOGGER_HANDLER, logger_simple_h).
-define(LOGGER_GC_MAX_QUEUE, 1000).
-type metric() :: memory | message_queue_len | reductions | total_heap_size.
-type row() :: map().
-export_type([metric/0, row/0]).
-spec snapshot() -> map().
snapshot() ->
#{
node => node(),
memory => erlang:memory(),
process_count => erlang:system_info(process_count),
run_queue => erlang:statistics(run_queue),
scheduler_wall_time => safe_scheduler_wall_time(),
reductions => erlang:statistics(reductions)
}.
-spec top_processes(metric() | term(), pos_integer()) -> [row()].
top_processes(memory, Limit) ->
top_by_info(memory, Limit);
top_processes(message_queue_len, Limit) ->
top_by_info(message_queue_len, Limit);
top_processes(reductions, Limit) ->
top_by_info(reductions, Limit);
top_processes(total_heap_size, Limit) ->
top_by_info(total_heap_size, Limit);
top_processes(_, Limit) ->
top_by_info(memory, Limit).
-spec sample_processes(integer(), pos_integer()) -> [row()].
sample_processes(Milliseconds, Limit) ->
Pids = process_scan_pids(),
Before = reductions_by_pid(Pids),
ok = gateway_retry_timer:wait(clamp_ms(Milliseconds)),
After = reductions_by_pid(Pids),
Rows = [
enrich_pid(Pid, #{reduction_delta => Delta})
|| {Pid, R1} <- maps:to_list(After),
R0 <- [maps:get(Pid, Before, R1)],
Delta <- [R1 - R0],
Delta > 0
],
take(Limit, lists:sort(fun row_reduction_delta_ge/2, Rows)).
-spec top_guilds(pos_integer()) -> [row()].
top_guilds(Limit) ->
Rows = [guild_row(GuildId, Pid) || {GuildId, Pid} <- local_guild_pids()],
take(Limit, lists:sort(fun row_memory_ge/2, Rows)).
-spec sample_guilds(integer(), pos_integer()) -> [row()].
sample_guilds(Milliseconds, Limit) ->
Guilds = local_guild_pids(),
Before = maps:from_list([{Pid, reductions(Pid)} || {_GuildId, Pid} <- Guilds]),
ok = gateway_retry_timer:wait(clamp_ms(Milliseconds)),
Rows = [
(guild_row(GuildId, Pid))#{
reduction_delta => reductions(Pid) - maps:get(Pid, Before, reductions(Pid))
}
|| {GuildId, Pid} <- Guilds
],
take(Limit, lists:sort(fun row_reduction_delta_ge/2, Rows)).
-spec guild_probe(integer()) -> row().
guild_probe(GuildId) when is_integer(GuildId) ->
try guild_manager:lookup(GuildId) of
{ok, Pid} when is_pid(Pid) ->
guild_row(GuildId, Pid);
Other ->
#{guild_id => GuildId, lookup => Other}
catch
Class:Reason ->
#{guild_id => GuildId, lookup => {error, {Class, Reason}}}
end.
-spec logger_status() -> row().
logger_status() ->
case whereis(?LOGGER_HANDLER) of
Pid when is_pid(Pid) -> enrich_pid(Pid, #{handler => ?LOGGER_HANDLER});
undefined -> #{handler => ?LOGGER_HANDLER, status => not_found}
end.
-spec gc_logger() -> row().
gc_logger() ->
case whereis(?LOGGER_HANDLER) of
Pid when is_pid(Pid) -> gc_logger_pid(Pid);
undefined -> #{handler => ?LOGGER_HANDLER, status => not_found}
end.
-spec gc_logger_pid(pid()) -> row().
gc_logger_pid(Pid) ->
Before = enrich_pid(Pid, #{handler => ?LOGGER_HANDLER}),
case maps:get(message_queue_len, Before, 0) of
Len when Len =< ?LOGGER_GC_MAX_QUEUE ->
Result = erlang:garbage_collect(Pid, [{type, major}]),
#{
handler => ?LOGGER_HANDLER,
status => ok,
result => Result,
before => Before,
after_info => enrich_pid(Pid, #{handler => ?LOGGER_HANDLER})
};
Len ->
#{
handler => ?LOGGER_HANDLER,
status => skipped,
reason => logger_queue_not_idle,
message_queue_len => Len,
max_queue => ?LOGGER_GC_MAX_QUEUE,
before => Before
}
end.
-spec top_by_info(metric(), pos_integer()) -> [row()].
top_by_info(Key, Limit) ->
Rows = [enrich_pid(Pid, #{}) || Pid <- process_scan_pids()],
Sorted = lists:sort(fun(A, B) -> maps:get(Key, A, 0) >= maps:get(Key, B, 0) end, Rows),
take(Limit, Sorted).
-spec local_guild_pids() -> [{integer(), pid()}].
local_guild_pids() ->
try bounded_ets_rows(guild_pid_cache, ?MAX_GUILD_PID_SCAN) of
Rows ->
lists:usort([
{GuildId, Pid}
|| {GuildId, Pid} <- Rows,
is_integer(GuildId),
is_pid(Pid),
node(Pid) =:= node()
])
catch
error:badarg -> []
end.
-spec process_scan_pids() -> [pid()].
process_scan_pids() ->
lists:sublist(erlang:processes(), ?MAX_PROCESS_SCAN).
-spec bounded_ets_rows(ets:table(), pos_integer()) -> [term()].
bounded_ets_rows(Table, Limit) ->
MatchSpec = [{{'$1', '$2'}, [], [{{'$1', '$2'}}]}],
case ets:select(Table, MatchSpec, Limit) of
{Rows, _Continuation} -> Rows;
'$end_of_table' -> []
end.
-spec guild_row(integer(), pid()) -> row().
guild_row(GuildId, Pid) ->
Base = enrich_pid(Pid, #{guild_id => GuildId}),
case safe_state(Pid) of
State when is_map(State) ->
Data = map_utils:ensure_map(maps:get(data, State, #{})),
Guild = map_utils:ensure_map(maps:get(<<"guild">>, Data, #{})),
Base#{
guild_name => maps:get(<<"name">>, Guild, <<"Unknown">>),
member_count => safe_member_count(Data),
session_count => map_size(map_utils:ensure_map(maps:get(sessions, State, #{}))),
presence_count => map_size(
map_utils:ensure_map(maps:get(presences, State, #{}))
)
};
_ ->
Base#{state => unavailable}
end.
-spec enrich_pid(pid(), map()) -> row().
enrich_pid(Pid, Extra) ->
Info = info_map(Pid),
maps:merge(Info#{pid => pid_to_list(Pid)}, Extra).
-spec info_map(pid()) -> map().
info_map(Pid) ->
case
erlang:process_info(Pid, [
memory,
message_queue_len,
reductions,
current_function,
initial_call,
registered_name,
total_heap_size,
heap_size,
stack_size,
garbage_collection
])
of
undefined ->
#{alive => false};
Info when is_list(Info) ->
maps:from_list(Info)
end.
-spec safe_state(pid()) -> term().
safe_state(Pid) ->
try sys:get_state(Pid, ?STATE_TIMEOUT_MS) of
State -> State
catch
_:_ -> unavailable
end.
-spec safe_member_count(term()) -> non_neg_integer().
safe_member_count(Data) ->
try guild_data_index:member_count(Data) of
Count when is_integer(Count), Count >= 0 -> Count;
_ -> 0
catch
_:_ -> 0
end.
-spec reductions_by_pid([pid()]) -> #{pid() => non_neg_integer()}.
reductions_by_pid(Pids) ->
maps:from_list([{Pid, reductions(Pid)} || Pid <- Pids]).
-spec reductions(pid()) -> non_neg_integer().
reductions(Pid) ->
case erlang:process_info(Pid, reductions) of
{reductions, R} when is_integer(R), R >= 0 -> R;
_ -> 0
end.
-spec safe_scheduler_wall_time() -> term().
safe_scheduler_wall_time() ->
try erlang:statistics(scheduler_wall_time) of
Value -> Value
catch
_:_ -> unavailable
end.
-spec clamp_ms(term()) -> pos_integer().
clamp_ms(Ms) when is_integer(Ms), Ms >= ?MIN_SAMPLE_MS, Ms =< ?MAX_SAMPLE_MS -> Ms;
clamp_ms(Ms) when is_integer(Ms), Ms > ?MAX_SAMPLE_MS -> ?MAX_SAMPLE_MS;
clamp_ms(_) -> ?DEFAULT_SAMPLE_MS.
-spec take(term(), [row()]) -> [row()].
take(Limit, Rows) when is_integer(Limit), Limit > 0 ->
lists:sublist(Rows, min(Limit, ?MAX_LIMIT));
take(_, Rows) ->
lists:sublist(Rows, ?DEFAULT_LIMIT).
-spec row_memory_ge(row(), row()) -> boolean().
row_memory_ge(A, B) ->
maps:get(memory, A, 0) >= maps:get(memory, B, 0).
-spec row_reduction_delta_ge(row(), row()) -> boolean().
row_reduction_delta_ge(A, B) ->
maps:get(reduction_delta, A, 0) >= maps:get(reduction_delta, B, 0).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
snapshot_returns_basic_runtime_info_test() ->
Snapshot = snapshot(),
?assert(is_atom(maps:get(node, Snapshot))),
?assert(is_list(maps:get(memory, Snapshot))),
?assert(is_integer(maps:get(process_count, Snapshot))).
top_processes_is_bounded_test() ->
Rows = top_processes(memory, 2),
?assert(length(Rows) =< 2),
?assert(lists:all(fun(Row) -> maps:is_key(pid, Row) end, Rows)).
sample_processes_is_bounded_test() ->
Rows = sample_processes(10, 2),
?assert(length(Rows) =< 2).
process_scan_pids_is_bounded_test() ->
?assert(length(process_scan_pids()) =< ?MAX_PROCESS_SCAN).
bounded_ets_rows_respects_limit_test() ->
Table = ets:new(?MODULE, [set]),
try
true = ets:insert(Table, [{1, self()}, {2, self()}, {3, self()}]),
Rows = bounded_ets_rows(Table, 2),
?assertEqual(2, length(Rows))
after
ets:delete(Table)
end.
guild_probe_without_manager_does_not_crash_test() ->
Row = guild_probe(1),
?assertEqual(1, maps:get(guild_id, Row)).
-endif.
-112
View File
@@ -1,112 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(list_ops).
-typing([eqwalizer]).
-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
]).
-export_type([item/0, id/0, item_list/0]).
-type item() :: map() | term().
-type id() :: binary() | integer().
-type item_list() :: [item()].
-spec replace_by_id(term(), id(), item()) -> item_list().
replace_by_id(Items, Id, NewItem) when is_list(Items) ->
lists:map(
fun(Item) -> replace_item_by_id(Item, Id, NewItem) end,
Items
);
replace_by_id(_, _, _) ->
[].
-spec remove_by_id(term(), id()) -> item_list().
remove_by_id(Items, Id) when is_list(Items) ->
lists:filter(fun(Item) -> keep_item_with_id(Item, Id) end, Items);
remove_by_id(_, _) ->
[].
-spec replace_by_user_id(term(), integer(), item()) -> item_list().
replace_by_user_id(Items, UserId, NewItem) when is_list(Items), is_integer(UserId) ->
lists:map(
fun(Item) -> replace_item_by_user_id(Item, UserId, NewItem) end,
Items
);
replace_by_user_id(_, _, _) ->
[].
-spec remove_by_user_id(term(), integer()) -> item_list().
remove_by_user_id(Items, UserId) when is_list(Items), is_integer(UserId) ->
lists:filter(fun(Item) -> keep_item_with_user_id(Item, UserId) end, Items);
remove_by_user_id(_, _) ->
[].
-spec bulk_update(term(), term()) -> item_list().
bulk_update(Items, Updates) when is_list(Items), is_list(Updates) ->
UpdateMap = lists:foldl(fun add_update_item/2, #{}, Updates),
lists:map(fun(Item) -> apply_update_item(Item, UpdateMap) end, Items);
bulk_update(Items, _) when is_list(Items) ->
Items;
bulk_update(_, _) ->
[].
-spec extract_user_id(map() | term()) -> pos_integer() | undefined.
extract_user_id(Item) ->
UserMap = map_utils:ensure_map(map_utils:get_safe(Item, <<"user">>, #{})),
type_conv:extract_id(UserMap, <<"id">>).
-spec replace_item_by_id(item(), id(), item()) -> item().
replace_item_by_id(Item, Id, NewItem) when is_map(Item) ->
case maps:get(<<"id">>, Item, undefined) of
Id -> NewItem;
_ -> Item
end;
replace_item_by_id(Item, _Id, _NewItem) ->
Item.
-spec keep_item_with_id(item(), id()) -> boolean().
keep_item_with_id(Item, Id) when is_map(Item) ->
maps:get(<<"id">>, Item, undefined) =/= Id;
keep_item_with_id(_Item, _Id) ->
true.
-spec replace_item_by_user_id(item(), integer(), item()) -> item().
replace_item_by_user_id(Item, UserId, NewItem) when is_map(Item) ->
case extract_user_id(Item) =:= UserId of
true -> NewItem;
false -> Item
end;
replace_item_by_user_id(Item, _UserId, _NewItem) ->
Item.
-spec keep_item_with_user_id(item(), integer()) -> boolean().
keep_item_with_user_id(Item, UserId) when is_map(Item) ->
extract_user_id(Item) =/= UserId;
keep_item_with_user_id(_Item, _UserId) ->
true.
-spec add_update_item(item(), map()) -> map().
add_update_item(Item, Acc) when is_map(Item) ->
case maps:get(<<"id">>, Item, undefined) of
undefined -> Acc;
ItemId -> Acc#{ItemId => Item}
end;
add_update_item(_Item, Acc) ->
Acc.
-spec apply_update_item(item(), map()) -> item().
apply_update_item(Item, UpdateMap) when is_map(Item) ->
ItemId = maps:get(<<"id">>, Item, undefined),
case maps:get(ItemId, UpdateMap, undefined) of
undefined -> Item;
UpdatedItem -> UpdatedItem
end;
apply_update_item(Item, _UpdateMap) ->
Item.
-150
View File
@@ -1,150 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(list_ops_bulk_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
make_item_with_id(Id) ->
#{<<"id">> => Id, <<"data">> => <<"test">>}.
default_items() ->
[
make_item_with_id(<<"1">>),
make_item_with_id(<<"2">>)
].
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 = list_ops: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 = list_ops: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 = default_items(),
Updates = [
#{<<"id">> => <<"99">>, <<"data">> => <<"new">>},
#{<<"id">> => <<"98">>, <<"data">> => <<"new2">>}
],
Result = list_ops:bulk_update(Items, Updates),
?assertEqual(Items, Result).
bulk_update_empty_lists_test() ->
?assertEqual([], list_ops:bulk_update([], [])),
?assertEqual([], list_ops:bulk_update([], [make_item_with_id(<<"1">>)])),
Items = [make_item_with_id(<<"1">>)],
?assertEqual(Items, list_ops:bulk_update(Items, [])).
bulk_update_updates_without_id_test() ->
Items = default_items(),
Updates = [
#{<<"name">> => <<"no_id">>},
#{<<"id">> => <<"2">>, <<"data">> => <<"updated">>}
],
Result = list_ops: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 = list_ops: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 = default_items(),
Updates = [
<<"non_map">>,
#{<<"id">> => <<"1">>, <<"data">> => <<"updated">>},
{tuple},
#{<<"no_id">> => <<"field">>}
],
Result = list_ops: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 = default_items(),
Updates = [
#{<<"id">> => <<"1">>, <<"data">> => <<"first_update">>},
#{<<"id">> => <<"1">>, <<"data">> => <<"second_update">>}
],
Result = list_ops: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, list_ops:bulk_update(Items, not_a_list)),
?assertEqual(Items, list_ops:bulk_update(Items, undefined)),
?assertEqual(Items, list_ops:bulk_update(Items, #{})),
?assertEqual([], list_ops:bulk_update(not_a_list, [make_item_with_id(<<"1">>)])),
?assertEqual([], list_ops: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 = list_ops: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)).
@@ -1,57 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(list_ops_extract_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
extract_user_id_valid_structure_test() ->
Item = #{<<"user">> => #{<<"id">> => <<"12345">>}},
?assertEqual(12345, list_ops:extract_user_id(Item)).
extract_user_id_missing_user_test() ->
Item = #{<<"other">> => <<"field">>},
?assertEqual(undefined, list_ops:extract_user_id(Item)).
extract_user_id_missing_id_test() ->
Item = #{<<"user">> => #{<<"name">> => <<"alice">>}},
?assertEqual(undefined, list_ops:extract_user_id(Item)).
extract_user_id_non_map_test() ->
?assertEqual(undefined, list_ops:extract_user_id(<<"string">>)),
?assertEqual(undefined, list_ops:extract_user_id([list])),
?assertEqual(undefined, list_ops:extract_user_id({tuple})),
?assertEqual(undefined, list_ops:extract_user_id(undefined)),
?assertEqual(undefined, list_ops:extract_user_id(123)).
extract_user_id_user_not_map_test() ->
Item = #{<<"user">> => <<"not_a_map">>},
?assertEqual(undefined, list_ops:extract_user_id(Item)).
extract_user_id_nested_structure_test() ->
Item = #{
<<"user">> => #{
<<"id">> => <<"999">>,
<<"name">> => <<"bob">>,
<<"extra">> => #{<<"nested">> => <<"data">>}
},
<<"role">> => <<"admin">>
},
?assertEqual(999, list_ops:extract_user_id(Item)).
extract_user_id_empty_id_test() ->
Item = #{<<"user">> => #{<<"id">> => <<>>}},
?assertEqual(undefined, list_ops:extract_user_id(Item)).
extract_user_id_empty_user_map_test() ->
Item = #{<<"user">> => #{}},
?assertEqual(undefined, list_ops:extract_user_id(Item)).
extract_user_id_invalid_id_format_test() ->
Item = #{<<"user">> => #{<<"id">> => <<"not_a_number">>}},
?assertEqual(undefined, list_ops:extract_user_id(Item)).
extract_user_id_rejects_zero_and_leading_zero_test() ->
?assertEqual(undefined, list_ops:extract_user_id(#{<<"user">> => #{<<"id">> => <<"0">>}})),
?assertEqual(
undefined, list_ops:extract_user_id(#{<<"user">> => #{<<"id">> => <<"001">>}})
).
-163
View File
@@ -1,163 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(list_ops_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
make_item_with_id(Id) ->
#{<<"id">> => Id, <<"data">> => <<"test">>}.
replace_by_id_success_test() ->
NewItem = #{<<"id">> => <<"2">>, <<"data">> => <<"updated">>},
assert_replace_default_id_two(
[
make_item_with_id(<<"1">>),
NewItem,
make_item_with_id(<<"3">>)
],
NewItem
).
replace_by_id_no_match_test() ->
NewItem = #{<<"id">> => <<"99">>, <<"data">> => <<"new">>},
assert_replace_by_id_no_change(<<"99">>, NewItem).
replace_by_id_empty_list_test() ->
Result = list_ops: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 = list_ops: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 = list_ops: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([], list_ops:replace_by_id(not_a_list, <<"1">>, #{})),
?assertEqual([], list_ops:replace_by_id(#{}, <<"1">>, #{})),
?assertEqual([], list_ops: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 = list_ops: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() ->
assert_remove_default_id_two(
[
make_item_with_id(<<"1">>),
make_item_with_id(<<"3">>)
]
).
remove_by_id_no_match_test() ->
assert_remove_by_id_no_change(<<"99">>).
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 = list_ops: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 = list_ops: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 = list_ops: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([], list_ops:remove_by_id(not_a_list, <<"1">>)),
?assertEqual([], list_ops:remove_by_id(undefined, <<"1">>)),
?assertEqual([], list_ops: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 = list_ops:remove_by_id(Items, <<"1">>),
?assertEqual([], Result).
id_items(Ids) ->
[make_item_with_id(Id) || Id <- Ids].
default_id_items() ->
id_items([<<"1">>, <<"2">>]).
default_three_id_items() ->
id_items([<<"1">>, <<"2">>, <<"3">>]).
assert_replace_by_id(Expected, Items, Id, NewItem) ->
?assertEqual(Expected, list_ops:replace_by_id(Items, Id, NewItem)).
assert_replace_default_id_two(Expected, NewItem) ->
assert_replace_by_id(Expected, default_three_id_items(), <<"2">>, NewItem).
assert_replace_by_id_no_change(Id, NewItem) ->
Items = default_id_items(),
assert_replace_by_id(Items, Items, Id, NewItem).
assert_remove_by_id(Expected, Items, Id) ->
?assertEqual(Expected, list_ops:remove_by_id(Items, Id)).
assert_remove_default_id_two(Expected) ->
assert_remove_by_id(Expected, default_three_id_items(), <<"2">>).
assert_remove_by_id_no_change(Id) ->
Items = default_id_items(),
assert_remove_by_id(Items, Items, Id).
-171
View File
@@ -1,171 +0,0 @@
%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(list_ops_user_tests).
-typing([eqwalizer]).
-include_lib("eunit/include/eunit.hrl").
make_item_with_user_id(UserId) ->
#{
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
<<"data">> => <<"member">>
}.
replace_by_user_id_success_test() ->
NewItem = #{
<<"user">> => #{<<"id">> => <<"200">>},
<<"data">> => <<"updated">>
},
assert_replace_by_user_id(
[
make_item_with_user_id(100),
NewItem,
make_item_with_user_id(300)
],
user_items([100, 200, 300]),
200,
NewItem
).
replace_by_user_id_no_match_test() ->
Items = user_items([100, 200]),
NewItem = make_item_with_user_id(999),
assert_replace_by_user_id(Items, Items, 999, NewItem).
replace_by_user_id_empty_list_test() ->
Result = list_ops: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 = list_ops: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 = list_ops: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 = list_ops: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([], list_ops:replace_by_user_id(not_a_list, 100, #{})),
?assertEqual([], list_ops:replace_by_user_id(undefined, 100, #{})),
?assertEqual(
[], list_ops:replace_by_user_id([make_item_with_user_id(100)], invalid_user_id(), #{})
).
remove_by_user_id_success_test() ->
assert_remove_by_user_id(
[
make_item_with_user_id(100),
make_item_with_user_id(300)
],
user_items([100, 200, 300]),
200
).
remove_by_user_id_no_match_test() ->
Items = user_items([100, 200]),
assert_remove_by_user_id(Items, Items, 999).
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 = list_ops: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 = list_ops: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 = list_ops: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 = list_ops:remove_by_user_id(Items, 999),
?assertEqual(4, length(Result)),
?assertEqual(Items, Result).
remove_by_user_id_invalid_input_test() ->
?assertEqual([], list_ops:remove_by_user_id(not_a_list, 100)),
?assertEqual([], list_ops:remove_by_user_id(undefined, 100)),
?assertEqual(
[], list_ops:remove_by_user_id([make_item_with_user_id(100)], invalid_user_id())
).
invalid_user_id() ->
eqwalizer:dynamic_cast(<<"not_integer">>).
user_items(UserIds) ->
[make_item_with_user_id(UserId) || UserId <- UserIds].
assert_replace_by_user_id(Expected, Items, UserId, NewItem) ->
?assertEqual(Expected, list_ops:replace_by_user_id(Items, UserId, NewItem)).
assert_remove_by_user_id(Expected, Items, UserId) ->
?assertEqual(Expected, list_ops:remove_by_user_id(Items, UserId)).