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#[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) .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 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 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 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) .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 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 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 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}