#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::fmt::Debug;
use serenity::all::{
AttachmentId, ChannelId, CommandData, CommandDataOption, CommandDataOptionValue,
CommandOptionType, CreateCommand, CreateCommandOption, GenericId, RoleId, UserId,
};
pub use serenity_commands_macros::BasicOption;
pub use serenity_commands_macros::Command;
pub use serenity_commands_macros::Commands;
pub use serenity_commands_macros::SubCommand;
pub use serenity_commands_macros::SubCommandGroup;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("unknown command: {0}")]
UnknownCommand(String),
#[error("incorrect command option type: got {got:?}, expected {expected:?}")]
IncorrectCommandOptionType {
got: CommandOptionType,
expected: CommandOptionType,
},
#[error("incorrect command option count: got {got}, expected {expected}")]
IncorrectCommandOptionCount {
got: usize,
expected: usize,
},
#[error("unknown command option: {0}")]
UnknownCommandOption(String),
#[error("unknown autocomplete option: {0}")]
UnknownAutocompleteOption(String),
#[error("required command option not provided")]
MissingRequiredCommandOption,
#[error("unexpected autocomplete option")]
UnexpectedAutocompleteOption,
#[error("autocomplete option not provided")]
MissingAutocompleteOption,
#[error("unknown choice: {0}")]
UnknownChoice(String),
#[error(transparent)]
Custom(#[from] Box<dyn std::error::Error + Send + Sync>),
}
pub trait Commands: Sized {
fn create_commands() -> Vec<CreateCommand>;
fn from_command_data(data: &CommandData) -> Result<Self>;
}
pub trait Command: Sized {
fn create_command(name: impl Into<String>, description: impl Into<String>) -> CreateCommand;
fn from_options(options: &[CommandDataOption]) -> Result<Self>;
}
pub trait SubCommandGroup: Sized {
fn create_option(
name: impl Into<String>,
description: impl Into<String>,
) -> CreateCommandOption;
fn from_value(value: &CommandDataOptionValue) -> Result<Self>;
}
pub trait SubCommand: SubCommandGroup {}
pub trait BasicOption: Sized {
type Partial: BasicOption;
fn create_option(
name: impl Into<String>,
description: impl Into<String>,
) -> CreateCommandOption;
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self>;
}
impl<T: BasicOption> BasicOption for Option<T> {
type Partial = T::Partial;
fn create_option(
name: impl Into<String>,
description: impl Into<String>,
) -> CreateCommandOption {
T::create_option(name, description).required(false)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
value.map(|option| T::from_value(Some(option))).transpose()
}
}
macro_rules! impl_command_option {
($($Variant:ident($($Ty:ty),* $(,)?)),* $(,)?) => {
$($(
impl BasicOption for $Ty {
type Partial = Self;
fn create_option(name: impl Into<String>, description: impl Into<String>) -> CreateCommandOption {
CreateCommandOption::new(CommandOptionType::$Variant, name, description)
.required(true)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
let value = value.ok_or(Error::MissingRequiredCommandOption)?;
match value {
CommandDataOptionValue::$Variant(v) => Ok(v.clone() as _),
_ => Err(Error::IncorrectCommandOptionType {
got: value.kind(),
expected: CommandOptionType::$Variant,
}),
}
}
}
)*)*
};
}
impl_command_option! {
String(String),
Boolean(bool),
User(UserId),
Channel(ChannelId),
Role(RoleId),
Mentionable(GenericId),
Attachment(AttachmentId),
}
macro_rules! impl_number_command_option {
($($Ty:ty),* $(,)?) => {
$(
impl BasicOption for $Ty {
type Partial = Self;
fn create_option(name: impl Into<String>, description: impl Into<String>) -> CreateCommandOption {
CreateCommandOption::new(CommandOptionType::Number, name, description)
.required(true)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
let value = value.ok_or(Error::MissingRequiredCommandOption)?;
#[allow(clippy::cast_possible_truncation)]
match value {
CommandDataOptionValue::Number(v) => Ok(*v as _),
_ => Err(Error::IncorrectCommandOptionType {
got: value.kind(),
expected: CommandOptionType::Number,
}),
}
}
}
)*
};
}
impl_number_command_option!(f32, f64);
macro_rules! impl_integer_command_option {
($($Ty:ty),* $(,)?) => {
$(
impl BasicOption for $Ty {
type Partial = i64;
fn create_option(name: impl Into<String>, description: impl Into<String>) -> CreateCommandOption {
CreateCommandOption::new(CommandOptionType::Integer, name, description)
.required(true)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
let value = value.ok_or(Error::MissingRequiredCommandOption)?;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_lossless
)]
match value {
CommandDataOptionValue::Integer(v) => Ok(*v as _),
_ => Err(Error::IncorrectCommandOptionType {
got: value.kind(),
expected: CommandOptionType::Integer,
}),
}
}
}
)*
};
}
impl_integer_command_option!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
impl BasicOption for char {
type Partial = String;
fn create_option(
name: impl Into<String>,
description: impl Into<String>,
) -> CreateCommandOption {
CreateCommandOption::new(CommandOptionType::String, name, description)
.min_length(1)
.max_length(1)
.required(true)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
let s = String::from_value(value)?;
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(c),
_ => Err(Error::Custom("expected single character".into())),
}
}
}
pub enum PartialOption<T: BasicOption> {
Value(T),
Partial(T::Partial, Error),
None,
}
impl<T: BasicOption> PartialOption<T> {
pub fn into_value(self) -> Option<T> {
match self {
Self::Value(value) => Some(value),
Self::Partial(_, _) | Self::None => None,
}
}
pub fn into_partial(self) -> Option<(T::Partial, Error)> {
match self {
Self::Partial(value, error) => Some((value, error)),
Self::Value(_) | Self::None => None,
}
}
pub const fn as_value(&self) -> Option<&T> {
match self {
Self::Value(value) => Some(value),
Self::Partial(_, _) | Self::None => None,
}
}
pub const fn as_partial(&self) -> Option<(&T::Partial, &Error)> {
match self {
Self::Partial(value, error) => Some((value, error)),
Self::Value(_) | Self::None => None,
}
}
pub const fn is_value(&self) -> bool {
matches!(self, Self::Value(_))
}
pub const fn is_partial(&self) -> bool {
matches!(self, Self::Partial(_, _))
}
pub const fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
impl<T: BasicOption<Partial = T>> PartialOption<T> {
pub fn into_inner(self) -> Option<T> {
match self {
Self::Value(value) | Self::Partial(value, _) => Some(value),
Self::None => None,
}
}
pub const fn as_inner(&self) -> Option<&T> {
match self {
Self::Value(value) | Self::Partial(value, _) => Some(value),
Self::None => None,
}
}
}
impl<T: BasicOption> PartialOption<Option<T>> {
pub fn flatten(self) -> PartialOption<T> {
match self {
Self::Value(Some(value)) => PartialOption::Value(value),
Self::Partial(value, error) => PartialOption::Partial(value, error),
Self::Value(None) | Self::None => PartialOption::None,
}
}
}
impl<T: BasicOption> BasicOption for PartialOption<T> {
type Partial = <T::Partial as BasicOption>::Partial;
fn create_option(
name: impl Into<String>,
description: impl Into<String>,
) -> CreateCommandOption {
T::create_option(name, description)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
match Option::<T>::from_value(value) {
Ok(Some(value)) => Ok(Self::Value(value)),
Ok(None) => Ok(Self::None),
Err(error) => Ok(Self::Partial(T::Partial::from_value(value)?, error)),
}
}
}
impl<T: BasicOption + Debug> Debug for PartialOption<T>
where
T::Partial: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Value(value) => f.debug_tuple("Value").field(value).finish(),
Self::Partial(value, error) => {
f.debug_tuple("Partial").field(value).field(error).finish()
}
Self::None => f.debug_tuple("None").finish(),
}
}
}
pub trait SupportsAutocomplete {
type Autocomplete;
}
pub type Autocomplete<T> = <T as SupportsAutocomplete>::Autocomplete;
pub trait AutocompleteCommands: Sized {
fn from_command_data(data: &CommandData) -> Result<Self>;
}
pub trait AutocompleteCommand: Sized {
fn from_options(options: &[CommandDataOption]) -> Result<Self>;
}
pub trait AutocompleteSubCommandOrGroup: Sized {
fn from_value(value: &CommandDataOptionValue) -> Result<Self>;
}
#[cfg(feature = "time")]
mod time {
use serenity::all::{CommandDataOptionValue, CreateCommandOption};
use time::OffsetDateTime;
use crate::{BasicOption, Error, Result};
impl BasicOption for OffsetDateTime {
type Partial = i64;
fn create_option(
name: impl Into<String>,
description: impl Into<String>,
) -> CreateCommandOption {
i64::create_option(name, description)
}
fn from_value(value: Option<&CommandDataOptionValue>) -> Result<Self> {
let value = i64::from_value(value)?;
Self::from_unix_timestamp(value).map_err(|e| Error::Custom(Box::new(e)))
}
}
}