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 CommandOptionValue {
364    pub const fn kind(&self) -> CommandOptionType {
365        match self {
366            CommandOptionValue::Attachment(_) => CommandOptionType::Attachment,
367            CommandOptionValue::Boolean(_) => CommandOptionType::Boolean,
368            CommandOptionValue::Channel(_) => CommandOptionType::Channel,
369            CommandOptionValue::Focused(_, t) => *t,
370            CommandOptionValue::Integer(_) => CommandOptionType::Integer,
371            CommandOptionValue::Mentionable(_) => CommandOptionType::Mentionable,
372            CommandOptionValue::Number(_) => CommandOptionType::Number,
373            CommandOptionValue::Role(_) => CommandOptionType::Role,
374            CommandOptionValue::String(_) => CommandOptionType::String,
375            CommandOptionValue::SubCommand(_) => CommandOptionType::SubCommand,
376            CommandOptionValue::SubCommandGroup(_) => CommandOptionType::SubCommandGroup,
377            CommandOptionValue::User(_) => CommandOptionType::User,
378        }
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use crate::{
385        application::{
386            command::{CommandOptionType, CommandType},
387            interaction::application_command::{
388                CommandData, CommandDataOption, CommandOptionValue,
389            },
390        },
391        id::Id,
392    };
393    use serde_test::Token;
394
395    #[test]
396    fn no_options() {
397        let value = CommandData {
398            guild_id: Some(Id::new(2)),
399            id: Id::new(1),
400            name: "permissions".to_owned(),
401            kind: CommandType::ChatInput,
402            options: Vec::new(),
403            resolved: None,
404            target_id: None,
405        };
406        serde_test::assert_tokens(
407            &value,
408            &[
409                Token::Struct {
410                    name: "CommandData",
411                    len: 4,
412                },
413                Token::Str("guild_id"),
414                Token::Some,
415                Token::NewtypeStruct { name: "Id" },
416                Token::Str("2"),
417                Token::Str("id"),
418                Token::NewtypeStruct { name: "Id" },
419                Token::Str("1"),
420                Token::Str("name"),
421                Token::Str("permissions"),
422                Token::Str("type"),
423                Token::U8(CommandType::ChatInput.into()),
424                Token::StructEnd,
425            ],
426        )
427    }
428
429    #[test]
430    fn with_option() {
431        let value = CommandData {
432            guild_id: Some(Id::new(2)),
433            id: Id::new(1),
434            name: "permissions".to_owned(),
435            kind: CommandType::ChatInput,
436            options: Vec::from([CommandDataOption {
437                name: "cat".to_owned(),
438                value: CommandOptionValue::Integer(42),
439            }]),
440            resolved: None,
441            target_id: None,
442        };
443
444        serde_test::assert_tokens(
445            &value,
446            &[
447                Token::Struct {
448                    name: "CommandData",
449                    len: 5,
450                },
451                Token::Str("guild_id"),
452                Token::Some,
453                Token::NewtypeStruct { name: "Id" },
454                Token::Str("2"),
455                Token::Str("id"),
456                Token::NewtypeStruct { name: "Id" },
457                Token::Str("1"),
458                Token::Str("name"),
459                Token::Str("permissions"),
460                Token::Str("type"),
461                Token::U8(CommandType::ChatInput.into()),
462                Token::Str("options"),
463                Token::Seq { len: Some(1) },
464                Token::Struct {
465                    name: "CommandDataOption",
466                    len: 3,
467                },
468                Token::Str("name"),
469                Token::Str("cat"),
470                Token::Str("type"),
471                Token::U8(CommandOptionType::Integer as u8),
472                Token::Str("value"),
473                Token::I64(42),
474                Token::StructEnd,
475                Token::SeqEnd,
476                Token::StructEnd,
477            ],
478        )
479    }
480
481    #[test]
482    fn with_normal_option_and_autocomplete() {
483        let value = CommandData {
484            guild_id: Some(Id::new(2)),
485            id: Id::new(1),
486            name: "permissions".to_owned(),
487            kind: CommandType::ChatInput,
488            options: Vec::from([
489                CommandDataOption {
490                    name: "cat".to_owned(),
491                    value: CommandOptionValue::Integer(42),
492                },
493                CommandDataOption {
494                    name: "dog".to_owned(),
495                    value: CommandOptionValue::Focused(
496                        "Shiba".to_owned(),
497                        CommandOptionType::String,
498                    ),
499                },
500            ]),
501            resolved: None,
502            target_id: None,
503        };
504
505        serde_test::assert_de_tokens(
506            &value,
507            &[
508                Token::Struct {
509                    name: "CommandData",
510                    len: 5,
511                },
512                Token::Str("guild_id"),
513                Token::Some,
514                Token::NewtypeStruct { name: "Id" },
515                Token::Str("2"),
516                Token::Str("id"),
517                Token::NewtypeStruct { name: "Id" },
518                Token::Str("1"),
519                Token::Str("name"),
520                Token::Str("permissions"),
521                Token::Str("type"),
522                Token::U8(CommandType::ChatInput.into()),
523                Token::Str("options"),
524                Token::Seq { len: Some(2) },
525                Token::Struct {
526                    name: "CommandDataOption",
527                    len: 3,
528                },
529                Token::Str("name"),
530                Token::Str("cat"),
531                Token::Str("type"),
532                Token::U8(CommandOptionType::Integer as u8),
533                Token::Str("value"),
534                Token::I64(42),
535                Token::StructEnd,
536                Token::Struct {
537                    name: "CommandDataOption",
538                    len: 4,
539                },
540                Token::Str("focused"),
541                Token::Some,
542                Token::Bool(true),
543                Token::Str("name"),
544                Token::Str("dog"),
545                Token::Str("type"),
546                Token::U8(CommandOptionType::String as u8),
547                Token::Str("value"),
548                Token::String("Shiba"),
549                Token::StructEnd,
550                Token::SeqEnd,
551                Token::StructEnd,
552            ],
553        )
554    }
555
556    #[test]
557    fn subcommand_without_option() {
558        let value = CommandData {
559            guild_id: None,
560            id: Id::new(1),
561            name: "photo".to_owned(),
562            kind: CommandType::ChatInput,
563            options: Vec::from([CommandDataOption {
564                name: "cat".to_owned(),
565                value: CommandOptionValue::SubCommand(Vec::new()),
566            }]),
567            resolved: None,
568            target_id: None,
569        };
570
571        serde_test::assert_tokens(
572            &value,
573            &[
574                Token::Struct {
575                    name: "CommandData",
576                    len: 4,
577                },
578                Token::Str("id"),
579                Token::NewtypeStruct { name: "Id" },
580                Token::Str("1"),
581                Token::Str("name"),
582                Token::Str("photo"),
583                Token::Str("type"),
584                Token::U8(CommandType::ChatInput.into()),
585                Token::Str("options"),
586                Token::Seq { len: Some(1) },
587                Token::Struct {
588                    name: "CommandDataOption",
589                    len: 2,
590                },
591                Token::Str("name"),
592                Token::Str("cat"),
593                Token::Str("type"),
594                Token::U8(CommandOptionType::SubCommand as u8),
595                Token::StructEnd,
596                Token::SeqEnd,
597                Token::StructEnd,
598            ],
599        );
600    }
601
602    #[test]
603    fn numbers() {
604        let value = CommandDataOption {
605            name: "opt".to_string(),
606            value: CommandOptionValue::Number(5.0),
607        };
608
609        serde_test::assert_de_tokens(
610            &value,
611            &[
612                Token::Struct {
613                    name: "CommandDataOption",
614                    len: 3,
615                },
616                Token::Str("name"),
617                Token::Str("opt"),
618                Token::Str("type"),
619                Token::U8(CommandOptionType::Number as u8),
620                Token::Str("value"),
621                Token::I64(5),
622                Token::StructEnd,
623            ],
624        );
625    }
626
627    #[test]
628    fn autocomplete() {
629        let value = CommandDataOption {
630            name: "opt".to_string(),
631            value: CommandOptionValue::Focused(
632                "not a number".to_owned(),
633                CommandOptionType::Number,
634            ),
635        };
636
637        serde_test::assert_de_tokens(
638            &value,
639            &[
640                Token::Struct {
641                    name: "CommandDataOption",
642                    len: 4,
643                },
644                Token::Str("focused"),
645                Token::Some,
646                Token::Bool(true),
647                Token::Str("name"),
648                Token::Str("opt"),
649                Token::Str("type"),
650                Token::U8(CommandOptionType::Number as u8),
651                Token::Str("value"),
652                Token::String("not a number"),
653                Token::StructEnd,
654            ],
655        );
656    }
657
658    #[test]
659    fn autocomplete_number() {
660        let value = CommandDataOption {
661            name: "opt".to_string(),
662            value: CommandOptionValue::Focused("1".to_owned(), CommandOptionType::Number),
663        };
664
665        serde_test::assert_de_tokens(
666            &value,
667            &[
668                Token::Struct {
669                    name: "CommandDataOption",
670                    len: 4,
671                },
672                Token::Str("focused"),
673                Token::Some,
674                Token::Bool(true),
675                Token::Str("name"),
676                Token::Str("opt"),
677                Token::Str("type"),
678                Token::U8(CommandOptionType::Number as u8),
679                Token::Str("value"),
680                Token::String("1"),
681                Token::StructEnd,
682            ],
683        );
684    }
685
686    #[test]
687    fn leading_zeroes_string_option_value() {
688        let value = CommandDataOption {
689            name: "opt".to_string(),
690            value: CommandOptionValue::String("0001".to_owned()),
691        };
692
693        serde_test::assert_de_tokens(
694            &value,
695            &[
696                Token::Struct {
697                    name: "CommandDataOption",
698                    len: 3,
699                },
700                Token::Str("name"),
701                Token::Str("opt"),
702                Token::Str("type"),
703                Token::U8(CommandOptionType::String as u8),
704                Token::Str("value"),
705                Token::String("0001"),
706                Token::StructEnd,
707            ],
708        );
709    }
710}