use crate::util::is_false;
use serde::{
de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::fmt::{Formatter, Result as FmtResult};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum CommandOption {
SubCommand(OptionsCommandOptionData),
SubCommandGroup(OptionsCommandOptionData),
String(ChoiceCommandOptionData),
Integer(ChoiceCommandOptionData),
Boolean(BaseCommandOptionData),
User(BaseCommandOptionData),
Channel(BaseCommandOptionData),
Role(BaseCommandOptionData),
Mentionable(BaseCommandOptionData),
}
impl CommandOption {
pub const fn kind(&self) -> CommandOptionType {
match self {
CommandOption::SubCommand(_) => CommandOptionType::SubCommand,
CommandOption::SubCommandGroup(_) => CommandOptionType::SubCommandGroup,
CommandOption::String(_) => CommandOptionType::String,
CommandOption::Integer(_) => CommandOptionType::Integer,
CommandOption::Boolean(_) => CommandOptionType::Boolean,
CommandOption::User(_) => CommandOptionType::User,
CommandOption::Channel(_) => CommandOptionType::Channel,
CommandOption::Role(_) => CommandOptionType::Role,
CommandOption::Mentionable(_) => CommandOptionType::Mentionable,
}
}
pub const fn is_required(&self) -> bool {
match self {
CommandOption::SubCommand(data) | CommandOption::SubCommandGroup(data) => data.required,
CommandOption::String(data) | CommandOption::Integer(data) => data.required,
CommandOption::Boolean(data)
| CommandOption::User(data)
| CommandOption::Channel(data)
| CommandOption::Role(data)
| CommandOption::Mentionable(data) => data.required,
}
}
}
impl<'de> Deserialize<'de> for CommandOption {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(OptionVisitor)
}
}
#[derive(Serialize)]
struct CommandOptionEnvelope<'ser> {
#[serde(skip_serializing_if = "Option::is_none")]
choices: Option<&'ser [CommandOptionChoice]>,
description: &'ser str,
name: &'ser str,
#[serde(skip_serializing_if = "Option::is_none")]
options: Option<&'ser [CommandOption]>,
#[serde(skip_serializing_if = "is_false")]
required: bool,
#[serde(rename = "type")]
kind: CommandOptionType,
}
impl Serialize for CommandOption {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let envelope = match self {
Self::SubCommand(data) | Self::SubCommandGroup(data) => CommandOptionEnvelope {
choices: None,
description: data.description.as_ref(),
name: data.name.as_ref(),
options: Some(data.options.as_ref()),
required: data.required,
kind: self.kind(),
},
Self::String(data) | Self::Integer(data) => CommandOptionEnvelope {
choices: Some(data.choices.as_ref()),
description: data.description.as_ref(),
name: data.name.as_ref(),
options: None,
required: data.required,
kind: self.kind(),
},
Self::Boolean(data)
| Self::User(data)
| Self::Channel(data)
| Self::Role(data)
| Self::Mentionable(data) => CommandOptionEnvelope {
choices: None,
description: data.description.as_ref(),
name: data.name.as_ref(),
options: None,
required: data.required,
kind: self.kind(),
},
};
envelope.serialize(serializer)
}
}
#[derive(Debug, Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum OptionField {
Choices,
Description,
Name,
Options,
Required,
Type,
}
struct OptionVisitor;
impl<'de> Visitor<'de> for OptionVisitor {
type Value = CommandOption;
fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str("struct CommandOption")
}
#[allow(clippy::too_many_lines)]
fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
let mut choices: Option<Option<Vec<CommandOptionChoice>>> = None;
let mut description: Option<String> = None;
let mut kind: Option<CommandOptionType> = None;
let mut name: Option<String> = None;
let mut options: Option<Option<Vec<CommandOption>>> = None;
let mut required: Option<bool> = None;
let span = tracing::trace_span!("deserializing command option");
let _span_enter = span.enter();
loop {
let span_child = tracing::trace_span!("iterating over command option");
let _span_child_enter = span_child.enter();
let key = match map.next_key() {
Ok(Some(key)) => {
tracing::trace!(?key, "found key");
key
}
Ok(None) => break,
Err(why) => {
map.next_value::<IgnoredAny>()?;
tracing::trace!("ran into an unknown key: {:?}", why);
continue;
}
};
match key {
OptionField::Choices => {
if choices.is_some() {
return Err(DeError::duplicate_field("choices"));
}
choices = Some(map.next_value()?);
}
OptionField::Description => {
if description.is_some() {
return Err(DeError::duplicate_field("description"));
}
description = Some(map.next_value()?);
}
OptionField::Name => {
if name.is_some() {
return Err(DeError::duplicate_field("name"));
}
name = Some(map.next_value()?);
}
OptionField::Options => {
if options.is_some() {
return Err(DeError::duplicate_field("options"));
}
options = Some(map.next_value()?);
}
OptionField::Required => {
if required.is_some() {
return Err(DeError::duplicate_field("required"));
}
required = Some(map.next_value()?);
}
OptionField::Type => {
if kind.is_some() {
return Err(DeError::duplicate_field("type"));
}
kind = Some(map.next_value()?);
}
}
}
let description = description.ok_or_else(|| DeError::missing_field("description"))?;
let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
let name = name.ok_or_else(|| DeError::missing_field("name"))?;
tracing::trace!(
%description,
?kind,
%name,
"common fields of all variants exist"
);
let required = required.unwrap_or_default();
Ok(match kind {
CommandOptionType::SubCommand => {
let options = options
.flatten()
.ok_or_else(|| DeError::missing_field("options"))?;
CommandOption::SubCommand(OptionsCommandOptionData {
description,
name,
options,
required,
})
}
CommandOptionType::SubCommandGroup => {
let options = options
.flatten()
.ok_or_else(|| DeError::missing_field("options"))?;
CommandOption::SubCommandGroup(OptionsCommandOptionData {
description,
name,
options,
required,
})
}
CommandOptionType::String => {
let choices = choices
.flatten()
.ok_or_else(|| DeError::missing_field("choices"))?;
CommandOption::String(ChoiceCommandOptionData {
choices,
description,
name,
required,
})
}
CommandOptionType::Integer => {
let choices = choices
.flatten()
.ok_or_else(|| DeError::missing_field("choices"))?;
CommandOption::Integer(ChoiceCommandOptionData {
choices,
description,
name,
required,
})
}
CommandOptionType::Boolean => CommandOption::Boolean(BaseCommandOptionData {
description,
name,
required,
}),
CommandOptionType::User => CommandOption::User(BaseCommandOptionData {
description,
name,
required,
}),
CommandOptionType::Channel => CommandOption::Channel(BaseCommandOptionData {
description,
name,
required,
}),
CommandOptionType::Role => CommandOption::Role(BaseCommandOptionData {
description,
name,
required,
}),
CommandOptionType::Mentionable => CommandOption::Mentionable(BaseCommandOptionData {
description,
name,
required,
}),
})
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct BaseCommandOptionData {
pub description: String,
pub name: String,
#[serde(default)]
pub required: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct OptionsCommandOptionData {
pub description: String,
pub name: String,
#[serde(default)]
pub options: Vec<CommandOption>,
#[serde(default)]
pub required: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct ChoiceCommandOptionData {
#[serde(default)]
pub choices: Vec<CommandOptionChoice>,
pub description: String,
pub name: String,
#[serde(default)]
pub required: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(untagged)]
pub enum CommandOptionChoice {
String { name: String, value: String },
Int { name: String, value: i64 },
}
#[derive(
Clone, Copy, Debug, Deserialize_repr, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize_repr,
)]
#[repr(u8)]
pub enum CommandOptionType {
SubCommand = 1,
SubCommandGroup = 2,
String = 3,
Integer = 4,
Boolean = 5,
User = 6,
Channel = 7,
Role = 8,
Mentionable = 9,
}
impl CommandOptionType {
pub const fn kind(self) -> &'static str {
match self {
CommandOptionType::SubCommand => "SubCommand",
CommandOptionType::SubCommandGroup => "SubCommandGroup",
CommandOptionType::String => "String",
CommandOptionType::Integer => "Integer",
CommandOptionType::Boolean => "Boolean",
CommandOptionType::User => "User",
CommandOptionType::Channel => "Channel",
CommandOptionType::Role => "Role",
CommandOptionType::Mentionable => "Mentionable",
}
}
}
#[cfg(test)]
mod tests {
use super::{
super::Command, BaseCommandOptionData, ChoiceCommandOptionData, CommandOption,
CommandOptionChoice, OptionsCommandOptionData,
};
use crate::id::{ApplicationId, CommandId, GuildId};
use serde_test::Token;
#[test]
#[allow(clippy::too_many_lines)]
fn test_command_option_full() {
let value = Command {
application_id: Some(ApplicationId(100)),
guild_id: Some(GuildId(300)),
name: "test command".into(),
default_permission: Some(true),
description: "this command is a test".into(),
id: Some(CommandId(200)),
options: vec![CommandOption::SubCommandGroup(OptionsCommandOptionData {
description: "sub group desc".into(),
name: "sub group name".into(),
options: vec![CommandOption::SubCommand(OptionsCommandOptionData {
description: "sub command desc".into(),
name: "sub command name".into(),
options: vec![
CommandOption::String(ChoiceCommandOptionData {
choices: vec![CommandOptionChoice::String {
name: "choicea".into(),
value: "choice_a".into(),
}],
description: "string desc".into(),
name: "string".into(),
required: false,
}),
CommandOption::Integer(ChoiceCommandOptionData {
choices: vec![CommandOptionChoice::Int {
name: "choice2".into(),
value: 2,
}],
description: "int desc".into(),
name: "int".into(),
required: false,
}),
CommandOption::Boolean(BaseCommandOptionData {
description: "bool desc".into(),
name: "bool".into(),
required: false,
}),
CommandOption::User(BaseCommandOptionData {
description: "user desc".into(),
name: "user".into(),
required: false,
}),
CommandOption::Channel(BaseCommandOptionData {
description: "channel desc".into(),
name: "channel".into(),
required: false,
}),
CommandOption::Role(BaseCommandOptionData {
description: "role desc".into(),
name: "role".into(),
required: false,
}),
CommandOption::Mentionable(BaseCommandOptionData {
description: "mentionable desc".into(),
name: "mentionable".into(),
required: false,
}),
],
required: false,
})],
required: true,
})],
};
serde_test::assert_tokens(
&value,
&[
Token::Struct {
name: "Command",
len: 7,
},
Token::Str("application_id"),
Token::Some,
Token::NewtypeStruct {
name: "ApplicationId",
},
Token::Str("100"),
Token::Str("guild_id"),
Token::Some,
Token::NewtypeStruct { name: "GuildId" },
Token::Str("300"),
Token::Str("name"),
Token::Str("test command"),
Token::Str("default_permission"),
Token::Some,
Token::Bool(true),
Token::Str("description"),
Token::Str("this command is a test"),
Token::Str("id"),
Token::Some,
Token::NewtypeStruct { name: "CommandId" },
Token::Str("200"),
Token::Str("options"),
Token::Seq { len: Some(1) },
Token::Struct {
name: "CommandOptionEnvelope",
len: 5,
},
Token::Str("description"),
Token::Str("sub group desc"),
Token::Str("name"),
Token::Str("sub group name"),
Token::Str("options"),
Token::Some,
Token::Seq { len: Some(1) },
Token::Struct {
name: "CommandOptionEnvelope",
len: 4,
},
Token::Str("description"),
Token::Str("sub command desc"),
Token::Str("name"),
Token::Str("sub command name"),
Token::Str("options"),
Token::Some,
Token::Seq { len: Some(7) },
Token::Struct {
name: "CommandOptionEnvelope",
len: 4,
},
Token::Str("choices"),
Token::Some,
Token::Seq { len: Some(1) },
Token::Struct {
name: "CommandOptionChoice",
len: 2,
},
Token::Str("name"),
Token::Str("choicea"),
Token::Str("value"),
Token::Str("choice_a"),
Token::StructEnd,
Token::SeqEnd,
Token::Str("description"),
Token::Str("string desc"),
Token::Str("name"),
Token::Str("string"),
Token::Str("type"),
Token::U8(3),
Token::StructEnd,
Token::Struct {
name: "CommandOptionEnvelope",
len: 4,
},
Token::Str("choices"),
Token::Some,
Token::Seq { len: Some(1) },
Token::Struct {
name: "CommandOptionChoice",
len: 2,
},
Token::Str("name"),
Token::Str("choice2"),
Token::Str("value"),
Token::I64(2),
Token::StructEnd,
Token::SeqEnd,
Token::Str("description"),
Token::Str("int desc"),
Token::Str("name"),
Token::Str("int"),
Token::Str("type"),
Token::U8(4),
Token::StructEnd,
Token::Struct {
name: "CommandOptionEnvelope",
len: 3,
},
Token::Str("description"),
Token::Str("bool desc"),
Token::Str("name"),
Token::Str("bool"),
Token::Str("type"),
Token::U8(5),
Token::StructEnd,
Token::Struct {
name: "CommandOptionEnvelope",
len: 3,
},
Token::Str("description"),
Token::Str("user desc"),
Token::Str("name"),
Token::Str("user"),
Token::Str("type"),
Token::U8(6),
Token::StructEnd,
Token::Struct {
name: "CommandOptionEnvelope",
len: 3,
},
Token::Str("description"),
Token::Str("channel desc"),
Token::Str("name"),
Token::Str("channel"),
Token::Str("type"),
Token::U8(7),
Token::StructEnd,
Token::Struct {
name: "CommandOptionEnvelope",
len: 3,
},
Token::Str("description"),
Token::Str("role desc"),
Token::Str("name"),
Token::Str("role"),
Token::Str("type"),
Token::U8(8),
Token::StructEnd,
Token::Struct {
name: "CommandOptionEnvelope",
len: 3,
},
Token::Str("description"),
Token::Str("mentionable desc"),
Token::Str("name"),
Token::Str("mentionable"),
Token::Str("type"),
Token::U8(9),
Token::StructEnd,
Token::SeqEnd,
Token::Str("type"),
Token::U8(1),
Token::StructEnd,
Token::SeqEnd,
Token::Str("required"),
Token::Bool(true),
Token::Str("type"),
Token::U8(2),
Token::StructEnd,
Token::SeqEnd,
Token::StructEnd,
],
);
}
}