Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(macro): add group macro #267

Open
wants to merge 16 commits into
base: current
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions macros/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,226 @@ pub struct CommandArgs {
member_cooldown: Option<u64>,
}

impl CommandArgs {
// Check if a field has the default value
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
value == &T::default()
}

// create a new CommandArgs from self, with default fields replaced by value from GroupArgs
pub fn from_group_args(&self, group_args: &GroupArgs) -> CommandArgs {
CommandArgs {
prefix_command: if Self::is_default(&self.prefix_command) {
group_args.prefix_command
} else {
self.prefix_command
},
slash_command: if Self::is_default(&self.slash_command) {
group_args.slash_command
} else {
self.slash_command
},
context_menu_command: if Self::is_default(&self.context_menu_command) {
group_args.context_menu_command.clone()
} else {
self.context_menu_command.clone()
},
subcommands: self.subcommands.clone(), // `GroupArgs` doesn't have `subcommands`
aliases: self.aliases.clone(), // `GroupArgs` doesn't have `aliases`
subcommand_required: self.subcommand_required, // `GroupArgs` doesn't have `subcommand_required`
invoke_on_edit: if Self::is_default(&self.invoke_on_edit) {
group_args.invoke_on_edit
} else {
self.invoke_on_edit
},
reuse_response: if Self::is_default(&self.reuse_response) {
group_args.reuse_response
} else {
self.reuse_response
},
track_deletion: if Self::is_default(&self.track_deletion) {
group_args.track_deletion
} else {
self.track_deletion
},
track_edits: if Self::is_default(&self.track_edits) {
group_args.track_edits
} else {
self.track_edits
},
broadcast_typing: if Self::is_default(&self.broadcast_typing) {
group_args.broadcast_typing
} else {
self.broadcast_typing
},
help_text_fn: if Self::is_default(&self.help_text_fn) {
group_args.help_text_fn.clone()
} else {
self.help_text_fn.clone()
},
check: if Self::is_default(&self.check) {
group_args.check.clone()
} else {
self.check.clone()
},
on_error: if Self::is_default(&self.on_error) {
group_args.on_error.clone()
} else {
self.on_error.clone()
},
rename: self.rename.clone(), // `GroupArgs` doesn't have `rename`
name_localized: if Self::is_default(&self.name_localized) {
group_args.name_localized.clone()
} else {
self.name_localized.clone()
},
description_localized: if Self::is_default(&self.description_localized) {
group_args.description_localized.clone()
} else {
self.description_localized.clone()
},
discard_spare_arguments: if Self::is_default(&self.discard_spare_arguments) {
group_args.discard_spare_arguments
} else {
self.discard_spare_arguments
},
hide_in_help: if Self::is_default(&self.hide_in_help) {
group_args.hide_in_help
} else {
self.hide_in_help
},
ephemeral: if Self::is_default(&self.ephemeral) {
group_args.ephemeral
} else {
self.ephemeral
},
default_member_permissions: if Self::is_default(&self.default_member_permissions) {
group_args.default_member_permissions.clone()
} else {
self.default_member_permissions.clone()
},
required_permissions: if Self::is_default(&self.required_permissions) {
group_args.required_permissions.clone()
} else {
self.required_permissions.clone()
},
required_bot_permissions: if Self::is_default(&self.required_bot_permissions) {
group_args.required_bot_permissions.clone()
} else {
self.required_bot_permissions.clone()
},
owners_only: if Self::is_default(&self.owners_only) {
group_args.owners_only
} else {
self.owners_only
},
guild_only: if Self::is_default(&self.guild_only) {
group_args.guild_only
} else {
self.guild_only
},
dm_only: if Self::is_default(&self.dm_only) {
group_args.dm_only
} else {
self.dm_only
},
nsfw_only: if Self::is_default(&self.nsfw_only) {
group_args.nsfw_only
} else {
self.nsfw_only
},
identifying_name: self.identifying_name.clone(), // `GroupArgs` doesn't have `identifying_name`
category: if Self::is_default(&self.category) {
group_args.category.clone()
} else {
self.category.clone()
},
custom_data: if Self::is_default(&self.custom_data) {
group_args.custom_data.clone()
} else {
self.custom_data.clone()
},
global_cooldown: if Self::is_default(&self.global_cooldown) {
group_args.global_cooldown
} else {
self.global_cooldown
},
user_cooldown: if Self::is_default(&self.user_cooldown) {
group_args.user_cooldown
} else {
self.user_cooldown
},
guild_cooldown: if Self::is_default(&self.guild_cooldown) {
group_args.guild_cooldown
} else {
self.guild_cooldown
},
channel_cooldown: if Self::is_default(&self.channel_cooldown) {
group_args.channel_cooldown
} else {
self.channel_cooldown
},
member_cooldown: if Self::is_default(&self.member_cooldown) {
group_args.member_cooldown
} else {
self.member_cooldown
},
}
}
}

/// Representation of the group attribute arguments (`#[group(...)]`)
///
/// Same as CommandArgs, but with some removed, because they wouldn't make sense
#[derive(Default, Debug, darling::FromMeta)]
#[darling(default)]
pub struct GroupArgs {
prefix_command: bool,
slash_command: bool,
context_menu_command: Option<String>,

// When changing these, document it in parent file!
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
// TODO: decide why darling(multiple) feels wrong here but not in e.g. localizations (because
// if it's actually irrational, the inconsistency should be fixed)
// subcommands: crate::util::List<syn::Path>,
// aliases: crate::util::List<String>,
// subcommand_required: bool,
invoke_on_edit: bool,
reuse_response: bool,
track_deletion: bool,
track_edits: bool,
broadcast_typing: bool,
help_text_fn: Option<syn::Path>,
#[darling(multiple)]
check: Vec<syn::Path>,
on_error: Option<syn::Path>,
// rename: Option<String>,
#[darling(multiple)]
name_localized: Vec<crate::util::Tuple2<String>>,
#[darling(multiple)]
description_localized: Vec<crate::util::Tuple2<String>>,
discard_spare_arguments: bool,
hide_in_help: bool,
ephemeral: bool,
default_member_permissions: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
required_permissions: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
required_bot_permissions: Option<syn::punctuated::Punctuated<syn::Ident, syn::Token![|]>>,
owners_only: bool,
guild_only: bool,
dm_only: bool,
nsfw_only: bool,
// identifying_name: Option<String>,
category: Option<String>,
custom_data: Option<syn::Expr>,

// In seconds
global_cooldown: Option<u64>,
user_cooldown: Option<u64>,
guild_cooldown: Option<u64>,
channel_cooldown: Option<u64>,
member_cooldown: Option<u64>,
}

/// Representation of the function parameter attribute arguments
#[derive(Default, Debug, darling::FromMeta)]
#[darling(default)]
Expand Down
116 changes: 116 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ mod command;
mod modal;
mod util;

use darling::Error;
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
use proc_macro::TokenStream;
use quote::quote;

/**
This macro transforms plain functions into poise bot commands.
Expand Down Expand Up @@ -277,3 +279,117 @@ pub fn modal(input: TokenStream) -> TokenStream {
Err(e) => e.write_errors().into(),
}
}

/**
Use this macro on an impl block to implement the CommandGroup trait.
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved

It implements a `commands()` function which returns a Vec with all the commands defined by `#[poise::command]`.

# Usage

The following code defines a command group with two commands,
both of which will have the `slash_command` and `prefix_command` attributes.

`command_one` will have the default user_cooldown of 1000, while `command_two` overrides it to 2000.

```rust
struct MyCommands;

#[poise::group(slash_command, prefix_command, user_cooldown=1000)]
impl MyCommands {
/// This is a command
#[poise::command()]
async fn command_one(ctx: Context<'_>) -> Result<(), Error> {
// code
}

/// This is another command
#[poise::command(user_cooldown=2000)]
async fn command_two(ctx: Context<'_>) -> Result<(), Error> {
// code
}
}
```
*/
#[proc_macro_attribute]
pub fn group(args: TokenStream, input_item: TokenStream) -> TokenStream {
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
match group_impl(args, input_item) {
Ok(x) => x,
Err(err) => err.write_errors().into(),
}
}

fn group_impl(args: TokenStream, input_item: TokenStream) -> Result<TokenStream, Error> {
let args = darling::ast::NestedMeta::parse_meta_list(args.into())?;

let group_args = <command::GroupArgs as darling::FromMeta>::from_list(&args)?;

// let item_impl = syn::parse_macro_input!(input_item as syn::ItemImpl);
let item_impl = match syn::parse::<syn::ItemImpl>(input_item) {
Ok(syntax_tree) => syntax_tree,
Err(err) => return Err(err.into()),
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
};
let name = item_impl.self_ty;

// vector of all #[poise::command(...)] command idents
let mut command_idents = vec![];

// collect each ImplItem in a stream
let mut impl_body = quote!();

for item in item_impl.items.iter() {
let mut item_stream = quote!(#item);

// if it's a function...
if let syn::ImplItem::Fn(f) = item {
// ... and it's a command
if let Some(attr) = f.attrs.iter().find(|attr| is_command_attr(attr)) {
// add to command list
command_idents.push(f.sig.ident.clone());

// Turn a syn::Attribute into command::CommandArgs
let attr_args = &attr.meta.require_list()?.tokens;

let command_args = darling::ast::NestedMeta::parse_meta_list(quote! {#attr_args})?;
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
let command_args =
<command::CommandArgs as darling::FromMeta>::from_list(&command_args)?;

let new_args = command_args.from_group_args(&group_args);
let function = syn::ItemFn {
attrs: vec![],
vis: f.vis.clone(),
sig: f.sig.clone(),
block: Box::new(f.block.clone()),
};
item_stream = command::command(new_args, function)?.into();
}
}
impl_body = quote!(
#impl_body
#item_stream
);
}

Ok(quote! {
impl #name {
fn commands() -> Vec<Command<Data, Error>> {
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
vec![#(#name::#command_idents()),*]
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
}

#impl_body
}
}
.into())
}

/**
Returns true if an `Attribute` has `path` equal to "poise::command" or "command"
*/
TitaniumBrain marked this conversation as resolved.
Show resolved Hide resolved
fn is_command_attr(attr: &syn::Attribute) -> bool {
let mut segments = attr.path().segments.iter();
match [segments.next(), segments.next(), segments.next()] {
[Some(first), Some(second), None] => first.ident == "poise" && second.ident == "command",
[Some(first), None, None] => first.ident == "command",
[_, _, _] => false,
}
}
4 changes: 2 additions & 2 deletions macros/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl syn::fold::Fold for AllLifetimesToStatic {
}

/// Darling utility type that accepts a list of things, e.g. `#[attr(thing1, thing2...)]`
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct List<T>(pub Vec<T>);
impl<T: darling::FromMeta> darling::FromMeta for List<T> {
fn from_list(items: &[::darling::ast::NestedMeta]) -> darling::Result<Self> {
Expand All @@ -70,7 +70,7 @@ impl<T> Default for List<T> {
}

/// Darling utility type that accepts a 2-tuple list of things, e.g. `#[attr(thing1, thing2)]`
#[derive(Debug)]
#[derive(Debug, PartialEq, Clone)]
pub struct Tuple2<T>(pub T, pub T);
impl<T: darling::FromMeta> darling::FromMeta for Tuple2<T> {
fn from_list(items: &[::darling::ast::NestedMeta]) -> darling::Result<Self> {
Expand Down