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}