pikadick_slash_framework/
command.rs

1use crate::{
2    ArgumentParam,
3    BoxError,
4    BoxFuture,
5    BuilderError,
6    CheckFn,
7    DataType,
8    FromOptions,
9};
10use serenity::{
11    builder::{
12        CreateCommand,
13        CreateCommandOption,
14    },
15    client::Context,
16    model::application::{
17        CommandInteraction,
18        CommandOptionType,
19    },
20};
21use std::{
22    collections::HashMap,
23    future::Future,
24    sync::Arc,
25};
26
27type OnProcessResult = Result<(), BoxError>;
28pub type OnProcessFuture = BoxFuture<'static, OnProcessResult>;
29
30// Keep these types in sync.
31type OnProcessFutureFn = Box<dyn Fn(Context, CommandInteraction) -> OnProcessFuture + Send + Sync>;
32type OnProcessFutureFnPtr<F, A> = fn(Context, CommandInteraction, A) -> F;
33
34type HelpOnProcessFutureFn = Box<
35    dyn Fn(Context, CommandInteraction, Arc<HashMap<Box<str>, Command>>) -> OnProcessFuture
36        + Send
37        + Sync,
38>;
39type HelpOnProcessFutureFnPtr<F, A> =
40    fn(Context, CommandInteraction, Arc<HashMap<Box<str>, Command>>, A) -> F;
41
42/// A slash framework command
43pub struct Command {
44    /// The name of the command
45    name: Box<str>,
46
47    /// Description
48    description: Box<str>,
49
50    /// Arguments
51    arguments: Box<[ArgumentParam]>,
52
53    /// The main "process" func
54    on_process: OnProcessFutureFn,
55
56    /// Checks that must pass before this command is run
57    checks: Vec<CheckFn>,
58}
59
60impl Command {
61    /// Get the command name
62    pub fn name(&self) -> &str {
63        &self.name
64    }
65
66    /// Get the command description
67    pub fn description(&self) -> &str {
68        &self.description
69    }
70
71    /// Get the command arguments
72    pub fn arguments(&self) -> &[ArgumentParam] {
73        &self.arguments
74    }
75
76    /// Fire the on_process hook
77    pub async fn fire_on_process(
78        &self,
79        ctx: Context,
80        interaction: CommandInteraction,
81    ) -> Result<(), BoxError> {
82        (self.on_process)(ctx, interaction).await
83    }
84
85    /// Get the inner checks
86    pub fn checks(&self) -> &[CheckFn] {
87        &self.checks
88    }
89
90    /// Register this command
91    pub fn register(&self, mut command: CreateCommand) -> CreateCommand {
92        command = command.name(self.name()).description(self.description());
93
94        for argument in self.arguments().iter() {
95            let option_kind = match argument.kind() {
96                DataType::Boolean => CommandOptionType::Boolean,
97                DataType::String => CommandOptionType::String,
98                DataType::Integer => CommandOptionType::Integer,
99            };
100            let option =
101                CreateCommandOption::new(option_kind, argument.name(), argument.description())
102                    .required(argument.required());
103            command = command.add_option(option);
104        }
105
106        command
107    }
108}
109
110impl std::fmt::Debug for Command {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("Command")
113            .field("name", &self.name)
114            .field("description", &self.description)
115            .field("arguments", &self.arguments)
116            .field("on_process", &"<func>")
117            .finish()
118    }
119}
120
121/// A builder for a [`Command`].
122pub struct CommandBuilder<'a, 'b> {
123    name: Option<&'a str>,
124    description: Option<&'b str>,
125    arguments: Vec<ArgumentParam>,
126
127    on_process: Option<OnProcessFutureFn>,
128    checks: Vec<CheckFn>,
129}
130
131impl<'a, 'b> CommandBuilder<'a, 'b> {
132    /// Make a new [`CommandBuilder`].
133    pub fn new() -> Self {
134        Self {
135            name: None,
136            description: None,
137            arguments: Vec::new(),
138
139            on_process: None,
140            checks: Vec::new(),
141        }
142    }
143
144    /// The command name
145    pub fn name(&mut self, name: &'a str) -> &mut Self {
146        self.name = Some(name);
147        self
148    }
149
150    /// The command description
151    pub fn description(&mut self, description: &'b str) -> &mut Self {
152        self.description = Some(description);
153        self
154    }
155
156    /// Add an argument
157    pub fn argument(&mut self, argument: ArgumentParam) -> &mut Self {
158        self.arguments.push(argument);
159        self
160    }
161
162    /// Add many arguments
163    pub fn arguments(&mut self, arguments: impl Iterator<Item = ArgumentParam>) -> &mut Self {
164        for argument in arguments {
165            self.argument(argument);
166        }
167        self
168    }
169
170    /// The on_process hook
171    pub fn on_process<F, A>(&mut self, on_process: OnProcessFutureFnPtr<F, A>) -> &mut Self
172    where
173        F: Future<Output = Result<(), BoxError>> + Send + 'static,
174        A: FromOptions + 'static,
175    {
176        // Trampoline so user does not have to box manually and parse their args manually
177        self.on_process = Some(Box::new(move |ctx, interaction| {
178            Box::pin(async move {
179                let args = A::from_options(&interaction)?;
180                (on_process)(ctx, interaction, args).await
181            })
182        }));
183
184        self
185    }
186
187    /// Add a check to this specific command
188    pub fn check(&mut self, check: CheckFn) -> &mut Self {
189        self.checks.push(check);
190        self
191    }
192
193    /// Build the [`Command`]
194    pub fn build(&mut self) -> Result<Command, BuilderError> {
195        #[allow(clippy::or_fun_call)]
196        let name = self.name.take().ok_or(BuilderError::MissingField("name"))?;
197        #[allow(clippy::or_fun_call)]
198        let description = self
199            .description
200            .take()
201            .ok_or(BuilderError::MissingField("description"))?;
202        #[allow(clippy::or_fun_call)]
203        let on_process = self
204            .on_process
205            .take()
206            .ok_or(BuilderError::MissingField("on_process"))?;
207        let checks = std::mem::take(&mut self.checks);
208
209        Ok(Command {
210            name: name.into(),
211            description: description.into(),
212            arguments: std::mem::take(&mut self.arguments).into_boxed_slice(),
213
214            on_process,
215            checks,
216        })
217    }
218}
219
220impl std::fmt::Debug for CommandBuilder<'_, '_> {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.debug_struct("CommandBuilder")
223            .field("name", &self.name)
224            .field("description", &self.description)
225            .field("arguments", &self.arguments)
226            .field("on_process", &self.on_process.as_ref().map(|_| "<func>"))
227            .finish()
228    }
229}
230
231impl Default for CommandBuilder<'_, '_> {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237/// A slash framework help command
238pub struct HelpCommand {
239    /// Description
240    description: Box<str>,
241
242    /// Arguments
243    arguments: Box<[ArgumentParam]>,
244
245    /// The main "process" func
246    on_process: HelpOnProcessFutureFn,
247}
248
249impl HelpCommand {
250    /// Get the help command description
251    pub fn description(&self) -> &str {
252        &self.description
253    }
254
255    /// Get the help command arguments
256    pub fn arguments(&self) -> &[ArgumentParam] {
257        &self.arguments
258    }
259
260    /// Fire the on_process hook
261    pub async fn fire_on_process(
262        &self,
263        ctx: Context,
264        interaction: CommandInteraction,
265        map: Arc<HashMap<Box<str>, Command>>,
266    ) -> Result<(), BoxError> {
267        (self.on_process)(ctx, interaction, map).await
268    }
269
270    /// Register this help command
271    pub fn register(&self, mut command: CreateCommand) -> CreateCommand {
272        command = command.name("help").description(self.description());
273
274        for argument in self.arguments().iter() {
275            let option_kind = match argument.kind() {
276                DataType::Boolean => CommandOptionType::Boolean,
277                DataType::String => CommandOptionType::String,
278                DataType::Integer => CommandOptionType::Integer,
279            };
280            let option =
281                CreateCommandOption::new(option_kind, argument.name(), argument.description())
282                    .required(argument.required());
283            command = command.add_option(option);
284        }
285
286        command
287    }
288}
289
290impl std::fmt::Debug for HelpCommand {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        f.debug_struct("HelpCommand")
293            .field("description", &self.description)
294            .field("arguments", &self.arguments)
295            .field("on_process", &"<func>")
296            .finish()
297    }
298}
299
300/// A builder for a [`HelpCommand`].
301pub struct HelpCommandBuilder<'a> {
302    description: Option<&'a str>,
303    arguments: Vec<ArgumentParam>,
304
305    on_process: Option<HelpOnProcessFutureFn>,
306}
307
308impl<'a> HelpCommandBuilder<'a> {
309    /// Make a new [`HelpCommandBuilder`].
310    pub fn new() -> Self {
311        Self {
312            description: None,
313            arguments: Vec::new(),
314
315            on_process: None,
316        }
317    }
318
319    /// The help command description
320    pub fn description(&mut self, description: &'a str) -> &mut Self {
321        self.description = Some(description);
322        self
323    }
324
325    /// Add an argument
326    pub fn argument(&mut self, argument: ArgumentParam) -> &mut Self {
327        self.arguments.push(argument);
328        self
329    }
330
331    /// The on_process hook
332    pub fn on_process<F, A>(&mut self, on_process: HelpOnProcessFutureFnPtr<F, A>) -> &mut Self
333    where
334        F: Future<Output = Result<(), BoxError>> + Send + 'static,
335        A: FromOptions + 'static,
336    {
337        // Trampoline so user does not have to box manually and parse their args manually
338        self.on_process = Some(Box::new(move |ctx, interaction, map| {
339            Box::pin(async move {
340                let args = A::from_options(&interaction)?;
341                (on_process)(ctx, interaction, map, args).await
342            })
343        }));
344
345        self
346    }
347
348    /// Build the [`HelpCommand`]
349    pub fn build(&mut self) -> Result<HelpCommand, BuilderError> {
350        #[allow(clippy::or_fun_call)]
351        let description = self
352            .description
353            .take()
354            .ok_or(BuilderError::MissingField("description"))?;
355        #[allow(clippy::or_fun_call)]
356        let on_process = self
357            .on_process
358            .take()
359            .ok_or(BuilderError::MissingField("on_process"))?;
360
361        Ok(HelpCommand {
362            description: description.into(),
363            arguments: std::mem::take(&mut self.arguments).into_boxed_slice(),
364
365            on_process,
366        })
367    }
368}
369
370impl std::fmt::Debug for HelpCommandBuilder<'_> {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        f.debug_struct("HelpCommandBuilder")
373            .field("description", &self.description)
374            .field("arguments", &self.arguments)
375            .field("on_process", &self.on_process.as_ref().map(|_| "<func>"))
376            .finish()
377    }
378}
379
380impl Default for HelpCommandBuilder<'_> {
381    fn default() -> Self {
382        Self::new()
383    }
384}