twilight_model/application/command/
option.rs

1use crate::channel::ChannelType;
2use serde::{Deserialize, Serialize};
3use serde_repr::{Deserialize_repr, Serialize_repr};
4use std::{
5    cmp::Eq,
6    collections::HashMap,
7    ops::{Range, RangeInclusive},
8};
9
10/// Option for a [`Command`].
11///
12/// Fields not applicable to the command option's [`CommandOptionType`] should
13/// be set to [`None`].
14///
15/// Fields' default values may be used by setting them to [`None`].
16///
17/// Choices, descriptions and names may be localized in any [available locale],
18/// see [Discord Docs/Localization].
19///
20/// [available locale]: https://discord.com/developers/docs/reference#locales
21/// [`Command`]: super::Command
22/// [Discord Docs/Localization]: https://discord.com/developers/docs/interactions/application-commands#localization
23#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
24pub struct CommandOption {
25    /// Whether the command supports autocomplete.
26    ///
27    /// Applicable for options of type [`Integer`], [`Number`], and [`String`].
28    ///
29    /// Defaults to `false`.
30    ///
31    /// **Note**: may not be set to `true` if `choices` are set.
32    ///
33    /// [`Integer`]: CommandOptionType::Integer
34    /// [`Number`]: CommandOptionType::Number
35    /// [`String`]: CommandOptionType::String
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub autocomplete: Option<bool>,
38    /// List of possible channel types users can select from.
39    ///
40    /// Applicable for options of type [`Channel`].
41    ///
42    /// Defaults to any channel type.
43    ///
44    /// [`Channel`]: CommandOptionType::Channel
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub channel_types: Option<Vec<ChannelType>>,
47    /// List of predetermined choices users can select from.
48    ///
49    /// Applicable for options of type [`Integer`], [`Number`], and [`String`].
50    ///
51    /// Defaults to no choices; users may input a value of their choice.
52    ///
53    /// Must be at most 25 options.
54    ///
55    /// **Note**: all choices must be of the same type.
56    ///
57    /// [`Integer`]: CommandOptionType::Integer
58    /// [`Number`]: CommandOptionType::Number
59    /// [`String`]: CommandOptionType::String
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub choices: Option<Vec<CommandOptionChoice>>,
62    /// Description of the option. Must be 100 characters or less.
63    pub description: String,
64    /// Localization dictionary for the [`description`] field.
65    ///
66    /// Defaults to no localizations.
67    ///
68    /// Keys must be valid locales and values must be 100 characters or less.
69    ///
70    /// [`description`]: Self::description
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub description_localizations: Option<HashMap<String, String>>,
73    /// Type of option.
74    #[serde(rename = "type")]
75    pub kind: CommandOptionType,
76    /// Maximum allowed value length.
77    ///
78    /// Applicable for options of type [`String`].
79    ///
80    /// Defaults to `6000`.
81    ///
82    /// Must be at least `1` and at most `6000`.
83    ///
84    /// [`String`]: CommandOptionType::String
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub max_length: Option<u16>,
87    /// Maximum allowed value.
88    ///
89    /// Applicable for options of type [`Integer`] and [`Number`].
90    ///
91    /// Defaults to no maximum.
92    ///
93    /// [`Integer`]: CommandOptionType::Integer
94    /// [`Number`]: CommandOptionType::Number
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub max_value: Option<CommandOptionValue>,
97    /// Minimum allowed value length.
98    ///
99    /// Applicable for options of type [`String`].
100    ///
101    /// Defaults to `0`.
102    ///
103    /// Must be at most `6000`.
104    ///
105    /// [`String`]: CommandOptionType::String
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub min_length: Option<u16>,
108    /// Minimum allowed value.
109    ///
110    /// Applicable for options of type [`Integer`] and [`Number`].
111    ///
112    /// Defaults to no minimum.
113    ///
114    /// [`Integer`]: CommandOptionType::Integer
115    /// [`Number`]: CommandOptionType::Number
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub min_value: Option<CommandOptionValue>,
118    /// Name of the option. Must be 32 characters or less.
119    pub name: String,
120    /// Localization dictionary for the [`name`] field.
121    ///
122    /// Defaults to no localizations.
123    ///
124    /// Keys must be valid locales and values must be 32 characters or less.
125    ///
126    /// [`name`]: Self::name
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub name_localizations: Option<HashMap<String, String>>,
129    /// Nested options.
130    ///
131    /// Applicable for options of type [`SubCommand`] and [`SubCommandGroup`].
132    ///
133    /// Defaults to no options.
134    ///
135    /// **Note**: at least one option is required and [`SubCommandGroup`] may
136    /// only contain [`SubCommand`]s.
137    ///
138    /// See [Discord Docs/Subcommands and Subcommand Groups].
139    ///
140    /// [Discord Docs/Subcommands and Subcommand Groups]: https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups
141    /// [`SubCommand`]: CommandOptionType::SubCommand
142    /// [`SubCommandGroup`]: CommandOptionType::SubCommandGroup
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub options: Option<Vec<CommandOption>>,
145    /// Whether the option is required.
146    ///
147    /// Applicable for all options except those of type [`SubCommand`] and
148    /// [`SubCommandGroup`].
149    ///
150    /// Defaults to `false`.
151    ///
152    /// [`SubCommand`]: CommandOptionType::SubCommand
153    /// [`SubCommandGroup`]: CommandOptionType::SubCommandGroup
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub required: Option<bool>,
156}
157
158impl CommandOption {
159    /// This range is the length a string may be.
160    pub const STRING_LENGTH_RANGE: RangeInclusive<u16> = 0..=6000;
161}
162
163/// A predetermined choice users can select.
164#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
165pub struct CommandOptionChoice {
166    /// Name of the choice. Must be 100 characters or less.
167    pub name: String,
168    /// Localization dictionary for the [`name`] field.
169    ///
170    /// Defaults to no localizations.
171    ///
172    /// Keys must be valid locales and values must be 100 characters or less.
173    ///
174    /// See [`CommandOption`]'s documentation for more info.
175    ///
176    /// [`name`]: Self::name
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub name_localizations: Option<HashMap<String, String>>,
179    /// Value of the choice.
180    pub value: CommandOptionChoiceValue,
181}
182
183/// Value of a [`CommandOptionChoice`].
184///
185/// Note that the right variant must be selected based on the
186/// [`CommandOption`]'s [`CommandOptionType`].
187#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
188#[serde(untagged)]
189pub enum CommandOptionChoiceValue {
190    /// String choice. Must be 100 characters or less.
191    String(String),
192    /// Integer choice.
193    Integer(i64),
194    /// Number choice.
195    Number(f64),
196}
197
198/// Type used in the `max_value` and `min_value` [`CommandOption`] field.
199///
200/// Note that the right variant must be selected based on the
201/// [`CommandOption`]'s [`CommandOptionType`].
202#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
203#[serde(untagged)]
204pub enum CommandOptionValue {
205    /// Integer type.
206    Integer(i64),
207    /// Number type.
208    Number(f64),
209}
210
211impl CommandOptionValue {
212    /// This range contains integer values that safely can be
213    /// represented as a 64-bit floating point value.
214    ///
215    /// Values outside of this range will result in a `400 Bad
216    /// Request`.
217    pub const INTEGER_RANGE: Range<i64> =
218        -(2_i64.pow(f64::MANTISSA_DIGITS) - 1)..(2_i64.pow(f64::MANTISSA_DIGITS));
219    /// This range contains all floating point values that can be
220    /// safely used as Discord Number values.
221    ///
222    /// Values outside of this range will result in a `400 Bad
223    /// Request`.
224    // As we can see above we are within 52 bits on the left, but uses
225    // 53 bits on the right.  We ensure to be within 52 bits on the
226    // right below by subtracting 1 first and using RangeInclusive.
227    #[allow(clippy::cast_precision_loss)]
228    pub const NUMBER_RANGE: RangeInclusive<f64> =
229        (Self::INTEGER_RANGE.start as f64)..=((Self::INTEGER_RANGE.end - 1) as f64);
230}
231
232/// Type of a [`CommandOption`].
233#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)]
234#[non_exhaustive]
235#[repr(u8)]
236pub enum CommandOptionType {
237    SubCommand = 1,
238    SubCommandGroup = 2,
239    String = 3,
240    Integer = 4,
241    Boolean = 5,
242    User = 6,
243    Channel = 7,
244    Role = 8,
245    Mentionable = 9,
246    Number = 10,
247    Attachment = 11,
248}
249
250impl CommandOptionType {
251    pub const fn kind(self) -> &'static str {
252        match self {
253            CommandOptionType::SubCommand => "SubCommand",
254            CommandOptionType::SubCommandGroup => "SubCommandGroup",
255            CommandOptionType::String => "String",
256            CommandOptionType::Integer => "Integer",
257            CommandOptionType::Boolean => "Boolean",
258            CommandOptionType::User => "User",
259            CommandOptionType::Channel => "Channel",
260            CommandOptionType::Role => "Role",
261            CommandOptionType::Mentionable => "Mentionable",
262            CommandOptionType::Number => "Number",
263            CommandOptionType::Attachment => "Attachment",
264        }
265    }
266}