tranquil_macros/
lib.rs

1use indoc::indoc;
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use syn::{
5    parse::Parse, parse_macro_input, spanned::Spanned, AttributeArgs, FnArg, Ident, ImplItem,
6    ItemEnum, ItemFn, ItemImpl, Lit, LitStr, Meta, MetaNameValue, NestedMeta, PatType,
7};
8
9// TODO: Use explicit trait methods in all quote! macros.
10
11#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
12enum CommandPath {
13    Command {
14        name: String,
15    },
16    Subcommand {
17        name: String,
18        subcommand: String,
19    },
20    Grouped {
21        name: String,
22        group: String,
23        subcommand: String,
24    },
25}
26
27#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
28enum Autocomplete {
29    DefaultName,
30    CustomName(Ident),
31}
32
33#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
34struct SlashAttributes<'a> {
35    default: Option<&'a Ident>,
36    rename: Option<CommandPath>,
37    autocomplete: Option<Autocomplete>,
38}
39
40trait CommandString: Spanned {
41    fn to_command_string(&self) -> String;
42}
43
44impl CommandString for LitStr {
45    fn to_command_string(&self) -> String {
46        self.value()
47    }
48}
49
50impl CommandString for Ident {
51    fn to_command_string(&self) -> String {
52        self.to_string()
53    }
54}
55
56fn parse_command(
57    rename: &impl CommandString,
58    split_char: char,
59) -> Result<CommandPath, TokenStream> {
60    let command_string = rename.to_command_string();
61
62    if command_string.is_empty() {
63        Err(TokenStream::from(
64            syn::Error::new(rename.span(), "commands cannot be empty").into_compile_error(),
65        ))?
66    }
67
68    let parts = command_string.split(split_char).collect::<Vec<_>>();
69
70    if parts.iter().any(|part| part.is_empty()) {
71        Err(TokenStream::from(
72            syn::Error::new(
73                rename.span(),
74                format!(
75                    indoc! {r#"
76                    invalid command name, valid command names are:
77                        `command`
78                        `command{}subcommand`
79                        `command{}group{}subcommand`
80                    "#},
81                    split_char, split_char, split_char
82                ),
83            )
84            .into_compile_error(),
85        ))?;
86    }
87
88    match parts.as_slice() {
89        [name, group, subcommand] => Ok(CommandPath::Grouped {
90            name: name.to_string(),
91            group: group.to_string(),
92            subcommand: subcommand.to_string(),
93        }),
94        [name, subcommand] => Ok(CommandPath::Subcommand {
95            name: name.to_string(),
96            subcommand: subcommand.to_string(),
97        }),
98        [name] => Ok(CommandPath::Command {
99            name: name.to_string(),
100        }),
101        _ => Err(TokenStream::from(
102            syn::Error::new(
103                rename.span(),
104                "commands can only have two levels of nesting",
105            )
106            .into_compile_error(),
107        ))?,
108    }
109}
110
111fn invalid_attribute(span: &impl Spanned) -> TokenStream {
112    syn::Error::new(
113        span.span(),
114        indoc! {r#"
115            available attributes are
116                `default`
117                `rename = "..."`
118        "#},
119    )
120    .into_compile_error()
121    .into()
122}
123
124fn invalid_rename_literal(span: &impl Spanned) -> TokenStream {
125    syn::Error::new(span.span(), "expected string")
126        .into_compile_error()
127        .into()
128}
129
130fn multiple_renames(span: &impl Spanned) -> TokenStream {
131    syn::Error::new(span.span(), "only one rename can be applied")
132        .into_compile_error()
133        .into()
134}
135
136fn default_on_base_command(span: &impl Spanned) -> TokenStream {
137    syn::Error::new(span.span(), "only subcommands can be `default`")
138        .into_compile_error()
139        .into()
140}
141
142fn multiple_autocompletes(span: &impl Spanned) -> TokenStream {
143    syn::Error::new(
144        span.span(),
145        "only one autocomplete function can be specified",
146    )
147    .into_compile_error()
148    .into()
149}
150
151fn invalid_autocomplete_ident(span: &impl Spanned) -> TokenStream {
152    syn::Error::new(span.span(), "expected identifier")
153        .into_compile_error()
154        .into()
155}
156
157#[proc_macro_attribute]
158pub fn slash(attr: TokenStream, item: TokenStream) -> TokenStream {
159    let mut errors = vec![];
160
161    let nested_metas = parse_macro_input!(attr as AttributeArgs);
162
163    let mut item_fn = parse_macro_input!(item as ItemFn);
164    let name = item_fn.sig.ident;
165    let impl_name = format_ident!("__{name}");
166    item_fn.sig.ident = impl_name.clone();
167
168    let attributes = {
169        let mut attributes = SlashAttributes::default();
170        for nested_meta in nested_metas.iter() {
171            match nested_meta {
172                NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => {
173                    let ident = path.get_ident();
174                    if ident.map_or(false, |ident| ident == "rename") {
175                        match lit {
176                            Lit::Str(lit_str) => {
177                                if attributes.rename.is_some() {
178                                    errors.push(multiple_renames(&nested_meta));
179                                } else {
180                                    match parse_command(lit_str, ' ') {
181                                        Ok(command) => attributes.rename = Some(command),
182                                        Err(error) => errors.push(error),
183                                    }
184                                }
185                            }
186                            _ => errors.push(invalid_rename_literal(&lit)),
187                        }
188                    } else if ident.map_or(false, |ident| ident == "autocomplete") {
189                        match lit {
190                            Lit::Str(lit_str) => {
191                                if attributes.autocomplete.is_some() {
192                                    errors.push(multiple_autocompletes(&nested_meta));
193                                } else {
194                                    match lit_str.parse_with(syn::Ident::parse) {
195                                        Ok(ident) => {
196                                            attributes.autocomplete =
197                                                Some(Autocomplete::CustomName(ident))
198                                        }
199                                        Err(_) => errors.push(invalid_autocomplete_ident(&lit)),
200                                    }
201                                }
202                            }
203                            _ => errors.push(invalid_autocomplete_ident(&lit)),
204                        }
205                    } else {
206                        errors.push(invalid_attribute(&nested_meta));
207                    }
208                }
209                NestedMeta::Meta(Meta::Path(path)) => {
210                    let ident = path.get_ident();
211                    if ident.map_or(false, |ident| ident == "default") {
212                        attributes.default = ident;
213                    } else if ident.map_or(false, |ident| ident == "autocomplete") {
214                        attributes.autocomplete = Some(Autocomplete::DefaultName);
215                    } else {
216                        errors.push(invalid_attribute(&nested_meta));
217                    }
218                }
219                _ => {
220                    errors.push(invalid_attribute(&nested_meta));
221                }
222            }
223        }
224        attributes
225    };
226
227    let command_path = attributes
228        .rename
229        .map_or_else(|| parse_command(&name, '_'), Ok)
230        .unwrap_or_else(|error| {
231            errors.push(error);
232            CommandPath::Command {
233                name: name.to_string(),
234            }
235        });
236
237    if let (Some(ident), CommandPath::Command { .. }) = (attributes.default, &command_path) {
238        errors.push(default_on_base_command(&ident));
239    }
240
241    let typed_parameters = item_fn
242        .sig
243        .inputs
244        .iter()
245        .skip(2) // TODO: Don't just skip &self and CommandContext.
246        .filter_map(|input| match input {
247            FnArg::Receiver(_) => None,
248            FnArg::Typed(pat_type) => Some(pat_type),
249        });
250
251    let parameters = typed_parameters
252        .clone()
253        .map(|PatType { pat, .. }| pat)
254        .collect::<Vec<_>>();
255
256    let parameter_names = parameters
257        .iter()
258        .map(|parameter| quote! { ::std::stringify!(#parameter) });
259
260    let parameter_resolvers = typed_parameters.clone().map(|PatType { ty, .. }| {
261        quote! {
262            <#ty as ::tranquil::resolve::Resolve>::resolve(
263                ::tranquil::resolve::ResolveContext {
264                    // Technically unwrap instead of flatten would also work, but better safe than sorry.
265                    option: options.next().flatten(),
266                    http: ctx.bot.http.clone(),
267                },
268            )
269        }
270    });
271
272    let join_futures = if parameters.is_empty() {
273        quote! {}
274    } else {
275        quote! {
276            let (#(#parameters),*,) = ::tranquil::serenity::futures::try_join!(#(#parameter_resolvers),*)?;
277        }
278    };
279
280    let autocompleter = if let Some(autocomplete) = attributes.autocomplete {
281        let autocompleter_name = match autocomplete {
282            Autocomplete::DefaultName => format_ident!("autocomplete_{name}"),
283            Autocomplete::CustomName(name) => format_ident!("{name}"),
284        };
285        quote! {
286            ::std::option::Option::Some(
287                ::std::boxed::Box::new(|module, ctx| {
288                    ::std::boxed::Box::pin(async move {
289                        module.#autocompleter_name(ctx).await
290                    })
291                })
292            )
293        }
294    } else {
295        quote! { ::std::option::Option::None }
296    };
297
298    let make_command_path = |reference| {
299        let command_path_or_ref = if reference {
300            quote! { l10n::CommandPathRef }
301        } else {
302            quote! { command::CommandPath }
303        };
304
305        let to_string = if reference {
306            quote! {}
307        } else {
308            quote! { .to_string() }
309        };
310
311        match &command_path {
312            CommandPath::Command { name } => {
313                quote! {
314                    ::tranquil::#command_path_or_ref::Command {
315                        name: #name #to_string
316                    }
317                }
318            }
319            CommandPath::Subcommand { name, subcommand } => quote! {
320                ::tranquil::#command_path_or_ref::Subcommand {
321                    name: #name #to_string,
322                    subcommand: #subcommand #to_string,
323                }
324            },
325            CommandPath::Grouped {
326                name,
327                group,
328                subcommand,
329            } => quote! {
330                ::tranquil::#command_path_or_ref::Grouped {
331                    name: #name #to_string,
332                    group: #group #to_string,
333                    subcommand: #subcommand #to_string,
334                }
335            },
336        }
337    };
338
339    let command_path = make_command_path(false);
340    let command_path_ref = make_command_path(true);
341
342    let command_options = typed_parameters.map(|PatType { pat, ty, .. }| {
343        quote! {
344            (
345                ::std::convert::From::from(::std::stringify!(#pat)),
346                (|l10n: &::tranquil::l10n::L10n| {
347                    let mut option = ::tranquil::serenity::builder::CreateApplicationCommandOption::default();
348                    <#ty as ::tranquil::resolve::Resolve>::describe(
349                        option
350                            .kind(<#ty as ::tranquil::resolve::Resolve>::KIND)
351                            .required(<#ty as ::tranquil::resolve::Resolve>::REQUIRED),
352                        l10n,
353                    );
354                    // TODO: This can technically be done outside of the macro, now that the name is accessible there.
355                    l10n.describe_command_option(#command_path_ref, ::std::stringify!(#pat), &mut option);
356                    option
357                }) as fn(&::tranquil::l10n::L10n) -> ::tranquil::serenity::builder::CreateApplicationCommandOption,
358            )
359        }
360    });
361
362    let is_default_option = attributes.default.is_some();
363
364    let mut result = TokenStream::from(quote! {
365        #item_fn
366
367        fn #name(
368            self: ::std::sync::Arc<Self>
369        ) -> (::tranquil::command::CommandPath, ::std::boxed::Box<dyn ::tranquil::command::Command>) {
370            (
371                #command_path,
372                ::std::boxed::Box::new(::tranquil::command::ModuleCommand::new(
373                    self,
374                    ::std::boxed::Box::new(|module, mut ctx| {
375                        ::std::boxed::Box::pin(async move {
376                            let mut options = ::tranquil::resolve::find_options(
377                                [#(#parameter_names),*],
378                                ::tranquil::resolve::resolve_command_options(
379                                    ::std::mem::take(&mut ctx.interaction.data.options)
380                                ),
381                            ).into_iter();
382                            #join_futures
383                            module.#impl_name(ctx, #(#parameters),*).await
384                        })
385                    }),
386                    #autocompleter,
387                    ::std::vec![#(#command_options),*],
388                    #is_default_option,
389                )),
390            )
391        }
392    });
393    result.extend(errors);
394    result
395}
396
397#[proc_macro_attribute]
398pub fn autocompleter(attr: TokenStream, item: TokenStream) -> TokenStream {
399    // TODO: Deduplicate code
400
401    let mut errors = vec![];
402
403    let nested_metas = parse_macro_input!(attr as AttributeArgs);
404
405    if let Some(meta) = nested_metas.first() {
406        errors.push(TokenStream::from(
407            syn::Error::new(meta.span(), "autocomplete does not support any parameters")
408                .to_compile_error(),
409        ))
410    }
411
412    let mut item_fn = parse_macro_input!(item as ItemFn);
413    let name = item_fn.sig.ident;
414    let impl_name = format_ident!("__{name}");
415    item_fn.sig.ident = impl_name.clone();
416
417    let typed_parameters = item_fn
418        .sig
419        .inputs
420        .iter()
421        .skip(2) // TODO: Don't just skip &self and AutocompleteContext.
422        .filter_map(|input| match input {
423            FnArg::Receiver(_) => None,
424            FnArg::Typed(pat_type) => Some(pat_type),
425        });
426
427    let parameters = typed_parameters
428        .clone()
429        .map(|PatType { pat, .. }| pat)
430        .collect::<Vec<_>>();
431
432    let parameter_names = parameters
433        .iter()
434        .map(|parameter| quote! { ::std::stringify!(#parameter) });
435
436    let parameter_resolvers = typed_parameters.map(|PatType { ty, .. }| {
437        quote! {
438            <#ty as ::tranquil::resolve::Resolve>::resolve(
439                ::tranquil::resolve::ResolveContext {
440                    // Technically unwrap instead of flatten would also work, but better safe than sorry.
441                    option: options.next().flatten(),
442                    http: ctx.bot.http.clone(),
443                },
444            )
445        }
446    });
447
448    let join_futures = if parameters.is_empty() {
449        quote! {}
450    } else {
451        quote! {
452            let (#(#parameters),*,) = ::tranquil::serenity::futures::try_join!(#(#parameter_resolvers),*)?;
453        }
454    };
455
456    let mut result = TokenStream::from(quote! {
457        #item_fn
458
459        async fn #name(
460            &self,
461            mut ctx: ::tranquil::autocomplete::AutocompleteContext,
462        ) -> ::tranquil::AnyResult<()> {
463            let mut options = ::tranquil::resolve::find_options(
464                [#(#parameter_names),*],
465                ::tranquil::resolve::resolve_command_options(
466                    ::std::mem::take(&mut ctx.interaction.data.options)
467                ),
468            ).into_iter();
469            #join_futures
470            self.#impl_name(ctx, #(#parameters),*).await
471        }
472    });
473    result.extend(errors);
474    result
475}
476
477#[proc_macro_attribute]
478pub fn command_provider(attr: TokenStream, item: TokenStream) -> TokenStream {
479    // TODO: Deduplicate code
480
481    let nested_metas = parse_macro_input!(attr as AttributeArgs);
482
483    let mut errors = vec![];
484
485    if let Some(meta) = nested_metas.first() {
486        errors.push(TokenStream::from(
487            syn::Error::new(
488                meta.span(),
489                "command_provider does not support any parameters",
490            )
491            .to_compile_error(),
492        ))
493    }
494
495    let impl_item = parse_macro_input!(item as ItemImpl);
496    let type_name = &impl_item.self_ty;
497
498    let commands = impl_item.items.iter().filter_map(|item| match item {
499        ImplItem::Method(impl_item_method) => Some(&impl_item_method.sig.ident),
500        _ => None,
501    });
502
503    let mut result = TokenStream::from(quote! {
504        #impl_item
505
506        impl ::tranquil::command::CommandProvider for #type_name {
507            fn command_map(
508                self: ::std::sync::Arc<Self>,
509            ) -> ::std::result::Result<::tranquil::command::CommandMap, ::tranquil::command::CommandMapMergeError> {
510                ::tranquil::command::CommandMap::new([
511                    #(Self::#commands(self.clone())),*
512                ])
513            }
514        }
515    });
516    result.extend(errors);
517    result
518}
519
520#[proc_macro_derive(Choices)]
521pub fn derive_choices(item: TokenStream) -> TokenStream {
522    // TODO: Better error messages for unsupported enums.
523
524    let enum_item = parse_macro_input!(item as ItemEnum);
525    let name = enum_item.ident;
526    let variants = enum_item.variants;
527
528    let choices = variants.iter().map(|variant| {
529        let name = &variant.ident;
530        quote! {
531            ::tranquil::resolve::Choice {
532                name: ::std::convert::From::from(::std::stringify!(#name)),
533                value: ::std::convert::From::from(::std::stringify!(#name)),
534            }
535        }
536    });
537
538    let resolvers = variants.iter().map(|variant| {
539        let name = &variant.ident;
540        quote! {
541            ::std::stringify!(#name) => ::std::option::Option::Some(Self::#name),
542        }
543    });
544
545    quote! {
546        impl ::tranquil::resolve::Choices for #name {
547            fn name() -> ::std::string::String {
548                ::std::convert::From::from(::std::stringify!(#name))
549            }
550
551            fn choices() -> ::std::vec::Vec<::tranquil::resolve::Choice> {
552                ::std::vec![#(#choices),*]
553            }
554
555            fn resolve(option: ::std::string::String) -> ::std::option::Option<Self> {
556                match ::std::convert::AsRef::as_ref(&option) {
557                    #(#resolvers)*
558                    _ => ::std::option::Option::None,
559                }
560            }
561        }
562    }
563    .into()
564}