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 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}