initial commit
This commit is contained in:
32
fluxer_gateway/src/utils/backoff_utils.erl
Normal file
32
fluxer_gateway/src/utils/backoff_utils.erl
Normal 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).
|
||||
368
fluxer_gateway/src/utils/constants.erl
Normal file
368
fluxer_gateway/src/utils/constants.erl
Normal 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.
|
||||
63
fluxer_gateway/src/utils/custom_status_validation.erl
Normal file
63
fluxer_gateway/src/utils/custom_status_validation.erl
Normal 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).
|
||||
647
fluxer_gateway/src/utils/list_ops.erl
Normal file
647
fluxer_gateway/src/utils/list_ops.erl
Normal 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.
|
||||
559
fluxer_gateway/src/utils/map_utils.erl
Normal file
559
fluxer_gateway/src/utils/map_utils.erl
Normal 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.
|
||||
636
fluxer_gateway/src/utils/type_conv.erl
Normal file
636
fluxer_gateway/src/utils/type_conv.erl
Normal 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.
|
||||
53
fluxer_gateway/src/utils/user_utils.erl
Normal file
53
fluxer_gateway/src/utils/user_utils.erl
Normal 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.
|
||||
144
fluxer_gateway/src/utils/utils.erl
Normal file
144
fluxer_gateway/src/utils/utils.erl
Normal 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
|
||||
).
|
||||
194
fluxer_gateway/src/utils/validation.erl
Normal file
194
fluxer_gateway/src/utils/validation.erl
Normal 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).
|
||||
Reference in New Issue
Block a user