twilight_http/request/
attachment.rs1use crate::request::Form;
2use serde::{Deserialize, Serialize};
3use twilight_model::{
4 http::attachment::Attachment,
5 id::{marker::AttachmentMarker, Id},
6};
7
8pub struct AttachmentManager<'a> {
9 files: Vec<&'a Attachment>,
10 ids: Vec<Id<AttachmentMarker>>,
11}
12
13impl<'a> AttachmentManager<'a> {
14 pub const fn new() -> Self {
15 Self {
16 files: Vec::new(),
17 ids: Vec::new(),
18 }
19 }
20
21 pub fn build_form(&self, fields: &'a [u8]) -> Form {
22 let mut form = Form::new().json_part(b"payload_json", fields);
23
24 for file in &self.files {
25 let mut name = Vec::with_capacity(7 + num_digits(file.id));
26 name.extend(b"files[");
27 push_digits(file.id, &mut name);
28 name.extend(b"]");
29
30 form = form.file_part(name.as_ref(), file.filename.as_bytes(), file.file.as_ref());
31 }
32
33 form
34 }
35
36 pub fn get_partial_attachments(&self) -> Vec<PartialAttachment<'a>> {
37 self.files
38 .iter()
39 .map(|attachment| PartialAttachment {
40 description: attachment.description.as_deref(),
41 filename: Some(attachment.filename.as_ref()),
42 id: attachment.id,
43 })
44 .chain(self.ids.iter().map(|id| PartialAttachment {
45 description: None,
46 filename: None,
47 id: id.get(),
48 }))
49 .collect()
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.files.is_empty() && self.ids.is_empty()
54 }
55
56 #[must_use = "has no effect if not built into a Form"]
57 pub fn set_files(mut self, files: Vec<&'a Attachment>) -> Self {
58 self.files = files;
59
60 self
61 }
62
63 #[must_use = "has no effect if not built into a Form"]
64 pub fn set_ids(mut self, ids: Vec<Id<AttachmentMarker>>) -> Self {
65 self.ids = ids;
66
67 self
68 }
69}
70
71impl Default for AttachmentManager<'_> {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
78pub struct PartialAttachment<'a> {
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub description: Option<&'a str>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub filename: Option<&'a str>,
83 pub id: u64,
84}
85
86const fn num_digits(index: u64) -> usize {
88 let mut index = index;
89 let mut len = 0;
90
91 if index < 10 {
92 return 1;
93 }
94
95 while index > 0 {
96 index /= 10;
97 len += 1;
98 }
99
100 len
101}
102
103const ASCII_NUMBER: u8 = 0x30;
105
106fn push_digits(mut id: u64, buf: &mut Vec<u8>) {
111 let mut inner_buf = [0_u8; 20];
113 let mut i = 0;
115
116 while id >= 10 {
120 #[allow(clippy::cast_possible_truncation)]
125 let ascii = (id % 10) as u8 + ASCII_NUMBER;
126 inner_buf[i] = ascii;
127 id /= 10;
128 i += 1;
129 }
130 #[allow(clippy::cast_possible_truncation)]
132 let ascii = (id % 10) as u8 + ASCII_NUMBER;
133 inner_buf[i] = ascii;
134 i += 1;
135
136 inner_buf[..i].reverse();
139
140 buf.extend_from_slice(&inner_buf[..i]);
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn push_digits_limits() {
149 let min_d = b"0";
150 let max_d = b"18446744073709551615";
151
152 let mut min_v = Vec::new();
153 let mut max_v = Vec::new();
154
155 push_digits(u64::MIN, &mut min_v);
156 push_digits(u64::MAX, &mut max_v);
157
158 assert_eq!(min_d[..], min_v[..]);
159 assert_eq!(max_d[..], max_v[..]);
160 }
161
162 #[test]
163 fn num_digits_count() {
164 assert_eq!(1, num_digits(0));
165 assert_eq!(1, num_digits(1));
166 assert_eq!(2, num_digits(10));
167 }
168}