twilight_http/request/
attachment.rs1use crate::request::Form;
2use serde::{Deserialize, Serialize};
3use twilight_model::{
4 http::attachment::Attachment,
5 id::{Id, marker::AttachmentMarker},
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 title: attachment.title.as_deref(),
41 description: attachment.description.as_deref(),
42 filename: Some(attachment.filename.as_ref()),
43 id: attachment.id,
44 duration_secs: attachment.duration_secs,
45 waveform: attachment.waveform.as_deref(),
46 is_spoiler: attachment.is_spoiler,
47 })
48 .chain(self.ids.iter().map(|id| PartialAttachment {
49 title: None,
50 description: None,
51 filename: None,
52 id: id.get(),
53 duration_secs: None,
54 waveform: None,
55 is_spoiler: None,
56 }))
57 .collect()
58 }
59
60 pub const fn is_empty(&self) -> bool {
61 self.files.is_empty() && self.ids.is_empty()
62 }
63
64 #[must_use = "has no effect if not built into a Form"]
65 pub fn set_files(mut self, files: Vec<&'a Attachment>) -> Self {
66 self.files = files;
67
68 self
69 }
70
71 #[must_use = "has no effect if not built into a Form"]
72 pub fn set_ids(mut self, ids: Vec<Id<AttachmentMarker>>) -> Self {
73 self.ids = ids;
74
75 self
76 }
77}
78
79impl Default for AttachmentManager<'_> {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
86pub struct PartialAttachment<'a> {
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub title: Option<&'a str>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub description: Option<&'a str>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub filename: Option<&'a str>,
93 pub id: u64,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub duration_secs: Option<f64>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub waveform: Option<&'a str>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub is_spoiler: Option<bool>,
100}
101
102const fn num_digits(index: u64) -> usize {
104 let mut index = index;
105 let mut len = 0;
106
107 if index < 10 {
108 return 1;
109 }
110
111 while index > 0 {
112 index /= 10;
113 len += 1;
114 }
115
116 len
117}
118
119const ASCII_NUMBER: u8 = 0x30;
121
122fn push_digits(mut id: u64, buf: &mut Vec<u8>) {
127 let mut inner_buf = [0_u8; 20];
129 let mut i = 0;
131
132 while id >= 10 {
136 #[allow(clippy::cast_possible_truncation)]
141 let ascii = (id % 10) as u8 + ASCII_NUMBER;
142 inner_buf[i] = ascii;
143 id /= 10;
144 i += 1;
145 }
146 #[allow(clippy::cast_possible_truncation)]
148 let ascii = (id % 10) as u8 + ASCII_NUMBER;
149 inner_buf[i] = ascii;
150 i += 1;
151
152 inner_buf[..i].reverse();
155
156 buf.extend_from_slice(&inner_buf[..i]);
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn push_digits_limits() {
165 let min_d = b"0";
166 let max_d = b"18446744073709551615";
167
168 let mut min_v = Vec::new();
169 let mut max_v = Vec::new();
170
171 push_digits(u64::MIN, &mut min_v);
172 push_digits(u64::MAX, &mut max_v);
173
174 assert_eq!(min_d[..], min_v[..]);
175 assert_eq!(max_d[..], max_v[..]);
176 }
177
178 #[test]
179 fn num_digits_count() {
180 assert_eq!(1, num_digits(0));
181 assert_eq!(1, num_digits(1));
182 assert_eq!(2, num_digits(10));
183 }
184}