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