initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
%% 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(backoff_utils).
-export([
calculate/1,
calculate/2
]).
-spec calculate(non_neg_integer()) -> non_neg_integer().
calculate(Attempt) ->
calculate(Attempt, 30000).
-spec calculate(non_neg_integer(), pos_integer()) -> non_neg_integer().
calculate(Attempt, MaxMs) ->
BackoffMs = round(1000 * math:pow(2, Attempt)),
min(BackoffMs, MaxMs).

View File

@@ -0,0 +1,368 @@
%% 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(constants).
-export([
gateway_opcode/1,
opcode_to_num/1,
close_code_to_num/1,
dispatch_event_atom/1,
status_type_atom/1,
max_payload_size/0,
heartbeat_interval/0,
heartbeat_timeout/0,
random_session_bytes/0,
view_channel_permission/0,
administrator_permission/0,
manage_roles_permission/0,
manage_channels_permission/0,
connect_permission/0,
speak_permission/0,
stream_permission/0,
use_vad_permission/0,
kick_members_permission/0,
ban_members_permission/0
]).
gateway_opcode(0) -> dispatch;
gateway_opcode(1) -> heartbeat;
gateway_opcode(2) -> identify;
gateway_opcode(3) -> presence_update;
gateway_opcode(4) -> voice_state_update;
gateway_opcode(5) -> voice_server_ping;
gateway_opcode(6) -> resume;
gateway_opcode(7) -> reconnect;
gateway_opcode(8) -> request_guild_members;
gateway_opcode(9) -> invalid_session;
gateway_opcode(10) -> hello;
gateway_opcode(11) -> heartbeat_ack;
gateway_opcode(12) -> gateway_error;
gateway_opcode(13) -> call_connect;
gateway_opcode(14) -> lazy_request;
gateway_opcode(_) -> unknown.
opcode_to_num(dispatch) -> 0;
opcode_to_num(heartbeat) -> 1;
opcode_to_num(identify) -> 2;
opcode_to_num(presence_update) -> 3;
opcode_to_num(voice_state_update) -> 4;
opcode_to_num(voice_server_ping) -> 5;
opcode_to_num(resume) -> 6;
opcode_to_num(reconnect) -> 7;
opcode_to_num(request_guild_members) -> 8;
opcode_to_num(invalid_session) -> 9;
opcode_to_num(hello) -> 10;
opcode_to_num(heartbeat_ack) -> 11;
opcode_to_num(gateway_error) -> 12;
opcode_to_num(call_connect) -> 13;
opcode_to_num(lazy_request) -> 14.
close_code_to_num(unknown_error) -> 4000;
close_code_to_num(unknown_opcode) -> 4001;
close_code_to_num(decode_error) -> 4002;
close_code_to_num(not_authenticated) -> 4003;
close_code_to_num(authentication_failed) -> 4004;
close_code_to_num(already_authenticated) -> 4005;
close_code_to_num(invalid_seq) -> 4007;
close_code_to_num(rate_limited) -> 4008;
close_code_to_num(session_timeout) -> 4009;
close_code_to_num(invalid_shard) -> 4010;
close_code_to_num(sharding_required) -> 4011;
close_code_to_num(invalid_api_version) -> 4012.
dispatch_event_atom(<<"READY">>) ->
ready;
dispatch_event_atom(<<"RESUMED">>) ->
resumed;
dispatch_event_atom(<<"SESSIONS_REPLACE">>) ->
sessions_replace;
dispatch_event_atom(<<"USER_UPDATE">>) ->
user_update;
dispatch_event_atom(<<"USER_SETTINGS_UPDATE">>) ->
user_settings_update;
dispatch_event_atom(<<"USER_GUILD_SETTINGS_UPDATE">>) ->
user_guild_settings_update;
dispatch_event_atom(<<"USER_PINNED_DMS_UPDATE">>) ->
user_pinned_dms_update;
dispatch_event_atom(<<"USER_NOTE_UPDATE">>) ->
user_note_update;
dispatch_event_atom(<<"RECENT_MENTION_DELETE">>) ->
recent_mention_delete;
dispatch_event_atom(<<"SAVED_MESSAGE_CREATE">>) ->
saved_message_create;
dispatch_event_atom(<<"SAVED_MESSAGE_DELETE">>) ->
saved_message_delete;
dispatch_event_atom(<<"AUTH_SESSION_CHANGE">>) ->
auth_session_change;
dispatch_event_atom(<<"PRESENCE_UPDATE">>) ->
presence_update;
dispatch_event_atom(<<"GUILD_CREATE">>) ->
guild_create;
dispatch_event_atom(<<"GUILD_UPDATE">>) ->
guild_update;
dispatch_event_atom(<<"GUILD_DELETE">>) ->
guild_delete;
dispatch_event_atom(<<"GUILD_MEMBER_ADD">>) ->
guild_member_add;
dispatch_event_atom(<<"GUILD_MEMBER_UPDATE">>) ->
guild_member_update;
dispatch_event_atom(<<"GUILD_MEMBER_REMOVE">>) ->
guild_member_remove;
dispatch_event_atom(<<"GUILD_ROLE_CREATE">>) ->
guild_role_create;
dispatch_event_atom(<<"GUILD_ROLE_UPDATE">>) ->
guild_role_update;
dispatch_event_atom(<<"GUILD_ROLE_UPDATE_BULK">>) ->
guild_role_update_bulk;
dispatch_event_atom(<<"GUILD_ROLE_DELETE">>) ->
guild_role_delete;
dispatch_event_atom(<<"GUILD_EMOJIS_UPDATE">>) ->
guild_emojis_update;
dispatch_event_atom(<<"GUILD_STICKERS_UPDATE">>) ->
guild_stickers_update;
dispatch_event_atom(<<"GUILD_BAN_ADD">>) ->
guild_ban_add;
dispatch_event_atom(<<"GUILD_BAN_REMOVE">>) ->
guild_ban_remove;
dispatch_event_atom(<<"GUILD_MEMBERS_CHUNK">>) ->
guild_members_chunk;
dispatch_event_atom(<<"CHANNEL_CREATE">>) ->
channel_create;
dispatch_event_atom(<<"CHANNEL_UPDATE">>) ->
channel_update;
dispatch_event_atom(<<"CHANNEL_UPDATE_BULK">>) ->
channel_update_bulk;
dispatch_event_atom(<<"PASSIVE_UPDATES">>) ->
passive_updates;
dispatch_event_atom(<<"CHANNEL_DELETE">>) ->
channel_delete;
dispatch_event_atom(<<"CHANNEL_RECIPIENT_ADD">>) ->
channel_recipient_add;
dispatch_event_atom(<<"CHANNEL_RECIPIENT_REMOVE">>) ->
channel_recipient_remove;
dispatch_event_atom(<<"CHANNEL_PINS_UPDATE">>) ->
channel_pins_update;
dispatch_event_atom(<<"CHANNEL_PINS_ACK">>) ->
channel_pins_ack;
dispatch_event_atom(<<"INVITE_CREATE">>) ->
invite_create;
dispatch_event_atom(<<"INVITE_DELETE">>) ->
invite_delete;
dispatch_event_atom(<<"MESSAGE_CREATE">>) ->
message_create;
dispatch_event_atom(<<"MESSAGE_UPDATE">>) ->
message_update;
dispatch_event_atom(<<"MESSAGE_DELETE">>) ->
message_delete;
dispatch_event_atom(<<"MESSAGE_DELETE_BULK">>) ->
message_delete_bulk;
dispatch_event_atom(<<"MESSAGE_REACTION_ADD">>) ->
message_reaction_add;
dispatch_event_atom(<<"MESSAGE_REACTION_REMOVE">>) ->
message_reaction_remove;
dispatch_event_atom(<<"MESSAGE_REACTION_REMOVE_ALL">>) ->
message_reaction_remove_all;
dispatch_event_atom(<<"MESSAGE_REACTION_REMOVE_EMOJI">>) ->
message_reaction_remove_emoji;
dispatch_event_atom(<<"MESSAGE_ACK">>) ->
message_ack;
dispatch_event_atom(<<"TYPING_START">>) ->
typing_start;
dispatch_event_atom(<<"WEBHOOKS_UPDATE">>) ->
webhooks_update;
dispatch_event_atom(<<"RELATIONSHIP_ADD">>) ->
relationship_add;
dispatch_event_atom(<<"RELATIONSHIP_UPDATE">>) ->
relationship_update;
dispatch_event_atom(<<"RELATIONSHIP_REMOVE">>) ->
relationship_remove;
dispatch_event_atom(<<"VOICE_STATE_UPDATE">>) ->
voice_state_update;
dispatch_event_atom(<<"VOICE_SERVER_UPDATE">>) ->
voice_server_update;
dispatch_event_atom(<<"FAVORITE_MEME_CREATE">>) ->
favorite_meme_create;
dispatch_event_atom(<<"FAVORITE_MEME_UPDATE">>) ->
favorite_meme_update;
dispatch_event_atom(<<"FAVORITE_MEME_DELETE">>) ->
favorite_meme_delete;
dispatch_event_atom(<<"CALL_CREATE">>) ->
call_create;
dispatch_event_atom(<<"CALL_UPDATE">>) ->
call_update;
dispatch_event_atom(<<"CALL_DELETE">>) ->
call_delete;
dispatch_event_atom(<<"GUILD_MEMBER_LIST_UPDATE">>) ->
guild_member_list_update;
dispatch_event_atom(<<"GUILD_SYNC">>) ->
guild_sync;
dispatch_event_atom(ready) ->
<<"READY">>;
dispatch_event_atom(resumed) ->
<<"RESUMED">>;
dispatch_event_atom(sessions_replace) ->
<<"SESSIONS_REPLACE">>;
dispatch_event_atom(user_update) ->
<<"USER_UPDATE">>;
dispatch_event_atom(user_settings_update) ->
<<"USER_SETTINGS_UPDATE">>;
dispatch_event_atom(user_guild_settings_update) ->
<<"USER_GUILD_SETTINGS_UPDATE">>;
dispatch_event_atom(user_pinned_dms_update) ->
<<"USER_PINNED_DMS_UPDATE">>;
dispatch_event_atom(user_note_update) ->
<<"USER_NOTE_UPDATE">>;
dispatch_event_atom(recent_mention_delete) ->
<<"RECENT_MENTION_DELETE">>;
dispatch_event_atom(saved_message_create) ->
<<"SAVED_MESSAGE_CREATE">>;
dispatch_event_atom(saved_message_delete) ->
<<"SAVED_MESSAGE_DELETE">>;
dispatch_event_atom(auth_session_change) ->
<<"AUTH_SESSION_CHANGE">>;
dispatch_event_atom(presence_update) ->
<<"PRESENCE_UPDATE">>;
dispatch_event_atom(guild_create) ->
<<"GUILD_CREATE">>;
dispatch_event_atom(guild_update) ->
<<"GUILD_UPDATE">>;
dispatch_event_atom(guild_delete) ->
<<"GUILD_DELETE">>;
dispatch_event_atom(guild_member_add) ->
<<"GUILD_MEMBER_ADD">>;
dispatch_event_atom(guild_member_update) ->
<<"GUILD_MEMBER_UPDATE">>;
dispatch_event_atom(guild_member_remove) ->
<<"GUILD_MEMBER_REMOVE">>;
dispatch_event_atom(guild_role_create) ->
<<"GUILD_ROLE_CREATE">>;
dispatch_event_atom(guild_role_update) ->
<<"GUILD_ROLE_UPDATE">>;
dispatch_event_atom(guild_role_update_bulk) ->
<<"GUILD_ROLE_UPDATE_BULK">>;
dispatch_event_atom(guild_role_delete) ->
<<"GUILD_ROLE_DELETE">>;
dispatch_event_atom(guild_emojis_update) ->
<<"GUILD_EMOJIS_UPDATE">>;
dispatch_event_atom(guild_stickers_update) ->
<<"GUILD_STICKERS_UPDATE">>;
dispatch_event_atom(guild_ban_add) ->
<<"GUILD_BAN_ADD">>;
dispatch_event_atom(guild_ban_remove) ->
<<"GUILD_BAN_REMOVE">>;
dispatch_event_atom(guild_members_chunk) ->
<<"GUILD_MEMBERS_CHUNK">>;
dispatch_event_atom(channel_create) ->
<<"CHANNEL_CREATE">>;
dispatch_event_atom(channel_update) ->
<<"CHANNEL_UPDATE">>;
dispatch_event_atom(channel_update_bulk) ->
<<"CHANNEL_UPDATE_BULK">>;
dispatch_event_atom(passive_updates) ->
<<"PASSIVE_UPDATES">>;
dispatch_event_atom(channel_delete) ->
<<"CHANNEL_DELETE">>;
dispatch_event_atom(channel_recipient_add) ->
<<"CHANNEL_RECIPIENT_ADD">>;
dispatch_event_atom(channel_recipient_remove) ->
<<"CHANNEL_RECIPIENT_REMOVE">>;
dispatch_event_atom(channel_pins_update) ->
<<"CHANNEL_PINS_UPDATE">>;
dispatch_event_atom(channel_pins_ack) ->
<<"CHANNEL_PINS_ACK">>;
dispatch_event_atom(invite_create) ->
<<"INVITE_CREATE">>;
dispatch_event_atom(invite_delete) ->
<<"INVITE_DELETE">>;
dispatch_event_atom(message_create) ->
<<"MESSAGE_CREATE">>;
dispatch_event_atom(message_update) ->
<<"MESSAGE_UPDATE">>;
dispatch_event_atom(message_delete) ->
<<"MESSAGE_DELETE">>;
dispatch_event_atom(message_delete_bulk) ->
<<"MESSAGE_DELETE_BULK">>;
dispatch_event_atom(message_reaction_add) ->
<<"MESSAGE_REACTION_ADD">>;
dispatch_event_atom(message_reaction_remove) ->
<<"MESSAGE_REACTION_REMOVE">>;
dispatch_event_atom(message_reaction_remove_all) ->
<<"MESSAGE_REACTION_REMOVE_ALL">>;
dispatch_event_atom(message_reaction_remove_emoji) ->
<<"MESSAGE_REACTION_REMOVE_EMOJI">>;
dispatch_event_atom(message_ack) ->
<<"MESSAGE_ACK">>;
dispatch_event_atom(typing_start) ->
<<"TYPING_START">>;
dispatch_event_atom(webhooks_update) ->
<<"WEBHOOKS_UPDATE">>;
dispatch_event_atom(relationship_add) ->
<<"RELATIONSHIP_ADD">>;
dispatch_event_atom(relationship_update) ->
<<"RELATIONSHIP_UPDATE">>;
dispatch_event_atom(relationship_remove) ->
<<"RELATIONSHIP_REMOVE">>;
dispatch_event_atom(voice_state_update) ->
<<"VOICE_STATE_UPDATE">>;
dispatch_event_atom(voice_server_update) ->
<<"VOICE_SERVER_UPDATE">>;
dispatch_event_atom(favorite_meme_create) ->
<<"FAVORITE_MEME_CREATE">>;
dispatch_event_atom(favorite_meme_update) ->
<<"FAVORITE_MEME_UPDATE">>;
dispatch_event_atom(favorite_meme_delete) ->
<<"FAVORITE_MEME_DELETE">>;
dispatch_event_atom(call_create) ->
<<"CALL_CREATE">>;
dispatch_event_atom(call_update) ->
<<"CALL_UPDATE">>;
dispatch_event_atom(call_delete) ->
<<"CALL_DELETE">>;
dispatch_event_atom(guild_member_list_update) ->
<<"GUILD_MEMBER_LIST_UPDATE">>;
dispatch_event_atom(guild_sync) ->
<<"GUILD_SYNC">>;
dispatch_event_atom(EventBinary) when is_binary(EventBinary) -> EventBinary;
dispatch_event_atom(EventAtom) when is_atom(EventAtom) ->
list_to_binary(string:uppercase(atom_to_list(EventAtom))).
status_type_atom(<<"online">>) -> online;
status_type_atom(<<"dnd">>) -> dnd;
status_type_atom(<<"idle">>) -> idle;
status_type_atom(<<"invisible">>) -> invisible;
status_type_atom(<<"offline">>) -> offline;
status_type_atom(online) -> <<"online">>;
status_type_atom(dnd) -> <<"dnd">>;
status_type_atom(idle) -> <<"idle">>;
status_type_atom(invisible) -> <<"invisible">>;
status_type_atom(offline) -> <<"offline">>.
max_payload_size() -> 4096.
heartbeat_interval() -> 41250.
heartbeat_timeout() -> 45000.
random_session_bytes() -> 16.
view_channel_permission() -> 1024.
administrator_permission() -> 8.
manage_roles_permission() -> 268435456.
manage_channels_permission() -> 16.
connect_permission() -> 1048576.
speak_permission() -> 2097152.
stream_permission() -> 512.
use_vad_permission() -> 33554432.
kick_members_permission() -> 2.
ban_members_permission() -> 4.

View File

@@ -0,0 +1,63 @@
%% 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(custom_status_validation).
-export([
validate/2
]).
-spec validate(integer(), map() | null) -> {ok, map()} | {error, term()}.
validate(_UserId, null) ->
{ok, null};
validate(UserId, CustomStatus) when is_map(CustomStatus) ->
Request = build_request(UserId, CustomStatus),
rpc_client:call(Request).
build_request(UserId, CustomStatus) ->
#{
<<"type">> => <<"validate_custom_status">>,
<<"user_id">> => type_conv:to_binary(UserId),
<<"custom_status">> => build_custom_status_payload(CustomStatus)
}.
build_custom_status_payload(CustomStatus) ->
Field1 = put_optional_field(
maps:new(),
<<"text">>,
maps:get(<<"text">>, CustomStatus, undefined)
),
Field2 = put_optional_field(
Field1,
<<"expires_at">>,
maps:get(<<"expires_at">>, CustomStatus, undefined)
),
Field3 = put_optional_field(
Field2,
<<"emoji_id">>,
maps:get(<<"emoji_id">>, CustomStatus, undefined)
),
put_optional_field(
Field3,
<<"emoji_name">>,
maps:get(<<"emoji_name">>, CustomStatus, undefined)
).
put_optional_field(Map, _Key, undefined) ->
Map;
put_optional_field(Map, Key, Value) ->
maps:put(Key, Value, Map).

View File

@@ -0,0 +1,647 @@
%% 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.

View File

@@ -0,0 +1,559 @@
%% 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(map_utils).
-export([
get_safe/3,
get_nested/3,
ensure_map/1,
ensure_list/1,
filter_by_field/3,
find_by_field/3,
get_integer/3,
get_binary/3
]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-type key() :: atom() | binary() | term().
-type path() :: [key()].
-type default() :: term().
-spec get_safe(Map :: map() | term(), Key :: key(), Default :: default()) -> term().
get_safe(Map, Key, Default) when is_map(Map) ->
maps:get(Key, Map, Default);
get_safe(_NotMap, _Key, Default) ->
Default.
-spec get_nested(Map :: map() | term(), Path :: path(), Default :: default()) -> term().
get_nested(Map, [], _Default) when is_map(Map) ->
Map;
get_nested(_NotMap, [], Default) ->
Default;
get_nested(Map, [Key | Rest], Default) when is_map(Map) ->
case maps:find(Key, Map) of
{ok, Value} ->
get_nested(Value, Rest, Default);
error ->
Default
end;
get_nested(_NotMap, _Path, Default) ->
Default.
-spec ensure_map(term()) -> map().
ensure_map(Map) when is_map(Map) ->
Map;
ensure_map(_NotMap) ->
#{}.
-spec ensure_list(term()) -> list().
ensure_list(List) when is_list(List) ->
List;
ensure_list(_NotList) ->
[].
-spec get_integer(map() | term(), key(), term()) -> integer() | term().
get_integer(Map, Key, Default) when is_map(Map) ->
case type_conv:to_integer(maps:get(Key, Map, undefined)) of
undefined -> Default;
Value -> Value
end;
get_integer(_NotMap, _Key, Default) ->
Default.
-spec get_binary(map() | term(), key(), term()) -> binary() | term().
get_binary(Map, Key, Default) when is_map(Map) ->
case type_conv:to_binary(maps:get(Key, Map, undefined)) of
undefined -> Default;
Value -> Value
end;
get_binary(_NotMap, _Key, Default) ->
Default.
-spec filter_by_field(List :: list(), Field :: key(), Value :: term()) -> list(map()).
filter_by_field(List, Field, Value) when is_list(List) ->
lists:filter(
fun
(Item) when is_map(Item) ->
case maps:find(Field, Item) of
{ok, Value} -> true;
_ -> false
end;
(_NotMap) ->
false
end,
List
);
filter_by_field(_NotList, _Field, _Value) ->
[].
-spec find_by_field(List :: list(), Field :: key(), Value :: term()) -> {ok, map()} | error.
find_by_field(List, Field, Value) when is_list(List) ->
find_by_field_loop(List, Field, Value);
find_by_field(_NotList, _Field, _Value) ->
error.
-spec find_by_field_loop(list(), key(), term()) -> {ok, map()} | error.
find_by_field_loop([], _Field, _Value) ->
error;
find_by_field_loop([Item | Rest], Field, Value) when is_map(Item) ->
case maps:find(Field, Item) of
{ok, Value} ->
{ok, Item};
_ ->
find_by_field_loop(Rest, Field, Value)
end;
find_by_field_loop([_NotMap | Rest], Field, Value) ->
find_by_field_loop(Rest, Field, Value).
-ifdef(TEST).
get_safe_basic_test() ->
Map = #{key => value, number => 42},
?assertEqual(value, get_safe(Map, key, default)),
?assertEqual(42, get_safe(Map, number, 0)),
?assertEqual(default, get_safe(Map, missing, default)),
?assertEqual(0, get_safe(Map, missing, 0)).
get_safe_various_input_types_test() ->
?assertEqual(default, get_safe(not_a_map, key, default)),
?assertEqual(default, get_safe([], key, default)),
?assertEqual(default, get_safe(123, key, default)),
?assertEqual(default, get_safe(<<"binary">>, key, default)),
?assertEqual(default, get_safe(undefined, key, default)),
?assertEqual(default, get_safe(atom, key, default)),
?assertEqual(default, get_safe({tuple, value}, key, default)),
?assertEqual(default, get_safe(self(), key, default)).
get_safe_various_key_types_test() ->
Map = #{
atom_key => atom_value,
<<"binary_key">> => binary_value,
123 => number_key_value,
{tuple, key} => tuple_key_value
},
?assertEqual(atom_value, get_safe(Map, atom_key, default)),
?assertEqual(binary_value, get_safe(Map, <<"binary_key">>, default)),
?assertEqual(number_key_value, get_safe(Map, 123, default)),
?assertEqual(tuple_key_value, get_safe(Map, {tuple, key}, default)),
?assertEqual(default, get_safe(Map, missing_atom, default)),
?assertEqual(default, get_safe(Map, <<"missing_binary">>, default)),
?assertEqual(default, get_safe(Map, 999, default)).
get_safe_default_types_test() ->
Map = #{key => value},
?assertEqual(nil, get_safe(Map, missing, nil)),
?assertEqual(0, get_safe(Map, missing, 0)),
?assertEqual(<<"default">>, get_safe(Map, missing, <<"default">>)),
?assertEqual([], get_safe(Map, missing, [])),
?assertEqual(#{}, get_safe(Map, missing, #{})),
?assertEqual({tuple, default}, get_safe(Map, missing, {tuple, default})).
get_nested_basic_test() ->
Map = #{
level1 => #{
level2 => #{
level3 => deep_value
},
other => other_value
},
simple => simple_value
},
?assertEqual(#{level3 => deep_value}, get_nested(Map, [level1, level2], default)),
?assertEqual(
#{other => other_value, level2 => #{level3 => deep_value}},
get_nested(Map, [level1], default)
),
?assertEqual(default, get_nested(Map, [level1, level2, level3], default)),
?assertEqual(default, get_nested(Map, [level1, other], default)),
?assertEqual(default, get_nested(Map, [simple], default)),
?assertEqual(Map, get_nested(Map, [], default)),
?assertEqual(default, get_nested(Map, [level1, missing], default)),
?assertEqual(default, get_nested(Map, [missing, level2], default)),
?assertEqual(default, get_nested(Map, [level1, level2, missing], default)).
get_nested_deep_nesting_test() ->
DeepMap = #{
l1 => #{
l2 => #{
l3 => #{
l4 => #{
l5 => final_value,
other5 => value5
},
other4 => value4
},
other3 => value3
}
}
},
Level4Map = get_nested(DeepMap, [l1, l2, l3, l4], default),
?assert(is_map(Level4Map)),
?assertEqual(final_value, maps:get(l5, Level4Map)),
?assertEqual(value5, maps:get(other5, Level4Map)),
Level3Map = get_nested(DeepMap, [l1, l2, l3], default),
?assert(is_map(Level3Map)),
?assertEqual(value4, maps:get(other4, Level3Map)),
Level2Map = get_nested(DeepMap, [l1, l2], default),
?assert(is_map(Level2Map)),
?assertEqual(value3, maps:get(other3, Level2Map)),
?assertEqual(default, get_nested(DeepMap, [l1, l2, l3, l4, l5], default)),
?assertEqual(default, get_nested(DeepMap, [l1, l2, l3, l4, other5], default)),
?assertEqual(default, get_nested(DeepMap, [l1, l2, l3, other4], default)),
?assertEqual(default, get_nested(DeepMap, [l1, l2, other3], default)),
?assertEqual(default, get_nested(DeepMap, [l1, l2, l3, l4, l5, extra], default)),
?assertEqual(default, get_nested(DeepMap, [l1, l2, missing, l4, l5], default)),
?assertEqual(default, get_nested(DeepMap, [missing, l2, l3, l4, l5], default)).
get_nested_partial_paths_test() ->
Map = #{
user => #{
name => <<"Alice">>,
age => 30,
address => #{
city => <<"New York">>,
zip => 10001
}
},
count => 42,
tags => [tag1, tag2, tag3]
},
UserMap = get_nested(Map, [user], default),
?assert(is_map(UserMap)),
?assertEqual(<<"Alice">>, maps:get(name, UserMap)),
AddressMap = get_nested(Map, [user, address], default),
?assert(is_map(AddressMap)),
?assertEqual(<<"New York">>, maps:get(city, AddressMap)),
?assertEqual(default, get_nested(Map, [count], default)),
?assertEqual(default, get_nested(Map, [user, name], default)),
?assertEqual(default, get_nested(Map, [user, age], default)),
?assertEqual(default, get_nested(Map, [tags], default)),
?assertEqual(default, get_nested(Map, [count, extra], default)),
?assertEqual(default, get_nested(Map, [count, deep, path], default)),
?assertEqual(default, get_nested(Map, [user, name, extra], default)),
?assertEqual(default, get_nested(Map, [tags, extra], default)),
?assertEqual(default, get_nested(Map, [user, age, extra, path], default)).
get_nested_edge_cases_test() ->
Map = #{key => #{nested => value}},
?assertEqual(default, get_nested(not_a_map, [], default)),
?assertEqual(default, get_nested([], [], default)),
?assertEqual(default, get_nested(123, [], default)),
?assertEqual(default, get_nested(not_a_map, [key], default)),
?assertEqual(default, get_nested([], [key], default)),
?assertEqual(default, get_nested(123, [key, nested], default)),
?assertEqual(default, get_safe(undefined, key, default)),
?assertEqual(#{nested => value}, get_nested(Map, [key], default)),
?assertEqual(default, get_nested(Map, [key, nested], default)),
BinaryMap = #{<<"key">> => #{<<"nested">> => <<"value">>}},
?assertEqual(
#{<<"nested">> => <<"value">>},
get_nested(BinaryMap, [<<"key">>], default)
),
?assertEqual(default, get_nested(BinaryMap, [<<"key">>, <<"nested">>], default)).
get_integer_basic_test() ->
Map = #{id => <<"42">>, <<"count">> => 10},
?assertEqual(42, get_integer(Map, id, 0)),
?assertEqual(10, get_integer(Map, <<"count">>, 0)),
?assertEqual(99, get_integer(Map, missing, 99)).
get_integer_invalid_input_test() ->
?assertEqual(7, get_integer(undefined, id, 7)),
?assertEqual(undefined, get_integer(#{}, id, undefined)),
?assertEqual(0, get_integer(#{id => <<"abc">>}, id, 0)).
get_binary_basic_test() ->
Map = #{<<"name">> => <<"fluxer">>, tag => atom},
?assertEqual(<<"fluxer">>, get_binary(Map, <<"name">>, <<"default">>)),
?assertEqual(<<"atom">>, get_binary(Map, tag, <<"default">>)).
get_binary_invalid_input_test() ->
?assertEqual(<<"default">>, get_binary(not_a_map, <<"id">>, <<"default">>)),
?assertEqual(undefined, get_binary(#{}, <<"missing">>, undefined)),
?assertEqual(<<"default">>, get_binary(#{num => 123}, <<"num">>, <<"default">>)).
ensure_map_test() ->
Map = #{key => value, nested => #{inner => data}},
?assertEqual(Map, ensure_map(Map)),
?assertEqual(#{}, ensure_map(#{})).
ensure_map_all_input_types_test() ->
?assertEqual(#{}, ensure_map(not_a_map)),
?assertEqual(#{}, ensure_map([])),
?assertEqual(#{}, ensure_map([1, 2, 3])),
?assertEqual(#{}, ensure_map(123)),
?assertEqual(#{}, ensure_map(123.456)),
?assertEqual(#{}, ensure_map(<<"binary">>)),
?assertEqual(#{}, ensure_map("string")),
?assertEqual(#{}, ensure_map(undefined)),
?assertEqual(#{}, ensure_map(atom)),
?assertEqual(#{}, ensure_map(true)),
?assertEqual(#{}, ensure_map(false)),
?assertEqual(#{}, ensure_map({tuple, value})),
?assertEqual(#{}, ensure_map(self())),
?assertEqual(#{}, ensure_map(make_ref())),
?assertEqual(#{}, ensure_map(fun() -> ok end)).
ensure_list_test() ->
List = [1, 2, 3],
?assertEqual(List, ensure_list(List)),
ComplexList = [#{a => 1}, {tuple}, <<"binary">>, atom],
?assertEqual(ComplexList, ensure_list(ComplexList)),
?assertEqual([], ensure_list([])).
ensure_list_all_input_types_test() ->
?assertEqual([], ensure_list(not_a_list)),
?assertEqual([], ensure_list(#{})),
?assertEqual([], ensure_list(#{key => value})),
?assertEqual([], ensure_list(123)),
?assertEqual([], ensure_list(123.456)),
?assertEqual([], ensure_list(<<"binary">>)),
?assertEqual("string", ensure_list("string")),
?assert(is_list(ensure_list("string"))),
?assertEqual([], ensure_list(undefined)),
?assertEqual([], ensure_list(atom)),
?assertEqual([], ensure_list(true)),
?assertEqual([], ensure_list(false)),
?assertEqual([], ensure_list({tuple, value})),
?assertEqual([], ensure_list(self())),
?assertEqual([], ensure_list(make_ref())),
?assertEqual([], ensure_list(fun() -> ok end)).
filter_by_field_basic_test() ->
List = [
#{id => 1, type => a, name => <<"first">>},
#{id => 2, type => b, name => <<"second">>},
#{id => 3, type => a, name => <<"third">>},
#{id => 4, type => c},
#{id => 5, type => a}
],
Filtered = filter_by_field(List, type, a),
?assertEqual(3, length(Filtered)),
?assert(lists:all(fun(M) -> maps:get(type, M) =:= a end, Filtered)),
?assertEqual(
[#{id => 2, type => b, name => <<"second">>}],
filter_by_field(List, id, 2)
),
?assertEqual([], filter_by_field(List, type, nonexistent)),
?assertEqual([], filter_by_field(List, missing_field, value)).
filter_by_field_mixed_lists_test() ->
MixedList = [
#{id => 1, type => a},
not_a_map,
#{id => 2, type => b},
123,
#{id => 3, type => a},
<<"binary">>,
undefined,
#{id => 4, type => a},
[],
{tuple, value},
#{id => 5, type => c}
],
Result = filter_by_field(MixedList, type, a),
?assertEqual(3, length(Result)),
?assert(lists:all(fun is_map/1, Result)),
?assert(lists:all(fun(M) -> maps:get(type, M) =:= a end, Result)),
Ids = [maps:get(id, M) || M <- Result],
?assertEqual([1, 3, 4], Ids),
ResultB = filter_by_field(MixedList, type, b),
?assertEqual(1, length(ResultB)),
?assertEqual([#{id => 2, type => b}], ResultB).
filter_by_field_edge_cases_test() ->
?assertEqual([], filter_by_field([], field, value)),
NonMaps = [123, atom, <<"binary">>, {tuple}, []],
?assertEqual([], filter_by_field(NonMaps, field, value)),
NoFieldList = [#{a => 1}, #{b => 2}, #{c => 3}],
?assertEqual([], filter_by_field(NoFieldList, missing, value)),
?assertEqual([], filter_by_field(not_a_list, field, value)),
?assertEqual([], filter_by_field(#{}, field, value)),
?assertEqual([], filter_by_field(123, field, value)),
BinaryList = [
#{<<"key">> => <<"value1">>},
#{<<"key">> => <<"value2">>},
#{<<"other">> => <<"value1">>}
],
?assertEqual(
[#{<<"key">> => <<"value1">>}],
filter_by_field(BinaryList, <<"key">>, <<"value1">>)
),
ComplexList = [
#{data => #{nested => value}},
#{data => [1, 2, 3]},
#{data => #{nested => value}},
#{other => data}
],
ComplexFiltered = filter_by_field(ComplexList, data, #{nested => value}),
?assertEqual(2, length(ComplexFiltered)).
find_by_field_basic_test() ->
List = [
#{id => 1, type => a},
#{id => 2, type => b},
#{id => 3, type => a},
#{id => 4, type => c}
],
?assertEqual({ok, #{id => 2, type => b}}, find_by_field(List, id, 2)),
?assertEqual({ok, #{id => 4, type => c}}, find_by_field(List, id, 4)),
?assertEqual(error, find_by_field(List, id, 999)),
?assertEqual(error, find_by_field(List, type, nonexistent)),
?assertEqual(error, find_by_field([], id, 1)).
find_by_field_multiple_matches_test() ->
List = [
#{id => 1, type => a, order => first},
#{id => 2, type => b, order => second},
#{id => 3, type => a, order => third},
#{id => 4, type => c, order => fourth},
#{id => 5, type => a, order => fifth}
],
{ok, First} = find_by_field(List, type, a),
?assertEqual(1, maps:get(id, First)),
?assertEqual(first, maps:get(order, First)),
?assertNotEqual(third, maps:get(order, First)),
?assertNotEqual(fifth, maps:get(order, First)),
List2 = [
#{name => <<"Alice">>, age => 25},
#{name => <<"Bob">>, age => 30},
#{name => <<"Charlie">>, age => 25},
#{name => <<"Diana">>, age => 25}
],
{ok, FirstAge25} = find_by_field(List2, age, 25),
?assertEqual(<<"Alice">>, maps:get(name, FirstAge25)).
find_by_field_no_matches_test() ->
List = [
#{id => 1, type => a},
#{id => 2, type => b},
#{id => 3, type => c}
],
?assertEqual(error, find_by_field(List, id, 999)),
?assertEqual(error, find_by_field(List, type, z)),
?assertEqual(error, find_by_field(List, missing_field, value)),
?assertEqual(error, find_by_field(List, id, <<"wrong_type">>)),
?assertEqual(error, find_by_field([], any_field, any_value)).
find_by_field_with_non_maps_test() ->
MixedList = [
not_a_map,
123,
#{id => 1, type => a},
<<"binary">>,
undefined,
#{id => 2, type => b},
[],
#{id => 3, type => a}
],
{ok, Found1} = find_by_field(MixedList, type, a),
?assertEqual(1, maps:get(id, Found1)),
{ok, Found2} = find_by_field(MixedList, id, 2),
?assertEqual(b, maps:get(type, Found2)),
MixedList2 = [atom, 456, {tuple}, #{id => 5, type => z}],
?assertEqual({ok, #{id => 5, type => z}}, find_by_field(MixedList2, id, 5)),
OnlyNonMaps = [atom, 123, <<"binary">>, {tuple}, []],
?assertEqual(error, find_by_field(OnlyNonMaps, field, value)).
find_by_field_invalid_input_test() ->
?assertEqual(error, find_by_field(not_a_list, field, value)),
?assertEqual(error, find_by_field(#{}, field, value)),
?assertEqual(error, find_by_field(123, field, value)),
?assertEqual(error, find_by_field(<<"binary">>, field, value)),
?assertEqual(error, find_by_field(undefined, field, value)),
?assertEqual(error, find_by_field(atom, field, value)),
?assertEqual(error, find_by_field({tuple}, field, value)).
find_by_field_complex_values_test() ->
List = [
#{<<"id">> => <<"first">>, <<"data">> => <<"value1">>},
#{<<"id">> => <<"second">>, <<"data">> => <<"value2">>},
#{<<"id">> => <<"third">>, <<"data">> => <<"value1">>}
],
{ok, Found} = find_by_field(List, <<"data">>, <<"value1">>),
?assertEqual(<<"first">>, maps:get(<<"id">>, Found)),
ComplexList = [
#{key => #{nested => value1}, id => 1},
#{key => [1, 2, 3], id => 2},
#{key => #{nested => value1}, id => 3}
],
{ok, ComplexFound} = find_by_field(ComplexList, key, #{nested => value1}),
?assertEqual(1, maps:get(id, ComplexFound)),
{ok, ListFound} = find_by_field(ComplexList, key, [1, 2, 3]),
?assertEqual(2, maps:get(id, ListFound)).
-endif.

View File

@@ -0,0 +1,636 @@
%% 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(type_conv).
-export([
to_integer/1,
to_binary/1,
to_list/1,
extract_id/2,
extract_id_required/2
]).
-type convertible_to_integer() :: integer() | binary() | list() | atom().
-type convertible_to_binary() :: binary() | integer() | list() | atom().
-type convertible_to_list() :: list() | binary() | atom().
-spec to_integer(convertible_to_integer() | undefined) -> integer() | undefined.
to_integer(undefined) ->
undefined;
to_integer(Value) when is_integer(Value) ->
Value;
to_integer(Value) when is_binary(Value) ->
try
binary_to_integer(Value)
catch
error:badarg ->
undefined
end;
to_integer(Value) when is_list(Value) ->
try
list_to_integer(Value)
catch
error:badarg ->
undefined
end;
to_integer(Value) when is_atom(Value) ->
try
list_to_integer(atom_to_list(Value))
catch
error:badarg ->
undefined
end;
to_integer(_) ->
undefined.
-spec to_binary(convertible_to_binary() | undefined) -> binary() | undefined.
to_binary(undefined) ->
undefined;
to_binary(Value) when is_binary(Value) ->
Value;
to_binary(Value) when is_integer(Value) ->
integer_to_binary(Value);
to_binary(Value) when is_list(Value) ->
try
list_to_binary(Value)
catch
error:badarg ->
undefined
end;
to_binary(Value) when is_atom(Value) ->
atom_to_binary(Value, utf8);
to_binary(_) ->
undefined.
-spec to_list(convertible_to_list() | undefined) -> list() | undefined.
to_list(undefined) ->
undefined;
to_list(Value) when is_list(Value) ->
Value;
to_list(Value) when is_binary(Value) ->
binary_to_list(Value);
to_list(Value) when is_atom(Value) ->
atom_to_list(Value);
to_list(_) ->
undefined.
-spec extract_id(map(), atom() | binary()) -> integer() | undefined.
extract_id(Map, Field) when is_map(Map), is_atom(Field) ->
case maps:get(Field, Map, undefined) of
undefined ->
undefined;
Value ->
to_integer(Value)
end;
extract_id(Map, Field) when is_map(Map), is_binary(Field) ->
case maps:get(Field, Map, undefined) of
undefined ->
undefined;
Value ->
to_integer(Value)
end;
extract_id(_, _) ->
undefined.
-spec extract_id_required(map(), atom() | binary()) -> integer().
extract_id_required(Map, Field) ->
case extract_id(Map, Field) of
undefined ->
0;
Value when is_integer(Value) ->
Value
end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
to_integer_with_integer_test() ->
?assertEqual(42, to_integer(42)),
?assertEqual(0, to_integer(0)),
?assertEqual(-100, to_integer(-100)).
to_integer_with_integer_edge_cases_test() ->
?assertEqual(1234567890123456789, to_integer(1234567890123456789)),
?assertEqual(9223372036854775807, to_integer(9223372036854775807)),
?assertEqual(-9223372036854775807, to_integer(-9223372036854775807)).
to_integer_with_binary_valid_test() ->
?assertEqual(123, to_integer(<<"123">>)),
?assertEqual(0, to_integer(<<"0">>)),
?assertEqual(-456, to_integer(<<"-456">>)).
to_integer_with_binary_edge_cases_test() ->
?assertEqual(1234567890123456789, to_integer(<<"1234567890123456789">>)),
?assertEqual(9223372036854775807, to_integer(<<"9223372036854775807">>)),
?assertEqual(-9223372036854775807, to_integer(<<"-9223372036854775807">>)),
?assertEqual(1, to_integer(<<"1">>)),
?assertEqual(123, to_integer(<<"00123">>)),
?assertEqual(0, to_integer(<<"0">>)).
to_integer_with_binary_invalid_test() ->
?assertEqual(undefined, to_integer(<<"not_a_number">>)),
?assertEqual(undefined, to_integer(<<"12.34">>)),
?assertEqual(undefined, to_integer(<<"">>)),
?assertEqual(undefined, to_integer(<<" ">>)),
?assertEqual(undefined, to_integer(<<"abc123">>)),
?assertEqual(undefined, to_integer(<<"123abc">>)),
?assertEqual(undefined, to_integer(<<"12 34">>)),
?assertEqual(undefined, to_integer(<<"--123">>)),
?assertEqual(undefined, to_integer(<<"+-123">>)).
to_integer_with_binary_special_chars_test() ->
?assertEqual(undefined, to_integer(<<"!@#$%">>)),
?assertEqual(undefined, to_integer(<<"">>)),
?assertEqual(undefined, to_integer(<<"①②③">>)),
?assertEqual(undefined, to_integer(<<"一二三">>)),
?assertEqual(undefined, to_integer(<<"null">>)),
?assertEqual(undefined, to_integer(<<"NaN">>)),
?assertEqual(undefined, to_integer(<<"Infinity">>)).
to_integer_with_list_valid_test() ->
?assertEqual(789, to_integer("789")),
?assertEqual(-123, to_integer("-123")),
?assertEqual(0, to_integer("0")).
to_integer_with_list_edge_cases_test() ->
?assertEqual(1234567890123456789, to_integer("1234567890123456789")),
?assertEqual(5, to_integer("5")),
?assertEqual(42, to_integer("00042")),
?assertEqual(-42, to_integer("-00042")).
to_integer_with_list_invalid_test() ->
?assertEqual(undefined, to_integer("invalid")),
?assertEqual(undefined, to_integer("12.34")),
?assertEqual(undefined, to_integer("")),
?assertEqual(undefined, to_integer(" ")),
?assertEqual(undefined, to_integer("abc")),
?assertEqual(undefined, to_integer("123abc")),
?assertEqual(undefined, to_integer("12 34")),
?assertEqual(undefined, to_integer([1, 2, 3])).
to_integer_with_list_special_chars_test() ->
?assertEqual(undefined, to_integer("!@#$%")),
?assertEqual(undefined, to_integer("hello world")),
?assertEqual(undefined, to_integer("--456")),
?assertEqual(undefined, to_integer("null")).
to_integer_with_atom_valid_test() ->
?assertEqual(123, to_integer('123')),
?assertEqual(-456, to_integer('-456')),
?assertEqual(0, to_integer('0')).
to_integer_with_atom_invalid_test() ->
?assertEqual(undefined, to_integer(test)),
?assertEqual(undefined, to_integer('not_a_number')),
?assertEqual(undefined, to_integer(hello)),
?assertEqual(undefined, to_integer(true)),
?assertEqual(undefined, to_integer(false)),
?assertEqual(undefined, to_integer(nil)),
?assertEqual(undefined, to_integer('')).
to_integer_with_undefined_test() ->
?assertEqual(undefined, to_integer(undefined)).
to_integer_with_invalid_types_test() ->
?assertEqual(undefined, to_integer(12.34)),
?assertEqual(undefined, to_integer(-45.67)),
?assertEqual(undefined, to_integer(0.0)),
?assertEqual(undefined, to_integer(#{key => value})),
?assertEqual(undefined, to_integer(#{})),
?assertEqual(undefined, to_integer({1, 2, 3})),
?assertEqual(undefined, to_integer({})),
Ref = make_ref(),
?assertEqual(undefined, to_integer(Ref)),
?assertEqual(undefined, to_integer(self())),
?assertEqual(undefined, to_integer(erlang:list_to_port("#Port<0.0>"))).
to_binary_with_binary_test() ->
?assertEqual(<<"test">>, to_binary(<<"test">>)),
?assertEqual(<<>>, to_binary(<<>>)).
to_binary_with_binary_edge_cases_test() ->
?assertEqual(<<"hello world">>, to_binary(<<"hello world">>)),
?assertEqual(<<"!@#$%^&*()">>, to_binary(<<"!@#$%^&*()">>)),
?assertEqual(<<"line1\nline2">>, to_binary(<<"line1\nline2">>)),
?assertEqual(<<"tab\there">>, to_binary(<<"tab\there">>)),
LongBinary = binary:copy(<<"x">>, 10000),
?assertEqual(LongBinary, to_binary(LongBinary)).
to_binary_with_binary_unicode_test() ->
?assertEqual(<<"Hello 世界"/utf8>>, to_binary(<<"Hello 世界"/utf8>>)),
?assertEqual(<<"Здравствуй мир"/utf8>>, to_binary(<<"Здравствуй мир"/utf8>>)),
?assertEqual(<<"مرحبا بالعالم"/utf8>>, to_binary(<<"مرحبا بالعالم"/utf8>>)),
?assertEqual(<<"🚀🌟💻"/utf8>>, to_binary(<<"🚀🌟💻"/utf8>>)),
?assertEqual(<<"Ñoño"/utf8>>, to_binary(<<"Ñoño"/utf8>>)),
?assertEqual(<<"Café"/utf8>>, to_binary(<<"Café"/utf8>>)).
to_binary_with_integer_test() ->
?assertEqual(<<"42">>, to_binary(42)),
?assertEqual(<<"0">>, to_binary(0)),
?assertEqual(<<"-100">>, to_binary(-100)).
to_binary_with_integer_edge_cases_test() ->
?assertEqual(<<"1234567890123456789">>, to_binary(1234567890123456789)),
?assertEqual(<<"9223372036854775807">>, to_binary(9223372036854775807)),
?assertEqual(<<"-9223372036854775807">>, to_binary(-9223372036854775807)),
?assertEqual(<<"1">>, to_binary(1)),
?assertEqual(<<"-1">>, to_binary(-1)).
to_binary_with_list_valid_test() ->
?assertEqual(<<"hello">>, to_binary("hello")),
?assertEqual(<<>>, to_binary("")).
to_binary_with_list_edge_cases_test() ->
?assertEqual(<<"hello world">>, to_binary("hello world")),
?assertEqual(<<"!@#$%">>, to_binary("!@#$%")),
?assertEqual(<<"line1\nline2">>, to_binary("line1\nline2")),
LongString = lists:duplicate(10000, $x),
LongBinary = binary:copy(<<"x">>, 10000),
?assertEqual(LongBinary, to_binary(LongString)).
to_binary_with_list_unicode_test() ->
?assertEqual(undefined, to_binary([72, 101, 108, 108, 111, 32, 19990, 30028])),
?assertEqual(undefined, to_binary([128640, 127775, 128187])),
?assertEqual(<<67, 97, 102, 233>>, to_binary([67, 97, 102, 233])),
?assertEqual(<<"Hello">>, to_binary([72, 101, 108, 108, 111])),
?assertEqual(<<0, 1, 127, 255>>, to_binary([0, 1, 127, 255])).
to_binary_with_list_invalid_test() ->
?assertEqual(<<1, 2, 3>>, to_binary([1, 2, 3])),
?assertEqual(undefined, to_binary([256])),
?assertEqual(undefined, to_binary([1000])),
?assertEqual(undefined, to_binary([-1])),
?assertEqual(undefined, to_binary([hello, world])),
?assertEqual(undefined, to_binary([1, 2, atom])).
to_binary_with_atom_test() ->
?assertEqual(<<"test">>, to_binary(test)),
?assertEqual(<<"hello_world">>, to_binary(hello_world)),
?assertEqual(<<"true">>, to_binary(true)),
?assertEqual(<<"false">>, to_binary(false)),
?assertEqual(<<"">>, to_binary('')).
to_binary_with_atom_edge_cases_test() ->
?assertEqual(<<"Hello World">>, to_binary('Hello World')),
?assertEqual(<<"123">>, to_binary('123')),
?assertEqual(<<"hello-world">>, to_binary('hello-world')),
?assertEqual(<<"test@example">>, to_binary('test@example')),
?assertEqual(undefined, to_binary(undefined)).
to_binary_with_invalid_types_test() ->
?assertEqual(undefined, to_binary(12.34)),
?assertEqual(undefined, to_binary(-45.67)),
?assertEqual(undefined, to_binary(0.0)),
?assertEqual(undefined, to_binary(#{key => value})),
?assertEqual(undefined, to_binary(#{})),
?assertEqual(undefined, to_binary({1, 2, 3})),
?assertEqual(undefined, to_binary({})),
Ref = make_ref(),
?assertEqual(undefined, to_binary(Ref)),
?assertEqual(undefined, to_binary(self())),
?assertEqual(undefined, to_binary(erlang:list_to_port("#Port<0.0>"))).
to_list_with_list_test() ->
?assertEqual("test", to_list("test")),
?assertEqual([], to_list([])),
?assertEqual([1, 2, 3], to_list([1, 2, 3])).
to_list_with_list_edge_cases_test() ->
?assertEqual("hello world", to_list("hello world")),
?assertEqual("!@#$%^&*()", to_list("!@#$%^&*()")),
?assertEqual([true, false, nil], to_list([true, false, nil])),
?assertEqual([[1, 2], [3, 4]], to_list([[1, 2], [3, 4]])),
LongList = lists:duplicate(10000, $x),
?assertEqual(LongList, to_list(LongList)).
to_list_with_list_unicode_test() ->
?assertEqual(
[72, 101, 108, 108, 111, 32, 19990, 30028],
to_list([72, 101, 108, 108, 111, 32, 19990, 30028])
),
?assertEqual([67, 97, 102, 233], to_list([67, 97, 102, 233])),
?assertEqual([128640, 127775, 128187], to_list([128640, 127775, 128187])).
to_list_with_binary_test() ->
?assertEqual("hello", to_list(<<"hello">>)),
?assertEqual("", to_list(<<>>)).
to_list_with_binary_edge_cases_test() ->
?assertEqual("hello world", to_list(<<"hello world">>)),
?assertEqual("!@#$%", to_list(<<"!@#$%">>)),
?assertEqual("line1\nline2", to_list(<<"line1\nline2">>)),
?assertEqual("tab\there", to_list(<<"tab\there">>)),
LongBinary = binary:copy(<<"x">>, 10000),
LongList = lists:duplicate(10000, $x),
?assertEqual(LongList, to_list(LongBinary)).
to_list_with_binary_unicode_test() ->
?assertEqual(
[72, 101, 108, 108, 111, 32, 228, 184, 150, 231, 149, 140], to_list(<<"Hello 世界"/utf8>>)
),
?assertEqual([67, 97, 102, 195, 169], to_list(<<"Café"/utf8>>)),
?assertEqual(<<"Hello 世界"/utf8>>, list_to_binary(to_list(<<"Hello 世界"/utf8>>))).
to_list_with_atom_test() ->
?assertEqual("test", to_list(test)),
?assertEqual("hello_world", to_list(hello_world)),
?assertEqual("true", to_list(true)),
?assertEqual("false", to_list(false)),
?assertEqual("", to_list('')).
to_list_with_atom_edge_cases_test() ->
?assertEqual([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100], to_list('Hello World')),
?assertEqual([49, 50, 51], to_list('123')),
?assertEqual([104, 101, 108, 108, 111, 45, 119, 111, 114, 108, 100], to_list('hello-world')),
?assertEqual(undefined, to_list(undefined)).
to_list_with_invalid_types_test() ->
?assertEqual(undefined, to_list(42)),
?assertEqual(undefined, to_list(-123)),
?assertEqual(undefined, to_list(0)),
?assertEqual(undefined, to_list(12.34)),
?assertEqual(undefined, to_list(-45.67)),
?assertEqual(undefined, to_list(0.0)),
?assertEqual(undefined, to_list(#{key => value})),
?assertEqual(undefined, to_list(#{})),
?assertEqual(undefined, to_list({1, 2, 3})),
?assertEqual(undefined, to_list({})),
Ref = make_ref(),
?assertEqual(undefined, to_list(Ref)),
?assertEqual(undefined, to_list(self())),
?assertEqual(undefined, to_list(erlang:list_to_port("#Port<0.0>"))).
extract_id_with_atom_key_integer_test() ->
Map1 = #{user_id => 123},
?assertEqual(123, extract_id(Map1, user_id)),
Map2 = #{user_id => 0},
?assertEqual(0, extract_id(Map2, user_id)),
Map3 = #{user_id => -456},
?assertEqual(-456, extract_id(Map3, user_id)).
extract_id_with_atom_key_binary_test() ->
Map1 = #{user_id => <<"456">>},
?assertEqual(456, extract_id(Map1, user_id)),
Map2 = #{user_id => <<"0">>},
?assertEqual(0, extract_id(Map2, user_id)),
Map3 = #{user_id => <<"-789">>},
?assertEqual(-789, extract_id(Map3, user_id)).
extract_id_with_atom_key_list_test() ->
Map1 = #{user_id => "789"},
?assertEqual(789, extract_id(Map1, user_id)),
Map2 = #{user_id => "0"},
?assertEqual(0, extract_id(Map2, user_id)),
Map3 = #{user_id => "-123"},
?assertEqual(-123, extract_id(Map3, user_id)).
extract_id_with_atom_key_edge_cases_test() ->
Map1 = #{user_id => 1234567890123456789},
?assertEqual(1234567890123456789, extract_id(Map1, user_id)),
Map2 = #{user_id => <<"9223372036854775807">>},
?assertEqual(9223372036854775807, extract_id(Map2, user_id)),
Map3 = #{user_id => <<"00123">>},
?assertEqual(123, extract_id(Map3, user_id)).
extract_id_with_atom_key_missing_test() ->
Map1 = #{other_field => 999},
?assertEqual(undefined, extract_id(Map1, user_id)),
Map2 = #{},
?assertEqual(undefined, extract_id(Map2, user_id)).
extract_id_with_atom_key_undefined_value_test() ->
Map1 = #{user_id => undefined},
?assertEqual(undefined, extract_id(Map1, user_id)).
extract_id_with_atom_key_invalid_value_test() ->
Map1 = #{user_id => "invalid"},
?assertEqual(undefined, extract_id(Map1, user_id)),
Map2 = #{user_id => <<"not_a_number">>},
?assertEqual(undefined, extract_id(Map2, user_id)),
Map3 = #{user_id => "12.34"},
?assertEqual(undefined, extract_id(Map3, user_id)),
Map4 = #{user_id => #{nested => map}},
?assertEqual(undefined, extract_id(Map4, user_id)),
Map5 = #{user_id => [1, 2, 3]},
?assertEqual(undefined, extract_id(Map5, user_id)),
Map6 = #{user_id => 12.34},
?assertEqual(undefined, extract_id(Map6, user_id)).
extract_id_with_binary_key_integer_test() ->
Map1 = #{<<"user_id">> => 123},
?assertEqual(123, extract_id(Map1, <<"user_id">>)),
Map2 = #{<<"user_id">> => 0},
?assertEqual(0, extract_id(Map2, <<"user_id">>)),
Map3 = #{<<"user_id">> => -789},
?assertEqual(-789, extract_id(Map3, <<"user_id">>)).
extract_id_with_binary_key_binary_test() ->
Map1 = #{<<"user_id">> => <<"456">>},
?assertEqual(456, extract_id(Map1, <<"user_id">>)),
Map2 = #{<<"user_id">> => <<"0">>},
?assertEqual(0, extract_id(Map2, <<"user_id">>)),
Map3 = #{<<"user_id">> => <<"-123">>},
?assertEqual(-123, extract_id(Map3, <<"user_id">>)).
extract_id_with_binary_key_list_test() ->
Map1 = #{<<"user_id">> => "789"},
?assertEqual(789, extract_id(Map1, <<"user_id">>)),
Map2 = #{<<"user_id">> => "0"},
?assertEqual(0, extract_id(Map2, <<"user_id">>)).
extract_id_with_binary_key_edge_cases_test() ->
Map1 = #{<<"user_id">> => 1234567890123456789},
?assertEqual(1234567890123456789, extract_id(Map1, <<"user_id">>)),
Map2 = #{<<>> => 123},
?assertEqual(123, extract_id(Map2, <<>>)),
Map3 = #{<<"user:id">> => 456},
?assertEqual(456, extract_id(Map3, <<"user:id">>)).
extract_id_with_binary_key_missing_test() ->
Map1 = #{<<"other_field">> => 999},
?assertEqual(undefined, extract_id(Map1, <<"user_id">>)),
Map2 = #{},
?assertEqual(undefined, extract_id(Map2, <<"user_id">>)).
extract_id_with_binary_key_undefined_value_test() ->
Map1 = #{<<"user_id">> => undefined},
?assertEqual(undefined, extract_id(Map1, <<"user_id">>)).
extract_id_with_binary_key_invalid_value_test() ->
Map1 = #{<<"user_id">> => "invalid"},
?assertEqual(undefined, extract_id(Map1, <<"user_id">>)),
Map2 = #{<<"user_id">> => <<"not_a_number">>},
?assertEqual(undefined, extract_id(Map2, <<"user_id">>)),
Map3 = #{<<"user_id">> => 12.34},
?assertEqual(undefined, extract_id(Map3, <<"user_id">>)).
extract_id_with_invalid_map_test() ->
?assertEqual(undefined, extract_id(not_a_map, user_id)),
?assertEqual(undefined, extract_id(123, user_id)),
?assertEqual(undefined, extract_id("string", user_id)),
?assertEqual(undefined, extract_id(<<"binary">>, user_id)),
?assertEqual(undefined, extract_id([1, 2, 3], user_id)),
?assertEqual(undefined, extract_id({tuple}, user_id)),
?assertEqual(undefined, extract_id(undefined, user_id)).
extract_id_with_invalid_key_type_test() ->
Map = #{user_id => 123},
?assertEqual(undefined, extract_id(Map, 123)),
?assertEqual(undefined, extract_id(Map, "user_id")),
?assertEqual(undefined, extract_id(Map, {user_id})),
?assertEqual(undefined, extract_id(Map, [user_id])),
?assertEqual(undefined, extract_id(Map, 12.34)).
extract_id_with_both_invalid_test() ->
?assertEqual(undefined, extract_id(not_a_map, 123)),
?assertEqual(undefined, extract_id(undefined, undefined)),
?assertEqual(undefined, extract_id(123, "key")).
extract_id_required_with_valid_integer_test() ->
Map1 = #{user_id => 123},
?assertEqual(123, extract_id_required(Map1, user_id)),
Map2 = #{user_id => 0},
?assertEqual(0, extract_id_required(Map2, user_id)),
Map3 = #{user_id => -456},
?assertEqual(-456, extract_id_required(Map3, user_id)).
extract_id_required_with_valid_binary_test() ->
Map1 = #{user_id => <<"456">>},
?assertEqual(456, extract_id_required(Map1, user_id)),
Map2 = #{<<"user_id">> => <<"789">>},
?assertEqual(789, extract_id_required(Map2, <<"user_id">>)).
extract_id_required_with_valid_list_test() ->
Map1 = #{user_id => "123"},
?assertEqual(123, extract_id_required(Map1, user_id)),
Map2 = #{user_id => "-456"},
?assertEqual(-456, extract_id_required(Map2, user_id)).
extract_id_required_with_edge_cases_test() ->
Map1 = #{user_id => 1234567890123456789},
?assertEqual(1234567890123456789, extract_id_required(Map1, user_id)),
Map2 = #{<<"user_id">> => <<"9223372036854775807">>},
?assertEqual(9223372036854775807, extract_id_required(Map2, <<"user_id">>)).
extract_id_required_with_missing_field_test() ->
Map1 = #{other_field => 999},
?assertEqual(0, extract_id_required(Map1, user_id)),
Map2 = #{},
?assertEqual(0, extract_id_required(Map2, user_id)),
Map3 = #{<<"other_field">> => 999},
?assertEqual(0, extract_id_required(Map3, <<"user_id">>)).
extract_id_required_with_undefined_value_test() ->
Map1 = #{user_id => undefined},
?assertEqual(0, extract_id_required(Map1, user_id)),
Map2 = #{<<"user_id">> => undefined},
?assertEqual(0, extract_id_required(Map2, <<"user_id">>)).
extract_id_required_with_invalid_value_test() ->
Map1 = #{user_id => "invalid"},
?assertEqual(0, extract_id_required(Map1, user_id)),
Map2 = #{user_id => <<"not_a_number">>},
?assertEqual(0, extract_id_required(Map2, user_id)),
Map3 = #{user_id => "12.34"},
?assertEqual(0, extract_id_required(Map3, user_id)),
Map4 = #{user_id => #{nested => map}},
?assertEqual(0, extract_id_required(Map4, user_id)),
Map5 = #{user_id => [1, 2, 3]},
?assertEqual(0, extract_id_required(Map5, user_id)),
Map6 = #{user_id => 12.34},
?assertEqual(0, extract_id_required(Map6, user_id)),
Map7 = #{user_id => test_atom},
?assertEqual(0, extract_id_required(Map7, user_id)).
extract_id_required_with_invalid_map_test() ->
?assertEqual(0, extract_id_required(not_a_map, user_id)),
?assertEqual(0, extract_id_required(123, user_id)),
?assertEqual(0, extract_id_required("string", user_id)),
?assertEqual(0, extract_id_required(<<"binary">>, user_id)),
?assertEqual(0, extract_id_required([1, 2, 3], user_id)),
?assertEqual(0, extract_id_required({tuple}, user_id)),
?assertEqual(0, extract_id_required(undefined, user_id)).
extract_id_required_with_invalid_key_test() ->
Map = #{user_id => 123},
?assertEqual(0, extract_id_required(Map, 123)),
?assertEqual(0, extract_id_required(Map, "user_id")),
?assertEqual(0, extract_id_required(Map, {user_id})),
?assertEqual(0, extract_id_required(Map, [user_id])).
extract_id_required_with_both_invalid_test() ->
?assertEqual(0, extract_id_required(not_a_map, 123)),
?assertEqual(0, extract_id_required(undefined, undefined)),
?assertEqual(0, extract_id_required(123, "key")).
extract_id_required_returns_integer_test() ->
Map1 = #{user_id => 123},
Result1 = extract_id_required(Map1, user_id),
?assert(is_integer(Result1)),
Map2 = #{other => value},
Result2 = extract_id_required(Map2, user_id),
?assert(is_integer(Result2)),
?assertEqual(0, Result2),
Result3 = extract_id_required(not_a_map, user_id),
?assert(is_integer(Result3)),
?assertEqual(0, Result3).
-endif.

View File

@@ -0,0 +1,53 @@
%% 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(user_utils).
-export([normalize_user/1]).
normalize_user(User) when is_map(User) ->
AllowedKeys = [
<<"id">>,
<<"username">>,
<<"discriminator">>,
<<"global_name">>,
<<"avatar">>,
<<"avatar_color">>,
<<"bot">>,
<<"system">>,
<<"flags">>,
<<"banner">>,
<<"banner_color">>
],
CleanPairs =
lists:foldl(
fun(Key, Acc) ->
Value = maps:get(Key, User, undefined),
case is_undefined(Value) of
true -> Acc;
false -> [{Key, Value} | Acc]
end
end,
[],
AllowedKeys
),
maps:from_list(lists:reverse(CleanPairs));
normalize_user(_) ->
#{}.
is_undefined(undefined) -> true;
is_undefined(_) -> false.

View File

@@ -0,0 +1,144 @@
%% 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(utils).
-import(type_conv, [to_integer/1]).
-export([
binary_to_integer_safe/1,
generate_session_id/0,
generate_resume_token/0,
hash_token/1,
parse_status/1,
safe_json_decode/1,
check_user_data_differs/2,
partial_user_fields/0,
parse_iso8601_to_unix_ms/1
]).
binary_to_integer_safe(Bin) when is_binary(Bin) ->
try
binary_to_integer(Bin)
catch
_:_ ->
try
list_to_integer(binary_to_list(Bin))
catch
_:_ -> undefined
end
end;
binary_to_integer_safe(Int) when is_integer(Int) -> Int;
binary_to_integer_safe(_) ->
undefined.
generate_session_id() ->
Bytes = crypto:strong_rand_bytes(constants:random_session_bytes()),
binary:encode_hex(Bytes).
generate_resume_token() ->
Bytes = crypto:strong_rand_bytes(32),
base64url:encode(Bytes).
hash_token(Token) ->
crypto:hash(sha256, Token).
parse_status(Status) when is_binary(Status) ->
constants:status_type_atom(Status);
parse_status(Status) when is_atom(Status) ->
Status;
parse_status(_) ->
online.
safe_json_decode(Bin) ->
try
jsx:decode(Bin, [{return_maps, true}])
catch
_:_ -> #{}
end.
parse_iso8601_to_unix_ms(Binary) when is_binary(Binary) ->
Pattern =
<<"^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,9}))?Z$">>,
case re:run(Binary, Pattern, [{capture, [1, 2, 3, 4, 5, 6, 7], list}]) of
{match, [YearBin, MonthBin, DayBin, HourBin, MinuteBin, SecondBin, FractionBin]} ->
Year = to_integer(YearBin),
Month = to_integer(MonthBin),
Day = to_integer(DayBin),
Hour = to_integer(HourBin),
Minute = to_integer(MinuteBin),
Second = to_integer(SecondBin),
FractionMs = fractional_ms(FractionBin),
case {Year, Month, Day, Hour, Minute, Second} of
{Y, M, D, H, Min, S} when
is_integer(Y) and is_integer(M) and is_integer(D) and is_integer(H) and
is_integer(Min) and is_integer(S)
->
Seconds = calendar:datetime_to_gregorian_seconds({{Y, M, D}, {H, Min, S}}),
Seconds * 1000 + FractionMs;
_ ->
undefined
end;
_ ->
undefined
end;
parse_iso8601_to_unix_ms(_) ->
undefined.
fractional_ms(Fraction) when is_list(Fraction) ->
Normalized =
case length(Fraction) of
Len when Len >= 3 -> lists:sublist(Fraction, 3);
Len when Len > 0 -> Fraction ++ lists:duplicate(3 - Len, $0);
_ -> "000"
end,
case Normalized of
[] ->
0;
_ ->
case catch list_to_integer(Normalized) of
{'EXIT', _} -> 0;
Value -> Value
end
end;
fractional_ms(_) ->
0.
partial_user_fields() ->
[
<<"id">>,
<<"username">>,
<<"discriminator">>,
<<"global_name">>,
<<"avatar">>,
<<"avatar_color">>,
<<"bot">>,
<<"system">>,
<<"flags">>,
<<"banner">>,
<<"banner_color">>
].
check_user_data_differs(CurrentUserData, NewUserData) ->
CheckedFields = partial_user_fields(),
lists:any(
fun(Field) ->
CurrentValue = maps:get(Field, CurrentUserData, undefined),
NewValue = maps:get(Field, NewUserData, undefined),
CurrentValue =/= NewValue orelse
(maps:is_key(Field, CurrentUserData) andalso not maps:is_key(Field, NewUserData))
end,
CheckedFields
).

View File

@@ -0,0 +1,194 @@
%% 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(validation).
-export([
validate_snowflake/1,
validate_snowflake/2,
validate_optional_snowflake/1,
validate_snowflake_list/1,
validate_snowflake_list/2,
snowflake_or_throw/2,
snowflake_or_default/2,
snowflake_or_default/3,
snowflake_list_or_throw/2,
extract_snowflake/2,
extract_snowflake/3,
extract_snowflakes/2,
get_field/2,
get_field/3,
get_required_field/3,
get_optional_field/3,
error_category_to_close_code/1
]).
-spec validate_snowflake(term()) -> {ok, integer()} | {error, atom(), atom()}.
validate_snowflake(Id) when is_integer(Id) ->
{ok, Id};
validate_snowflake(Bin) when is_binary(Bin) ->
try
Id = binary_to_integer(Bin),
{ok, Id}
catch
error:badarg ->
gateway_errors:error(validation_invalid_snowflake)
end;
validate_snowflake(null) ->
gateway_errors:error(validation_null_snowflake);
validate_snowflake(_) ->
gateway_errors:error(validation_invalid_snowflake).
-spec validate_snowflake(binary(), term()) -> {ok, integer()} | {error, atom(), atom()}.
validate_snowflake(_FieldName, Value) ->
validate_snowflake(Value).
-spec validate_optional_snowflake(term()) -> {ok, integer() | null} | {error, atom(), atom()}.
validate_optional_snowflake(null) ->
{ok, null};
validate_optional_snowflake(Value) ->
validate_snowflake(Value).
-spec validate_snowflake_list(list()) -> {ok, [integer()]} | {error, atom(), atom()}.
validate_snowflake_list(List) when is_list(List) ->
try
Ids = lists:map(
fun(Item) ->
case validate_snowflake(Item) of
{ok, Id} -> Id;
{error, _, _} -> throw(invalid)
end
end,
List
),
{ok, Ids}
catch
throw:invalid ->
gateway_errors:error(validation_invalid_snowflake_list)
end;
validate_snowflake_list(_) ->
gateway_errors:error(validation_expected_list).
-spec validate_snowflake_list(binary(), list()) -> {ok, [integer()]} | {error, atom(), atom()}.
validate_snowflake_list(_FieldName, Value) ->
validate_snowflake_list(Value).
-spec snowflake_or_throw(binary(), term()) -> integer().
snowflake_or_throw(FieldName, Value) ->
case validate_snowflake(FieldName, Value) of
{ok, Id} -> Id;
{error, _, Reason} -> throw({error, Reason})
end.
-spec snowflake_or_default(term(), integer()) -> integer().
snowflake_or_default(Value, Default) ->
case validate_snowflake(Value) of
{ok, Id} -> Id;
{error, _, _} -> Default
end.
-spec snowflake_or_default(binary(), term(), integer()) -> integer().
snowflake_or_default(FieldName, Value, Default) ->
case validate_snowflake(FieldName, Value) of
{ok, Id} -> Id;
{error, _, _} -> Default
end.
-spec snowflake_list_or_throw(binary(), list()) -> [integer()].
snowflake_list_or_throw(FieldName, Value) ->
case validate_snowflake_list(FieldName, Value) of
{ok, Ids} -> Ids;
{error, _, Reason} -> throw({error, Reason})
end.
-spec extract_snowflake(binary(), map()) -> {ok, integer()} | {error, atom(), atom()}.
extract_snowflake(FieldName, Map) ->
case get_field(FieldName, Map) of
{ok, Value} ->
validate_snowflake(FieldName, Value);
{error, _, _} = Error ->
Error
end.
-spec extract_snowflake(binary(), map(), integer()) -> integer().
extract_snowflake(FieldName, Map, Default) ->
case get_field(FieldName, Map) of
{ok, Value} ->
snowflake_or_default(FieldName, Value, Default);
{error, _, _} ->
Default
end.
-spec extract_snowflakes(list({atom(), binary()}), map()) ->
{ok, #{atom() => integer()}} | {error, atom(), atom()}.
extract_snowflakes(FieldSpecs, Map) ->
extract_snowflakes_loop(FieldSpecs, Map, #{}).
extract_snowflakes_loop([], _Map, Acc) ->
{ok, Acc};
extract_snowflakes_loop([{KeyAtom, FieldName} | Rest], Map, Acc) ->
case extract_snowflake(FieldName, Map) of
{ok, Value} ->
extract_snowflakes_loop(Rest, Map, maps:put(KeyAtom, Value, Acc));
{error, _, _} = Error ->
Error
end.
-spec get_field(term(), map()) -> {ok, term()} | {error, atom(), atom()}.
get_field(Key, Map) when is_map(Map) ->
case maps:get(Key, Map, undefined) of
undefined ->
gateway_errors:error(validation_missing_field);
Value ->
{ok, Value}
end;
get_field(_Key, _NotMap) ->
gateway_errors:error(validation_expected_map).
-spec get_field(term(), map(), term()) -> term().
get_field(Key, Map, Default) when is_map(Map) ->
maps:get(Key, Map, Default);
get_field(_Key, _NotMap, Default) ->
Default.
-spec get_required_field(binary(), map(), fun((term()) -> {ok, term()} | {error, atom(), atom()})) ->
{ok, term()} | {error, atom(), atom()}.
get_required_field(FieldName, Map, Validator) ->
case get_field(FieldName, Map) of
{ok, Value} ->
Validator(Value);
{error, _, _} = Error ->
Error
end.
-spec get_optional_field(term(), map(), fun((term()) -> {ok, term()} | {error, atom(), atom()})) ->
{ok, term() | undefined} | {error, atom(), atom()}.
get_optional_field(FieldName, Map, Validator) ->
case maps:get(FieldName, Map, undefined) of
undefined ->
{ok, undefined};
Value ->
Validator(Value)
end.
-spec error_category_to_close_code(atom()) -> integer().
error_category_to_close_code(rate_limited) ->
constants:close_code_to_num(rate_limited);
error_category_to_close_code(auth_failed) ->
constants:close_code_to_num(authentication_failed);
error_category_to_close_code(_) ->
constants:close_code_to_num(unknown_error).