api-surface #1

Merged
luna merged 51 commits from api-surface into mistress 2026-07-05 14:43:13 +00:00
4 changed files with 215 additions and 1 deletions
Showing only changes of commit c9c4a3c445 - Show all commits

feat(model): Member/PartialMember via shared field group, thread + forum satellites

Luna 2026-07-05 03:57:11 -03:00

View file

@ -0,0 +1,49 @@
defmodule Dexcord.ThreadMetadata do
@moduledoc false
use Dexcord.Struct
discord_struct do
field :archived, :boolean
field :auto_archive_duration, :integer
field :archive_timestamp, :datetime
field :locked, :boolean
field :invitable, :boolean
field :create_timestamp, :datetime
end
end
defmodule Dexcord.ThreadMember do
@moduledoc false
use Dexcord.Struct
discord_struct do
field :id, :snowflake
field :user_id, :snowflake
field :join_timestamp, :datetime
field :flags, :integer
field :member, {:struct, Dexcord.Member}
end
end
defmodule Dexcord.ForumTag do
@moduledoc false
use Dexcord.Struct
discord_struct do
field :id, :snowflake
field :name, :string
field :moderated, :boolean
field :emoji_id, :snowflake
field :emoji_name, :string
end
end
defmodule Dexcord.DefaultReaction do
@moduledoc false
use Dexcord.Struct
discord_struct do
field :emoji_id, :snowflake
field :emoji_name, :string
end
end

View file

@ -0,0 +1,45 @@
defmodule Dexcord.Model.MemberShared do
@moduledoc false
# Field group shared by Dexcord.Member and Dexcord.PartialMember.
def __included_fields__ do
[
{:nick, :string, []},
{:avatar, :string, []},
{:banner, :string, []},
{:roles, {:list, :snowflake}, [default: []]},
{:joined_at, :datetime, []},
{:premium_since, :datetime, []},
{:deaf, :boolean, []},
{:mute, :boolean, []},
{:flags, {:flags, Dexcord.GuildMemberFlags}, []},
{:pending, :boolean, []},
{:permissions, {:flags, Dexcord.Permissions}, [wire_string: true]},
{:communication_disabled_until, :datetime, []},
{:avatar_decoration_data, {:struct, Dexcord.AvatarDecorationData}, []},
{:collectibles, :raw, []}
]
end
end
defmodule Dexcord.Member do
@moduledoc "A guild member (with its user). https://docs.discord.com/developers/resources/guild"
use Dexcord.Struct
discord_struct do
field :user, {:struct, Dexcord.User}
include_fields Dexcord.Model.MemberShared
end
end
defmodule Dexcord.PartialMember do
@moduledoc """
A guild member without the nested user the shape Discord sends inside
MESSAGE_CREATE/UPDATE (`message.member`) and interaction resolved data.
"""
use Dexcord.Struct
discord_struct do
include_fields Dexcord.Model.MemberShared
end
end

View file

@ -0,0 +1,118 @@
defmodule Dexcord.ModelMemberTest do
use ExUnit.Case, async: true
@member_map %{
"user" => %{"id" => "80351110224678912", "username" => "Nelly", "discriminator" => "1337"},
"nick" => "NOT nelly",
"roles" => ["11111111111111111", "22222222222222222"],
"joined_at" => "2015-04-26T06:26:56.936000+00:00",
"premium_since" => "2016-05-15T06:26:56.936000+00:00",
"deaf" => false,
"mute" => false,
"flags" => 0,
"pending" => false,
"permissions" => "2147483648"
}
describe "Member.from_map/1" do
test "full member decodes nested user, snowflake roles, and datetime joined_at" do
member = Dexcord.Member.from_map(@member_map)
assert %Dexcord.User{} = member.user
assert member.user.id == 80_351_110_224_678_912
assert member.nick == "NOT nelly"
assert member.roles == [11_111_111_111_111_111, 22_222_222_222_222_222]
assert %DateTime{} = member.joined_at
assert %DateTime{} = member.premium_since
assert member.deaf == false
assert member.flags == 0
assert member.permissions == 2_147_483_648
end
end
describe "PartialMember.from_map/1" do
test "a member map without user decodes and the struct has no :user key" do
map = Map.delete(@member_map, "user")
pm = Dexcord.PartialMember.from_map(map)
assert %Dexcord.PartialMember{} = pm
assert pm.nick == "NOT nelly"
assert pm.roles == [11_111_111_111_111_111, 22_222_222_222_222_222]
refute Map.has_key?(Map.from_struct(pm), :user)
end
end
describe "shared field group parity" do
test "Member and PartialMember __fields__ differ by exactly the :user entry" do
member_names = Enum.map(Dexcord.Member.__fields__(), & &1.name)
partial_names = Enum.map(Dexcord.PartialMember.__fields__(), & &1.name)
assert member_names -- partial_names == [:user]
assert partial_names -- member_names == []
end
end
describe "ThreadMember.from_map/1" do
test "thread member with nested member decodes" do
map = %{
"id" => "1",
"user_id" => "2",
"join_timestamp" => "2021-01-01T00:00:00.000000+00:00",
"flags" => 1,
"member" => @member_map
}
tm = Dexcord.ThreadMember.from_map(map)
assert tm.id == 1
assert tm.user_id == 2
assert %DateTime{} = tm.join_timestamp
assert tm.flags == 1
assert %Dexcord.Member{} = tm.member
assert %Dexcord.User{} = tm.member.user
end
end
describe "ThreadMetadata / ForumTag / DefaultReaction" do
test "thread metadata decodes timestamps and booleans" do
map = %{
"archived" => true,
"auto_archive_duration" => 1440,
"archive_timestamp" => "2021-01-01T00:00:00.000000+00:00",
"locked" => false,
"invitable" => true,
"create_timestamp" => "2021-01-01T00:00:00.000000+00:00"
}
tmeta = Dexcord.ThreadMetadata.from_map(map)
assert tmeta.archived == true
assert tmeta.auto_archive_duration == 1440
assert %DateTime{} = tmeta.archive_timestamp
assert tmeta.locked == false
assert tmeta.invitable == true
end
test "forum tag decodes emoji fields" do
tag =
Dexcord.ForumTag.from_map(%{
"id" => "1",
"name" => "help",
"moderated" => true,
"emoji_id" => "2",
"emoji_name" => nil
})
assert tag.id == 1
assert tag.name == "help"
assert tag.moderated == true
assert tag.emoji_id == 2
assert tag.emoji_name == nil
end
test "default reaction decodes" do
dr = Dexcord.DefaultReaction.from_map(%{"emoji_id" => nil, "emoji_name" => "🔥"})
assert dr.emoji_id == nil
assert dr.emoji_name == "🔥"
end
end
end

View file

@ -104,7 +104,9 @@ defmodule Dexcord.ModelUserTest do
end
test "custom emoji decodes id, name, and animated" do
emoji = Dexcord.PartialEmoji.from_map(%{"id" => "123", "name" => "blob", "animated" => true})
emoji =
Dexcord.PartialEmoji.from_map(%{"id" => "123", "name" => "blob", "animated" => true})
assert emoji.id == 123
assert emoji.name == "blob"
assert emoji.animated == true