twilight_http/request/
attachment.rs

1use 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
86/// Count the number of digits in a given number.
87const 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
103/// Value of '0' in ascii
104const ASCII_NUMBER: u8 = 0x30;
105
106/// Extend the buffer with the digits of the integer `id`.
107///
108/// The reason for this is to get around a allocation by for example using
109/// `format!("files[{id}]")`.
110fn push_digits(mut id: u64, buf: &mut Vec<u8>) {
111    // The largest 64 bit integer is 20 digits.
112    let mut inner_buf = [0_u8; 20];
113    // Amount of digits written to the inner buffer.
114    let mut i = 0;
115
116    // While the number have more than one digit we print the last digit by
117    // taking the rest after modulo 10. We then divide with 10 to truncate the
118    // number from the right and then loop
119    while id >= 10 {
120        // To go from the integer to the ascii value we add the ascii value of
121        // '0'.
122        //
123        // (id % 10) will always be less than 10 so truncation cannot happen.
124        #[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    // (id % 10) will always be less than 10 so truncation cannot happen.
131    #[allow(clippy::cast_possible_truncation)]
132    let ascii = (id % 10) as u8 + ASCII_NUMBER;
133    inner_buf[i] = ascii;
134    i += 1;
135
136    // As we have written the digits in reverse we reverse the area of the array
137    // we have been using to get the characters in the correct order.
138    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}