serenity_commands/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2//! A library for creating/parsing [`serenity`] slash commands.
3//!
4//! # Examples
5//!
6//! ```rust
7//! use serenity::all::{
8//!     async_trait, Client, Context, CreateInteractionResponse, CreateInteractionResponseMessage,
9//!     EventHandler, GatewayIntents, GuildId, Interaction,
10//! };
11//! use serenity_commands::{Command, Commands, SubCommand};
12//!
13//! #[derive(Debug, Commands)]
14//! enum AllCommands {
15//!     /// Ping the bot.
16//!     Ping,
17//!
18//!     /// Echo a message.
19//!     Echo {
20//!         /// The message to echo.
21//!         message: String,
22//!     },
23//!
24//!     /// Perform math operations.
25//!     Math(MathCommand),
26//! }
27//!
28//! impl AllCommands {
29//!     fn run(self) -> String {
30//!         match self {
31//!             Self::Ping => "Pong!".to_string(),
32//!             Self::Echo { message } => message,
33//!             Self::Math(math) => math.run().to_string(),
34//!         }
35//!     }
36//! }
37//!
38//! #[derive(Debug, Command)]
39//! enum MathCommand {
40//!     /// Add two numbers.
41//!     Add(BinaryOperation),
42//!
43//!     /// Subtract two numbers.
44//!     Subtract(BinaryOperation),
45//!
46//!     /// Multiply two numbers.
47//!     Multiply(BinaryOperation),
48//!
49//!     /// Divide two numbers.
50//!     Divide(BinaryOperation),
51//!
52//!     /// Negate a number.
53//!     Negate {
54//!         /// The number to negate.
55//!         a: f64,
56//!     },
57//! }
58//!
59//! impl MathCommand {
60//!     fn run(self) -> f64 {
61//!         match self {
62//!             Self::Add(BinaryOperation { a, b }) => a + b,
63//!             Self::Subtract(BinaryOperation { a, b }) => a - b,
64//!             Self::Multiply(BinaryOperation { a, b }) => a * b,
65//!             Self::Divide(BinaryOperation { a, b }) => a / b,
66//!             Self::Negate { a } => -a,
67//!         }
68//!     }
69//! }
70//!
71//! #[derive(Debug, SubCommand)]
72//! struct BinaryOperation {
73//!     /// The first number.
74//!     a: f64,
75//!
76//!     /// The second number.
77//!     b: f64,
78//! }
79//!
80//! struct Handler {
81//!     guild_id: GuildId,
82//! }
83//!
84//! #[async_trait]
85//! impl EventHandler for Handler {
86//!     async fn ready(&self, ctx: Context, _: serenity::all::Ready) {
87//!         self.guild_id
88//!             .set_commands(&ctx, AllCommands::create_commands())
89//!             .await
90//!             .unwrap();
91//!     }
92//!
93//!     async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
94//!         if let Interaction::Command(command) = interaction {
95//!             let command_data = AllCommands::from_command_data(&command.data).unwrap();
96//!             command
97//!                 .create_response(
98//!                     ctx,
99//!                     CreateInteractionResponse::Message(
100//!                         CreateInteractionResponseMessage::new().content(command_data.run()),
101//!                     ),
102//!                 )
103//!                 .await
104//!                 .unwrap();
105//!         }
106//!     }
107//! }
108//! ```
109
110use std::fmt::Debug;
111
112use serenity::all::{
113    AttachmentId, ChannelId, CommandData, CommandDataOption, CommandDataOptionValue,
114    CommandOptionType, CreateCommand, CreateCommandOption, GenericId, RoleId, UserId,
115};
116/// Derives [`BasicOption`].
117///
118/// `option_type` can be `"string"`, `"integer"`, or `"number"`.
119///
120/// # Examples
121///
122/// ```rust
123/// use serenity_commands::BasicOption;
124///
125/// #[derive(Debug, BasicOption)]
126/// #[choice(option_type = "integer")]
127/// enum Medal {
128///     #[choice(name = "Gold", value = 1)]
129///     Gold,
130///
131///     #[choice(name = "Silver", value = 2)]
132///     Silver,
133///
134///     #[choice(name = "Bronze", value = 3)]
135///     Bronze,
136/// }
137/// ```
138pub use serenity_commands_macros::BasicOption;
139/// Derives [`Command`].
140///
141/// # Examples
142///
143/// ## Struct
144///
145/// Each field must implement [`BasicOption`].
146///
147/// ```rust
148/// use serenity_commands::Command;
149///
150/// #[derive(Command)]
151/// struct Add {
152///     /// First number.
153///     a: f64,
154///
155///     /// Second number.
156///     b: f64,
157/// }
158/// ```
159///
160/// ## Enum
161///
162/// Each field of named variants must implement [`BasicOption`].
163///
164/// The inner type of newtype variants must implement [`SubCommandGroup`] (or,
165/// by extension, [`SubCommand`], as [`SubCommand`] is a sub-trait of
166/// [`SubCommandGroup`]).
167///
168/// ```rust
169/// use serenity_commands::{Command, SubCommandGroup};
170///
171/// #[derive(SubCommandGroup)]
172/// enum ModUtilities {
173///     // ...
174/// }
175///
176/// #[derive(Command)]
177/// enum Utilities {
178///     /// Ping the bot.
179///     Ping,
180///
181///     /// Add two numbers.
182///     Add {
183///         /// First number.
184///         a: f64,
185///
186///         /// Second number.
187///         b: f64,
188///     },
189///
190///     /// Moderation utilities.
191///     Mod(ModUtilities),
192/// }
193pub use serenity_commands_macros::Command;
194/// Derives [`Commands`].
195///
196/// # Examples
197///
198/// Each field of named variants must implement [`Command`].
199///
200/// The inner type of newtype variants must implement [`Command`].
201///
202/// ```rust
203/// use serenity_commands::{Command, Commands};
204///
205/// #[derive(Command)]
206/// enum MathCommand {
207///     // ...
208/// }
209///
210/// #[derive(Commands)]
211/// enum AllCommands {
212///     /// Ping the bot.
213///     Ping,
214///
215///     /// Echo a message.
216///     Echo {
217///         /// The message to echo.
218///         message: String,
219///     },
220///
221///     /// Do math operations.
222///     Math(MathCommand),
223/// }
224pub use serenity_commands_macros::Commands;
225/// Derives [`SubCommand`].
226///
227/// Each field must implement [`BasicOption`].
228///
229/// # Examples
230///
231/// ```rust
232/// use serenity_commands::SubCommand;
233///
234/// #[derive(SubCommand)]
235/// struct Add {
236///     /// First number.
237///     a: f64,
238///
239///     /// Second number.
240///     b: f64,
241/// }
242/// ```
243pub use serenity_commands_macros::SubCommand;
244/// Derives [`SubCommandGroup`].
245///
246/// Each field of named variants must implement [`BasicOption`].
247///
248/// The inner type of newtype variants must implement [`SubCommand`].
249///
250/// # Examples
251///
252/// ```rust
253/// use serenity_commands::{SubCommand, SubCommandGroup};
254///
255/// #[derive(SubCommand)]
256/// struct AddSubCommand {
257///     /// First number.
258///     a: f64,
259///
260///     /// Second number.
261///     b: f64,
262/// }
263///
264/// #[derive(SubCommandGroup)]
265/// enum Math {
266///     /// Add two numbers.
267///     Add(AddSubCommand),
268///
269///     /// Negate a number.
270///     Negate {
271///         /// The number to negate.
272///         a: f64,
273///     },
274/// }
275pub use serenity_commands_macros::SubCommandGroup;
276use thiserror::Error;
277
278/// A type alias for [`std::result::Result`]s which use [`Error`] as the error
279/// type.
280///
281/// [`Error`]: enum@Error
282pub type Result<T> = std::result::Result<T, Error>;
283
284/// An error which can occur when extracting data from a command interaction.
285#[derive(Debug, Error)]
286pub enum Error {
287    /// An unknown command was provided.
288    #[error("unknown command: {0}")]
289    UnknownCommand(String),
290
291    /// An incorrect command option type was provided.
292    #[error("incorrect command option type: got {got:?}, expected {expected:?}")]
293    IncorrectCommandOptionType {
294        /// The type of command option that was provided.
295        got: CommandOptionType,
296
297        /// The type of command option that was expected.
298        expected: CommandOptionType,
299    },
300
301    /// An incorrect number of command options were provided.
302    #[error("incorrect command option count: got {got}, expected {expected}")]
303    IncorrectCommandOptionCount {
304        /// The number of command options that were provided.
305        got: usize,
306
307        /// The number of command options that were expected.
308        expected: usize,
309    },
310
311    /// An unknown command option was provided.
312    #[error("unknown command option: {0}")]
313    UnknownCommandOption(String),
314
315    /// An unknown autocomplete option was provided.
316    #[error("unknown autocomplete option: {0}")]
317    UnknownAutocompleteOption(String),
318
319    /// A required command option was not provided.
320    #[error("required command option not provided")]
321    MissingRequiredCommandOption,
322
323    /// An unexpected autocomplete option was provided.
324    #[error("unexpected autocomplete option")]
325    UnexpectedAutocompleteOption,
326
327    /// An autocomplete option was not provided.
328    #[error("autocomplete option not provided")]
329    MissingAutocompleteOption,
330
331    /// An unknown choice was provided.
332    #[error("unknown choice: {0}")]
333    UnknownChoice(String),
334
335    /// An error occurred within a custom implementation.
336    #[error(transparent)]
337    Custom(#[from] Box<dyn std::error::Error + Send + Sync>),
338}
339
340/// A utility for creating commands and extracting their data from application
341/// commands.
342pub trait Commands: Sized {
343    /// List of top-level commands.
344    fn create_commands() -> Vec<CreateCommand>;
345
346    /// Extract data from [`CommandData`].
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if the implementation fails.
351    fn from_command_data(data: &CommandData) -> Result<Self>;
352}
353
354/// A top-level command for use with [`Commands`].
355pub trait Command: Sized {
356    /// Create the command.
357    fn create_command(name: impl Into<String>, description: impl Into<String>) -> CreateCommand;
358
359    /// Extract data from a list of [`CommandDataOption`]s.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the implementation fails.
364    fn from_options(options: &[CommandDataOption]) -> Result<Self>;
365}
366
367/// A sub-command group which can be nested inside of a [`Command`] and contains
368/// [`SubCommand`]s.
369///
370/// This is a super-trait of [`SubCommand`], as a [`SubCommand`] can be used
371/// anywhere a [`SubCommandGroup`] can.
372pub trait SubCommandGroup: Sized {
373    /// Create the command option.
374    fn create_option(
375        name: impl Into<String>,
376        description: impl Into<String>,
377    ) -> CreateCommandOption;
378
379    /// Extract data from a [`CommandDataOptionValue`].
380    ///
381    /// # Errors
382    ///
383    /// Returns an error if the implementation fails.
384    fn from_value(value: &CommandDataOptionValue) -> Result<Self>;
385}
386
387/// A sub-command which can be nested inside of a [`Command`] or
388/// [`SubCommandGroup`].
389///
390/// This is a sub-trait of [`SubCommandGroup`], as a [`SubCommand`] can be used
391/// anywhere a [`SubCommandGroup`] can.
392pub trait SubCommand: SubCommandGroup {}
393
394/// A basic option which can be nested inside of [`Command`]s or
395/// [`SubCommand`]s.
396///
397/// This trait is implemented already for most primitive types.
398pub trait BasicOption: Sized {
399    /// The type of this option when it may not be fully parseable.
400    ///
401    /// This will usually occur when this field is part of an autocomplete
402    /// interaction. This will usually be [`String`] or an integer type, and is
403    /// present so an autocomplete interaction can still be handled if a field
404    /// is not yet parseable to the type of the option.
405    ///
406    /// As this should be a type that can reliably be parsed from a
407    /// [`CommandDataOptionValue`], it's [`BasicOption::from_value`]
408    /// implementation should ideally be infallible.
409    type Partial: BasicOption;
410
411    /// Create the command option.
412    fn create_option(
413        name: impl Into<String>,
414        description: impl Into<String>,
415    ) -> CreateCommandOption;
416
417    /// Extract data from a [`CommandDataOptionValue`].
418    ///
419    /// # Errors
420    ///
421    /// Returns an error if the implementation fails.
422    fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self>;
423}
424
425impl<T: BasicOption> BasicOption for Option<T> {
426    /// Delegates to `T`'s [`BasicOption::Partial`] type.
427    type Partial = T::Partial;
428
429    /// Delegates to `T`'s [`BasicOption::create_option`] implementation, but
430    /// sets [`CreateCommandOption::required`] to `false` afterwards.
431    fn create_option(
432        name: impl Into<String>,
433        description: impl Into<String>,
434    ) -> CreateCommandOption {
435        T::create_option(name, description).required(false)
436    }
437
438    /// Only delegates to `T`'s [`BasicOption::from_value`] implementation if
439    /// `value` is [`Some`].
440    fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
441        value.map(|option| T::from_value(Some(option))).transpose()
442    }
443}
444
445macro_rules! impl_command_option {
446    ($($Variant:ident($($Ty:ty),* $(,)?)),* $(,)?) => {
447        $($(
448            impl BasicOption for $Ty {
449                type Partial = Self;
450
451                fn create_option(name: impl Into<String>, description: impl Into<String>) -> CreateCommandOption {
452                    CreateCommandOption::new(CommandOptionType::$Variant, name, description)
453                        .required(true)
454                }
455
456                fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
457                    let value = value.ok_or(Error::MissingRequiredCommandOption)?;
458
459                    match value {
460                        CommandDataOptionValue::$Variant(v) => Ok(v.clone() as _),
461                        _ => Err(Error::IncorrectCommandOptionType {
462                            got: value.kind(),
463                            expected: CommandOptionType::$Variant,
464                        }),
465                    }
466                }
467            }
468        )*)*
469    };
470}
471
472impl_command_option! {
473    String(String),
474    Boolean(bool),
475    User(UserId),
476    Channel(ChannelId),
477    Role(RoleId),
478    Mentionable(GenericId),
479    Attachment(AttachmentId),
480}
481
482macro_rules! impl_number_command_option {
483    ($($Ty:ty),* $(,)?) => {
484        $(
485            impl BasicOption for $Ty {
486                type Partial = Self;
487
488                fn create_option(name: impl Into<String>, description: impl Into<String>) -> CreateCommandOption {
489                    CreateCommandOption::new(CommandOptionType::Number, name, description)
490                        .required(true)
491                }
492
493                fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
494                    let value = value.ok_or(Error::MissingRequiredCommandOption)?;
495
496                    #[allow(clippy::cast_possible_truncation)]
497                    match value {
498                        CommandDataOptionValue::Number(v) => Ok(*v as _),
499                        _ => Err(Error::IncorrectCommandOptionType {
500                            got: value.kind(),
501                            expected: CommandOptionType::Number,
502                        }),
503                    }
504                }
505            }
506
507        )*
508    };
509}
510
511impl_number_command_option!(f32, f64);
512
513macro_rules! impl_integer_command_option {
514    ($($Ty:ty),* $(,)?) => {
515        $(
516            impl BasicOption for $Ty {
517                type Partial = i64;
518
519                fn create_option(name: impl Into<String>, description: impl Into<String>) -> CreateCommandOption {
520                    CreateCommandOption::new(CommandOptionType::Integer, name, description)
521                        .required(true)
522                }
523
524                fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
525                    let value = value.ok_or(Error::MissingRequiredCommandOption)?;
526
527                    #[allow(
528                        clippy::cast_possible_truncation,
529                        clippy::cast_sign_loss,
530                        clippy::cast_lossless
531                    )]
532                    match value {
533                        CommandDataOptionValue::Integer(v) => Ok(*v as _),
534                        _ => Err(Error::IncorrectCommandOptionType {
535                            got: value.kind(),
536                            expected: CommandOptionType::Integer,
537                        }),
538                    }
539                }
540            }
541        )*
542    };
543}
544
545impl_integer_command_option!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
546
547impl BasicOption for char {
548    type Partial = String;
549
550    fn create_option(
551        name: impl Into<String>,
552        description: impl Into<String>,
553    ) -> CreateCommandOption {
554        CreateCommandOption::new(CommandOptionType::String, name, description)
555            .min_length(1)
556            .max_length(1)
557            .required(true)
558    }
559
560    fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
561        let s = String::from_value(value)?;
562
563        let mut chars = s.chars();
564
565        match (chars.next(), chars.next()) {
566            (Some(c), None) => Ok(c),
567            _ => Err(Error::Custom("expected single character".into())),
568        }
569    }
570}
571
572/// A field which may be partially parsed.
573///
574/// This is used for fields which may not be fully parseable, such as when
575/// handling autocomplete interactions.
576pub enum PartialOption<T: BasicOption> {
577    /// A successfully parsed value.
578    Value(T),
579
580    /// A partially parsed value, along with the error that occurred while
581    /// attempting to parse it.
582    Partial(T::Partial, Error),
583
584    /// A value that has not been entered yet.
585    None,
586}
587
588impl<T: BasicOption> PartialOption<T> {
589    /// Extract the parsed value from this field.
590    pub fn into_value(self) -> Option<T> {
591        match self {
592            Self::Value(value) => Some(value),
593            Self::Partial(_, _) | Self::None => None,
594        }
595    }
596
597    /// Extract the partially parsed value and the error that occurred while
598    /// attempting to parse it from this field.
599    pub fn into_partial(self) -> Option<(T::Partial, Error)> {
600        match self {
601            Self::Partial(value, error) => Some((value, error)),
602            Self::Value(_) | Self::None => None,
603        }
604    }
605
606    /// Get a reference to the parsed value from this field.
607    pub const fn as_value(&self) -> Option<&T> {
608        match self {
609            Self::Value(value) => Some(value),
610            Self::Partial(_, _) | Self::None => None,
611        }
612    }
613
614    /// Get a reference to the partially parsed value and the error that
615    /// occurred while attempting to parse it from this field.
616    pub const fn as_partial(&self) -> Option<(&T::Partial, &Error)> {
617        match self {
618            Self::Partial(value, error) => Some((value, error)),
619            Self::Value(_) | Self::None => None,
620        }
621    }
622
623    /// Check if this field is a parsed value.
624    pub const fn is_value(&self) -> bool {
625        matches!(self, Self::Value(_))
626    }
627
628    /// Check if this field is a partially parsed value.
629    pub const fn is_partial(&self) -> bool {
630        matches!(self, Self::Partial(_, _))
631    }
632
633    /// Check if this field is empty.
634    pub const fn is_none(&self) -> bool {
635        matches!(self, Self::None)
636    }
637}
638
639impl<T: BasicOption<Partial = T>> PartialOption<T> {
640    /// Convert this field into the parsed value, if it is present.
641    pub fn into_inner(self) -> Option<T> {
642        match self {
643            Self::Value(value) | Self::Partial(value, _) => Some(value),
644            Self::None => None,
645        }
646    }
647
648    /// Get a reference to the parsed value, if it is present.
649    pub const fn as_inner(&self) -> Option<&T> {
650        match self {
651            Self::Value(value) | Self::Partial(value, _) => Some(value),
652            Self::None => None,
653        }
654    }
655}
656
657impl<T: BasicOption> PartialOption<Option<T>> {
658    /// Flatten this field into a [`PartialOption<T>`].
659    pub fn flatten(self) -> PartialOption<T> {
660        match self {
661            Self::Value(Some(value)) => PartialOption::Value(value),
662            Self::Partial(value, error) => PartialOption::Partial(value, error),
663            Self::Value(None) | Self::None => PartialOption::None,
664        }
665    }
666}
667
668impl<T: BasicOption> BasicOption for PartialOption<T> {
669    type Partial = <T::Partial as BasicOption>::Partial;
670
671    fn create_option(
672        name: impl Into<String>,
673        description: impl Into<String>,
674    ) -> CreateCommandOption {
675        T::create_option(name, description)
676    }
677
678    fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
679        match Option::<T>::from_value(value) {
680            Ok(Some(value)) => Ok(Self::Value(value)),
681            Ok(None) => Ok(Self::None),
682            Err(error) => Ok(Self::Partial(T::Partial::from_value(value)?, error)),
683        }
684    }
685}
686
687impl<T: BasicOption + Debug> Debug for PartialOption<T>
688where
689    T::Partial: Debug,
690{
691    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692        match self {
693            Self::Value(value) => f.debug_tuple("Value").field(value).finish(),
694            Self::Partial(value, error) => {
695                f.debug_tuple("Partial").field(value).field(error).finish()
696            }
697            Self::None => f.debug_tuple("None").finish(),
698        }
699    }
700}
701
702/// A trait for identifying types that support autocomplete interactions.
703pub trait SupportsAutocomplete {
704    /// The type of the autocomplete interaction.
705    type Autocomplete;
706}
707
708/// A helper type alias for extracting the autocomplete type from a type that
709/// supports autocomplete.
710pub type Autocomplete<T> = <T as SupportsAutocomplete>::Autocomplete;
711
712/// A utility for extracting data from an autocomplete interaction.
713pub trait AutocompleteCommands: Sized {
714    /// Extract data from [`CommandData`].
715    ///
716    /// # Errors
717    ///
718    /// Returns an error if the implementation fails.
719    fn from_command_data(data: &CommandData) -> Result<Self>;
720}
721
722/// A top-level command for use with [`AutocompleteCommands`].
723pub trait AutocompleteCommand: Sized {
724    /// Extract data from a list of [`CommandDataOption`]s.
725    ///
726    /// # Errors
727    ///
728    /// Returns an error if the implementation fails.
729    fn from_options(options: &[CommandDataOption]) -> Result<Self>;
730}
731
732/// A sub-command group which can be nested inside of an [`AutocompleteCommand`]
733/// and contains [`AutocompleteSubCommandOrGroup`]s.
734pub trait AutocompleteSubCommandOrGroup: Sized {
735    /// Extract data from a [`CommandDataOptionValue`].
736    ///
737    /// # Errors
738    ///
739    /// Returns an error if the implementation fails.
740    fn from_value(value: &CommandDataOptionValue) -> Result<Self>;
741}
742
743#[cfg(feature = "time")]
744mod time {
745    use serenity::all::{CommandDataOptionValue, CreateCommandOption};
746    use time::OffsetDateTime;
747
748    use crate::{BasicOption, Error, Result};
749
750    impl BasicOption for OffsetDateTime {
751        type Partial = i64;
752
753        fn create_option(
754            name: impl Into<String>,
755            description: impl Into<String>,
756        ) -> CreateCommandOption {
757            i64::create_option(name, description)
758        }
759
760        fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
761            let value = i64::from_value(value)?;
762
763            Self::from_unix_timestamp(value).map_err(|e| Error::Custom(Box::new(e)))
764        }
765    }
766}