api-surface #1
4 changed files with 613 additions and 0 deletions
feat(model): channel structs + factory dispatch on wire type
commit
b278e3eeaa
258
lib/dexcord/model/channel.ex
Normal file
258
lib/dexcord/model/channel.ex
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
defmodule Dexcord.Model.ChannelShared do
|
||||
@moduledoc false
|
||||
# Field group shared by the guild-channel structs (everything with a guild).
|
||||
|
||||
def __included_fields__ do
|
||||
[
|
||||
{:id, :snowflake, []},
|
||||
{:type, {:enum, Dexcord.ChannelType}, []},
|
||||
{:guild_id, :snowflake, []},
|
||||
{:name, :string, []},
|
||||
{:position, :integer, []},
|
||||
{:permission_overwrites, {:list, {:struct, Dexcord.Overwrite}}, [default: []]},
|
||||
{:parent_id, :snowflake, []},
|
||||
{:nsfw, :boolean, [default: false]},
|
||||
{:flags, {:flags, Dexcord.ChannelFlags}, []}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.TextChannel do
|
||||
@moduledoc "A guild text channel (type 0)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
field :topic, :string
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
field :last_pin_timestamp, :datetime
|
||||
field :default_auto_archive_duration, :integer
|
||||
field :default_thread_rate_limit_per_user, :integer
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.AnnouncementChannel do
|
||||
@moduledoc "A guild announcement channel (type 5)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
field :topic, :string
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
field :last_pin_timestamp, :datetime
|
||||
field :default_auto_archive_duration, :integer
|
||||
field :default_thread_rate_limit_per_user, :integer
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.VoiceChannel do
|
||||
@moduledoc "A guild voice channel (type 2)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
field :bitrate, :integer
|
||||
field :user_limit, :integer
|
||||
field :rtc_region, :string
|
||||
field :video_quality_mode, {:enum, Dexcord.VideoQualityMode}
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.StageChannel do
|
||||
@moduledoc "A guild stage voice channel (type 13)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
field :bitrate, :integer
|
||||
field :user_limit, :integer
|
||||
field :rtc_region, :string
|
||||
field :video_quality_mode, {:enum, Dexcord.VideoQualityMode}
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.CategoryChannel do
|
||||
@moduledoc "A guild category channel (type 4)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.DirectoryChannel do
|
||||
@moduledoc "A guild directory channel (type 14)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.ForumChannel do
|
||||
@moduledoc "A guild forum channel (type 15)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
field :topic, :string
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
field :default_auto_archive_duration, :integer
|
||||
field :default_thread_rate_limit_per_user, :integer
|
||||
field :available_tags, {:list, {:struct, Dexcord.ForumTag}}, default: []
|
||||
field :default_reaction_emoji, {:struct, Dexcord.DefaultReaction}
|
||||
field :default_sort_order, {:enum, Dexcord.SortOrderType}
|
||||
field :default_forum_layout, {:enum, Dexcord.ForumLayoutType}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.MediaChannel do
|
||||
@moduledoc "A guild media channel (type 16) — like a forum, without a layout mode."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
include_fields Dexcord.Model.ChannelShared
|
||||
field :topic, :string
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
field :default_auto_archive_duration, :integer
|
||||
field :default_thread_rate_limit_per_user, :integer
|
||||
field :available_tags, {:list, {:struct, Dexcord.ForumTag}}, default: []
|
||||
field :default_reaction_emoji, {:struct, Dexcord.DefaultReaction}
|
||||
field :default_sort_order, {:enum, Dexcord.SortOrderType}
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.DMChannel do
|
||||
@moduledoc "A one-to-one DM channel (type 1). No guild fields."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
field :id, :snowflake
|
||||
field :type, {:enum, Dexcord.ChannelType}
|
||||
field :flags, {:flags, Dexcord.ChannelFlags}
|
||||
field :last_message_id, :snowflake
|
||||
field :recipients, {:list, {:struct, Dexcord.User}}, default: []
|
||||
field :last_pin_timestamp, :datetime
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.GroupDMChannel do
|
||||
@moduledoc "A group DM channel (type 3)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
field :id, :snowflake
|
||||
field :type, {:enum, Dexcord.ChannelType}
|
||||
field :flags, {:flags, Dexcord.ChannelFlags}
|
||||
field :last_message_id, :snowflake
|
||||
field :recipients, {:list, {:struct, Dexcord.User}}, default: []
|
||||
field :last_pin_timestamp, :datetime
|
||||
field :name, :string
|
||||
field :icon, :string
|
||||
field :owner_id, :snowflake
|
||||
field :application_id, :snowflake
|
||||
field :managed, :boolean
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.Thread do
|
||||
@moduledoc "A thread channel (types 10 announcement, 11 public, 12 private)."
|
||||
use Dexcord.Struct
|
||||
|
||||
discord_struct do
|
||||
field :id, :snowflake
|
||||
field :type, {:enum, Dexcord.ChannelType}
|
||||
field :guild_id, :snowflake
|
||||
field :name, :string
|
||||
field :flags, {:flags, Dexcord.ChannelFlags}
|
||||
field :parent_id, :snowflake
|
||||
field :owner_id, :snowflake
|
||||
field :last_message_id, :snowflake
|
||||
field :rate_limit_per_user, :integer
|
||||
field :message_count, :integer
|
||||
field :member_count, :integer
|
||||
field :total_message_sent, :integer
|
||||
field :thread_metadata, {:struct, Dexcord.ThreadMetadata}
|
||||
field :member, {:struct, Dexcord.ThreadMember}
|
||||
field :applied_tags, {:list, :snowflake}, default: []
|
||||
field :last_pin_timestamp, :datetime
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Dexcord.UnknownChannel do
|
||||
@moduledoc "Fallback for channel types Dexcord doesn't recognize. Raw payload preserved."
|
||||
defstruct [:type, :raw]
|
||||
|
||||
@type t :: %__MODULE__{type: {:unknown, integer()} | nil, raw: map()}
|
||||
|
||||
@doc false
|
||||
def from_map(%{"type" => n} = map) when is_integer(n),
|
||||
do: %__MODULE__{type: {:unknown, n}, raw: map}
|
||||
|
||||
def from_map(map) when is_map(map), do: %__MODULE__{type: nil, raw: map}
|
||||
def from_map(_), do: nil
|
||||
|
||||
@doc false
|
||||
# No __fields__/0 here (hand-written struct): re-encoding yields the raw
|
||||
# payload verbatim. The Codec routes struct encodes through the struct's
|
||||
# own to_map/1, so this keeps nested unknown channels encodable.
|
||||
def to_map(%__MODULE__{raw: raw}), do: raw
|
||||
end
|
||||
|
||||
defmodule Dexcord.Channel do
|
||||
@moduledoc """
|
||||
Channel factory: decodes a wire channel map into the struct for its type.
|
||||
|
||||
Capability is struct identity — a `%Dexcord.CategoryChannel{}` is not
|
||||
Messageable; sending to it fails at match time, by design.
|
||||
"""
|
||||
|
||||
@type_map %{
|
||||
0 => Dexcord.TextChannel,
|
||||
1 => Dexcord.DMChannel,
|
||||
2 => Dexcord.VoiceChannel,
|
||||
3 => Dexcord.GroupDMChannel,
|
||||
4 => Dexcord.CategoryChannel,
|
||||
5 => Dexcord.AnnouncementChannel,
|
||||
10 => Dexcord.Thread,
|
||||
11 => Dexcord.Thread,
|
||||
12 => Dexcord.Thread,
|
||||
13 => Dexcord.StageChannel,
|
||||
14 => Dexcord.DirectoryChannel,
|
||||
15 => Dexcord.ForumChannel,
|
||||
16 => Dexcord.MediaChannel
|
||||
}
|
||||
|
||||
@type t ::
|
||||
Dexcord.TextChannel.t()
|
||||
| Dexcord.DMChannel.t()
|
||||
| Dexcord.VoiceChannel.t()
|
||||
| Dexcord.GroupDMChannel.t()
|
||||
| Dexcord.CategoryChannel.t()
|
||||
| Dexcord.AnnouncementChannel.t()
|
||||
| Dexcord.Thread.t()
|
||||
| Dexcord.StageChannel.t()
|
||||
| Dexcord.DirectoryChannel.t()
|
||||
| Dexcord.ForumChannel.t()
|
||||
| Dexcord.MediaChannel.t()
|
||||
| Dexcord.UnknownChannel.t()
|
||||
|
||||
@doc "Decodes a wire channel map into the struct for its `\"type\"`."
|
||||
@spec from_map(term()) :: t() | nil
|
||||
def from_map(%{"type" => type} = map) when is_map_key(@type_map, type),
|
||||
do: @type_map[type].from_map(map)
|
||||
|
||||
def from_map(map) when is_map(map), do: Dexcord.UnknownChannel.from_map(map)
|
||||
def from_map(_), do: nil
|
||||
|
||||
@doc false
|
||||
def __type_map__, do: @type_map
|
||||
end
|
||||
124
test/dexcord/model_channel_test.exs
Normal file
124
test/dexcord/model_channel_test.exs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
defmodule Dexcord.ModelChannelTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
# Literal expectation table — deliberately NOT reusing
|
||||
# Dexcord.Channel.__type_map__/0 (that would test the code with itself).
|
||||
@expected %{
|
||||
0 => Dexcord.TextChannel,
|
||||
1 => Dexcord.DMChannel,
|
||||
2 => Dexcord.VoiceChannel,
|
||||
3 => Dexcord.GroupDMChannel,
|
||||
4 => Dexcord.CategoryChannel,
|
||||
5 => Dexcord.AnnouncementChannel,
|
||||
10 => Dexcord.Thread,
|
||||
11 => Dexcord.Thread,
|
||||
12 => Dexcord.Thread,
|
||||
13 => Dexcord.StageChannel,
|
||||
14 => Dexcord.DirectoryChannel,
|
||||
15 => Dexcord.ForumChannel,
|
||||
16 => Dexcord.MediaChannel
|
||||
}
|
||||
|
||||
setup_all do
|
||||
{:ok, channels: Dexcord.Fixtures.load!("channel_types.json")}
|
||||
end
|
||||
|
||||
describe "api-surface.AC2.14: factory dispatch on wire type" do
|
||||
test "every documented type decodes to exactly its expected struct", %{channels: channels} do
|
||||
# The fixture covers all 13 documented types, once each.
|
||||
assert Enum.map(channels, & &1["type"]) |> Enum.sort() ==
|
||||
Enum.sort(Map.keys(@expected))
|
||||
|
||||
for obj <- channels do
|
||||
type = obj["type"]
|
||||
expected = Map.fetch!(@expected, type)
|
||||
decoded = Dexcord.Channel.from_map(obj)
|
||||
|
||||
assert decoded.__struct__ == expected,
|
||||
"type #{type} decoded to #{inspect(decoded.__struct__)}, expected #{inspect(expected)}"
|
||||
end
|
||||
end
|
||||
|
||||
test "threads 10/11/12 all decode to %Dexcord.Thread{}", %{channels: channels} do
|
||||
threads = Enum.filter(channels, &(&1["type"] in [10, 11, 12]))
|
||||
assert length(threads) == 3
|
||||
|
||||
for obj <- threads do
|
||||
assert %Dexcord.Thread{} = Dexcord.Channel.from_map(obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "unknown-type fallback" do
|
||||
test "a recognized-shape map with an unknown type int preserves the raw payload" do
|
||||
raw = %{"type" => 99, "id" => "1"}
|
||||
|
||||
assert %Dexcord.UnknownChannel{type: {:unknown, 99}, raw: ^raw} =
|
||||
Dexcord.Channel.from_map(raw)
|
||||
end
|
||||
|
||||
test "a map with no type field decodes to UnknownChannel with nil type" do
|
||||
assert %Dexcord.UnknownChannel{type: nil} = Dexcord.Channel.from_map(%{"id" => "1"})
|
||||
end
|
||||
|
||||
test "a non-map decodes to nil" do
|
||||
assert Dexcord.Channel.from_map("junk") == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "field-depth decoding" do
|
||||
test "forum available_tags decode to ForumTag structs with moderated set", %{
|
||||
channels: channels
|
||||
} do
|
||||
forum = channel_of_type(channels, 15)
|
||||
decoded = Dexcord.Channel.from_map(forum)
|
||||
|
||||
assert [%Dexcord.ForumTag{} = tag] = decoded.available_tags
|
||||
assert tag.moderated == true
|
||||
assert tag.name == "resolved"
|
||||
end
|
||||
|
||||
test "thread metadata and member timestamps decode to DateTime", %{channels: channels} do
|
||||
thread = channel_of_type(channels, 11)
|
||||
decoded = Dexcord.Channel.from_map(thread)
|
||||
|
||||
assert %Dexcord.ThreadMetadata{} = decoded.thread_metadata
|
||||
assert decoded.thread_metadata.archived == false
|
||||
assert %Dexcord.ThreadMember{} = decoded.member
|
||||
assert %DateTime{} = decoded.member.join_timestamp
|
||||
end
|
||||
|
||||
test "text channel overwrites decode allow to an integer", %{channels: channels} do
|
||||
text = channel_of_type(channels, 0)
|
||||
decoded = Dexcord.Channel.from_map(text)
|
||||
|
||||
assert [%Dexcord.Overwrite{} = ow] = decoded.permission_overwrites
|
||||
assert is_integer(ow.allow)
|
||||
assert ow.allow == 1024
|
||||
assert ow.deny == 2048
|
||||
end
|
||||
|
||||
test "DM channel decodes its recipients to User structs", %{channels: channels} do
|
||||
dm = channel_of_type(channels, 1)
|
||||
decoded = Dexcord.Channel.from_map(dm)
|
||||
|
||||
assert [%Dexcord.User{} = user] = decoded.recipients
|
||||
assert user.username == "Nelly"
|
||||
end
|
||||
end
|
||||
|
||||
describe "type field carries the enum atom" do
|
||||
test "decoded structs carry type as the enum atom", %{channels: channels} do
|
||||
by_type = Map.new(channels, &{&1["type"], Dexcord.Channel.from_map(&1)})
|
||||
|
||||
assert by_type[0].type == :guild_text
|
||||
assert by_type[1].type == :dm
|
||||
assert by_type[2].type == :guild_voice
|
||||
assert by_type[11].type == :public_thread
|
||||
assert by_type[15].type == :guild_forum
|
||||
end
|
||||
end
|
||||
|
||||
defp channel_of_type(channels, type),
|
||||
do: Enum.find(channels, &(&1["type"] == type))
|
||||
end
|
||||
222
test/fixtures/channel_types.json
vendored
Normal file
222
test/fixtures/channel_types.json
vendored
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
[
|
||||
{
|
||||
"id": "100000000000000000",
|
||||
"type": 0,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "general",
|
||||
"position": 0,
|
||||
"flags": 0,
|
||||
"topic": "Welcome to the server",
|
||||
"last_message_id": "100000000000000099",
|
||||
"rate_limit_per_user": 5,
|
||||
"nsfw": false,
|
||||
"parent_id": "104000000000000000",
|
||||
"default_auto_archive_duration": 1440,
|
||||
"permission_overwrites": [
|
||||
{
|
||||
"id": "900000000000000000",
|
||||
"type": 0,
|
||||
"allow": "1024",
|
||||
"deny": "2048"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "101000000000000000",
|
||||
"type": 1,
|
||||
"flags": 0,
|
||||
"last_message_id": "101000000000000099",
|
||||
"recipients": [
|
||||
{
|
||||
"id": "800000000000000000",
|
||||
"username": "Nelly",
|
||||
"discriminator": "1337",
|
||||
"global_name": "Nelly"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "102000000000000000",
|
||||
"type": 2,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "Voice Chat",
|
||||
"position": 1,
|
||||
"flags": 0,
|
||||
"bitrate": 64000,
|
||||
"user_limit": 10,
|
||||
"rtc_region": null,
|
||||
"video_quality_mode": 1,
|
||||
"parent_id": "104000000000000000"
|
||||
},
|
||||
{
|
||||
"id": "103000000000000000",
|
||||
"type": 3,
|
||||
"flags": 0,
|
||||
"name": "Study Group",
|
||||
"icon": "a1b2c3d4e5f6",
|
||||
"owner_id": "800000000000000000",
|
||||
"last_message_id": "103000000000000099",
|
||||
"recipients": [
|
||||
{
|
||||
"id": "800000000000000000",
|
||||
"username": "Nelly",
|
||||
"discriminator": "1337"
|
||||
},
|
||||
{
|
||||
"id": "800000000000000001",
|
||||
"username": "Wumpus",
|
||||
"discriminator": "0001"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "104000000000000000",
|
||||
"type": 4,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "Text Channels",
|
||||
"position": 0,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "105000000000000000",
|
||||
"type": 5,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "announcements",
|
||||
"position": 2,
|
||||
"flags": 0,
|
||||
"topic": "Server announcements",
|
||||
"last_message_id": "105000000000000099",
|
||||
"nsfw": false
|
||||
},
|
||||
{
|
||||
"id": "110000000000000000",
|
||||
"type": 10,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "announcement-thread",
|
||||
"flags": 0,
|
||||
"parent_id": "105000000000000000",
|
||||
"owner_id": "800000000000000000",
|
||||
"message_count": 3,
|
||||
"member_count": 2,
|
||||
"total_message_sent": 3,
|
||||
"thread_metadata": {
|
||||
"archived": false,
|
||||
"auto_archive_duration": 1440,
|
||||
"archive_timestamp": "2021-01-01T00:00:00.000000+00:00",
|
||||
"locked": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "111000000000000000",
|
||||
"type": 11,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "public-thread",
|
||||
"flags": 0,
|
||||
"parent_id": "100000000000000000",
|
||||
"owner_id": "800000000000000000",
|
||||
"message_count": 12,
|
||||
"member_count": 5,
|
||||
"total_message_sent": 12,
|
||||
"rate_limit_per_user": 0,
|
||||
"applied_tags": ["150000000000000001"],
|
||||
"thread_metadata": {
|
||||
"archived": false,
|
||||
"auto_archive_duration": 4320,
|
||||
"archive_timestamp": "2021-02-01T00:00:00.000000+00:00",
|
||||
"locked": false,
|
||||
"invitable": true
|
||||
},
|
||||
"member": {
|
||||
"id": "111000000000000000",
|
||||
"user_id": "800000000000000000",
|
||||
"join_timestamp": "2021-02-01T12:30:00.000000+00:00",
|
||||
"flags": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "112000000000000000",
|
||||
"type": 12,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "private-thread",
|
||||
"flags": 0,
|
||||
"parent_id": "100000000000000000",
|
||||
"owner_id": "800000000000000000",
|
||||
"message_count": 1,
|
||||
"member_count": 1,
|
||||
"total_message_sent": 1,
|
||||
"thread_metadata": {
|
||||
"archived": true,
|
||||
"auto_archive_duration": 10080,
|
||||
"archive_timestamp": "2021-03-01T00:00:00.000000+00:00",
|
||||
"locked": true,
|
||||
"invitable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "113000000000000000",
|
||||
"type": 13,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "Stage",
|
||||
"position": 3,
|
||||
"flags": 0,
|
||||
"bitrate": 40000,
|
||||
"user_limit": 0,
|
||||
"rtc_region": "us-east",
|
||||
"video_quality_mode": 1
|
||||
},
|
||||
{
|
||||
"id": "114000000000000000",
|
||||
"type": 14,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "Server Directory",
|
||||
"position": 4,
|
||||
"flags": 0
|
||||
},
|
||||
{
|
||||
"id": "115000000000000000",
|
||||
"type": 15,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "help-forum",
|
||||
"position": 5,
|
||||
"flags": 16,
|
||||
"topic": "Read the pinned guidelines before posting",
|
||||
"last_message_id": "115000000000000099",
|
||||
"rate_limit_per_user": 10,
|
||||
"default_auto_archive_duration": 4320,
|
||||
"default_sort_order": 0,
|
||||
"default_forum_layout": 1,
|
||||
"available_tags": [
|
||||
{
|
||||
"id": "150000000000000001",
|
||||
"name": "resolved",
|
||||
"moderated": true,
|
||||
"emoji_id": null,
|
||||
"emoji_name": "✅"
|
||||
}
|
||||
],
|
||||
"default_reaction_emoji": {
|
||||
"emoji_id": null,
|
||||
"emoji_name": "👍"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "116000000000000000",
|
||||
"type": 16,
|
||||
"guild_id": "900000000000000000",
|
||||
"name": "gallery",
|
||||
"position": 6,
|
||||
"flags": 0,
|
||||
"topic": "Share your art",
|
||||
"default_auto_archive_duration": 1440,
|
||||
"default_sort_order": 1,
|
||||
"available_tags": [
|
||||
{
|
||||
"id": "160000000000000001",
|
||||
"name": "sketch",
|
||||
"moderated": false,
|
||||
"emoji_id": null,
|
||||
"emoji_name": "🎨"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
9
test/support/fixtures.ex
Normal file
9
test/support/fixtures.ex
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Dexcord.Fixtures do
|
||||
@moduledoc false
|
||||
|
||||
@fixtures_dir Path.expand("../fixtures", __DIR__)
|
||||
|
||||
def load!(name) do
|
||||
@fixtures_dir |> Path.join(name) |> File.read!() |> JSON.decode!()
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue