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#[derive(Clone, Debug, PartialEq)]
21pub struct CommandDataOption {
22 pub name: String,
24 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 #[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 #[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#[derive(Clone, Debug, PartialEq)]
327pub enum CommandOptionValue {
328 Attachment(Id<AttachmentMarker>),
330 Boolean(bool),
332 Channel(Id<ChannelMarker>),
334 Focused(String, CommandOptionType),
346 Integer(i64),
348 Mentionable(Id<GenericMarker>),
350 Number(f64),
352 Role(Id<RoleMarker>),
354 String(String),
356 SubCommand(Vec<CommandDataOption>),
358 SubCommandGroup(Vec<CommandDataOption>),
360 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}