use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::{spanned::Spanned, Attribute, Error, Lit, Result, Type};
use crate::{
command::user_application::{ApplicationIntegrationType, InteractionContextType},
parse::{
attribute::{NamedAttrs, ParseAttribute, ParseSpanned},
parsers::{CommandDescription, CommandName, FunctionPath},
syntax::{extract_generic, find_attr},
},
};
pub struct StructField {
pub span: Span,
pub ident: Ident,
pub ty: Type,
pub raw_attrs: Vec<Attribute>,
pub attributes: FieldAttribute,
pub kind: FieldType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldType {
Autocomplete,
Optional,
Required,
}
impl StructField {
pub fn from_field(field: syn::Field) -> Result<Self> {
let (kind, ty) = match extract_generic(&field.ty, "Option") {
Some(ty) => match extract_generic(&ty, "AutocompleteValue") {
Some(_) => {
return Err(Error::new_spanned(
ty,
"`AutocompleteValue` cannot be wrapped in `Option<T>`",
))
}
None => (FieldType::Optional, ty),
},
None => match extract_generic(&field.ty, "AutocompleteValue") {
Some(ty) => (FieldType::Autocomplete, ty),
None => (FieldType::Required, field.ty.clone()),
},
};
let attributes = match find_attr(&field.attrs, "command") {
Some(attr) => FieldAttribute::parse(attr)?,
None => FieldAttribute::default(),
};
let Some(ident) = field.ident else {
return Err(Error::new_spanned(
field,
"expected struct field to have an identifier",
));
};
Ok(Self {
span: field.ty.span(),
ident,
ty,
raw_attrs: field.attrs,
attributes,
kind,
})
}
pub fn from_fields(fields: syn::FieldsNamed) -> Result<Vec<Self>> {
fields.named.into_iter().map(Self::from_field).collect()
}
}
impl FieldType {
pub fn required(&self) -> bool {
match self {
Self::Required => true,
Self::Autocomplete | Self::Optional => false,
}
}
}
pub struct TypeAttribute {
pub autocomplete: Option<bool>,
pub name: Option<CommandName>,
pub name_localizations: Option<FunctionPath>,
pub desc: Option<CommandDescription>,
pub desc_localizations: Option<FunctionPath>,
pub default_permissions: Option<FunctionPath>,
pub dm_permission: Option<bool>,
pub nsfw: Option<bool>,
pub contexts: Option<Vec<InteractionContextType>>,
pub integration_types: Option<Vec<ApplicationIntegrationType>>,
}
impl TypeAttribute {
const VALID_ATTRIBUTES: &'static [&'static str] = &[
"autocomplete",
"name",
"name_localizations",
"desc",
"desc_localizations",
"default_permissions",
"dm_permission",
"nsfw",
"contexts",
"integration_types",
];
pub fn parse(attr: &Attribute) -> Result<Self> {
let mut parser = NamedAttrs::parse(attr, Self::VALID_ATTRIBUTES)?;
Ok(Self {
autocomplete: parser.optional("autocomplete")?,
name: parser.optional("name")?,
name_localizations: parser.optional("name_localizations")?,
desc: parser.optional("desc")?,
desc_localizations: parser.optional("desc_localizations")?,
default_permissions: parser.optional("default_permissions")?,
dm_permission: parser.optional("dm_permission")?,
nsfw: parser.optional("nsfw")?,
contexts: parser.optional("contexts")?,
integration_types: parser.optional("integration_types")?,
})
}
}
#[derive(Default)]
pub struct FieldAttribute {
pub rename: Option<CommandName>,
pub name_localizations: Option<FunctionPath>,
pub desc: Option<CommandDescription>,
pub desc_localizations: Option<FunctionPath>,
pub autocomplete: bool,
pub channel_types: Vec<ChannelType>,
pub max_value: Option<CommandOptionValue>,
pub min_value: Option<CommandOptionValue>,
pub max_length: Option<u16>,
pub min_length: Option<u16>,
}
impl FieldAttribute {
const VALID_ATTRIBUTES: &'static [&'static str] = &[
"rename",
"name_localizations",
"desc",
"desc_localizations",
"autocomplete",
"channel_types",
"max_value",
"min_value",
"max_length",
"min_length",
];
pub fn parse(attr: &Attribute) -> Result<Self> {
let mut parser = NamedAttrs::parse(attr, Self::VALID_ATTRIBUTES)?;
Ok(Self {
rename: parser.optional("rename")?,
name_localizations: parser.optional("name_localizations")?,
desc: parser.optional("desc")?,
desc_localizations: parser.optional("desc_localizations")?,
autocomplete: parser.optional("autocomplete")?.unwrap_or_default(),
channel_types: parser.optional("channel_types")?.unwrap_or_default(),
max_value: parser.optional("max_value")?,
min_value: parser.optional("min_value")?,
max_length: parser.optional("max_length")?,
min_length: parser.optional("min_length")?,
})
}
pub fn name_default(&self, default: String) -> String {
match &self.rename {
Some(name) => name.clone().into(),
None => default,
}
}
}
pub enum ChannelType {
GuildText,
Private,
GuildVoice,
Group,
GuildCategory,
GuildAnnouncement,
GuildStore,
AnnouncementThread,
PublicThread,
PrivateThread,
GuildStageVoice,
GuildDirectory,
GuildForum,
GuildMedia,
}
impl ParseAttribute for Vec<ChannelType> {
fn parse_attribute(input: Lit) -> Result<Self> {
let spanned: ParseSpanned<String> = ParseAttribute::parse_attribute(input)?;
spanned
.inner
.split_ascii_whitespace()
.map(|value| ChannelType::parse(value, spanned.span))
.collect()
}
}
impl ChannelType {
fn parse(value: &str, span: Span) -> Result<Self> {
match value {
"guild_text" => Ok(Self::GuildText),
"private" => Ok(Self::Private),
"guild_voice" => Ok(Self::GuildVoice),
"group" => Ok(Self::Group),
"guild_category" => Ok(Self::GuildCategory),
"guild_announcement" | "guild_news" => Ok(Self::GuildAnnouncement),
"guild_store" => Ok(Self::GuildStore),
"announcement_thread" | "guild_news_thread" => Ok(Self::AnnouncementThread),
"public_thread" | "guild_public_thread" => Ok(Self::PublicThread),
"private_thread" | "guild_private_thread" => Ok(Self::PrivateThread),
"guild_stage_voice" => Ok(Self::GuildStageVoice),
"guild_directory" => Ok(Self::GuildDirectory),
"guild_forum" => Ok(Self::GuildForum),
"guild_media" => Ok(Self::GuildMedia),
invalid => Err(Error::new(
span,
format!("`{invalid}` is not a valid channel type"),
)),
}
}
}
#[derive(Clone, Copy)]
pub enum CommandOptionValue {
Integer(i64),
Number(f64),
}
impl ParseAttribute for CommandOptionValue {
fn parse_attribute(input: Lit) -> Result<Self> {
match input {
Lit::Int(inner) => Ok(Self::Integer(inner.base10_parse()?)),
Lit::Float(inner) => Ok(Self::Number(inner.base10_parse()?)),
_ => Err(Error::new_spanned(
input,
"expected integer or floating point literal",
)),
}
}
}
pub fn channel_type(kind: &ChannelType) -> TokenStream {
match kind {
ChannelType::GuildText => quote!(::twilight_model::channel::ChannelType::GuildText),
ChannelType::Private => quote!(::twilight_model::channel::ChannelType::Private),
ChannelType::GuildVoice => quote!(::twilight_model::channel::ChannelType::GuildVoice),
ChannelType::Group => quote!(::twilight_model::channel::ChannelType::Group),
ChannelType::GuildCategory => quote!(::twilight_model::channel::ChannelType::GuildCategory),
ChannelType::GuildAnnouncement => {
quote!(::twilight_model::channel::ChannelType::GuildAnnouncement)
}
ChannelType::GuildStore => quote!(::twilight_model::channel::ChannelType::GuildStore),
ChannelType::AnnouncementThread => {
quote!(::twilight_model::channel::ChannelType::AnnouncementThread)
}
ChannelType::PublicThread => {
quote!(::twilight_model::channel::ChannelType::PublicThread)
}
ChannelType::PrivateThread => {
quote!(::twilight_model::channel::ChannelType::PrivateThread)
}
ChannelType::GuildStageVoice => {
quote!(::twilight_model::channel::ChannelType::GuildStageVoice)
}
ChannelType::GuildDirectory => {
quote!(::twilight_model::channel::ChannelType::GuildDirectory)
}
ChannelType::GuildForum => quote!(::twilight_model::channel::ChannelType::GuildForum),
ChannelType::GuildMedia => quote!(::twilight_model::channel::ChannelType::GuildMedia),
}
}
pub fn command_option_value(value: Option<CommandOptionValue>) -> TokenStream {
match value {
None => quote!(::std::option::Option::None),
Some(CommandOptionValue::Integer(inner)) => {
quote!(::std::option::Option::Some(::twilight_model::application::command::CommandOptionValue::Integer(#inner)))
}
Some(CommandOptionValue::Number(inner)) => {
quote!(::std::option::Option::Some(::twilight_model::application::command::CommandOptionValue::Number(#inner)))
}
}
}