twilight_model/application/interaction/application_command/
option.rs

1use crate::{
2    application::command::CommandOptionType,
3    id::{
4        marker::{AttachmentMarker, ChannelMarker, GenericMarker, RoleMarker, UserMarker},
5        Id,
6    },
7};
8use serde::{
9    de::{Error as DeError, IgnoredAny, MapAccess, Unexpected, Visitor},
10    ser::SerializeStruct,
11    Deserialize, Deserializer, Serialize, Serializer,
12};
13use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
14
15/// Data received when a user fills in a command option.
16///
17/// See [Discord Docs/Application Command Object].
18///
19/// [Discord Docs/Application Command Object]: https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-interaction-data-option-structure
20#[derive(Clone, Debug, PartialEq)]
21pub struct CommandDataOption {
22    /// Name of the option.
23    pub name: String,
24    /// Value of the option.
25    pub value: CommandOptionValue,
26}
27
28impl Serialize for CommandDataOption {
29    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
30        let subcommand_is_empty = matches!(
31            &self.value,
32            CommandOptionValue::SubCommand(o)
33            | CommandOptionValue::SubCommandGroup(o)
34                if o.is_empty()
35        );
36
37        let focused = matches!(&self.value, CommandOptionValue::Focused(_, _));
38
39        let len = 2 + usize::from(!subcommand_is_empty) + usize::from(focused);
40
41        let mut state = serializer.serialize_struct("CommandDataOption", len)?;
42
43        if focused {
44            state.serialize_field("focused", &focused)?;
45        }
46
47        state.serialize_field("name", &self.name)?;
48
49        state.serialize_field("type", &self.value.kind())?;
50
51        match &self.value {
52            CommandOptionValue::Attachment(a) => state.serialize_field("value", a)?,
53            CommandOptionValue::Boolean(b) => state.serialize_field("value", b)?,
54            CommandOptionValue::Channel(c) => state.serialize_field("value", c)?,
55            CommandOptionValue::Focused(f, _) => state.serialize_field("value", f)?,
56            CommandOptionValue::Integer(i) => state.serialize_field("value", i)?,
57            CommandOptionValue::Mentionable(m) => state.serialize_field("value", m)?,
58            CommandOptionValue::Number(n) => state.serialize_field("value", n)?,
59            CommandOptionValue::Role(r) => state.serialize_field("value", r)?,
60            CommandOptionValue::String(s) => state.serialize_field("value", s)?,
61            CommandOptionValue::User(u) => state.serialize_field("value", u)?,
62            CommandOptionValue::SubCommand(s) | CommandOptionValue::SubCommandGroup(s) => {
63                if !subcommand_is_empty {
64                    state.serialize_field("options", s)?
65                }
66            }
67        }
68
69        state.end()
70    }
71}
72
73impl<'de> Deserialize<'de> for CommandDataOption {
74    #[allow(clippy::too_many_lines)]
75    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
76        #[derive(Debug, Deserialize)]
77        #[serde(field_identifier, rename_all = "snake_case")]
78        enum Fields {
79            Name,
80            Type,
81            Value,
82            Options,
83            Focused,
84        }
85
86        // An `Id` variant is purposely not present here to prevent wrongly
87        // parsing string options as numbers, trimming leading zeroes.
88        #[derive(Debug, Deserialize)]
89        #[serde(untagged)]
90        enum ValueEnvelope {
91            Boolean(bool),
92            Integer(i64),
93            Number(f64),
94            String(String),
95        }
96
97        impl ValueEnvelope {
98            #[allow(clippy::missing_const_for_fn)]
99            fn as_unexpected(&self) -> Unexpected<'_> {
100                match self {
101                    Self::Boolean(b) => Unexpected::Bool(*b),
102                    Self::Integer(i) => Unexpected::Signed(*i),
103                    Self::Number(f) => Unexpected::Float(*f),
104                    Self::String(s) => Unexpected::Str(s),
105                }
106            }
107        }
108
109        impl Display for ValueEnvelope {
110            fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
111                match self {
112                    Self::Boolean(b) => Display::fmt(b, f),
113                    Self::Integer(i) => Display::fmt(i, f),
114                    Self::Number(n) => Display::fmt(n, f),
115                    Self::String(s) => Display::fmt(s, f),
116                }
117            }
118        }
119
120        struct CommandDataOptionVisitor;
121
122        impl<'de> Visitor<'de> for CommandDataOptionVisitor {
123            type Value = CommandDataOption;
124
125            fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
126                formatter.write_str("CommandDataOption")
127            }
128
129            #[allow(clippy::too_many_lines)]
130            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
131                let mut name_opt = None;
132                let mut kind_opt = None;
133                let mut options = Vec::new();
134                let mut value_opt: Option<ValueEnvelope> = None;
135                let mut focused = None;
136
137                loop {
138                    let key = match map.next_key() {
139                        Ok(Some(key)) => key,
140                        Ok(None) => break,
141                        Err(_) => {
142                            map.next_value::<IgnoredAny>()?;
143
144                            continue;
145                        }
146                    };
147
148                    match key {
149                        Fields::Name => {
150                            if name_opt.is_some() {
151                                return Err(DeError::duplicate_field("name"));
152                            }
153
154                            name_opt = Some(map.next_value()?);
155                        }
156                        Fields::Type => {
157                            if kind_opt.is_some() {
158                                return Err(DeError::duplicate_field("type"));
159                            }
160
161                            kind_opt = Some(map.next_value()?);
162                        }
163                        Fields::Value => {
164                            if value_opt.is_some() {
165                                return Err(DeError::duplicate_field("value"));
166                            }
167
168                            value_opt = Some(map.next_value()?);
169                        }
170                        Fields::Options => {
171                            if !options.is_empty() {
172                                return Err(DeError::duplicate_field("options"));
173                            }
174
175                            options = map.next_value()?;
176                        }
177                        Fields::Focused => {
178                            if focused.is_some() {
179                                return Err(DeError::duplicate_field("focused"));
180                            }
181
182                            focused = map.next_value()?;
183                        }
184                    }
185                }
186
187                let focused = focused.unwrap_or_default();
188                let name = name_opt.ok_or_else(|| DeError::missing_field("name"))?;
189                let kind = kind_opt.ok_or_else(|| DeError::missing_field("type"))?;
190
191                let value = if focused {
192                    let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
193
194                    CommandOptionValue::Focused(val.to_string(), kind)
195                } else {
196                    match kind {
197                        CommandOptionType::Attachment => {
198                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
199
200                            if let ValueEnvelope::String(id) = &val {
201                                CommandOptionValue::Attachment(id.parse().map_err(|_| {
202                                    DeError::invalid_type(val.as_unexpected(), &"attachment id")
203                                })?)
204                            } else {
205                                return Err(DeError::invalid_type(
206                                    val.as_unexpected(),
207                                    &"attachment id",
208                                ));
209                            }
210                        }
211                        CommandOptionType::Boolean => {
212                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
213
214                            if let ValueEnvelope::Boolean(b) = val {
215                                CommandOptionValue::Boolean(b)
216                            } else {
217                                return Err(DeError::invalid_type(val.as_unexpected(), &"boolean"));
218                            }
219                        }
220                        CommandOptionType::Channel => {
221                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
222
223                            if let ValueEnvelope::String(id) = &val {
224                                CommandOptionValue::Channel(id.parse().map_err(|_| {
225                                    DeError::invalid_type(val.as_unexpected(), &"channel id")
226                                })?)
227                            } else {
228                                return Err(DeError::invalid_type(
229                                    val.as_unexpected(),
230                                    &"channel id",
231                                ));
232                            }
233                        }
234                        CommandOptionType::Integer => {
235                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
236
237                            if let ValueEnvelope::Integer(i) = val {
238                                CommandOptionValue::Integer(i)
239                            } else {
240                                return Err(DeError::invalid_type(val.as_unexpected(), &"integer"));
241                            }
242                        }
243                        CommandOptionType::Mentionable => {
244                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
245
246                            if let ValueEnvelope::String(id) = &val {
247                                CommandOptionValue::Mentionable(id.parse().map_err(|_| {
248                                    DeError::invalid_type(val.as_unexpected(), &"mentionable id")
249                                })?)
250                            } else {
251                                return Err(DeError::invalid_type(
252                                    val.as_unexpected(),
253                                    &"mentionable id",
254                                ));
255                            }
256                        }
257                        CommandOptionType::Number => {
258                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
259
260                            match val {
261                                ValueEnvelope::Integer(i) => {
262                                    // As json allows sending floating
263                                    // points without the tailing decimals
264                                    // it may be interpreted as a integer
265                                    // but it is safe to cast as there can
266                                    // not occur any loss.
267                                    #[allow(clippy::cast_precision_loss)]
268                                    CommandOptionValue::Number(i as f64)
269                                }
270                                ValueEnvelope::Number(f) => CommandOptionValue::Number(f),
271                                other => {
272                                    return Err(DeError::invalid_type(
273                                        other.as_unexpected(),
274                                        &"number",
275                                    ));
276                                }
277                            }
278                        }
279                        CommandOptionType::Role => {
280                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
281
282                            if let ValueEnvelope::String(id) = &val {
283                                CommandOptionValue::Role(id.parse().map_err(|_| {
284                                    DeError::invalid_type(val.as_unexpected(), &"role id")
285                                })?)
286                            } else {
287                                return Err(DeError::invalid_type(val.as_unexpected(), &"role id"));
288                            }
289                        }
290                        CommandOptionType::String => {
291                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
292
293                            if let ValueEnvelope::String(s) = val {
294                                CommandOptionValue::String(s)
295                            } else {
296                                return Err(DeError::invalid_type(val.as_unexpected(), &"string"));
297                            }
298                        }
299                        CommandOptionType::SubCommand => CommandOptionValue::SubCommand(options),
300                        CommandOptionType::SubCommandGroup => {
301                            CommandOptionValue::SubCommandGroup(options)
302                        }
303                        CommandOptionType::User => {
304                            let val = value_opt.ok_or_else(|| DeError::missing_field("value"))?;
305
306                            if let ValueEnvelope::String(id) = &val {
307                                CommandOptionValue::User(id.parse().map_err(|_| {
308                                    DeError::invalid_type(val.as_unexpected(), &"user id")
309                                })?)
310                            } else {
311                                return Err(DeError::invalid_type(val.as_unexpected(), &"user id"));
312                            }
313                        }
314                    }
315                };
316
317                Ok(CommandDataOption { name, value })
318            }
319        }
320
321        deserializer.deserialize_map(CommandDataOptionVisitor)
322    }
323}
324
325/// Combined value and value type for a [`CommandDataOption`].
326#[derive(Clone, Debug, PartialEq)]
327pub enum CommandOptionValue {
328    /// Attachment option.
329    Attachment(Id<AttachmentMarker>),
330    /// Boolean option.
331    Boolean(bool),
332    /// Channel option.
333    Channel(Id<ChannelMarker>),
334    /// Focused option.
335    ///
336    /// Since Discord does not validate focused fields, they are sent as strings.
337    /// This means that you will not necessarily get a valid number from number options.
338    ///
339    /// See [Discord Docs/Autocomplete].
340    ///
341    /// The actual [`CommandOptionType`] is available through the second tuple value.
342    ///
343    /// [Discord Docs/Autocomplete]: https://discord.com/developers/docs/interactions/application-commands#autocomplete
344    /// [`CommandOptionType`]: crate::application::command::CommandOptionType
345    Focused(String, CommandOptionType),
346    /// Integer option.
347    Integer(i64),
348    /// Mentionable option.
349    Mentionable(Id<GenericMarker>),
350    /// Number option.
351    Number(f64),
352    /// Role option.
353    Role(Id<RoleMarker>),
354    /// String option.
355    String(String),
356    /// Subcommand option.
357    SubCommand(Vec<CommandDataOption>),
358    /// Subcommand group option.
359    SubCommandGroup(Vec<CommandDataOption>),
360    /// User option.
361    User(Id<UserMarker>),
362}
363
364impl From<Id<AttachmentMarker>> for CommandOptionValue {
365    fn from(value: Id<AttachmentMarker>) -> Self {
366        CommandOptionValue::Attachment(value)
367    }
368}
369
370impl From<bool> for CommandOptionValue {
371    fn from(value: bool) -> Self {
372        CommandOptionValue::Boolean(value)
373    }
374}
375
376impl From<Id<ChannelMarker>> for CommandOptionValue {
377    fn from(value: Id<ChannelMarker>) -> Self {
378        CommandOptionValue::Channel(value)
379    }
380}
381
382impl From<i64> for CommandOptionValue {
383    fn from(value: i64) -> Self {
384        CommandOptionValue::Integer(value)
385    }
386}
387
388impl From<f64> for CommandOptionValue {
389    fn from(value: f64) -> Self {
390        CommandOptionValue::Number(value)
391    }
392}
393
394impl From<Id<RoleMarker>> for CommandOptionValue {
395    fn from(value: Id<RoleMarker>) -> Self {
396        CommandOptionValue::Role(value)
397    }
398}
399
400impl From<String> for CommandOptionValue {
401    fn from(value: String) -> Self {
402        CommandOptionValue::String(value)
403    }
404}
405
406impl From<Id<UserMarker>> for CommandOptionValue {
407    fn from(value: Id<UserMarker>) -> Self {
408        CommandOptionValue::User(value)
409    }
410}
411
412impl CommandOptionValue {
413    pub const fn kind(&self) -> CommandOptionType {
414        match self {
415            CommandOptionValue::Attachment(_) => CommandOptionType::Attachment,
416            CommandOptionValue::Boolean(_) => CommandOptionType::Boolean,
417            CommandOptionValue::Channel(_) => CommandOptionType::Channel,
418            CommandOptionValue::Focused(_, t) => *t,
419            CommandOptionValue::Integer(_) => CommandOptionType::Integer,
420            CommandOptionValue::Mentionable(_) => CommandOptionType::Mentionable,
421            CommandOptionValue::Number(_) => CommandOptionType::Number,
422            CommandOptionValue::Role(_) => CommandOptionType::Role,
423            CommandOptionValue::String(_) => CommandOptionType::String,
424            CommandOptionValue::SubCommand(_) => CommandOptionType::SubCommand,
425            CommandOptionValue::SubCommandGroup(_) => CommandOptionType::SubCommandGroup,
426            CommandOptionValue::User(_) => CommandOptionType::User,
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use crate::{
434        application::{
435            command::{CommandOptionType, CommandType},
436            interaction::application_command::{
437                CommandData, CommandDataOption, CommandOptionValue,
438            },
439        },
440        id::Id,
441    };
442    use serde_test::Token;
443
444    #[test]
445    fn no_options() {
446        let value = CommandData {
447            guild_id: Some(Id::new(2)),
448            id: Id::new(1),
449            name: "permissions".to_owned(),
450            kind: CommandType::ChatInput,
451            options: Vec::new(),
452            resolved: None,
453            target_id: None,
454        };
455        serde_test::assert_tokens(
456            &value,
457            &[
458                Token::Struct {
459                    name: "CommandData",
460                    len: 4,
461                },
462                Token::Str("guild_id"),
463                Token::Some,
464                Token::NewtypeStruct { name: "Id" },
465                Token::Str("2"),
466                Token::Str("id"),
467                Token::NewtypeStruct { name: "Id" },
468                Token::Str("1"),
469                Token::Str("name"),
470                Token::Str("permissions"),
471                Token::Str("type"),
472                Token::U8(CommandType::ChatInput.into()),
473                Token::StructEnd,
474            ],
475        )
476    }
477
478    #[test]
479    fn with_option() {
480        let value = CommandData {
481            guild_id: Some(Id::new(2)),
482            id: Id::new(1),
483            name: "permissions".to_owned(),
484            kind: CommandType::ChatInput,
485            options: Vec::from([CommandDataOption {
486                name: "cat".to_owned(),
487                value: CommandOptionValue::Integer(42),
488            }]),
489            resolved: None,
490            target_id: None,
491        };
492
493        serde_test::assert_tokens(
494            &value,
495            &[
496                Token::Struct {
497                    name: "CommandData",
498                    len: 5,
499                },
500                Token::Str("guild_id"),
501                Token::Some,
502                Token::NewtypeStruct { name: "Id" },
503                Token::Str("2"),
504                Token::Str("id"),
505                Token::NewtypeStruct { name: "Id" },
506                Token::Str("1"),
507                Token::Str("name"),
508                Token::Str("permissions"),
509                Token::Str("type"),
510                Token::U8(CommandType::ChatInput.into()),
511                Token::Str("options"),
512                Token::Seq { len: Some(1) },
513                Token::Struct {
514                    name: "CommandDataOption",
515                    len: 3,
516                },
517                Token::Str("name"),
518                Token::Str("cat"),
519                Token::Str("type"),
520                Token::U8(CommandOptionType::Integer as u8),
521                Token::Str("value"),
522                Token::I64(42),
523                Token::StructEnd,
524                Token::SeqEnd,
525                Token::StructEnd,
526            ],
527        )
528    }
529
530    #[test]
531    fn with_normal_option_and_autocomplete() {
532        let value = CommandData {
533            guild_id: Some(Id::new(2)),
534            id: Id::new(1),
535            name: "permissions".to_owned(),
536            kind: CommandType::ChatInput,
537            options: Vec::from([
538                CommandDataOption {
539                    name: "cat".to_owned(),
540                    value: CommandOptionValue::Integer(42),
541                },
542                CommandDataOption {
543                    name: "dog".to_owned(),
544                    value: CommandOptionValue::Focused(
545                        "Shiba".to_owned(),
546                        CommandOptionType::String,
547                    ),
548                },
549            ]),
550            resolved: None,
551            target_id: None,
552        };
553
554        serde_test::assert_de_tokens(
555            &value,
556            &[
557                Token::Struct {
558                    name: "CommandData",
559                    len: 5,
560                },
561                Token::Str("guild_id"),
562                Token::Some,
563                Token::NewtypeStruct { name: "Id" },
564                Token::Str("2"),
565                Token::Str("id"),
566                Token::NewtypeStruct { name: "Id" },
567                Token::Str("1"),
568                Token::Str("name"),
569                Token::Str("permissions"),
570                Token::Str("type"),
571                Token::U8(CommandType::ChatInput.into()),
572                Token::Str("options"),
573                Token::Seq { len: Some(2) },
574                Token::Struct {
575                    name: "CommandDataOption",
576                    len: 3,
577                },
578                Token::Str("name"),
579                Token::Str("cat"),
580                Token::Str("type"),
581                Token::U8(CommandOptionType::Integer as u8),
582                Token::Str("value"),
583                Token::I64(42),
584                Token::StructEnd,
585                Token::Struct {
586                    name: "CommandDataOption",
587                    len: 4,
588                },
589                Token::Str("focused"),
590                Token::Some,
591                Token::Bool(true),
592                Token::Str("name"),
593                Token::Str("dog"),
594                Token::Str("type"),
595                Token::U8(CommandOptionType::String as u8),
596                Token::Str("value"),
597                Token::String("Shiba"),
598                Token::StructEnd,
599                Token::SeqEnd,
600                Token::StructEnd,
601            ],
602        )
603    }
604
605    #[test]
606    fn subcommand_without_option() {
607        let value = CommandData {
608            guild_id: None,
609            id: Id::new(1),
610            name: "photo".to_owned(),
611            kind: CommandType::ChatInput,
612            options: Vec::from([CommandDataOption {
613                name: "cat".to_owned(),
614                value: CommandOptionValue::SubCommand(Vec::new()),
615            }]),
616            resolved: None,
617            target_id: None,
618        };
619
620        serde_test::assert_tokens(
621            &value,
622            &[
623                Token::Struct {
624                    name: "CommandData",
625                    len: 4,
626                },
627                Token::Str("id"),
628                Token::NewtypeStruct { name: "Id" },
629                Token::Str("1"),
630                Token::Str("name"),
631                Token::Str("photo"),
632                Token::Str("type"),
633                Token::U8(CommandType::ChatInput.into()),
634                Token::Str("options"),
635                Token::Seq { len: Some(1) },
636                Token::Struct {
637                    name: "CommandDataOption",
638                    len: 2,
639                },
640                Token::Str("name"),
641                Token::Str("cat"),
642                Token::Str("type"),
643                Token::U8(CommandOptionType::SubCommand as u8),
644                Token::StructEnd,
645                Token::SeqEnd,
646                Token::StructEnd,
647            ],
648        );
649    }
650
651    #[test]
652    fn numbers() {
653        let value = CommandDataOption {
654            name: "opt".to_string(),
655            value: CommandOptionValue::Number(5.0),
656        };
657
658        serde_test::assert_de_tokens(
659            &value,
660            &[
661                Token::Struct {
662                    name: "CommandDataOption",
663                    len: 3,
664                },
665                Token::Str("name"),
666                Token::Str("opt"),
667                Token::Str("type"),
668                Token::U8(CommandOptionType::Number as u8),
669                Token::Str("value"),
670                Token::I64(5),
671                Token::StructEnd,
672            ],
673        );
674    }
675
676    #[test]
677    fn autocomplete() {
678        let value = CommandDataOption {
679            name: "opt".to_string(),
680            value: CommandOptionValue::Focused(
681                "not a number".to_owned(),
682                CommandOptionType::Number,
683            ),
684        };
685
686        serde_test::assert_de_tokens(
687            &value,
688            &[
689                Token::Struct {
690                    name: "CommandDataOption",
691                    len: 4,
692                },
693                Token::Str("focused"),
694                Token::Some,
695                Token::Bool(true),
696                Token::Str("name"),
697                Token::Str("opt"),
698                Token::Str("type"),
699                Token::U8(CommandOptionType::Number as u8),
700                Token::Str("value"),
701                Token::String("not a number"),
702                Token::StructEnd,
703            ],
704        );
705    }
706
707    #[test]
708    fn autocomplete_number() {
709        let value = CommandDataOption {
710            name: "opt".to_string(),
711            value: CommandOptionValue::Focused("1".to_owned(), CommandOptionType::Number),
712        };
713
714        serde_test::assert_de_tokens(
715            &value,
716            &[
717                Token::Struct {
718                    name: "CommandDataOption",
719                    len: 4,
720                },
721                Token::Str("focused"),
722                Token::Some,
723                Token::Bool(true),
724                Token::Str("name"),
725                Token::Str("opt"),
726                Token::Str("type"),
727                Token::U8(CommandOptionType::Number as u8),
728                Token::Str("value"),
729                Token::String("1"),
730                Token::StructEnd,
731            ],
732        );
733    }
734
735    #[test]
736    fn leading_zeroes_string_option_value() {
737        let value = CommandDataOption {
738            name: "opt".to_string(),
739            value: CommandOptionValue::String("0001".to_owned()),
740        };
741
742        serde_test::assert_de_tokens(
743            &value,
744            &[
745                Token::Struct {
746                    name: "CommandDataOption",
747                    len: 3,
748                },
749                Token::Str("name"),
750                Token::Str("opt"),
751                Token::Str("type"),
752                Token::U8(CommandOptionType::String as u8),
753                Token::Str("value"),
754                Token::String("0001"),
755                Token::StructEnd,
756            ],
757        );
758    }
759}