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    /// 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    /// Integer choice.
191    Integer(i64),
192    /// Number choice.
193    Number(f64),
194    /// String choice. Must be 100 characters or less.
195    String(String),
196}
197
198impl From<i64> for CommandOptionChoiceValue {
199    fn from(value: i64) -> Self {
200        CommandOptionChoiceValue::Integer(value)
201    }
202}
203
204impl From<f64> for CommandOptionChoiceValue {
205    fn from(value: f64) -> Self {
206        CommandOptionChoiceValue::Number(value)
207    }
208}
209
210impl From<String> for CommandOptionChoiceValue {
211    fn from(value: String) -> Self {
212        CommandOptionChoiceValue::String(value)
213    }
214}
215
216impl TryFrom<CommandOptionChoiceValue> for i64 {
217    type Error = CommandOptionChoiceValue;
218
219    fn try_from(value: CommandOptionChoiceValue) -> Result<Self, Self::Error> {
220        match value {
221            CommandOptionChoiceValue::Integer(inner) => Ok(inner),
222            _ => Err(value),
223        }
224    }
225}
226
227impl TryFrom<CommandOptionChoiceValue> for f64 {
228    type Error = CommandOptionChoiceValue;
229
230    fn try_from(value: CommandOptionChoiceValue) -> Result<Self, Self::Error> {
231        match value {
232            CommandOptionChoiceValue::Number(inner) => Ok(inner),
233            _ => Err(value),
234        }
235    }
236}
237
238impl TryFrom<CommandOptionChoiceValue> for String {
239    type Error = CommandOptionChoiceValue;
240
241    fn try_from(value: CommandOptionChoiceValue) -> Result<Self, Self::Error> {
242        match value {
243            CommandOptionChoiceValue::String(inner) => Ok(inner),
244            _ => Err(value),
245        }
246    }
247}
248
249/// Type used in the `max_value` and `min_value` [`CommandOption`] field.
250///
251/// Note that the right variant must be selected based on the
252/// [`CommandOption`]'s [`CommandOptionType`].
253#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
254#[serde(untagged)]
255pub enum CommandOptionValue {
256    /// Integer type.
257    Integer(i64),
258    /// Number type.
259    Number(f64),
260}
261
262impl From<i64> for CommandOptionValue {
263    fn from(value: i64) -> Self {
264        CommandOptionValue::Integer(value)
265    }
266}
267
268impl From<f64> for CommandOptionValue {
269    fn from(value: f64) -> Self {
270        CommandOptionValue::Number(value)
271    }
272}
273
274impl TryFrom<CommandOptionValue> for i64 {
275    type Error = CommandOptionValue;
276
277    fn try_from(value: CommandOptionValue) -> Result<Self, Self::Error> {
278        #[allow(clippy::match_wildcard_for_single_variants)]
279        match value {
280            CommandOptionValue::Integer(inner) => Ok(inner),
281            _ => Err(value),
282        }
283    }
284}
285
286impl TryFrom<CommandOptionValue> for f64 {
287    type Error = CommandOptionValue;
288
289    fn try_from(value: CommandOptionValue) -> Result<Self, Self::Error> {
290        #[allow(clippy::match_wildcard_for_single_variants)]
291        match value {
292            CommandOptionValue::Number(inner) => Ok(inner),
293            _ => Err(value),
294        }
295    }
296}
297
298impl CommandOptionValue {
299    /// This range contains integer values that safely can be
300    /// represented as a 64-bit floating point value.
301    ///
302    /// Values outside of this range will result in a `400 Bad
303    /// Request`.
304    pub const INTEGER_RANGE: Range<i64> =
305        -(2_i64.pow(f64::MANTISSA_DIGITS) - 1)..(2_i64.pow(f64::MANTISSA_DIGITS));
306    /// This range contains all floating point values that can be
307    /// safely used as Discord Number values.
308    ///
309    /// Values outside of this range will result in a `400 Bad
310    /// Request`.
311    // As we can see above we are within 52 bits on the left, but uses
312    // 53 bits on the right.  We ensure to be within 52 bits on the
313    // right below by subtracting 1 first and using RangeInclusive.
314    #[allow(clippy::cast_precision_loss)]
315    pub const NUMBER_RANGE: RangeInclusive<f64> =
316        (Self::INTEGER_RANGE.start as f64)..=((Self::INTEGER_RANGE.end - 1) as f64);
317}
318
319/// Type of a [`CommandOption`].
320#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)]
321#[non_exhaustive]
322#[repr(u8)]
323pub enum CommandOptionType {
324    SubCommand = 1,
325    SubCommandGroup = 2,
326    String = 3,
327    Integer = 4,
328    Boolean = 5,
329    User = 6,
330    Channel = 7,
331    Role = 8,
332    Mentionable = 9,
333    Number = 10,
334    Attachment = 11,
335}
336
337impl CommandOptionType {
338    pub const fn kind(self) -> &'static str {
339        match self {
340            CommandOptionType::SubCommand => "SubCommand",
341            CommandOptionType::SubCommandGroup => "SubCommandGroup",
342            CommandOptionType::String => "String",
343            CommandOptionType::Integer => "Integer",
344            CommandOptionType::Boolean => "Boolean",
345            CommandOptionType::User => "User",
346            CommandOptionType::Channel => "Channel",
347            CommandOptionType::Role => "Role",
348            CommandOptionType::Mentionable => "Mentionable",
349            CommandOptionType::Number => "Number",
350            CommandOptionType::Attachment => "Attachment",
351        }
352    }
353}