api-surface #1

Merged
luna merged 51 commits from api-surface into mistress 2026-07-05 14:43:13 +00:00
2 changed files with 213 additions and 0 deletions
Showing only changes of commit 490ad53a54 - Show all commits

feat(model): ApplicationCommand family with recursive options

Luna 2026-07-05 04:40:51 -03:00

View file

@ -0,0 +1,80 @@
defmodule Dexcord.ApplicationCommand do
@moduledoc "An application command. https://docs.discord.com/developers/interactions/application-commands"
use Dexcord.Struct
discord_struct do
field :id, :snowflake
field :type, {:enum, Dexcord.ApplicationCommandType}, default: :chat_input
field :application_id, :snowflake
field :guild_id, :snowflake
field :name, :string
field :name_localizations, :raw
field :description, :string
field :description_localizations, :raw
field :options, {:list, {:struct, Dexcord.ApplicationCommand.Option}}, default: []
field :default_member_permissions, {:flags, Dexcord.Permissions}, wire_string: true
field :dm_permission, :boolean
field :default_permission, :boolean
field :nsfw, :boolean, default: false
field :integration_types, {:list, {:enum, Dexcord.ApplicationIntegrationType}}
field :contexts, {:list, {:enum, Dexcord.InteractionContextType}}
field :version, :snowflake
field :handler, {:enum, Dexcord.EntryPointHandlerType}
end
end
defmodule Dexcord.ApplicationCommand.Option do
@moduledoc "A command option (recursive — sub-command groups nest options)."
use Dexcord.Struct
discord_struct do
field :type, {:enum, Dexcord.ApplicationCommandOptionType}
field :name, :string
field :name_localizations, :raw
field :description, :string
field :description_localizations, :raw
field :required, :boolean, default: false
field :choices, {:list, {:struct, Dexcord.ApplicationCommand.OptionChoice}}, default: []
field :options, {:list, {:struct, __MODULE__}}, default: []
field :channel_types, {:list, :integer}
field :min_value, :number
field :max_value, :number
field :min_length, :integer
field :max_length, :integer
field :autocomplete, :boolean
end
end
defmodule Dexcord.ApplicationCommand.OptionChoice do
@moduledoc "A choice for a command option."
use Dexcord.Struct
discord_struct do
field :name, :string
field :name_localizations, :raw
field :value, :raw
end
end
defmodule Dexcord.GuildApplicationCommandPermissions do
@moduledoc "The permissions for a command in a guild."
use Dexcord.Struct
discord_struct do
field :id, :snowflake
field :application_id, :snowflake
field :guild_id, :snowflake
field :permissions, {:list, {:struct, Dexcord.ApplicationCommandPermission}}, default: []
end
end
defmodule Dexcord.ApplicationCommandPermission do
@moduledoc "A single command permission override (role/user/channel target)."
use Dexcord.Struct
discord_struct do
field :id, :snowflake
field :type, {:enum, Dexcord.ApplicationCommandPermissionType}
field :permission, :boolean
end
end

View file

@ -0,0 +1,133 @@
defmodule Dexcord.ModelApplicationCommandTest do
use ExUnit.Case, async: true
alias Dexcord.ApplicationCommand
alias Dexcord.ApplicationCommand.{Option, OptionChoice}
alias Dexcord.{ApplicationCommandPermission, GuildApplicationCommandPermissions}
describe "ApplicationCommand recursive options" do
setup do
map = %{
"id" => "1000000000000000001",
"type" => 1,
"application_id" => "2000000000000000002",
"guild_id" => "3000000000000000003",
"name" => "config",
"description" => "Configure the bot",
"default_member_permissions" => "8",
"nsfw" => false,
"integration_types" => [0, 1],
"contexts" => [0, 1, 2],
"version" => "9000000000000000009",
"options" => [
%{
"type" => 2,
"name" => "channels",
"description" => "Channel settings",
"options" => [
%{
"type" => 1,
"name" => "set",
"description" => "Set a channel",
"options" => [
%{
"type" => 3,
"name" => "kind",
"description" => "Which kind",
"required" => true,
"choices" => [
%{"name" => "Logs", "value" => "logs"},
%{"name" => "Welcome", "value" => "welcome"}
]
}
]
}
]
}
]
}
%{command: ApplicationCommand.from_map(map)}
end
test "decodes the top-level command with typed enums", %{command: cmd} do
assert %ApplicationCommand{
id: 1_000_000_000_000_000_001,
type: :chat_input,
application_id: 2_000_000_000_000_000_002,
guild_id: 3_000_000_000_000_000_003,
name: "config",
nsfw: false,
integration_types: [:guild_install, :user_install],
contexts: [:guild, :bot_dm, :private_channel],
version: 9_000_000_000_000_000_009
} = cmd
end
test "default_member_permissions decodes the wire string to an integer bitfield", %{
command: cmd
} do
assert cmd.default_member_permissions == 8
end
test "decodes the sub_command_group -> sub_command -> string option tree recursively", %{
command: cmd
} do
assert [%Option{type: :sub_command_group, name: "channels"} = group] = cmd.options
assert [%Option{type: :sub_command, name: "set"} = sub] = group.options
assert [
%Option{
type: :string,
name: "kind",
required: true,
choices: [
%OptionChoice{name: "Logs", value: "logs"},
%OptionChoice{name: "Welcome", value: "welcome"}
]
}
] = sub.options
end
test "to_map re-encodes default_member_permissions as a wire string", %{command: cmd} do
assert %{"default_member_permissions" => "8"} = ApplicationCommand.to_map(cmd)
end
test "type defaults to :chat_input when the wire omits it" do
cmd = ApplicationCommand.from_map(%{"name" => "ping", "description" => "pong"})
assert cmd.type == :chat_input
end
end
describe "GuildApplicationCommandPermissions" do
test "decodes permission overrides with typed permission targets" do
map = %{
"id" => "1000000000000000001",
"application_id" => "2000000000000000002",
"guild_id" => "3000000000000000003",
"permissions" => [
%{"id" => "4000000000000000004", "type" => 1, "permission" => true},
%{"id" => "5000000000000000005", "type" => 2, "permission" => false}
]
}
assert %GuildApplicationCommandPermissions{
id: 1_000_000_000_000_000_001,
application_id: 2_000_000_000_000_000_002,
guild_id: 3_000_000_000_000_000_003,
permissions: [
%ApplicationCommandPermission{
id: 4_000_000_000_000_000_004,
type: :role,
permission: true
},
%ApplicationCommandPermission{
id: 5_000_000_000_000_000_005,
type: :user,
permission: false
}
]
} = GuildApplicationCommandPermissions.from_map(map)
end
end
end