added functionality for reusable hook functions that will execute on commands
This commit is contained in:
@ -13,3 +13,4 @@ proc-macro = true
|
||||
quote = "^1.0"
|
||||
syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
|
||||
proc-macro2 = "1.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
@ -8,7 +8,7 @@ use syn::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
structures::{ApplicationCommandOptionType, Arg, PermissionLevel},
|
||||
structures::{ApplicationCommandOptionType, Arg},
|
||||
util::{AsOption, LitExt},
|
||||
};
|
||||
|
||||
@ -46,24 +46,15 @@ impl fmt::Display for ValueKind {
|
||||
|
||||
fn to_ident(p: Path) -> Result<Ident> {
|
||||
if p.segments.is_empty() {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"cannot convert an empty path to an identifier",
|
||||
));
|
||||
return Err(Error::new(p.span(), "cannot convert an empty path to an identifier"));
|
||||
}
|
||||
|
||||
if p.segments.len() > 1 {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"the path must not have more than one segment",
|
||||
));
|
||||
return Err(Error::new(p.span(), "the path must not have more than one segment"));
|
||||
}
|
||||
|
||||
if !p.segments[0].arguments.is_empty() {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"the singular path segment must not have any arguments",
|
||||
));
|
||||
return Err(Error::new(p.span(), "the singular path segment must not have any arguments"));
|
||||
}
|
||||
|
||||
Ok(p.segments[0].ident.clone())
|
||||
@ -85,12 +76,7 @@ impl Values {
|
||||
literals: Vec<(Option<String>, Lit)>,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
Values {
|
||||
name,
|
||||
literals,
|
||||
kind,
|
||||
span,
|
||||
}
|
||||
Values { name, literals, kind, span }
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,11 +131,7 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
||||
}
|
||||
}
|
||||
|
||||
let kind = if lits.len() == 1 {
|
||||
ValueKind::SingleList
|
||||
} else {
|
||||
ValueKind::List
|
||||
};
|
||||
let kind = if lits.len() == 1 { ValueKind::SingleList } else { ValueKind::List };
|
||||
|
||||
Ok(Values::new(name, kind, lits, attr.span()))
|
||||
} else {
|
||||
@ -183,12 +165,7 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
||||
let name = to_ident(meta.path)?;
|
||||
let lit = meta.lit;
|
||||
|
||||
Ok(Values::new(
|
||||
name,
|
||||
ValueKind::Equals,
|
||||
vec![(None, lit)],
|
||||
attr.span(),
|
||||
))
|
||||
Ok(Values::new(name, ValueKind::Equals, vec![(None, lit)], attr.span()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,10 +208,7 @@ fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> {
|
||||
return Err(Error::new(
|
||||
values.span,
|
||||
// Using the `_args` version here to avoid an allocation.
|
||||
format_args!(
|
||||
"the attribute must be in of these forms:\n{}",
|
||||
DisplaySlice(forms)
|
||||
),
|
||||
format_args!("the attribute must be in of these forms:\n{}", DisplaySlice(forms)),
|
||||
));
|
||||
}
|
||||
|
||||
@ -254,11 +228,7 @@ impl AttributeOption for Vec<String> {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
validate(&values, &[ValueKind::List])?;
|
||||
|
||||
Ok(values
|
||||
.literals
|
||||
.into_iter()
|
||||
.map(|(_, l)| l.to_str())
|
||||
.collect())
|
||||
Ok(values.literals.into_iter().map(|(_, l)| l.to_str()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,37 +264,18 @@ impl AttributeOption for Vec<Ident> {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
validate(&values, &[ValueKind::List])?;
|
||||
|
||||
Ok(values
|
||||
.literals
|
||||
.into_iter()
|
||||
.map(|(_, l)| l.to_ident())
|
||||
.collect())
|
||||
Ok(values.literals.into_iter().map(|(_, l)| l.to_ident()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeOption for Option<String> {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
validate(
|
||||
&values,
|
||||
&[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList],
|
||||
)?;
|
||||
validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?;
|
||||
|
||||
Ok(values.literals.get(0).map(|(_, l)| l.to_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeOption for PermissionLevel {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
validate(&values, &[ValueKind::SingleList])?;
|
||||
|
||||
Ok(values
|
||||
.literals
|
||||
.get(0)
|
||||
.map(|(_, l)| PermissionLevel::from_str(&*l.to_str()).unwrap())
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeOption for Arg {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
validate(&values, &[ValueKind::EqualsList])?;
|
||||
|
@ -2,6 +2,8 @@ pub mod suffixes {
|
||||
pub const COMMAND: &str = "COMMAND";
|
||||
pub const ARG: &str = "ARG";
|
||||
pub const SUBCOMMAND: &str = "SUBCOMMAND";
|
||||
pub const CHECK: &str = "CHECK";
|
||||
pub const HOOK: &str = "HOOK";
|
||||
}
|
||||
|
||||
pub use self::suffixes::*;
|
||||
|
@ -5,6 +5,7 @@ use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::quote;
|
||||
use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit, Type};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) mod attributes;
|
||||
pub(crate) mod consts;
|
||||
@ -43,6 +44,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
fun.name.to_string()
|
||||
};
|
||||
|
||||
let mut hooks: Vec<Ident> = Vec::new();
|
||||
let mut options = Options::new();
|
||||
|
||||
for attribute in &fun.attributes {
|
||||
@ -76,11 +78,13 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
util::append_line(&mut options.description, line);
|
||||
}
|
||||
}
|
||||
"hook" => {
|
||||
hooks.push(propagate_err!(attributes::parse(values)));
|
||||
}
|
||||
_ => {
|
||||
match_options!(name, values, options, span => [
|
||||
aliases;
|
||||
group;
|
||||
required_permissions;
|
||||
can_blacklist;
|
||||
supports_dm
|
||||
]);
|
||||
@ -93,7 +97,6 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
description,
|
||||
group,
|
||||
examples,
|
||||
required_permissions,
|
||||
can_blacklist,
|
||||
supports_dm,
|
||||
mut cmd_args,
|
||||
@ -235,10 +238,10 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
desc: #description,
|
||||
group: #group,
|
||||
examples: &[#(#examples),*],
|
||||
required_permissions: #required_permissions,
|
||||
can_blacklist: #can_blacklist,
|
||||
supports_dm: #supports_dm,
|
||||
args: &[#(&#arg_idents),*],
|
||||
hooks: &[#(&#hooks),*],
|
||||
};
|
||||
});
|
||||
|
||||
@ -256,3 +259,44 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
||||
tokens.into()
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut fun = parse_macro_input!(input as CommandFun);
|
||||
|
||||
let n = fun.name.clone();
|
||||
let name = n.with_suffix(HOOK);
|
||||
let fn_name = n.with_suffix(CHECK);
|
||||
let visibility = fun.visibility;
|
||||
|
||||
let cooked = fun.cooked;
|
||||
let body = fun.body;
|
||||
let ret = fun.ret;
|
||||
populate_fut_lifetimes_on_refs(&mut fun.args);
|
||||
let args = fun.args;
|
||||
|
||||
let hook_path = quote!(crate::framework::Hook);
|
||||
let uuid = Uuid::new_v4().as_u128();
|
||||
|
||||
(quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
#visibility fn #fn_name<'fut>(#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> {
|
||||
use ::serenity::futures::future::FutureExt;
|
||||
|
||||
async move {
|
||||
let _output: #ret = { #(#body)* };
|
||||
#[allow(unreachable_code)]
|
||||
_output
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
pub static #name: #hook_path = #hook_path {
|
||||
fun: #fn_name,
|
||||
uuid: #uuid,
|
||||
};
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use syn::{
|
||||
braced,
|
||||
parse::{Error, Parse, ParseStream, Result},
|
||||
spanned::Spanned,
|
||||
Attribute, Block, FnArg, Ident, Pat, Stmt, Token, Visibility,
|
||||
Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility,
|
||||
};
|
||||
|
||||
use crate::util::{Argument, Parenthesised};
|
||||
@ -78,6 +78,7 @@ pub struct CommandFun {
|
||||
pub visibility: Visibility,
|
||||
pub name: Ident,
|
||||
pub args: Vec<Argument>,
|
||||
pub ret: Type,
|
||||
pub body: Vec<Stmt>,
|
||||
}
|
||||
|
||||
@ -97,6 +98,11 @@ impl Parse for CommandFun {
|
||||
// (...)
|
||||
let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?;
|
||||
|
||||
let ret = match input.parse::<ReturnType>()? {
|
||||
ReturnType::Type(_, t) => (*t).clone(),
|
||||
ReturnType::Default => Type::Verbatim(quote!(())),
|
||||
};
|
||||
|
||||
// { ... }
|
||||
let bcont;
|
||||
braced!(bcont in input);
|
||||
@ -104,72 +110,23 @@ impl Parse for CommandFun {
|
||||
|
||||
let args = args.into_iter().map(parse_argument).collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Self { attributes, cooked, visibility, name, args, body })
|
||||
Ok(Self { attributes, cooked, visibility, name, args, ret, body })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CommandFun {
|
||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||
let Self { attributes: _, cooked, visibility, name, args, body } = self;
|
||||
let Self { attributes: _, cooked, visibility, name, args, ret, body } = self;
|
||||
|
||||
stream.extend(quote! {
|
||||
#(#cooked)*
|
||||
#visibility async fn #name (#(#args),*) {
|
||||
#visibility async fn #name (#(#args),*) -> #ret {
|
||||
#(#body)*
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PermissionLevel {
|
||||
Unrestricted,
|
||||
Managed,
|
||||
Restricted,
|
||||
}
|
||||
|
||||
impl Default for PermissionLevel {
|
||||
fn default() -> Self {
|
||||
Self::Unrestricted
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionLevel {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
Some(match s.to_uppercase().as_str() {
|
||||
"UNRESTRICTED" => Self::Unrestricted,
|
||||
"MANAGED" => Self::Managed,
|
||||
"RESTRICTED" => Self::Restricted,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PermissionLevel {
|
||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||
let path = quote!(crate::framework::PermissionLevel);
|
||||
let variant;
|
||||
|
||||
match self {
|
||||
Self::Unrestricted => {
|
||||
variant = quote!(Unrestricted);
|
||||
}
|
||||
|
||||
Self::Managed => {
|
||||
variant = quote!(Managed);
|
||||
}
|
||||
|
||||
Self::Restricted => {
|
||||
variant = quote!(Restricted);
|
||||
}
|
||||
}
|
||||
|
||||
stream.extend(quote! {
|
||||
#path::#variant
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ApplicationCommandOptionType {
|
||||
SubCommand,
|
||||
@ -272,7 +229,6 @@ pub(crate) struct Options {
|
||||
pub description: String,
|
||||
pub group: String,
|
||||
pub examples: Vec<String>,
|
||||
pub required_permissions: PermissionLevel,
|
||||
pub can_blacklist: bool,
|
||||
pub supports_dm: bool,
|
||||
pub cmd_args: Vec<Arg>,
|
||||
|
Reference in New Issue
Block a user