Skip to content

Commit 588bbe6

Browse files
Support #[string] in slash commands (#387)
1 parent 38ded9c commit 588bbe6

File tree

4 files changed

+73
-63
lines changed

4 files changed

+73
-63
lines changed

examples/feature_showcase/parameter_attributes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub async fn test_flag(ctx: Context<'_>, #[flag] flag: bool) -> Result<(), Error
9292
}
9393

9494
/// Demonstrates `#[string]`
95-
#[poise::command(prefix_command)]
95+
#[poise::command(prefix_command, slash_command)]
9696
pub async fn test_fromstr(
9797
ctx: Context<'_>,
9898
#[string] ip_addr: std::net::IpAddr,

macros/src/command/slash.rs

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub fn generate_parameters(inv: &Invocation) -> Result<Vec<proc_macro2::TokenStr
7878
true => {
7979
if let Some(_choices) = &param.args.choices {
8080
quote::quote! { Some(|o| o.kind(::poise::serenity_prelude::CommandOptionType::Integer)) }
81+
} else if param.args.string {
82+
quote::quote! { Some(|o| o.kind(::poise::serenity_prelude::CommandOptionType::String)) }
8183
} else {
8284
quote::quote! { Some(|o| {
8385
<#type_ as ::poise::SlashArgument>::create(o)
@@ -100,6 +102,8 @@ pub fn generate_parameters(inv: &Invocation) -> Result<Vec<proc_macro2::TokenStr
100102
localizations: Cow::Borrowed(&[]),
101103
__non_exhaustive: (),
102104
} ),*]) }
105+
} else if param.args.string {
106+
quote::quote! { Cow::Borrowed(&[]) }
103107
} else {
104108
quote::quote! { <#type_ as ::poise::SlashArgument>::choices() }
105109
}
@@ -194,8 +198,8 @@ pub fn generate_slash_action(inv: &Invocation) -> Result<proc_macro2::TokenStrea
194198
}
195199

196200
fn parse_slash_param(param: &CommandParameter) -> proc_macro2::TokenStream {
197-
fn extract_slash_argument(ty: &syn::Type) -> syn::Expr {
198-
syn::parse_quote! {
201+
fn extract_slash_argument(ty: impl quote::ToTokens) -> proc_macro2::TokenStream {
202+
quote::quote! {
199203
<#ty as ::poise::SlashArgument>::extract(
200204
serenity_ctx,
201205
interaction,
@@ -210,76 +214,64 @@ fn parse_slash_param(param: &CommandParameter) -> proc_macro2::TokenStream {
210214

211215
if param.args.flag {
212216
// Extract #[flag]
213-
let extract = extract_slash_argument(&syn::parse_quote! { bool });
214-
quote::quote! {
217+
let extract = extract_slash_argument(quote::quote! { bool });
218+
return quote::quote! {
215219
match args.iter().find(|arg| arg.name == #name) {
216220
Some(arg) => #extract,
217221
None => false,
218222
}
219-
}
220-
} else if let Some(choices) = &param.args.choices {
223+
};
224+
}
225+
226+
enum Wrapper {
227+
Option,
228+
Vec,
229+
Plain,
230+
}
231+
232+
let (wrapper, ty) = if let Some(ty) = unwrap_generic(ty, "Option") {
233+
(Wrapper::Option, ty)
234+
} else if let Some(ty) = unwrap_generic(ty, "Vec") {
235+
(Wrapper::Vec, ty)
236+
} else {
237+
(Wrapper::Plain, ty)
238+
};
239+
240+
let extract = if let Some(choices) = &param.args.choices {
221241
// Extract #[choices(...)]
222242
let choice_indices = (0..choices.0.len()).map(syn::Index::from);
223243
let choice_vals = &choices.0;
224244

225-
// Allow `Option<T>` for choice parameters
226-
let (choices, not_found) = if unwrap_generic(ty, "Option").is_some() {
227-
(
228-
quote::quote! { #( #choice_indices => Some(#choice_vals), )* },
229-
quote::quote! { None },
230-
)
231-
} else {
232-
(
233-
quote::quote! { #( #choice_indices => #choice_vals, )* },
234-
quote::quote! {
245+
quote::quote! {{
246+
let ::poise::serenity_prelude::ResolvedValue::Integer(index) = arg.value else {
247+
return Err(::poise::SlashArgError::new_command_structure_mismatch(
248+
"expected integer, as the index for an inline choice parameter")
249+
);
250+
};
251+
match index {
252+
#( #choice_indices => #choice_vals, )*
253+
_ => {
235254
return Err(::poise::SlashArgError::new_command_structure_mismatch(
236-
"a required argument is missing"
255+
"out of range index for inline choice parameter"
237256
));
238-
},
239-
)
240-
};
241-
242-
quote::quote! {
243-
if let Some(arg) = args.iter().find(|arg| arg.name == #name) {
244-
let ::poise::serenity_prelude::ResolvedValue::Integer(index) = arg.value else {
245-
return Err(::poise::SlashArgError::new_command_structure_mismatch(
246-
"expected integer, as the index for an inline choice parameter")
247-
);
248-
};
249-
match index {
250-
#choices
251-
_ => {
252-
return Err(::poise::SlashArgError::new_command_structure_mismatch(
253-
"out of range index for inline choice parameter"
254-
));
255-
}
256257
}
257-
} else {
258-
#not_found
259-
}
260-
}
261-
} else if let Some(ty) = unwrap_generic(ty, "Option") {
262-
// Extract Option<T>
263-
let extract = extract_slash_argument(ty);
264-
quote::quote! {
265-
match args.iter().find(|arg| arg.name == #name) {
266-
Some(arg) => Some(#extract),
267-
None => None,
268-
}
269-
}
270-
} else if let Some(ty) = unwrap_generic(ty, "Vec") {
271-
// Extract Vec<T> (slash commands don't support variadic arguments right now
272-
let extract = extract_slash_argument(ty);
273-
quote::quote! {
274-
match args.iter().find(|arg| arg.name == #name) {
275-
Some(arg) => vec![#extract],
276-
None => vec![]
277258
}
278-
}
259+
}}
260+
} else if param.args.string {
261+
let extract = extract_slash_argument(quote::quote!(String));
262+
263+
quote::quote! {{
264+
let input = #extract;
265+
<#ty as ::std::str::FromStr>::from_str(&input)
266+
.map_err(|e| ::poise::SlashArgError::new_parse(e.into(), input))?
267+
}}
279268
} else {
280269
// Extract T
281-
let extract = extract_slash_argument(ty);
282-
quote::quote! {
270+
extract_slash_argument(ty)
271+
};
272+
273+
match wrapper {
274+
Wrapper::Plain => quote::quote! {
283275
match args.iter().find(|arg| arg.name == #name) {
284276
Some(arg) => #extract,
285277
None => {
@@ -288,7 +280,20 @@ fn parse_slash_param(param: &CommandParameter) -> proc_macro2::TokenStream {
288280
));
289281
}
290282
}
291-
}
283+
},
284+
Wrapper::Option => quote::quote! {
285+
match args.iter().find(|arg| arg.name == #name) {
286+
Some(arg) => Some(#extract),
287+
None => None,
288+
}
289+
},
290+
// Slash commands don't support variadic arguments right now
291+
Wrapper::Vec => quote::quote! {
292+
match args.iter().find(|arg| arg.name == #name) {
293+
Some(arg) => vec![#extract],
294+
None => vec![],
295+
}
296+
},
292297
}
293298
}
294299

macros/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,13 @@ attributes you can use on parameters:
109109
- `#[min_length = 0]`: Minimum length for this string parameter (slash-only)
110110
- `#[max_length = 1]`: Maximum length for this string parameter (slash-only)
111111
112-
## Parser settings (prefix only)
112+
## Parser settings
113113
- `#[string]`: Indicates that a type implements `FromStr` and should be parsed from a string argument.
114114
- `#[rest]`: Use the entire rest of the message for this parameter (prefix-only)
115115
- `#[lazy]`: Can be used on Option and Vec parameters and is equivalent to regular expressions' laziness (prefix-only)
116-
- `#[flag]`: Can be used on a bool parameter to set the bool to true if the user typed the parameter name literally (prefix-only)
117-
- For example with `async fn my_command(ctx: Context<'_>, #[flag] my_flag: bool)`, `~my_command` would set my_flag to false, and `~my_command my_flag` would set my_flag to true
116+
- `#[flag]`: Can be used on a bool parameter to make it optional and default to `false`; additionally,
117+
in prefix commands only, the user can pass in the parameter name literally to set it to `true`.
118+
- For example with `async fn my_command(ctx: Context<'_>, #[flag] my_flag: bool)`, `~my_command` or `/my_command` would set `my_flag` to false, while `~my_command my_flag` would set `my_flag` to true
118119
119120
# Help text
120121

src/slash_argument.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ impl SlashArgError {
231231
Self::CommandStructureMismatch { description }
232232
}
233233

234+
pub fn new_parse(error: Box<dyn std::error::Error + Send + Sync>, input: String) -> Self {
235+
Self::Parse { error, input }
236+
}
237+
234238
pub fn to_framework_error<U, E>(
235239
self,
236240
ctx: crate::ApplicationContext<'_, U, E>,

0 commit comments

Comments
 (0)