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 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 #[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#[derive(Clone, Debug, PartialEq)]
326pub enum CommandOptionValue {
327 Attachment(Id<AttachmentMarker>),
329 Boolean(bool),
331 Channel(Id<ChannelMarker>),
333 Focused(String, CommandOptionType),
345 Integer(i64),
347 Mentionable(Id<GenericMarker>),
349 Number(f64),
351 Role(Id<RoleMarker>),
353 String(String),
355 SubCommand(Vec<CommandDataOption>),
357 SubCommandGroup(Vec<CommandDataOption>),
359 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}