Skip to main content

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    /// File types to filter for.
146    ///
147    /// Applicable for options of type [`Attachment`].
148    ///
149    /// Can be `image`, `video`, `audio`, or any dot-prefixed extension such as `.pdf`.
150    ///
151    /// Defaults to no restriction.
152    ///
153    /// [`Attachment`]: CommandOptionType::Attachment
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub file_types: Option<Vec<String>>,
156    /// Whether the option is required.
157    ///
158    /// Applicable for all options except those of type [`SubCommand`] and
159    /// [`SubCommandGroup`].
160    ///
161    /// Defaults to `false`.
162    ///
163    /// [`SubCommand`]: CommandOptionType::SubCommand
164    /// [`SubCommandGroup`]: CommandOptionType::SubCommandGroup
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub required: Option<bool>,
167}
168
169impl CommandOption {
170    /// This range is the length a string may be.
171    pub const STRING_LENGTH_RANGE: RangeInclusive<u16> = 0..=6000;
172}
173
174/// A predetermined choice users can select.
175#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
176pub struct CommandOptionChoice {
177    /// Name of the choice. Must be 100 characters or less.
178    pub name: String,
179    /// Localization dictionary for the [`name`] field.
180    ///
181    /// Defaults to no localizations.
182    ///
183    /// Keys must be valid locales and values must be 100 characters or less.
184    ///
185    /// See [`CommandOption`]'s documentation for more info.
186    ///
187    /// [`name`]: Self::name
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub name_localizations: Option<HashMap<String, String>>,
190    /// Value of the choice.
191    pub value: CommandOptionChoiceValue,
192}
193
194/// Value of a [`CommandOptionChoice`].
195///
196/// Note that the right variant must be selected based on the
197/// [`CommandOption`]'s [`CommandOptionType`].
198#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
199#[serde(untagged)]
200pub enum CommandOptionChoiceValue {
201    /// Integer choice.
202    Integer(i64),
203    /// Number choice.
204    Number(f64),
205    /// String choice. Must be 100 characters or less.
206    String(String),
207}
208
209impl From<i64> for CommandOptionChoiceValue {
210    fn from(value: i64) -> Self {
211        CommandOptionChoiceValue::Integer(value)
212    }
213}
214
215impl From<f64> for CommandOptionChoiceValue {
216    fn from(value: f64) -> Self {
217        CommandOptionChoiceValue::Number(value)
218    }
219}
220
221impl From<String> for CommandOptionChoiceValue {
222    fn from(value: String) -> Self {
223        CommandOptionChoiceValue::String(value)
224    }
225}
226
227impl TryFrom<CommandOptionChoiceValue> for i64 {
228    type Error = CommandOptionChoiceValue;
229
230    fn try_from(value: CommandOptionChoiceValue) -> Result<Self, Self::Error> {
231        match value {
232            CommandOptionChoiceValue::Integer(inner) => Ok(inner),
233            _ => Err(value),
234        }
235    }
236}
237
238impl TryFrom<CommandOptionChoiceValue> for f64 {
239    type Error = CommandOptionChoiceValue;
240
241    fn try_from(value: CommandOptionChoiceValue) -> Result<Self, Self::Error> {
242        match value {
243            CommandOptionChoiceValue::Number(inner) => Ok(inner),
244            _ => Err(value),
245        }
246    }
247}
248
249impl TryFrom<CommandOptionChoiceValue> for String {
250    type Error = CommandOptionChoiceValue;
251
252    fn try_from(value: CommandOptionChoiceValue) -> Result<Self, Self::Error> {
253        match value {
254            CommandOptionChoiceValue::String(inner) => Ok(inner),
255            _ => Err(value),
256        }
257    }
258}
259
260/// Type used in the `max_value` and `min_value` [`CommandOption`] field.
261///
262/// Note that the right variant must be selected based on the
263/// [`CommandOption`]'s [`CommandOptionType`].
264#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
265#[serde(untagged)]
266pub enum CommandOptionValue {
267    /// Integer type.
268    Integer(i64),
269    /// Number type.
270    Number(f64),
271}
272
273impl From<i64> for CommandOptionValue {
274    fn from(value: i64) -> Self {
275        CommandOptionValue::Integer(value)
276    }
277}
278
279impl From<f64> for CommandOptionValue {
280    fn from(value: f64) -> Self {
281        CommandOptionValue::Number(value)
282    }
283}
284
285impl TryFrom<CommandOptionValue> for i64 {
286    type Error = CommandOptionValue;
287
288    fn try_from(value: CommandOptionValue) -> Result<Self, Self::Error> {
289        #[allow(clippy::match_wildcard_for_single_variants)]
290        match value {
291            CommandOptionValue::Integer(inner) => Ok(inner),
292            _ => Err(value),
293        }
294    }
295}
296
297impl TryFrom<CommandOptionValue> for f64 {
298    type Error = CommandOptionValue;
299
300    fn try_from(value: CommandOptionValue) -> Result<Self, Self::Error> {
301        #[allow(clippy::match_wildcard_for_single_variants)]
302        match value {
303            CommandOptionValue::Number(inner) => Ok(inner),
304            _ => Err(value),
305        }
306    }
307}
308
309impl CommandOptionValue {
310    /// This range contains integer values that safely can be
311    /// represented as a 64-bit floating point value.
312    ///
313    /// Values outside of this range will result in a `400 Bad
314    /// Request`.
315    pub const INTEGER_RANGE: Range<i64> =
316        -(2_i64.pow(f64::MANTISSA_DIGITS) - 1)..(2_i64.pow(f64::MANTISSA_DIGITS));
317    /// This range contains all floating point values that can be
318    /// safely used as Discord Number values.
319    ///
320    /// Values outside of this range will result in a `400 Bad
321    /// Request`.
322    // As we can see above we are within 52 bits on the left, but uses
323    // 53 bits on the right.  We ensure to be within 52 bits on the
324    // right below by subtracting 1 first and using RangeInclusive.
325    #[allow(clippy::cast_precision_loss)]
326    pub const NUMBER_RANGE: RangeInclusive<f64> =
327        (Self::INTEGER_RANGE.start as f64)..=((Self::INTEGER_RANGE.end - 1) as f64);
328}
329
330/// Type of a [`CommandOption`].
331#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)]
332#[non_exhaustive]
333#[repr(u8)]
334pub enum CommandOptionType {
335    SubCommand = 1,
336    SubCommandGroup = 2,
337    String = 3,
338    Integer = 4,
339    Boolean = 5,
340    User = 6,
341    Channel = 7,
342    Role = 8,
343    Mentionable = 9,
344    Number = 10,
345    Attachment = 11,
346}
347
348impl CommandOptionType {
349    pub const fn kind(self) -> &'static str {
350        match self {
351            CommandOptionType::SubCommand => "SubCommand",
352            CommandOptionType::SubCommandGroup => "SubCommandGroup",
353            CommandOptionType::String => "String",
354            CommandOptionType::Integer => "Integer",
355            CommandOptionType::Boolean => "Boolean",
356            CommandOptionType::User => "User",
357            CommandOptionType::Channel => "Channel",
358            CommandOptionType::Role => "Role",
359            CommandOptionType::Mentionable => "Mentionable",
360            CommandOptionType::Number => "Number",
361            CommandOptionType::Attachment => "Attachment",
362        }
363    }
364}