use std::borrow::Cow;
use twilight_model::{
application::{
command::CommandOptionValue as NumberCommandOptionValue,
interaction::{
application_command::{CommandData, CommandDataOption, CommandOptionValue},
InteractionChannel, InteractionDataResolved, InteractionMember,
},
},
channel::Attachment,
guild::Role,
id::{
marker::{AttachmentMarker, ChannelMarker, GenericMarker, RoleMarker, UserMarker},
Id,
},
user::User,
};
use super::internal::CommandOptionData;
use crate::error::{ParseError, ParseOptionError, ParseOptionErrorType};
pub trait CommandModel: Sized {
fn from_interaction(data: CommandInputData) -> Result<Self, ParseError>;
}
impl<T: CommandModel> CommandModel for Box<T> {
fn from_interaction(data: CommandInputData) -> Result<Self, ParseError> {
T::from_interaction(data).map(Box::new)
}
}
impl CommandModel for Vec<CommandDataOption> {
fn from_interaction(data: CommandInputData) -> Result<Self, ParseError> {
Ok(data.options)
}
}
pub trait CommandOption: Sized {
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType>;
}
#[derive(Debug, Clone, PartialEq)]
pub struct CommandInputData<'a> {
pub options: Vec<CommandDataOption>,
pub resolved: Option<Cow<'a, InteractionDataResolved>>,
}
impl<'a> CommandInputData<'a> {
pub fn parse_field<T>(&self, name: &str) -> Result<Option<T>, ParseError>
where
T: CommandOption,
{
let value = match self
.options
.iter()
.find(|option| option.name == name)
.map(|option| &option.value)
{
Some(value) => value.clone(),
None => return Ok(None),
};
match CommandOption::from_option(
value,
CommandOptionData::default(),
self.resolved.as_deref(),
) {
Ok(value) => Ok(Some(value)),
Err(kind) => Err(ParseError::Option(ParseOptionError {
field: name.to_string(),
kind,
})),
}
}
pub fn focused(&self) -> Option<&str> {
self.options
.iter()
.find(|option| matches!(option.value, CommandOptionValue::Focused(_, _)))
.map(|option| &*option.name)
}
pub fn from_option(
value: CommandOptionValue,
resolved: Option<&'a InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let options = match value {
CommandOptionValue::SubCommand(options)
| CommandOptionValue::SubCommandGroup(options) => options,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
Ok(CommandInputData {
options,
resolved: resolved.map(Cow::Borrowed),
})
}
}
impl From<CommandData> for CommandInputData<'_> {
fn from(data: CommandData) -> Self {
Self {
options: data.options,
resolved: data.resolved.map(Cow::Owned),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedUser {
pub resolved: User,
pub member: Option<InteractionMember>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(
clippy::large_enum_variant,
reason = "minor impact, boxing would add additional indirection"
)]
pub enum ResolvedMentionable {
User(ResolvedUser),
Role(Role),
}
impl ResolvedMentionable {
pub fn id(&self) -> Id<GenericMarker> {
match self {
ResolvedMentionable::User(user) => user.resolved.id.cast(),
ResolvedMentionable::Role(role) => role.id.cast(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AutocompleteValue<T> {
None,
Focused(String),
Completed(T),
}
macro_rules! lookup {
($resolved:ident.$cat:ident, $id:expr) => {
$resolved
.and_then(|resolved| resolved.$cat.get(&$id).cloned())
.ok_or_else(|| ParseOptionErrorType::LookupFailed($id.get()))
};
}
impl CommandOption for CommandOptionValue {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
Ok(value)
}
}
impl<T> CommandOption for AutocompleteValue<T>
where
T: CommandOption,
{
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::Focused(value, _) => Ok(Self::Focused(value)),
other => {
let parsed = T::from_option(other, data, resolved)?;
Ok(Self::Completed(parsed))
}
}
}
}
impl CommandOption for String {
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let value = match value {
CommandOptionValue::String(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
if let Some(min) = data.min_length {
if value.len() < usize::from(min) {
todo!()
}
}
if let Some(max) = data.max_length {
if value.len() > usize::from(max) {
todo!()
}
}
Ok(value)
}
}
impl CommandOption for Cow<'_, str> {
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
String::from_option(value, data, resolved).map(Cow::Owned)
}
}
impl CommandOption for i64 {
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let value = match value {
CommandOptionValue::Integer(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
if let Some(NumberCommandOptionValue::Integer(min)) = data.min_value {
if value < min {
return Err(ParseOptionErrorType::IntegerOutOfRange(value));
}
}
if let Some(NumberCommandOptionValue::Integer(max)) = data.max_value {
if value > max {
return Err(ParseOptionErrorType::IntegerOutOfRange(value));
}
}
Ok(value)
}
}
impl CommandOption for f64 {
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let value = match value {
CommandOptionValue::Number(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
if let Some(NumberCommandOptionValue::Number(min)) = data.min_value {
if value < min {
return Err(ParseOptionErrorType::NumberOutOfRange(value));
}
}
if let Some(NumberCommandOptionValue::Number(max)) = data.max_value {
if value > max {
return Err(ParseOptionErrorType::NumberOutOfRange(value));
}
}
Ok(value)
}
}
impl CommandOption for bool {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::Boolean(value) => Ok(value),
other => Err(ParseOptionErrorType::InvalidType(other.kind())),
}
}
}
impl CommandOption for Id<UserMarker> {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::User(value) => Ok(value),
other => Err(ParseOptionErrorType::InvalidType(other.kind())),
}
}
}
impl CommandOption for Id<ChannelMarker> {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::Channel(value) => Ok(value),
other => Err(ParseOptionErrorType::InvalidType(other.kind())),
}
}
}
impl CommandOption for Id<RoleMarker> {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::Role(value) => Ok(value),
other => Err(ParseOptionErrorType::InvalidType(other.kind())),
}
}
}
impl CommandOption for Id<GenericMarker> {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::Mentionable(value) => Ok(value),
other => Err(ParseOptionErrorType::InvalidType(other.kind())),
}
}
}
impl CommandOption for Id<AttachmentMarker> {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
_resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
match value {
CommandOptionValue::Attachment(value) => Ok(value),
other => Err(ParseOptionErrorType::InvalidType(other.kind())),
}
}
}
impl CommandOption for Attachment {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let attachment_id = match value {
CommandOptionValue::Attachment(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
lookup!(resolved.attachments, attachment_id)
}
}
impl CommandOption for User {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let user_id = match value {
CommandOptionValue::User(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
lookup!(resolved.users, user_id)
}
}
impl CommandOption for ResolvedUser {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let user_id = match value {
CommandOptionValue::User(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
Ok(Self {
resolved: lookup!(resolved.users, user_id)?,
member: lookup!(resolved.members, user_id).ok(),
})
}
}
impl CommandOption for ResolvedMentionable {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let id = match value {
CommandOptionValue::Mentionable(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
let user_id = id.cast();
if let Ok(user) = lookup!(resolved.users, user_id) {
let resolved_user = ResolvedUser {
resolved: user,
member: lookup!(resolved.members, user_id).ok(),
};
return Ok(Self::User(resolved_user));
}
let role_id = id.cast();
if let Ok(role) = lookup!(resolved.roles, role_id) {
return Ok(Self::Role(role));
}
Err(ParseOptionErrorType::LookupFailed(id.into()))
}
}
impl CommandOption for InteractionChannel {
fn from_option(
value: CommandOptionValue,
data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let resolved = match value {
CommandOptionValue::Channel(value) => lookup!(resolved.channels, value)?,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
if let Some(channel_types) = data.channel_types {
if !channel_types.contains(&resolved.kind) {
return Err(ParseOptionErrorType::InvalidChannelType(resolved.kind));
}
}
Ok(resolved)
}
}
impl CommandOption for Role {
fn from_option(
value: CommandOptionValue,
_data: CommandOptionData,
resolved: Option<&InteractionDataResolved>,
) -> Result<Self, ParseOptionErrorType> {
let role_id = match value {
CommandOptionValue::Role(value) => value,
other => return Err(ParseOptionErrorType::InvalidType(other.kind())),
};
lookup!(resolved.roles, role_id)
}
}