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}