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}